From 52c6b14c7a45d1470a661692829bab63f1aefde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20Dur=C3=A1n=20=28midudev=29?= Date: Tue, 21 Jul 2026 12:50:18 +0200 Subject: [PATCH] feat: add light theme support Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/components/ArrayVisualizer.tsx | 2 +- src/components/CodePanel.tsx | 38 ++- src/components/ComplexityChart.tsx | 42 ++- src/components/ConceptVisualizer.tsx | 398 ++++++++++++++++++++------- src/components/GraphVisualizer.tsx | 24 +- src/components/Header.tsx | 47 ++++ src/components/MatrixVisualizer.tsx | 17 +- src/layouts/Layout.astro | 36 ++- src/lib/highlight-colors.ts | 14 +- src/styles/global.css | 182 +++++++++++- 10 files changed, 649 insertions(+), 151 deletions(-) diff --git a/src/components/ArrayVisualizer.tsx b/src/components/ArrayVisualizer.tsx index b9c88fa..ec99aeb 100644 --- a/src/components/ArrayVisualizer.tsx +++ b/src/components/ArrayVisualizer.tsx @@ -77,7 +77,7 @@ export default function ArrayVisualizer({ step }: ArrayVisualizerProps) { ? highlightColors[highlight] : isSorted ? highlightColors.sorted - : '#333' + : 'var(--viz-faint)' return (
{ + const updateEditorTheme = (event?: Event) => { + const theme = + event instanceof CustomEvent ? event.detail : document.documentElement.dataset.theme + monacoRef.current?.editor.setTheme(theme === 'light' ? 'algoviz-light' : 'algoviz-dark') + } + window.addEventListener('themechange', updateEditorTheme) + return () => window.removeEventListener('themechange', updateEditorTheme) + }, []) + const handleEditorDidMount = useCallback( (editor: any, monacoInstance: Monaco) => { - defineTheme(monacoInstance) - monacoInstance.editor.setTheme('algoviz-dark') + defineThemes(monacoInstance) + monacoInstance.editor.setTheme( + document.documentElement.dataset.theme === 'light' ? 'algoviz-light' : 'algoviz-dark', + ) editorRef.current = editor monacoRef.current = monacoInstance @@ -232,7 +256,11 @@ export default function CodePanel({ - new RegExp(`${prefix}:\\s*(O\\((?:[^()]+|\\([^)]*\\))*\\))`, 'i') + const oRe = (prefix: string) => new RegExp(`${prefix}:\\s*(O\\((?:[^()]+|\\([^)]*\\))*\\))`, 'i') const best = block.match(oRe(isSpanish ? 'Mejor' : 'Best')) const avg = block.match(oRe(isSpanish ? 'Promedio' : 'Average')) @@ -85,15 +84,28 @@ function parseTimeComplexity(description: string): Entry[] { if (best || avg || worst) { if (best) { const k = normalizeToKey(best[1]) - if (k) entries.push({ label: isSpanish ? 'Mejor' : 'Best', raw: best[1], key: k, color: '#34d399' }) + if (k) + entries.push({ + label: isSpanish ? 'Mejor' : 'Best', + raw: best[1], + key: k, + color: '#34d399', + }) } if (avg) { const k = normalizeToKey(avg[1]) - if (k) entries.push({ label: isSpanish ? 'Prom' : 'Avg', raw: avg[1], key: k, color: '#fbbf24' }) + if (k) + entries.push({ label: isSpanish ? 'Prom' : 'Avg', raw: avg[1], key: k, color: '#fbbf24' }) } if (worst) { const k = normalizeToKey(worst[1]) - if (k) entries.push({ label: isSpanish ? 'Peor' : 'Worst', raw: worst[1], key: k, color: '#f87171' }) + if (k) + entries.push({ + label: isSpanish ? 'Peor' : 'Worst', + raw: worst[1], + key: k, + color: '#f87171', + }) } } else { const single = block.match(O_RE) @@ -162,7 +174,13 @@ function resolveOverlaps( return sorted } -export default function ComplexityChart({ description, locale = 'en' }: { description: string; locale?: string }) { +export default function ComplexityChart({ + description, + locale = 'en', +}: { + description: string + locale?: string +}) { const entries = useMemo(() => parseTimeComplexity(description), [description]) if (entries.length === 0) return null @@ -179,7 +197,12 @@ export default function ComplexityChart({ description, locale = 'en' }: { descri const labels: { y: number; text: string; color: string; opacity: number }[] = [] for (const key of REFERENCE_KEYS) { if (!highlightedKeys.has(key)) { - labels.push({ y: endY(COMPLEXITY_FNS[key]), text: key, color: '#fff', opacity: 0.18 }) + labels.push({ + y: endY(COMPLEXITY_FNS[key]), + text: key, + color: 'var(--foreground)', + opacity: 0.18, + }) } } for (const [key, group] of grouped) { @@ -300,10 +323,7 @@ export default function ComplexityChart({ description, locale = 'en' }: { descri
{entries.map((e, i) => (
-
+
{e.label ? `${e.label}: ` : ''} {e.raw} diff --git a/src/components/ConceptVisualizer.tsx b/src/components/ConceptVisualizer.tsx index 599298f..b936850 100644 --- a/src/components/ConceptVisualizer.tsx +++ b/src/components/ConceptVisualizer.tsx @@ -112,13 +112,11 @@ function BigOChart({ state }: { state: BigOState }) { style={{ maxHeight: '340px' }} role="img" aria-label={`Big O complexity chart${ - visibleCurves.length > 0 - ? `: ${visibleCurves.map((c) => c.name).join(', ')}` - : '' + visibleCurves.length > 0 ? `: ${visibleCurves.map((c) => c.name).join(', ')}` : '' }`} > {/* Background */} - + {/* Horizontal grid lines */} {Array.from({ length: yTicks + 1 }, (_, i) => { @@ -126,8 +124,22 @@ function BigOChart({ state }: { state: BigOState }) { const val = maxY - (i / yTicks) * maxY return ( - - + + {val < 10 ? val.toFixed(1) : Math.round(val)} @@ -140,8 +152,22 @@ function BigOChart({ state }: { state: BigOState }) { const x = toX(n) return ( - - + + {Math.round(n)} @@ -149,18 +175,37 @@ function BigOChart({ state }: { state: BigOState }) { })} {/* Axes */} - - + + {/* Axis labels */} - + n (input size) - {curve.name} + + {curve.name} +
))}
@@ -240,7 +287,7 @@ function BigOChart({ state }: { state: BigOState }) { // ════════════════════════════════════════════════════════════════ const FRAME_COLORS: Record = { - waiting: { bg: 'rgba(255,255,255,0.04)', border: 'rgba(255,255,255,0.1)', text: '#888' }, + waiting: { bg: 'var(--subtle)', border: 'var(--viz-border)', text: 'var(--viz-label)' }, active: { bg: 'rgba(251,146,60,0.12)', border: 'rgba(251,146,60,0.35)', text: '#fb923c' }, base: { bg: 'rgba(74,222,128,0.12)', border: 'rgba(74,222,128,0.35)', text: '#4ade80' }, resolved: { bg: 'rgba(52,211,153,0.08)', border: 'rgba(52,211,153,0.2)', text: '#34d399' }, @@ -260,7 +307,9 @@ function CallStackViz({ state }: { state: CallStackState }) { return (
{/* Stack label */} -
Call Stack
+
+ Call Stack +
{/* Frames — top of stack (last frame) is rendered first */}
@@ -292,9 +341,10 @@ function CallStackViz({ state }: { state: CallStackState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: frame.state === 'active' || frame.state === 'base' - ? `0 0 20px ${colors.border}` - : 'none', + boxShadow: + frame.state === 'active' || frame.state === 'base' + ? `0 0 20px ${colors.border}` + : 'none', }} > {/* Pulse animation for active/base frame */} @@ -307,15 +357,16 @@ function CallStackViz({ state }: { state: CallStackState }) {
{frame.label} - {frame.detail && ( - {frame.detail} - )} + {frame.detail && {frame.detail}}
{/* TOP indicator */} {isTop && ( -
+
← top
)} @@ -366,7 +417,9 @@ function StackViz({ return (
{/* Title */} -
Stack · LIFO
+
+ Stack · LIFO +
{/* Operation badge */} {operation && ( @@ -446,7 +499,9 @@ function QueueViz({ return (
{/* Title */} -
Queue · FIFO
+
+ Queue · FIFO +
{/* Operation badge */} {operation && ( @@ -468,8 +523,12 @@ function QueueViz({
{items.length > 0 && ( <> - front - back + + front + + + back + )}
@@ -515,10 +574,20 @@ function QueueViz({ - +
-
processing direction
+
+ processing direction +
) @@ -541,7 +610,9 @@ function LinkedListViz({ state }: { state: LinkedListState }) { return (
-
Linked List
+
+ Linked List +
{operation && (
@@ -555,9 +626,16 @@ function LinkedListViz({ state }: { state: LinkedListState }) {
{/* HEAD label */}
- head + + head + - +
@@ -579,7 +657,13 @@ function LinkedListViz({ state }: { state: LinkedListState }) { {node.value}
{/* Arrow to next */} - + @@ -620,7 +704,9 @@ function HashTableViz({ state }: { state: HashTableState }) { return (
-
Hash Table
+
+ Hash Table +
{operation && (
@@ -631,7 +717,12 @@ function HashTableViz({ state }: { state: HashTableState }) { {hashingKey != null && (
hash("{hashingKey}") - {hashResult != null && = {hashResult}} + {hashResult != null && ( + + {' '} + = {hashResult} + + )}
)} @@ -645,9 +736,9 @@ function HashTableViz({ state }: { state: HashTableState }) {
{idx} @@ -674,14 +765,27 @@ function HashTableViz({ state }: { state: HashTableState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: entry.state !== 'normal' ? `0 0 12px ${colors.border}` : 'none', + boxShadow: + entry.state !== 'normal' ? `0 0 12px ${colors.border}` : 'none', }} > {entry.key}:{entry.value}
{ei < entries.length - 1 && ( - - + + )} @@ -736,15 +840,16 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { return { x, y } } - const label = treeType === 'heap' - ? `${heapType === 'min' ? 'Min' : 'Max'} Heap` - : 'Binary Search Tree' + const label = + treeType === 'heap' ? `${heapType === 'min' ? 'Min' : 'Max'} Heap` : 'Binary Search Tree' const nonNullNodes = nodes.reduce((acc, n) => acc + (n ? 1 : 0), 0) return (
-
{label}
+
+ {label} +
{operation && (
@@ -774,7 +879,7 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { y1={parentPos.y} x2={childPos.x} y2={childPos.y} - stroke="rgba(255,255,255,0.1)" + stroke="var(--viz-border)" strokeWidth="1.5" className="transition-all duration-300" /> @@ -824,7 +929,9 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { {/* Heap array view */} {treeType === 'heap' && nonNullNodes > 0 && (
-
array view
+
+ array view +
{nodes.map((node, idx) => { if (!node) return null @@ -857,11 +964,11 @@ function BinaryTreeViz({ state }: { state: BinaryTreeState }) { // ════════════════════════════════════════════════════════════════ const TP_COLORS: Record = { - default: { bg: 'rgba(255,255,255,0.04)', border: 'rgba(255,255,255,0.1)', text: '#888' }, + default: { bg: 'var(--subtle)', border: 'var(--viz-border)', text: 'var(--viz-label)' }, left: { bg: 'rgba(96,165,250,0.15)', border: 'rgba(96,165,250,0.4)', text: '#60a5fa' }, right: { bg: 'rgba(192,132,252,0.15)', border: 'rgba(192,132,252,0.4)', text: '#c084fc' }, found: { bg: 'rgba(74,222,128,0.18)', border: 'rgba(74,222,128,0.5)', text: '#4ade80' }, - checked: { bg: 'rgba(255,255,255,0.06)', border: 'rgba(255,255,255,0.12)', text: '#555' }, + checked: { bg: 'var(--viz-grid)', border: 'var(--viz-border)', text: 'var(--viz-muted)' }, } function TwoPointersViz({ state }: { state: TwoPointersState }) { @@ -869,7 +976,9 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { return (
-
Two Pointers
+
+ Two Pointers +
{operation && (
@@ -885,8 +994,12 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { const isRight = i === right return (
- {isLeft && L ↓} - {isRight && !isLeft && R ↓} + {isLeft && ( + L ↓ + )} + {isRight && !isLeft && ( + R ↓ + )}
) })} @@ -905,7 +1018,8 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { backgroundColor: colors.bg, borderColor: colors.border, color: colors.text, - boxShadow: hl !== 'default' && hl !== 'checked' ? `0 0 12px ${colors.border}` : 'none', + boxShadow: + hl !== 'default' && hl !== 'checked' ? `0 0 12px ${colors.border}` : 'none', }} > {val} @@ -917,7 +1031,12 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { {/* Index row */}
{array.map((_, i) => ( -
{i}
+
+ {i} +
))}
@@ -925,8 +1044,16 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { {/* Sum display */} {sum != null && target != null && (
- arr[{left}] + arr[{right}] = {array[left]} + {array[right]} = {sum} - {sum === target ? ' ✓' : sum < target ? ` < ${target} → move L →` : ` > ${target} → ← move R`} + arr[{left}] + arr[{right}] = {array[left]} +{' '} + {array[right]} ={' '} + + {sum} + + {sum === target + ? ' ✓' + : sum < target + ? ` < ${target} → move L →` + : ` > ${target} → ← move R`}
)}
@@ -938,7 +1065,7 @@ function TwoPointersViz({ state }: { state: TwoPointersState }) { // ════════════════════════════════════════════════════════════════ const SW_COLORS: Record = { - outside: { bg: 'rgba(255,255,255,0.03)', border: 'rgba(255,255,255,0.08)', text: '#555' }, + outside: { bg: 'var(--subtle)', border: 'var(--viz-border)', text: 'var(--viz-muted)' }, inWindow: { bg: 'rgba(96,165,250,0.12)', border: 'rgba(96,165,250,0.3)', text: '#60a5fa' }, current: { bg: 'rgba(74,222,128,0.15)', border: 'rgba(74,222,128,0.4)', text: '#4ade80' }, duplicate: { bg: 'rgba(248,113,113,0.15)', border: 'rgba(248,113,113,0.4)', text: '#f87171' }, @@ -952,7 +1079,9 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { return (
-
Sliding Window
+
+ Sliding Window +
{operation && (
@@ -988,14 +1117,21 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { {/* Index row */}
{chars.map((_, i) => ( -
{i}
+
+ {i} +
))}
{/* Window bracket */} {windowEnd >= windowStart && windowEnd >= 0 && (
- window [{windowStart}..{windowEnd}] + + window [{windowStart}..{windowEnd}] + "{windowStr}" len={windowStr.length}
@@ -1005,7 +1141,8 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { {/* Best so far */} {bestStr && (
- best = "{bestStr}" (length {bestStr.length}) + best = "{bestStr}" (length{' '} + {bestStr.length})
)}
@@ -1017,7 +1154,7 @@ function SlidingWindowViz({ state }: { state: SlidingWindowState }) { // ════════════════════════════════════════════════════════════════ const MEMO_COLORS: Record = { - empty: { bg: 'rgba(255,255,255,0.02)', border: 'rgba(255,255,255,0.06)', text: '#444' }, + empty: { bg: 'var(--subtle)', border: 'var(--viz-grid)', text: 'var(--viz-muted)' }, computing: { bg: 'rgba(251,146,60,0.15)', border: 'rgba(251,146,60,0.4)', text: '#fb923c' }, cached: { bg: 'rgba(96,165,250,0.1)', border: 'rgba(96,165,250,0.25)', text: '#60a5fa' }, hit: { bg: 'rgba(74,222,128,0.18)', border: 'rgba(74,222,128,0.5)', text: '#4ade80' }, @@ -1028,7 +1165,9 @@ function MemoTableViz({ state }: { state: MemoTableState }) { return (
-
Memoization
+
+ Memoization +
{operation && (
@@ -1036,9 +1175,7 @@ function MemoTableViz({ state }: { state: MemoTableState }) {
)} - {currentCall && ( -
{currentCall}
- )} + {currentCall &&
{currentCall}
} {/* Memo table grid */}
@@ -1059,8 +1196,17 @@ function MemoTableViz({ state }: { state: MemoTableState }) { > {entry.value != null ? entry.value : '—'}
-
- {entry.state === 'hit' ? '↑ HIT' : entry.state === 'computing' ? '...' : entry.state === 'cached' ? '✓' : ''} +
+ {entry.state === 'hit' + ? '↑ HIT' + : entry.state === 'computing' + ? '...' + : entry.state === 'cached' + ? '✓' + : ''}
) @@ -1085,12 +1231,16 @@ function MemoTableViz({ state }: { state: MemoTableState }) { function CoinChangeViz({ state }: { state: CoinChangeState }) { const { coins, target, selected, remaining, approach, greedyResult, dpResult, operation } = state - const approachLabel = approach === 'greedy' ? 'Greedy' : approach === 'dp' ? 'Dynamic Programming' : 'Comparison' - const approachColor = approach === 'greedy' ? '#fb923c' : approach === 'dp' ? '#60a5fa' : '#c084fc' + const approachLabel = + approach === 'greedy' ? 'Greedy' : approach === 'dp' ? 'Dynamic Programming' : 'Comparison' + const approachColor = + approach === 'greedy' ? '#fb923c' : approach === 'dp' ? '#60a5fa' : '#c084fc' return (
-
Greedy vs DP
+
+ Greedy vs DP +
{operation && (
@@ -1099,14 +1249,25 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { )} {/* Approach label */} -
+
{approachLabel}
{/* Target */}
target = {target} - {remaining > 0 && remaining < target && remaining: {remaining}} + {remaining > 0 && remaining < target && ( + + remaining: {remaining} + + )}
{/* Available coins */} @@ -1114,7 +1275,10 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { coins:
{coins.map((c, i) => ( -
+
{c}
))} @@ -1141,7 +1305,9 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) {
))}
- = {selected.reduce((a, b) => a + b, 0)} ({selected.length} coins) + + = {selected.reduce((a, b) => a + b, 0)} ({selected.length} coins) +
)} @@ -1149,19 +1315,33 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { {approach === 'compare' && greedyResult && dpResult && (
- Greedy + + Greedy +
{greedyResult.map((c, i) => ( -
{c}
+
+ {c} +
))}
{greedyResult.length} coins
- DP (optimal) + + DP (optimal) +
{dpResult.map((c, i) => ( -
{c}
+
+ {c} +
))}
{dpResult.length} coins ✓ @@ -1208,11 +1388,15 @@ function BucketsViz({ state }: { state: BucketsState }) {
- Current Min + + Current Min + {min ?? '—'}
- Current Max + + Current Max + {max ?? '—'}
@@ -1220,7 +1404,9 @@ function BucketsViz({ state }: { state: BucketsState }) { {/* Bucket Calculation Formula */} {buckets.length > 0 && (
-
Bucket Count Calculation
+
+ Bucket Count Calculation +
floor((max - min) / size) + 1
@@ -1228,7 +1414,8 @@ function BucketsViz({ state }: { state: BucketsState }) {
- {max} - {min} + {max} -{' '} + {min}
{bucketSize}
@@ -1269,10 +1456,7 @@ function BucketsViz({ state }: { state: BucketsState }) { const isProcessing = i === currentElementIndex const isCollected = phase === 'collecting' && i < currentElementIndex! return ( -
+
{rangeStart}–{rangeEnd} @@ -1332,8 +1516,8 @@ function BucketsViz({ state }: { state: BucketsState }) {
@@ -1341,11 +1525,20 @@ function BucketsViz({ state }: { state: BucketsState }) { const highlight = isActive ? innerHighlights?.[vIdx] : undefined const getHighlightStyles = () => { switch (highlight) { - case 'comparing': return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', text: '#fff' } - case 'active': return { bg: 'rgba(234,179,8,0.3)', border: '#eab308', text: '#fff' } - case 'current': return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', text: '#fff' } - case 'found': return { bg: 'rgba(74,222,128,0.2)', border: '#4ade80', text: '#4ade80' } - default: return { bg: 'rgba(38,38,38,1)', border: 'rgba(255,255,255,0.1)', text: '#60a5fa' } + case 'comparing': + return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', text: '#fff' } + case 'active': + return { bg: 'rgba(234,179,8,0.3)', border: '#eab308', text: '#fff' } + case 'current': + return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', text: '#fff' } + case 'found': + return { bg: 'rgba(74,222,128,0.2)', border: '#4ade80', text: '#4ade80' } + default: + return { + bg: 'var(--subtle-strong)', + border: 'var(--viz-border)', + text: '#60a5fa', + } } } const styles = getHighlightStyles() @@ -1359,7 +1552,10 @@ function BucketsViz({ state }: { state: BucketsState }) { borderColor: styles.border, borderWidth: '1px', color: styles.text, - animation: phase === 'distributing' && isActive && vIdx === bucket.length - 1 ? 'pop 0.3s ease-out' : 'none', + animation: + phase === 'distributing' && isActive && vIdx === bucket.length - 1 + ? 'pop 0.3s ease-out' + : 'none', transform: highlight ? 'scale(1.05)' : 'none', zIndex: highlight ? 10 : 1, }} @@ -1383,8 +1579,8 @@ function BucketsViz({ state }: { state: BucketsState }) {
diff --git a/src/components/GraphVisualizer.tsx b/src/components/GraphVisualizer.tsx index 11e789a..36ea134 100644 --- a/src/components/GraphVisualizer.tsx +++ b/src/components/GraphVisualizer.tsx @@ -5,17 +5,17 @@ import { translations } from '@i18n/translations' const NODE_RADIUS = 22 const colors = { - nodeDefault: '#1a1a1a', - nodeVisited: '#555', - nodeCurrent: '#fff', - edgeDefault: '#222', - edgeVisited: '#555', - edgeCurrent: '#fff', - strokeDefault: '#333', - strokeVisited: '#666', - strokeCurrent: '#fff', - weightText: '#888', - weightBg: '#000', + nodeDefault: 'var(--subtle-strong)', + nodeVisited: 'var(--viz-muted)', + nodeCurrent: 'var(--foreground)', + edgeDefault: 'var(--viz-border)', + edgeVisited: 'var(--viz-muted)', + edgeCurrent: 'var(--foreground)', + strokeDefault: 'var(--viz-faint)', + strokeVisited: 'var(--viz-muted)', + strokeCurrent: 'var(--foreground)', + weightText: 'var(--viz-label)', + weightBg: 'var(--surface)', } interface GraphVisualizerProps { @@ -195,7 +195,7 @@ export default function GraphVisualizer({ step, locale = 'en' }: GraphVisualizer y={node.y} textAnchor="middle" dominantBaseline="central" - fill={isCurrent ? '#000' : '#fff'} + fill={isCurrent ? 'var(--surface)' : 'var(--foreground)'} fontSize="13" fontWeight="600" fontFamily="Inter, system-ui, sans-serif" diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 148c5e2..453c554 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -56,6 +56,18 @@ export default function Header({ onToggleMobileSidebar, onToggleMobileCodePanel, }: HeaderProps) { + const toggleTheme = () => { + const root = document.documentElement + const nextTheme = root.dataset.theme === 'light' ? 'dark' : 'light' + root.dataset.theme = nextTheme + root.style.colorScheme = nextTheme + document.cookie = `theme=${nextTheme}; Path=/; Max-Age=31536000; SameSite=Lax` + document + .querySelector('meta[name="theme-color"]') + ?.setAttribute('content', nextTheme === 'light' ? '#ffffff' : '#000000') + window.dispatchEvent(new CustomEvent('themechange', { detail: nextTheme })) + } + return (
+ {isMobile && selectedAlgorithm && (