@thiennq/docs-viewer
Advanced tools
| function Badge({ type = 'default', children }) { | ||
| const variant = String(children || '').toLowerCase().trim(); | ||
| let customStyle = {}; | ||
| if (variant === 'easy') customStyle = { background: 'rgba(16, 185, 129, 0.15)', color: '#10b981', border: '1px solid rgba(16, 185, 129, 0.3)', padding: '2px 8px', borderRadius: '10px', fontWeight: 700, fontSize: '0.75rem' }; | ||
| else if (variant === 'medium') customStyle = { background: 'rgba(59, 130, 246, 0.15)', color: '#3b82f6', border: '1px solid rgba(59, 130, 246, 0.3)', padding: '2px 8px', borderRadius: '10px', fontWeight: 700, fontSize: '0.75rem' }; | ||
| else if (variant === 'hard') customStyle = { background: 'rgba(245, 158, 11, 0.15)', color: '#f59e0b', border: '1px solid rgba(245, 158, 11, 0.3)', padding: '2px 8px', borderRadius: '10px', fontWeight: 700, fontSize: '0.75rem' }; | ||
| else if (variant === 'nightmare') customStyle = { background: 'rgba(239, 68, 68, 0.15)', color: '#ef4444', border: '1px solid rgba(239, 68, 68, 0.3)', padding: '2px 8px', borderRadius: '10px', fontWeight: 700, fontSize: '0.75rem' }; | ||
| return ( | ||
| <span className={`mdx-badge mdx-badge-${type}`} style={customStyle}> | ||
| {children} | ||
| </span> | ||
| ); | ||
| } |
| function Button({ variant = 'primary', onClick, children }) { | ||
| return ( | ||
| <button className={`mdx-button mdx-button-${variant}`} onClick={onClick || (() => {})}> | ||
| {children} | ||
| </button> | ||
| ); | ||
| } |
| function Callout({ type = 'info', title, children }) { | ||
| const icons = { info: '💡', warning: '⚠️', success: '✅', error: '🛑' }; | ||
| return ( | ||
| <div className={`mdx-callout mdx-callout-${type}`}> | ||
| <div className="mdx-callout-header"> | ||
| <span className="mdx-callout-icon">{icons[type] || '💡'}</span> | ||
| {title && <strong className="mdx-callout-title">{title}</strong>} | ||
| </div> | ||
| <div className="mdx-callout-content">{children}</div> | ||
| </div> | ||
| ); | ||
| } |
| function Card({ title, icon = '📁', children }) { | ||
| return ( | ||
| <div className="mdx-card"> | ||
| {title && ( | ||
| <div className="mdx-card-header"> | ||
| <span className="mdx-card-icon">{icon}</span> | ||
| <span className="mdx-card-title">{title}</span> | ||
| </div> | ||
| )} | ||
| <div className="mdx-card-body">{children}</div> | ||
| </div> | ||
| ); | ||
| } |
| function StatCard({ label, value, sub, color = '#6366f1' }) { | ||
| return ( | ||
| <div style={{ | ||
| background: 'rgba(22, 30, 49, 0.8)', | ||
| border: '1px solid rgba(255, 255, 255, 0.08)', | ||
| borderRadius: '16px', | ||
| padding: '20px', | ||
| position: 'relative', | ||
| overflow: 'hidden' | ||
| }}> | ||
| <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '3px', background: color }} /> | ||
| <div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em' }}> | ||
| {label} | ||
| </div> | ||
| <div style={{ fontSize: '2rem', fontWeight: 800, color: '#f8fafc', margin: '6px 0 2px 0' }}> | ||
| {value} | ||
| </div> | ||
| {sub && <div style={{ fontSize: '0.82rem', color: '#64748b' }}>{sub}</div>} | ||
| </div> | ||
| ); | ||
| } |
| function StatGrid({ children }) { | ||
| return ( | ||
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '16px', margin: '20px 0' }}> | ||
| {children} | ||
| </div> | ||
| ); | ||
| } |
| import React from 'react'; | ||
| import styled from 'styled-components'; | ||
| const CardWrapper = styled.div` | ||
| background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.15)); | ||
| border: 1px solid rgba(99, 102, 241, 0.4); | ||
| border-radius: 16px; | ||
| padding: 24px; | ||
| margin: 20px 0; | ||
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); | ||
| backdrop-filter: blur(12px); | ||
| `; | ||
| const CardTitle = styled.h4` | ||
| font-size: 1.1rem; | ||
| font-weight: 700; | ||
| color: #818cf8; | ||
| margin: 0 0 8px 0; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 8px; | ||
| `; | ||
| const CardBody = styled.div` | ||
| color: #cbd5e1; | ||
| font-size: 0.92rem; | ||
| line-height: 1.6; | ||
| `; | ||
| export default function StyledCard({ title = 'CSS-in-JS Styled Component', children }) { | ||
| return ( | ||
| <CardWrapper> | ||
| <CardTitle>✨ {title}</CardTitle> | ||
| <CardBody>{children}</CardBody> | ||
| </CardWrapper> | ||
| ); | ||
| } |
| function ThresholdViewer() { | ||
| const [threshold, setThreshold] = React.useState('50'); | ||
| const barCanvasRef = React.useRef(null); | ||
| const pieCanvasRef = React.useRef(null); | ||
| const barChartRef = React.useRef(null); | ||
| const pieChartRef = React.useRef(null); | ||
| const data = { | ||
| "50": { | ||
| total: 31406119, | ||
| pctTotal: "62.81%", | ||
| easy: 12500000, | ||
| medium: 9581731, | ||
| hard: 8970875, | ||
| nightmare: 353513, | ||
| avgEng: { easy: 63.8, medium: 57.2, hard: 56.4, nightmare: 51.2 } | ||
| }, | ||
| "60": { | ||
| total: 17987423, | ||
| pctTotal: "35.97%", | ||
| easy: 12500000, | ||
| medium: 2567818, | ||
| hard: 2903366, | ||
| nightmare: 16239, | ||
| avgEng: { easy: 63.8, medium: 63.1, hard: 63.4, nightmare: 61.5 } | ||
| }, | ||
| "70": { | ||
| total: 354572, | ||
| pctTotal: "0.71%", | ||
| easy: 0, | ||
| medium: 237870, | ||
| hard: 115996, | ||
| nightmare: 706, | ||
| avgEng: { easy: 0, medium: 72.8, hard: 73.1, nightmare: 70.8 } | ||
| }, | ||
| "80": { | ||
| total: 67, | ||
| pctTotal: "< 0.001%", | ||
| easy: 0, | ||
| medium: 32, | ||
| hard: 35, | ||
| nightmare: 0, | ||
| avgEng: { easy: 0, medium: 86.4, hard: 87.2, nightmare: 0 } | ||
| }, | ||
| "90": { | ||
| total: 5, | ||
| pctTotal: "< 0.0001%", | ||
| easy: 0, | ||
| medium: 3, | ||
| hard: 2, | ||
| nightmare: 0, | ||
| avgEng: { easy: 0, medium: 93.3, hard: 96.4, nightmare: 0 } | ||
| } | ||
| }; | ||
| const curr = data[threshold]; | ||
| const diffs = [ | ||
| { key: 'easy', label: 'EASY', color: '#10b981', count: curr.easy }, | ||
| { key: 'medium', label: 'MEDIUM', color: '#3b82f6', count: curr.medium }, | ||
| { key: 'hard', label: 'HARD', color: '#f59e0b', count: curr.hard }, | ||
| { key: 'nightmare', label: 'NIGHTMARE', color: '#ef4444', count: curr.nightmare } | ||
| ]; | ||
| const maxCount = Math.max(...diffs.map(d => d.count), 1); | ||
| React.useEffect(() => { | ||
| if (typeof Chart === 'undefined') return; | ||
| if (barCanvasRef.current) { | ||
| if (barChartRef.current) barChartRef.current.destroy(); | ||
| const ctxBar = barCanvasRef.current.getContext('2d'); | ||
| barChartRef.current = new Chart(ctxBar, { | ||
| type: 'bar', | ||
| data: { | ||
| labels: ['Easy', 'Medium', 'Hard', 'Nightmare'], | ||
| datasets: [{ | ||
| label: 'Map Count', | ||
| data: [curr.easy, curr.medium, curr.hard, curr.nightmare], | ||
| backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'], | ||
| borderRadius: 8 | ||
| }] | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| legend: { display: false }, | ||
| tooltip: { | ||
| callbacks: { label: (ctx) => ` ${ctx.raw.toLocaleString()} maps` } | ||
| } | ||
| }, | ||
| scales: { | ||
| x: { grid: { display: false }, ticks: { color: '#94a3b8', font: { weight: '600' } } }, | ||
| y: { grid: { color: 'rgba(255, 255, 255, 0.06)' }, ticks: { color: '#94a3b8' } } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (pieCanvasRef.current) { | ||
| if (pieChartRef.current) pieChartRef.current.destroy(); | ||
| const ctxPie = pieCanvasRef.current.getContext('2d'); | ||
| pieChartRef.current = new Chart(ctxPie, { | ||
| type: 'doughnut', | ||
| data: { | ||
| labels: ['Easy', 'Medium', 'Hard', 'Nightmare'], | ||
| datasets: [{ | ||
| data: [curr.easy, curr.medium, curr.hard, curr.nightmare], | ||
| backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'], | ||
| borderWidth: 0 | ||
| }] | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| legend: { position: 'bottom', labels: { color: '#94a3b8', font: { size: 12, weight: '600' } } } | ||
| }, | ||
| cutout: '70%' | ||
| } | ||
| }); | ||
| } | ||
| return () => { | ||
| if (barChartRef.current) barChartRef.current.destroy(); | ||
| if (pieChartRef.current) pieChartRef.current.destroy(); | ||
| }; | ||
| }, [threshold]); | ||
| return ( | ||
| <div style={{ | ||
| background: 'rgba(22, 30, 49, 0.85)', | ||
| border: '1px solid rgba(255, 255, 255, 0.1)', | ||
| borderRadius: '16px', | ||
| padding: '24px', | ||
| margin: '24px 0' | ||
| }}> | ||
| {/* Header & Tabs */} | ||
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}> | ||
| <h3 style={{ fontSize: '1.15rem', fontWeight: 700, margin: 0, color: '#f8fafc', display: 'flex', alignItems: 'center', gap: '8px' }}> | ||
| <span style={{ width: '8px', height: '18px', background: '#6366f1', borderRadius: '4px' }} /> | ||
| Engagement Threshold Distribution | ||
| </h3> | ||
| <div style={{ display: 'flex', gap: '6px', background: 'rgba(15, 23, 42, 0.7)', padding: '4px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.08)' }}> | ||
| {['50', '60', '70', '80', '90'].map(t => ( | ||
| <button | ||
| key={t} | ||
| onClick={() => setThreshold(t)} | ||
| style={{ | ||
| padding: '6px 14px', | ||
| borderRadius: '6px', | ||
| border: 'none', | ||
| background: threshold === t ? '#6366f1' : 'transparent', | ||
| color: threshold === t ? '#fff' : '#94a3b8', | ||
| fontWeight: 600, | ||
| fontSize: '0.85rem', | ||
| cursor: 'pointer', | ||
| transition: 'all 0.2s' | ||
| }} | ||
| > | ||
| E ≥ {t} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| {/* Chart.js Canvas Row */} | ||
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '20px', marginBottom: '24px', height: '300px' }}> | ||
| <div style={{ position: 'relative', height: '100%', width: '100%', background: 'rgba(15, 23, 42, 0.5)', padding: '12px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> | ||
| <canvas ref={barCanvasRef} /> | ||
| </div> | ||
| <div style={{ position: 'relative', height: '100%', width: '100%', background: 'rgba(15, 23, 42, 0.5)', padding: '12px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> | ||
| <canvas ref={pieCanvasRef} /> | ||
| </div> | ||
| </div> | ||
| {/* Visual Progress Bars */} | ||
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '16px', marginBottom: '24px' }}> | ||
| {diffs.map(d => { | ||
| const pctHeight = Math.max(4, Math.round((d.count / maxCount) * 100)); | ||
| const pctGroup = curr.total > 0 ? ((d.count / curr.total) * 100).toFixed(2) : '0.00'; | ||
| return ( | ||
| <div key={d.key} style={{ background: 'rgba(15, 23, 42, 0.6)', padding: '16px', borderRadius: '12px', border: '1px solid rgba(255, 255, 255, 0.05)' }}> | ||
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px', fontSize: '0.8rem', fontWeight: 700, color: d.color }}> | ||
| <span>{d.label}</span> | ||
| <span>{pctGroup}%</span> | ||
| </div> | ||
| <div style={{ fontSize: '1.25rem', fontWeight: 800, color: '#f8fafc', marginBottom: '12px' }}> | ||
| {d.count.toLocaleString()} | ||
| </div> | ||
| <div style={{ height: '8px', background: 'rgba(255,255,255,0.06)', borderRadius: '4px', overflow: 'hidden' }}> | ||
| <div style={{ width: `${pctHeight}%`, height: '100%', background: d.color, borderRadius: '4px', transition: 'width 0.4s ease' }} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| {/* Data Table */} | ||
| <div style={{ overflowX: 'auto', borderRadius: '10px', border: '1px solid rgba(255, 255, 255, 0.08)' }}> | ||
| <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.88rem', textAlign: 'left' }}> | ||
| <thead> | ||
| <tr style={{ background: 'rgba(15, 23, 42, 0.9)', color: '#94a3b8', fontSize: '0.78rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}> | ||
| <th style={{ padding: '12px 16px' }}>Difficulty</th> | ||
| <th style={{ padding: '12px 16px' }}>Map Count</th> | ||
| <th style={{ padding: '12px 16px' }}>Share in Threshold</th> | ||
| <th style={{ padding: '12px 16px' }}>Share of Total 50M</th> | ||
| <th style={{ padding: '12px 16px' }}>Avg Engagement</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {diffs.map(d => { | ||
| const pctGroup = curr.total > 0 ? ((d.count / curr.total) * 100).toFixed(2) + '%' : '0.00%'; | ||
| const pct50M = ((d.count / 50000000) * 100).toFixed(2) + '%'; | ||
| const avg = curr.avgEng[d.key] ? curr.avgEng[d.key] : '-'; | ||
| return ( | ||
| <tr key={d.key} style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}> | ||
| <td style={{ padding: '12px 16px', fontWeight: 700, color: d.color }}>{d.label}</td> | ||
| <td style={{ padding: '12px 16px', fontWeight: 700, color: '#f8fafc' }}>{d.count.toLocaleString()}</td> | ||
| <td style={{ padding: '12px 16px', color: '#cbd5e1' }}>{pctGroup}</td> | ||
| <td style={{ padding: '12px 16px', color: '#94a3b8' }}>{pct50M}</td> | ||
| <td style={{ padding: '12px 16px', color: '#cbd5e1' }}>{avg}</td> | ||
| </tr> | ||
| ); | ||
| })} | ||
| <tr style={{ background: 'rgba(99, 102, 241, 0.12)', fontWeight: 700 }}> | ||
| <td style={{ padding: '12px 16px', color: '#818cf8' }}>TỔNG (E ≥ {threshold})</td> | ||
| <td style={{ padding: '12px 16px', color: '#fff' }}>{curr.total.toLocaleString()}</td> | ||
| <td style={{ padding: '12px 16px', color: '#fff' }}>100.00%</td> | ||
| <td style={{ padding: '12px 16px', color: '#818cf8' }}>{curr.pctTotal}</td> | ||
| <td style={{ padding: '12px 16px', color: '#fff' }}>-</td> | ||
| </tr> | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| /* ─────────────────────────────────────────────────────────── | ||
| DYNAMIC NO-BUILD JSX COMPONENT LOADER (UNIVERSAL ENGINE) | ||
| Supports all 5 JSX Patterns + ES6 Imports/Exports + CSS-in-JS (styled) | ||
| ─────────────────────────────────────────────────────────────*/ | ||
| window.MDXBuiltInComponents = window.MDXBuiltInComponents || {}; | ||
| async function loadJSXComponent(componentName, fileUrl) { | ||
| try { | ||
| const res = await fetch(fileUrl); | ||
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | ||
| const rawJsx = await res.text(); | ||
| // 1. Strip top-level import statements and export keywords | ||
| const cleanJsx = rawJsx | ||
| .replace(/^import\s+[\s\S]*?;?\s*$/gm, '') | ||
| .replace(/^export\s+default\s+/gm, '') | ||
| .replace(/^export\s+/gm, ''); | ||
| // 2. Transpile JSX string on-the-fly via Babel Standalone | ||
| const compiled = Babel.transform(cleanJsx, { | ||
| presets: ['react'] | ||
| }).code; | ||
| // 3. Resolve styled-components UMD global | ||
| const styledEngine = window.styled || (window.styledComponents ? (window.styledComponents.default || window.styledComponents) : null); | ||
| // 4. Create function in browser scope and evaluate component | ||
| const createComp = new Function('React', 'Chart', 'styled', compiled + `; return (typeof ${componentName} !== "undefined" ? ${componentName} : null);`); | ||
| const Component = createComp(React, window.Chart, styledEngine); | ||
| if (Component) { | ||
| window.MDXBuiltInComponents[componentName] = Component; | ||
| return Component; | ||
| } else { | ||
| console.warn(`Could not resolve component [${componentName}] from ${fileUrl}`); | ||
| } | ||
| } catch (err) { | ||
| console.error(`Failed to load JSX component [${componentName}] from ${fileUrl}:`, err); | ||
| } | ||
| } | ||
| async function loadAllJSXComponents() { | ||
| if (typeof Babel === 'undefined' || typeof React === 'undefined') return; | ||
| const componentList = [ | ||
| { name: 'Callout', url: '/components/Callout.jsx' }, | ||
| { name: 'Card', url: '/components/Card.jsx' }, | ||
| { name: 'Badge', url: '/components/Badge.jsx' }, | ||
| { name: 'Button', url: '/components/Button.jsx' }, | ||
| { name: 'StatCard', url: '/components/StatCard.jsx' }, | ||
| { name: 'StatGrid', url: '/components/StatGrid.jsx' }, | ||
| { name: 'ThresholdViewer', url: '/components/ThresholdViewer.jsx' }, | ||
| { name: 'StyledCard', url: '/components/StyledCard.jsx' } | ||
| ]; | ||
| await Promise.all(componentList.map(c => loadJSXComponent(c.name, c.url))); | ||
| } | ||
| // Auto-trigger universal component loader | ||
| window._jsxComponentsLoaded = loadAllJSXComponents(); |
| /* ─────────────────────────────────────────────────────────── | ||
| DOCS VIEWER — MDX PARSING & TRANSPILATION ENGINE | ||
| ─────────────────────────────────────────────────────────────*/ | ||
| async function renderMDXContent(mdxText, container) { | ||
| if (window._jsxComponentsLoaded) { | ||
| await window._jsxComponentsLoaded; | ||
| } | ||
| if (typeof Babel === 'undefined' || typeof React === 'undefined' || typeof ReactDOM === 'undefined') { | ||
| container.innerHTML = ` | ||
| <div class="mdx-error-boundary"> | ||
| <div class="mdx-error-header">⚠️ Missing Client Transpiler Libraries</div> | ||
| <div class="mdx-error-message">Babel Standalone or React/ReactDOM library is not loaded. Please check your network connection or CDN script tags.</div> | ||
| </div> | ||
| `; | ||
| return; | ||
| } | ||
| try { | ||
| // 1. Strip top-level import and export statements | ||
| let cleanText = mdxText | ||
| .replace(/^import\s+[\s\S]*?;?\s*$/gm, '') | ||
| .replace(/^export\s+[\s\S]*?;?\s*$/gm, ''); | ||
| // 2. Parse Markdown portion to HTML while retaining JSX tags | ||
| let htmlFromMarkdown = marked.parse(cleanText); | ||
| // 2b. Remove HTML comments which break JSX | ||
| htmlFromMarkdown = htmlFromMarkdown.replace(/<!--[\s\S]*?-->/g, ''); | ||
| // 2c. Convert HTML void tags (hr, br, img, input, etc.) to self-closing JSX tags (<hr />) | ||
| const voidTags = ['hr', 'br', 'img', 'input', 'meta', 'link', 'source', 'param', 'embed']; | ||
| voidTags.forEach(tag => { | ||
| const reg = new RegExp(`<${tag}\\b([^>]*)(?<!\\/)>`, 'gi'); | ||
| htmlFromMarkdown = htmlFromMarkdown.replace(reg, `<${tag}$1 />`); | ||
| }); | ||
| // 2d. Sanitize HTML for JSX to escape < in codeblocks and raw text nodes | ||
| htmlFromMarkdown = htmlFromMarkdown.replace(/(<code[^>]*>)([\s\S]*?)(<\/code>)/gi, (match, openTag, codeContent, closeTag) => { | ||
| const safeCode = codeContent.replace(/</g, '<').replace(/>/g, '>'); | ||
| return openTag + safeCode + closeTag; | ||
| }).replace(/<(?![a-zA-Z_\/!?])/g, '<'); | ||
| // 3. Construct React Component code string | ||
| const jsxCode = ` | ||
| function MDXDocument({ components }) { | ||
| const { Callout, Card, Badge, Button, StatGrid, StatCard, ThresholdViewer, StyledCard } = components; | ||
| return ( | ||
| <React.Fragment> | ||
| ${htmlFromMarkdown} | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
| `; | ||
| // 4. Transpile JSX using Babel Standalone | ||
| const compiled = Babel.transform(jsxCode, { | ||
| presets: ['react'] | ||
| }).code; | ||
| // 5. Evaluate compiled JS code to obtain React Component | ||
| const createComponent = new Function('React', compiled + '; return MDXDocument;'); | ||
| const MDXDocument = createComponent(React); | ||
| // 6. Render with ReactDOM Root | ||
| const builtInComponents = window.MDXBuiltInComponents || (typeof getMDXBuiltInComponents === 'function' ? getMDXBuiltInComponents() : {}); | ||
| container.innerHTML = ''; | ||
| const root = ReactDOM.createRoot(container); | ||
| root.render(React.createElement(MDXDocument, { components: builtInComponents })); | ||
| // Allow React DOM flush before post-processing | ||
| await new Promise(r => setTimeout(r, 60)); | ||
| } catch (err) { | ||
| container.innerHTML = ` | ||
| <div class="mdx-error-boundary"> | ||
| <div class="mdx-error-header">⚠️ MDX Compilation / Runtime Error</div> | ||
| <pre class="mdx-error-message">${typeof escapeHtml === 'function' ? escapeHtml(err.stack || err.message) : (err.stack || err.message)}</pre> | ||
| </div> | ||
| `; | ||
| } | ||
| } | ||
| window.renderMDXContent = renderMDXContent; |
+18
-0
@@ -5,2 +5,20 @@ # Docs Viewer CHANGELOG | ||
| ## [1.6.0] - 2026-07-26 | ||
| ### Added | ||
| * **Modular No-Build React/JSX Component Architecture**: | ||
| * `public/loader.js`: Implemented lightweight client-side dynamic loader to import and transpile `.jsx` components on the fly without bundlers or build steps. | ||
| * `public/mdx.js`: Extracted MDX parsing and Babel Standalone transpilation pipeline from `app.js` into a dedicated modular engine. | ||
| * **Chart.js Integration & Data Visualization**: | ||
| * `public/index.html`: Loaded Chart.js v4 library via CDN. | ||
| * `ThresholdViewer.jsx`: Added interactive component powered by Chart.js for visualizing statistical metrics, difficulty distribution, and threshold analysis directly in MDX documentation. | ||
| * **CSS-in-JS & Styled Components Support**: | ||
| * `public/index.html`: Added CDN dependencies for `styled-components` v5.3.11 and `react-is` v18.2.0. | ||
| * `StyledCard.jsx`: Added styled-components based card component for custom CSS-in-JS document styling. | ||
| * **Expanded Built-in Component Library (`public/components/`)**: | ||
| * Added `StatCard.jsx` and `StatGrid.jsx` for dashboard metric cards. | ||
| * Modularized `Callout.jsx`, `Card.jsx`, `Badge.jsx`, and `Button.jsx` as standalone `.jsx` component files. | ||
| --- | ||
| ## [1.5.0] - 2026-07-22 | ||
@@ -7,0 +25,0 @@ |
+1
-1
| { | ||
| "name": "@thiennq/docs-viewer", | ||
| "version": "1.5.1", | ||
| "version": "1.6.0", | ||
| "description": "Standalone Docs Viewer CLI — static HTTP server & browser reader for Markdown, MDX, HTML and Mermaid diagrams", | ||
@@ -5,0 +5,0 @@ "main": "server.js", |
+12
-0
@@ -33,2 +33,5 @@ <!DOCTYPE html> | ||
| <!-- Chart.js --> | ||
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | ||
| <!-- React & ReactDOM --> | ||
@@ -38,2 +41,8 @@ <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script> | ||
| <!-- React-Is dependency for Styled Components --> | ||
| <script src="https://cdn.jsdelivr.net/npm/react-is@18.2.0/umd/react-is.production.min.js"></script> | ||
| <!-- Styled Components for CSS-in-JS (depends on React & React-Is) --> | ||
| <script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.min.js"></script> | ||
| <!-- Babel Standalone for MDX Transpilation --> | ||
@@ -196,2 +205,5 @@ <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.6/babel.min.js"></script> | ||
| <!-- Modular No-Build JSX Components & MDX Engine --> | ||
| <script src="loader.js"></script> | ||
| <script src="mdx.js"></script> | ||
| <!-- App logic --> | ||
@@ -198,0 +210,0 @@ <script src="app.js"></script> |
+6
-2
@@ -15,5 +15,9 @@ # 📚 Docs Viewer (`@thiennq/docs-viewer`) | ||
| * **Markdown (`.md`)**: Full GFM support, task lists, tables, auto-generated Table of Contents (TOC). | ||
| * **MDX (`.mdx`)**: Client-side transpilation via Babel & React with built-in UI components (`<Callout>`, `<Card>`, `<Badge>`, `<Button>`). | ||
| * **MDX (`.mdx`)**: Client-side transpilation via Babel & React with dynamic no-build `.jsx` component loader (`loader.js`, `mdx.js`). | ||
| * **HTML (`.html`, `.htm`)**: Sanitized inline HTML rendering via DOMPurify. | ||
| * 📊 **Rich Visuals & Formulas**: | ||
| * ⚛️ **Modular No-Build Component Suite**: | ||
| * Built-in & custom JSX components (`<Callout>`, `<Card>`, `<StyledCard>`, `<Badge>`, `<Button>`, `<StatCard>`, `<StatGrid>`, `<ThresholdViewer>`). | ||
| * **Styled Components (CSS-in-JS)**: First-class support for styled-components without build tooling. | ||
| * 📊 **Rich Visuals, Charts & Formulas**: | ||
| * **Chart.js Visualizations**: Render interactive data charts and threshold analytics directly in MDX (`<ThresholdViewer>`). | ||
| * **Mermaid Diagrams**: Interactive rendering with dark/light theme switching and lightbox zoom modal. | ||
@@ -20,0 +24,0 @@ * **KaTeX**: Fast math formula rendering ($\LaTeX$). |
Sorry, the diff of this file is too big to display
187097
11.33%22
83.33%4334
9.42%115
3.6%11
22.22%