Spaces:
No application file
No application file
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>index</title> | |
| <script> | |
| // Iframe-friendly navigation router | |
| (function() { | |
| const isIframe = window.self !== window.top; | |
| if (!isIframe) return; // Only activate in iframe context | |
| // On load: if hash points to a different page, navigate there | |
| const hash = window.location.hash; | |
| if (hash && hash.startsWith('#/')) { | |
| const targetPath = hash.slice(2); // Remove '#/' | |
| const currentPath = window.location.pathname.split('/').pop(); | |
| // Only navigate if we're not already on the target page | |
| if (targetPath !== currentPath) { | |
| window.location.href = targetPath; | |
| return; // Stop execution, we're navigating away | |
| } | |
| } | |
| // Intercept all link clicks for hash-based navigation | |
| document.addEventListener('click', function(e) { | |
| const link = e.target.closest('a'); | |
| if (!link) return; | |
| const href = link.getAttribute('href'); | |
| // Skip external links, anchors, and javascript: links | |
| if (!href || href.startsWith('#') || href.startsWith('http') || href.startsWith('javascript:')) { | |
| return; | |
| } | |
| e.preventDefault(); | |
| // Convert relative/absolute path to hash-based navigation | |
| const url = new URL(href, window.location.href); | |
| let fullPath = url.pathname; | |
| // Remove leading slash if present for cleaner paths | |
| if (fullPath.startsWith('/')) { | |
| fullPath = fullPath.slice(1); | |
| } | |
| // Update parent URL hash | |
| window.location.hash = '#/' + fullPath; | |
| // For HTML files, navigate within iframe | |
| if (fullPath.endsWith('.html') || fullPath.endsWith('/')) { | |
| const pathParts = fullPath.split('/').filter(p => p); | |
| const targetFile = pathParts[pathParts.length - 1] || 'index.html'; | |
| window.location.href = targetFile; | |
| } else { | |
| // For non-HTML files (raw .py, etc), open directly | |
| window.open(href, '_blank'); | |
| } | |
| }); | |
| })(); | |
| // Apply theme and widget visibility immediately to prevent flicker | |
| (function() { | |
| const configTheme = 'dark'; | |
| const hasConfigUi = false; | |
| const configUi = hasConfigUi ? 'None' : null; | |
| const hasWidgetsConfig = false; | |
| const widgetsOn = hasWidgetsConfig ? false : true; | |
| let theme; | |
| if (configTheme === 'auto') { | |
| theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | |
| } else { | |
| theme = localStorage.getItem('uvnote-theme') || configTheme; | |
| } | |
| document.documentElement.setAttribute('data-theme', theme); | |
| // Initialize UI theme (css theme) | |
| let ui = hasConfigUi ? configUi : (localStorage.getItem('uvnote-ui') || 'default'); | |
| if (ui !== 'default' && ui !== 'none' && ui !== 'monocolor') { ui = 'default'; } | |
| document.documentElement.setAttribute('data-ui', ui); | |
| // Apply widgets visibility | |
| document.documentElement.setAttribute('data-widgets', widgetsOn ? 'on' : 'off'); | |
| })(); | |
| </script> | |
| <style> | |
| :root[data-theme="light"] { | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f6f8fa; | |
| --bg-tertiary: #f8f9fa; | |
| --bg-code: #f8f9fa; | |
| --bg-error: #fdf2f2; | |
| --bg-artifact: #e6f3ff; | |
| --bg-artifact-hover: #d0e7ff; | |
| --text-primary: #333; | |
| --text-secondary: #656d76; | |
| --text-error: #c53030; | |
| --text-link: #0969da; | |
| --border-primary: #e1e5e9; | |
| --border-error: #e53e3e; | |
| --border-cell-failed: #d73a49; | |
| --shadow: rgba(0, 0, 0, 0.1); | |
| } | |
| :root[data-theme="dark"] { | |
| --bg-primary: #0a0a0a; | |
| --bg-secondary: #121212; | |
| --bg-tertiary: #181818; | |
| --bg-code: #0d0d0d; | |
| --bg-error: #1a0f0f; | |
| --bg-artifact: #151515; | |
| --bg-artifact-hover: #1a1a1a; | |
| --text-primary: #e0e0e0; | |
| --text-secondary: #888888; | |
| --text-error: #ff6b6b; | |
| --text-link: #64b5f6; | |
| --border-primary: #2a2a2a; | |
| --border-error: #ff6b6b; | |
| --border-cell-failed: #ff6b6b; | |
| --shadow: rgba(255, 255, 255, 0.05); | |
| } | |
| /* Monocolor UI theme: black/white background, all text/borders single blue */ | |
| :root[data-ui="monocolor"] { --mono-color: #0a66ff; } | |
| :root[data-ui="monocolor"][data-theme="light"] { | |
| --bg-primary: #ffffff; | |
| } | |
| :root[data-ui="monocolor"][data-theme="dark"] { | |
| --bg-primary: #000000; | |
| } | |
| :root[data-ui="monocolor"] { | |
| --bg-secondary: var(--bg-primary); | |
| --bg-tertiary: var(--bg-primary); | |
| --bg-code: var(--bg-primary); | |
| --bg-error: var(--bg-primary); | |
| --bg-artifact: var(--bg-primary); | |
| --bg-artifact-hover: var(--bg-primary); | |
| --text-primary: var(--mono-color); | |
| --text-secondary: var(--mono-color); | |
| --text-error: var(--mono-color); | |
| --text-link: var(--mono-color); | |
| --border-primary: var(--mono-color); | |
| --border-error: var(--mono-color); | |
| --border-cell-failed: var(--mono-color); | |
| --shadow: none; | |
| } | |
| :root[data-ui="monocolor"] a { color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .menu-button, | |
| :root[data-ui="monocolor"] .theme-toggle, | |
| :root[data-ui="monocolor"] .reset-toggle, | |
| :root[data-ui="monocolor"] .back-button { background: var(--bg-primary); color: var(--mono-color); border-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .menu-button:hover, | |
| :root[data-ui="monocolor"] .theme-toggle:hover, | |
| :root[data-ui="monocolor"] .reset-toggle:hover, | |
| :root[data-ui="monocolor"] .back-button:hover { background: var(--bg-primary); color: var(--mono-color); border-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .menu-dropdown { background: var(--bg-primary); border-color: var(--mono-color); box-shadow: none; } | |
| :root[data-ui="monocolor"] .menu-item { color: var(--mono-color); border-bottom-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .system-info { background: var(--bg-primary); border-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .cell { border-color: var(--mono-color); background: var(--bg-primary); } | |
| :root[data-ui="monocolor"] .cell-header { background: var(--bg-primary); border-bottom-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .artifact { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .artifact:hover { background: var(--bg-primary); } | |
| :root[data-ui="monocolor"] .artifact-preview img, | |
| :root[data-ui="monocolor"] .artifact-preview svg { border-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .status-widget { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .minimap, | |
| :root[data-ui="monocolor"] .file-explorer, | |
| :root[data-ui="monocolor"] .tools-widget { | |
| background: var(--bg-primary); | |
| border-color: var(--mono-color); | |
| color: var(--mono-color); | |
| } | |
| :root[data-ui="monocolor"] .cell-code { | |
| background: var(--bg-primary); | |
| border-bottom-color: var(--mono-color); | |
| } | |
| :root[data-ui="monocolor"] .tools-title, | |
| :root[data-ui="monocolor"] .file-explorer-section-title, | |
| :root[data-ui="monocolor"] .minimap-title { color: var(--mono-color); border-bottom-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .tool-button { background: var(--bg-primary); border-color: var(--mono-color); color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .tool-button.active { border-color: var(--mono-color); } | |
| :root[data-ui="monocolor"] .file-explorer-item, | |
| :root[data-ui="monocolor"] .minimap-item { color: var(--mono-color); } | |
| /* Force Pygments code to mono blue on mono bg */ | |
| :root[data-ui="monocolor"] .highlight { background: var(--bg-primary) ; color: var(--mono-color) ; } | |
| :root[data-ui="monocolor"] .highlight *, | |
| :root[data-ui="monocolor"] .highlight .hll { color: var(--mono-color) ; background: transparent ; border-color: var(--mono-color) ; } | |
| /* Default code font + metrics (overridable via frontmatter) */ | |
| :root { --code-font-size: 0.95rem; --code-line-height: 1.5; --code-pad-y: 0.75rem; } | |
| /* Minimal UI theme overrides base variables for a flatter, 90s look */ | |
| :root[data-ui="none"] { | |
| --bg-primary: #ffffff; | |
| --bg-secondary: transparent; | |
| --bg-tertiary: transparent; | |
| --bg-code: #f9f9f9; | |
| --bg-error: #fff0f0; | |
| --bg-artifact: #f0f7ff; | |
| --bg-artifact-hover: #e5f1ff; | |
| --text-primary: #000000; | |
| --text-secondary: #222222; | |
| --text-error: #a00000; | |
| --text-link: #0000ee; | |
| --border-primary: #cccccc; | |
| --border-error: #cc0000; | |
| --border-cell-failed: #cc0000; | |
| --shadow: none; | |
| } | |
| html { | |
| overscroll-behavior: none; | |
| } | |
| body { | |
| font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; | |
| line-height: 1.4; | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| padding: 15px; | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| transition: background-color 0.2s ease, color 0.2s ease; | |
| overscroll-behavior: none; | |
| } | |
| /* Minimal "none" UI theme overrides */ | |
| :root[data-ui="none"] body { | |
| font-family: 'Times New Roman', Times, serif; | |
| line-height: 1.5; | |
| max-width: 860px; | |
| padding: 12px; | |
| background: #ffffff; | |
| color: #000000; | |
| transition: none; | |
| } | |
| /* Two panel layout removed */ | |
| .controls { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-end; | |
| gap: 0.25rem; | |
| z-index: 1000; | |
| } | |
| .controls-buttons { display: flex; gap: 0.5rem; } | |
| .menu-button { | |
| position: relative; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| padding: 8px 12px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| user-select: none; | |
| } | |
| /* Keep default control styling when widgets are enabled, even in minimal UI */ | |
| :root[data-ui="none"][data-widgets="on"] .menu-button, | |
| :root[data-ui="none"][data-widgets="on"] .theme-toggle, | |
| :root[data-ui="none"][data-widgets="on"] .reset-toggle, | |
| :root[data-ui="none"][data-widgets="on"] .back-button { | |
| background: #f6f6f6; | |
| border: 1px solid #cccccc; | |
| color: #222222; | |
| } | |
| .menu-button:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-tertiary); | |
| } | |
| /* Controls state indicator (top-right) */ | |
| /* Status widget (bottom-right) */ | |
| .status-widget { | |
| position: fixed; | |
| right: 20px; | |
| bottom: 20px; | |
| width: auto; | |
| max-width: 260px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| padding: 6px 8px; | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| z-index: 100; | |
| } | |
| .status-widget strong { color: var(--text-primary); } | |
| :root[data-ui="none"][data-widgets="on"] .status-widget { background: #f6f6f6; border-color: #ccc; color: #222; } | |
| :root[data-ui="none"][data-widgets="on"] .menu-button:hover, | |
| :root[data-ui="none"][data-widgets="on"] .theme-toggle:hover, | |
| :root[data-ui="none"][data-widgets="on"] .reset-toggle:hover, | |
| :root[data-ui="none"][data-widgets="on"] .back-button:hover { | |
| background: #ededed; | |
| border-color: #bbbbbb; | |
| color: #000000; | |
| } | |
| .menu-dropdown { | |
| position: absolute; | |
| top: 100%; | |
| right: 0; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 4px; | |
| box-shadow: 0 4px 12px var(--shadow); | |
| min-width: 160px; | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translateY(-8px); | |
| transition: all 0.2s ease; | |
| z-index: 1001; | |
| margin-top: 4px; | |
| } | |
| :root[data-ui="none"][data-widgets="on"] .menu-dropdown { background: #ffffff; border: 1px solid #cccccc; box-shadow: none; } | |
| .menu-button.active .menu-dropdown { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateY(0); | |
| } | |
| .menu-item { | |
| display: block; | |
| padding: 8px 12px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: 0.85rem; | |
| border-bottom: 1px solid var(--border-primary); | |
| cursor: pointer; | |
| } | |
| :root[data-ui="none"] .menu-item { color: #000; border-bottom: 1px solid #eee; } | |
| .menu-item:last-child { | |
| border-bottom: none; | |
| } | |
| .menu-item:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| .menu-checkbox { | |
| display: inline-block; | |
| width: 16px; | |
| font-family: monospace; | |
| color: var(--text-link); | |
| } | |
| .theme-toggle, | |
| .reset-toggle, | |
| .back-button { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| user-select: none; | |
| } | |
| .back-button { | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .theme-toggle:hover, | |
| .reset-toggle:hover, | |
| .back-button:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-tertiary); | |
| } | |
| .system-info { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 4px; | |
| padding: 8px 12px; | |
| margin-bottom: 16px; | |
| font-size: 0.85em; | |
| color: var(--text-secondary); | |
| } | |
| .system-info-header { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 2px; | |
| } | |
| .system-info-content { | |
| font-family: monospace; | |
| } | |
| .theme-toggle, .reset-toggle { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| /* padding: 0.4rem 0.6rem; */ | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| user-select: none; | |
| transition: all 0.2s ease; | |
| text-transform: lowercase; | |
| letter-spacing: 0; | |
| } | |
| .theme-toggle:hover, .reset-toggle:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--text-secondary); | |
| color: var(--text-primary); | |
| } | |
| .minimap { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| width: 220px; | |
| max-height: 400px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| padding: 0.5rem; | |
| font-size: 0.7rem; | |
| overflow-y: auto; | |
| z-index: 100; | |
| opacity: 0.9; | |
| transition: opacity 0.2s ease; | |
| } | |
| /* Hide widgets and controls when disabled via frontmatter */ | |
| :root[data-widgets="off"] .controls, | |
| :root[data-widgets="off"] .minimap, | |
| :root[data-widgets="off"] .file-explorer, | |
| :root[data-widgets="off"] .tools-widget, | |
| :root[data-widgets="off"] .status-widget { display: none ; } | |
| .file-explorer { | |
| position: fixed; | |
| bottom: 20px; /* default; JS will stack */ | |
| right: 20px; | |
| left: auto; | |
| top: auto; | |
| width: 220px; | |
| max-height: 400px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| padding: 0.5rem; | |
| font-size: 0.7rem; | |
| overflow-y: auto; | |
| z-index: 100; | |
| opacity: 0.9; | |
| transition: opacity 0.2s ease; | |
| } | |
| /* Drawing overlay */ | |
| .draw-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| z-index: 80; /* under widgets (100) and controls (1000) */ | |
| display: block; | |
| pointer-events: none; /* enabled only when a tool is active */ | |
| } | |
| /* Tools widget */ | |
| .tools-widget { | |
| position: fixed; | |
| bottom: 20px; /* default; JS will stack */ | |
| right: 20px; | |
| left: auto; | |
| top: auto; | |
| width: 220px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| padding: 0.5rem; | |
| font-size: 0.7rem; | |
| z-index: 100; | |
| opacity: 0.95; | |
| } | |
| .tools-title { | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| padding-bottom: 0.25rem; | |
| border-bottom: 1px solid var(--border-primary); | |
| cursor: grab; | |
| user-select: none; | |
| } | |
| .tools-row { display: flex; gap: 0.4rem; flex-wrap: wrap; } | |
| .tool-button { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| padding: 0.25rem 0.4rem; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| font-family: inherit; | |
| font-size: 0.75rem; | |
| user-select: none; | |
| } | |
| .tool-button:hover { color: var(--text-primary); } | |
| .tool-button.active { color: var(--text-primary); border-color: var(--text-secondary); background: var(--bg-secondary); } | |
| .minimap:hover, .file-explorer:hover { | |
| opacity: 1; | |
| } | |
| .minimap-title { | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| padding-bottom: 0.25rem; | |
| border-bottom: 1px solid var(--border-primary); | |
| cursor: grab; /* drag handle */ | |
| user-select: none; | |
| } | |
| .minimap-item { | |
| display: block; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.15rem 0; | |
| border-left: 2px solid transparent; | |
| padding-left: 0.5rem; | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| } | |
| .minimap-item:hover { | |
| color: var(--text-primary); | |
| border-left-color: var(--text-secondary); | |
| } | |
| .minimap-item.active { | |
| color: var(--text-primary); | |
| border-left-color: var(--text-link); | |
| } | |
| .minimap-heading { | |
| font-weight: normal; | |
| } | |
| .minimap-heading.h1 { padding-left: 0.5rem; } | |
| .minimap-heading.h2 { padding-left: 1rem; } | |
| .minimap-heading.h3 { padding-left: 1.5rem; } | |
| .minimap-heading.h4 { padding-left: 2rem; } | |
| .minimap-heading.h5 { padding-left: 2.5rem; } | |
| .minimap-heading.h6 { padding-left: 3rem; } | |
| .minimap-cell { | |
| color: var(--text-link); | |
| opacity: 0.8; | |
| font-style: italic; | |
| } | |
| .minimap-cell:hover { | |
| opacity: 1; | |
| } | |
| .file-explorer-title { | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| padding-bottom: 0.25rem; | |
| border-bottom: 1px solid var(--border-primary); | |
| cursor: grab; /* drag handle */ | |
| user-select: none; | |
| } | |
| .file-explorer-section { | |
| margin-bottom: 0.75rem; | |
| } | |
| .file-explorer-section-title { | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| font-size: 0.65rem; | |
| margin-bottom: 0.25rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .file-explorer-item { | |
| display: block; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.1rem 0; | |
| margin-left: 0.5rem; | |
| transition: color 0.2s ease; | |
| cursor: pointer; | |
| font-family: monospace; | |
| } | |
| .file-explorer-item:hover { | |
| color: var(--text-primary); | |
| } | |
| .file-explorer-item.script { | |
| color: var(--text-link); | |
| } | |
| .file-explorer-item.artifact { | |
| color: var(--text-secondary); | |
| opacity: 0.8; | |
| } | |
| /* Hide widgets on smaller screens */ | |
| @media (max-width: 768px) { | |
| .minimap, .file-explorer, .tools-widget { | |
| display: none; | |
| } | |
| } | |
| .cell { | |
| margin: 1rem 0; | |
| border: 1px solid var(--border-primary); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| background: var(--bg-secondary); | |
| } | |
| :root[data-ui="none"] .cell { margin: 1em 0; border: none; background: transparent; } | |
| .cell-header { | |
| background: var(--bg-secondary); | |
| padding: 0.5rem 1rem; | |
| border-bottom: 1px solid var(--border-primary); | |
| font-family: inherit; | |
| font-size: 0.85rem; | |
| } | |
| :root[data-ui="none"] .cell-header { background: transparent; border: none; padding: 0; font-weight: bold; } | |
| :root[data-ui="none"] .cell-content { padding: 0; } | |
| :root[data-ui="none"] .copy-button, | |
| :root[data-ui="none"] .collapse-indicators, | |
| :root[data-ui="none"] .cell-meta, | |
| :root[data-ui="none"] .cell-outputs-header { display: none ; } | |
| :root[data-ui="none"] pre, :root[data-ui="none"] code { font-family: Menlo, Monaco, 'Courier New', monospace; } | |
| :root[data-ui="none"] .code-content pre { background: #f9f9f9; border: 1px solid #ddd; padding: 8px; } | |
| :root[data-ui="none"] .output { background: transparent; border: none; padding: 0.25em 0; } | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| user-select: none; | |
| transition: background-color 0.2s ease; | |
| } | |
| .cell-header:hover { | |
| background: var(--bg-tertiary); | |
| } | |
| .collapse-indicators { | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| } | |
| .collapse-indicators span:hover { | |
| color: var(--text-primary); | |
| opacity: 1; | |
| } | |
| .cell-code { | |
| display: block; | |
| background: var(--bg-code); | |
| } | |
| .cell-code.collapsed { | |
| display: none; | |
| } | |
| .cell-code pre { | |
| margin: 0; | |
| padding: 0.75rem; | |
| background: var(--bg-code); | |
| overflow-x: auto; | |
| color: var(--text-primary); | |
| } | |
| .cell-output { | |
| padding: 0.75rem; | |
| /* background: var(--bg-primary); */ | |
| background: var(--bg-secondary); | |
| } | |
| .cell-output.collapsed { | |
| display: none; | |
| } | |
| .cell-stdout { | |
| background: var(--bg-tertiary); | |
| padding: 0.75rem; | |
| border-radius: 1px; | |
| /* margin: 0.25rem 0; */ | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| white-space: pre-wrap; | |
| color: var(--text-primary); | |
| } | |
| .cell-stdout { | |
| background: var(--bg-tertiary); | |
| padding: 0.75rem; | |
| border-radius: 1px; | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| color: var(--text-primary); | |
| /* key bits */ | |
| overflow: auto; /* show scrollbars when needed */ | |
| max-width: 100%; /* respects whatever layout width you give it */ | |
| } | |
| .cell-stdout .stdout-text { | |
| margin: 0; /* reset pre default margin */ | |
| white-space: pre; /* keep line breaks, NO wrapping */ | |
| display: inline-block; /* shrink-to-content */ | |
| min-width: max-content; /* allow very long lines to define intrinsic width */ | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| tab-size: 2; | |
| } | |
| .cell-stderr { | |
| background: var(--bg-error); | |
| border-left: 2px solid var(--border-error); | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| color: var(--text-error); | |
| white-space: pre-wrap; | |
| } | |
| .uv-install-logs { | |
| margin: 0.5rem 0; | |
| } | |
| .uv-logs-header { | |
| cursor: pointer; | |
| padding: 0.75rem; | |
| border-left: 3px solid var(--border-color); | |
| font-family: inherit; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| user-select: none; | |
| } | |
| .uv-logs-content { | |
| background: var(--bg-secondary); | |
| padding: 1rem; | |
| border-left: 3px solid var(--border-color); | |
| white-space: pre-wrap; | |
| font-family: monospace; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| overflow-x: auto; | |
| } | |
| .cell-artifacts { | |
| margin: 1rem 0; | |
| } | |
| .cell-artifacts h4 { | |
| margin: 0 0 0.5rem 0; | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| } | |
| .artifact { | |
| display: inline-block; | |
| background: var(--bg-artifact); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 1px; | |
| margin: 0.25rem 0.5rem 0.25rem 0; | |
| font-family: inherit; | |
| font-size: 0.8rem; | |
| color: var(--text-link); | |
| text-decoration: none; | |
| transition: background-color 0.2s ease; | |
| border: 1px solid var(--border-primary); | |
| } | |
| .artifact:hover { | |
| background: var(--bg-artifact-hover); | |
| } | |
| .artifact-preview { | |
| margin-top: 1rem; | |
| } | |
| .artifact-preview img { | |
| max-width: 100%; | |
| height: auto; | |
| border: 1px solid var(--border-primary); | |
| border-radius: 1px; | |
| } | |
| .artifact-preview svg { | |
| max-width: 100%; | |
| height: auto; | |
| border: 1px solid var(--border-primary); | |
| border-radius: 1px; | |
| display: block; | |
| } | |
| /* Style SVG text elements */ | |
| .artifact-preview svg g { | |
| fill: var(--text-primary) ; | |
| } | |
| /* Auto-theme SVG elements */ | |
| .artifact-preview svg { | |
| background: transparent; | |
| } | |
| /* CSV table styling */ | |
| .artifact-csv { | |
| margin-top: 1rem; | |
| overflow-x: auto; | |
| } | |
| .csv-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.9rem; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border-primary); | |
| border-radius: 1px; | |
| } | |
| .csv-table th, | |
| .csv-table td { | |
| padding: 0.5rem 0.75rem; | |
| text-align: left; | |
| border: 1px solid var(--border-primary); | |
| } | |
| .csv-table th { | |
| background: var(--bg-tertiary); | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .csv-table tbody tr:hover { | |
| background: var(--bg-artifact-hover); | |
| } | |
| .artifact-csv-error { | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| background: var(--bg-error); | |
| color: var(--text-error); | |
| border: 1px solid var(--border-error); | |
| border-radius: 1px; | |
| } | |
| .cell-failed { | |
| border-color: var(--border-cell-failed); | |
| } | |
| .cell-failed .cell-header { | |
| background: var(--bg-error); | |
| color: var(--text-error); | |
| } | |
| .cell-commented { | |
| opacity: 0.6; | |
| border-style: dashed; | |
| } | |
| .cell-commented .cell-header { | |
| background: var(--bg-secondary); | |
| color: var(--text-secondary); | |
| font-style: italic; | |
| } | |
| .run-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.75em; | |
| font-family: inherit; | |
| margin-left: 4px; | |
| } | |
| .run-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| } | |
| .run-btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .copy-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.75em; | |
| font-family: inherit; | |
| margin-left: 4px; | |
| } | |
| .copy-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| } | |
| .copy-btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .copy-btn.copied { | |
| color: #4caf50; | |
| background: var(--bg-primary); | |
| border-color: #4caf50; | |
| transition: all 0.2s ease; | |
| } | |
| .raw-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.75em; | |
| font-family: inherit; | |
| margin-left: 4px; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .raw-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| text-decoration: none; | |
| } | |
| .github-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.75em; | |
| font-family: inherit; | |
| margin-left: 4px; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .github-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| text-decoration: none; | |
| } | |
| .hf-btn { | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.75em; | |
| font-family: inherit; | |
| margin-left: 4px; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .hf-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--bg-primary); | |
| text-decoration: none; | |
| } | |
| .output-stale { | |
| opacity: 0.5; | |
| position: relative; | |
| } | |
| .output-stale::after { | |
| content: '⏳ updating...'; | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| background: var(--bg-secondary); | |
| padding: 4px 8px; | |
| border-radius: 2px; | |
| font-size: 0.75em; | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border-primary); | |
| } | |
| h1, h2, h3, h4, h5, h6 { | |
| margin-top: 1.5rem; | |
| margin-bottom: 0.75rem; | |
| color: var(--text-primary); | |
| } | |
| h1 { | |
| margin-top: 0; | |
| margin-bottom: 1rem; | |
| } | |
| p { | |
| margin: 0.75rem 0; | |
| color: var(--text-primary); | |
| } | |
| a { | |
| color: var(--text-link); | |
| } | |
| img { | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 1px; | |
| box-shadow: none; | |
| } | |
| pre, code { | |
| font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; | |
| font-size: var(--code-font-size); | |
| } | |
| .code-wrap { position: relative; } | |
| .code-line-highlight { display: none; position: absolute; left: 0; right: 0; height: 1.5em; background: rgba(255, 235, 170, 0.35); pointer-events: none; border-left: 3px solid #f4c542; } | |
| .line-number { cursor: pointer; text-decoration: none; color: var(--text-secondary); padding: 0 0.25rem; } | |
| .line-number.selected { background: rgba(255, 235, 170, 0.4); color: var(--text-primary); } | |
| /* Line numbers */ | |
| .highlight-with-lines { | |
| display: flex; | |
| } | |
| .line-numbers { | |
| background: var(--bg-tertiary); | |
| padding: var(--code-pad-y) 0.5rem; | |
| font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace; | |
| font-size: var(--code-font-size); | |
| line-height: var(--code-line-height); | |
| color: var(--text-secondary); | |
| user-select: none; | |
| text-align: right; | |
| border-right: 1px solid var(--border-primary); | |
| } | |
| .line-numbers .line-number { | |
| display: block; | |
| line-height: var(--code-line-height); | |
| } | |
| .highlight-with-lines .highlight { | |
| flex: 1; | |
| } | |
| .highlight .hll { background-color: transparent; } /* don't conflict with our highlight */ | |
| .highlight pre { | |
| white-space: pre; | |
| margin: 0; | |
| padding: var(--code-pad-y) 0.75rem; | |
| line-height: var(--code-line-height); | |
| } | |
| /* Collapsed code styling */ | |
| .cell-code.collapsed { | |
| display: none; | |
| } | |
| .cell-code.expanded { | |
| display: block; | |
| } | |
| .cell-code { | |
| display: block; | |
| border-bottom: 1px solid var(--border-primary); | |
| } | |
| pre { line-height: 125%; } | |
| td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } | |
| span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } | |
| td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } | |
| span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } | |
| [data-theme="light"] .highlight .hll { background-color: #ffffcc } | |
| [data-theme="light"] .highlight { background: #f8f8f8; } | |
| [data-theme="light"] .highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ | |
| [data-theme="light"] .highlight .err { border: 1px solid #F00 } /* Error */ | |
| [data-theme="light"] .highlight .k { color: #008000; font-weight: bold } /* Keyword */ | |
| [data-theme="light"] .highlight .o { color: #666 } /* Operator */ | |
| [data-theme="light"] .highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ | |
| [data-theme="light"] .highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ | |
| [data-theme="light"] .highlight .cp { color: #9C6500 } /* Comment.Preproc */ | |
| [data-theme="light"] .highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ | |
| [data-theme="light"] .highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ | |
| [data-theme="light"] .highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ | |
| [data-theme="light"] .highlight .gd { color: #A00000 } /* Generic.Deleted */ | |
| [data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ | |
| [data-theme="light"] .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ | |
| [data-theme="light"] .highlight .gr { color: #E40000 } /* Generic.Error */ | |
| [data-theme="light"] .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ | |
| [data-theme="light"] .highlight .gi { color: #008400 } /* Generic.Inserted */ | |
| [data-theme="light"] .highlight .go { color: #717171 } /* Generic.Output */ | |
| [data-theme="light"] .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ | |
| [data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ | |
| [data-theme="light"] .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ | |
| [data-theme="light"] .highlight .gt { color: #04D } /* Generic.Traceback */ | |
| [data-theme="light"] .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ | |
| [data-theme="light"] .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ | |
| [data-theme="light"] .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ | |
| [data-theme="light"] .highlight .kp { color: #008000 } /* Keyword.Pseudo */ | |
| [data-theme="light"] .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ | |
| [data-theme="light"] .highlight .kt { color: #B00040 } /* Keyword.Type */ | |
| [data-theme="light"] .highlight .m { color: #666 } /* Literal.Number */ | |
| [data-theme="light"] .highlight .s { color: #BA2121 } /* Literal.String */ | |
| [data-theme="light"] .highlight .na { color: #687822 } /* Name.Attribute */ | |
| [data-theme="light"] .highlight .nb { color: #008000 } /* Name.Builtin */ | |
| [data-theme="light"] .highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ | |
| [data-theme="light"] .highlight .no { color: #800 } /* Name.Constant */ | |
| [data-theme="light"] .highlight .nd { color: #A2F } /* Name.Decorator */ | |
| [data-theme="light"] .highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ | |
| [data-theme="light"] .highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ | |
| [data-theme="light"] .highlight .nf { color: #00F } /* Name.Function */ | |
| [data-theme="light"] .highlight .nl { color: #767600 } /* Name.Label */ | |
| [data-theme="light"] .highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ | |
| [data-theme="light"] .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ | |
| [data-theme="light"] .highlight .nv { color: #19177C } /* Name.Variable */ | |
| [data-theme="light"] .highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ | |
| [data-theme="light"] .highlight .w { color: #BBB } /* Text.Whitespace */ | |
| [data-theme="light"] .highlight .mb { color: #666 } /* Literal.Number.Bin */ | |
| [data-theme="light"] .highlight .mf { color: #666 } /* Literal.Number.Float */ | |
| [data-theme="light"] .highlight .mh { color: #666 } /* Literal.Number.Hex */ | |
| [data-theme="light"] .highlight .mi { color: #666 } /* Literal.Number.Integer */ | |
| [data-theme="light"] .highlight .mo { color: #666 } /* Literal.Number.Oct */ | |
| [data-theme="light"] .highlight .sa { color: #BA2121 } /* Literal.String.Affix */ | |
| [data-theme="light"] .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ | |
| [data-theme="light"] .highlight .sc { color: #BA2121 } /* Literal.String.Char */ | |
| [data-theme="light"] .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ | |
| [data-theme="light"] .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ | |
| [data-theme="light"] .highlight .s2 { color: #BA2121 } /* Literal.String.Double */ | |
| [data-theme="light"] .highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ | |
| [data-theme="light"] .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ | |
| [data-theme="light"] .highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ | |
| [data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ | |
| [data-theme="light"] .highlight .sr { color: #A45A77 } /* Literal.String.Regex */ | |
| [data-theme="light"] .highlight .s1 { color: #BA2121 } /* Literal.String.Single */ | |
| [data-theme="light"] .highlight .ss { color: #19177C } /* Literal.String.Symbol */ | |
| [data-theme="light"] .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ | |
| [data-theme="light"] .highlight .fm { color: #00F } /* Name.Function.Magic */ | |
| [data-theme="light"] .highlight .vc { color: #19177C } /* Name.Variable.Class */ | |
| [data-theme="light"] .highlight .vg { color: #19177C } /* Name.Variable.Global */ | |
| [data-theme="light"] .highlight .vi { color: #19177C } /* Name.Variable.Instance */ | |
| [data-theme="light"] .highlight .vm { color: #19177C } /* Name.Variable.Magic */ | |
| [data-theme="light"] .highlight .il { color: #666 } /* Literal.Number.Integer.Long */ | |
| pre { line-height: 125%; } | |
| td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } | |
| span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } | |
| td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } | |
| span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } | |
| [data-theme="dark"] .highlight .hll { background-color: #49483e } | |
| [data-theme="dark"] .highlight { background: #272822; color: #F8F8F2 } | |
| [data-theme="dark"] .highlight .c { color: #959077 } /* Comment */ | |
| [data-theme="dark"] .highlight .err { color: #ED007E; background-color: #1E0010 } /* Error */ | |
| [data-theme="dark"] .highlight .esc { color: #F8F8F2 } /* Escape */ | |
| [data-theme="dark"] .highlight .g { color: #F8F8F2 } /* Generic */ | |
| [data-theme="dark"] .highlight .k { color: #66D9EF } /* Keyword */ | |
| [data-theme="dark"] .highlight .l { color: #AE81FF } /* Literal */ | |
| [data-theme="dark"] .highlight .n { color: #F8F8F2 } /* Name */ | |
| [data-theme="dark"] .highlight .o { color: #FF4689 } /* Operator */ | |
| [data-theme="dark"] .highlight .x { color: #F8F8F2 } /* Other */ | |
| [data-theme="dark"] .highlight .p { color: #F8F8F2 } /* Punctuation */ | |
| [data-theme="dark"] .highlight .ch { color: #959077 } /* Comment.Hashbang */ | |
| [data-theme="dark"] .highlight .cm { color: #959077 } /* Comment.Multiline */ | |
| [data-theme="dark"] .highlight .cp { color: #959077 } /* Comment.Preproc */ | |
| [data-theme="dark"] .highlight .cpf { color: #959077 } /* Comment.PreprocFile */ | |
| [data-theme="dark"] .highlight .c1 { color: #959077 } /* Comment.Single */ | |
| [data-theme="dark"] .highlight .cs { color: #959077 } /* Comment.Special */ | |
| [data-theme="dark"] .highlight .gd { color: #FF4689 } /* Generic.Deleted */ | |
| [data-theme="dark"] .highlight .ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */ | |
| [data-theme="dark"] .highlight .ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ | |
| [data-theme="dark"] .highlight .gr { color: #F8F8F2 } /* Generic.Error */ | |
| [data-theme="dark"] .highlight .gh { color: #F8F8F2 } /* Generic.Heading */ | |
| [data-theme="dark"] .highlight .gi { color: #A6E22E } /* Generic.Inserted */ | |
| [data-theme="dark"] .highlight .go { color: #66D9EF } /* Generic.Output */ | |
| [data-theme="dark"] .highlight .gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */ | |
| [data-theme="dark"] .highlight .gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */ | |
| [data-theme="dark"] .highlight .gu { color: #959077 } /* Generic.Subheading */ | |
| [data-theme="dark"] .highlight .gt { color: #F8F8F2 } /* Generic.Traceback */ | |
| [data-theme="dark"] .highlight .kc { color: #66D9EF } /* Keyword.Constant */ | |
| [data-theme="dark"] .highlight .kd { color: #66D9EF } /* Keyword.Declaration */ | |
| [data-theme="dark"] .highlight .kn { color: #FF4689 } /* Keyword.Namespace */ | |
| [data-theme="dark"] .highlight .kp { color: #66D9EF } /* Keyword.Pseudo */ | |
| [data-theme="dark"] .highlight .kr { color: #66D9EF } /* Keyword.Reserved */ | |
| [data-theme="dark"] .highlight .kt { color: #66D9EF } /* Keyword.Type */ | |
| [data-theme="dark"] .highlight .ld { color: #E6DB74 } /* Literal.Date */ | |
| [data-theme="dark"] .highlight .m { color: #AE81FF } /* Literal.Number */ | |
| [data-theme="dark"] .highlight .s { color: #E6DB74 } /* Literal.String */ | |
| [data-theme="dark"] .highlight .na { color: #A6E22E } /* Name.Attribute */ | |
| [data-theme="dark"] .highlight .nb { color: #F8F8F2 } /* Name.Builtin */ | |
| [data-theme="dark"] .highlight .nc { color: #A6E22E } /* Name.Class */ | |
| [data-theme="dark"] .highlight .no { color: #66D9EF } /* Name.Constant */ | |
| [data-theme="dark"] .highlight .nd { color: #A6E22E } /* Name.Decorator */ | |
| [data-theme="dark"] .highlight .ni { color: #F8F8F2 } /* Name.Entity */ | |
| [data-theme="dark"] .highlight .ne { color: #A6E22E } /* Name.Exception */ | |
| [data-theme="dark"] .highlight .nf { color: #A6E22E } /* Name.Function */ | |
| [data-theme="dark"] .highlight .nl { color: #F8F8F2 } /* Name.Label */ | |
| [data-theme="dark"] .highlight .nn { color: #F8F8F2 } /* Name.Namespace */ | |
| [data-theme="dark"] .highlight .nx { color: #A6E22E } /* Name.Other */ | |
| [data-theme="dark"] .highlight .py { color: #F8F8F2 } /* Name.Property */ | |
| [data-theme="dark"] .highlight .nt { color: #FF4689 } /* Name.Tag */ | |
| [data-theme="dark"] .highlight .nv { color: #F8F8F2 } /* Name.Variable */ | |
| [data-theme="dark"] .highlight .ow { color: #FF4689 } /* Operator.Word */ | |
| [data-theme="dark"] .highlight .pm { color: #F8F8F2 } /* Punctuation.Marker */ | |
| [data-theme="dark"] .highlight .w { color: #F8F8F2 } /* Text.Whitespace */ | |
| [data-theme="dark"] .highlight .mb { color: #AE81FF } /* Literal.Number.Bin */ | |
| [data-theme="dark"] .highlight .mf { color: #AE81FF } /* Literal.Number.Float */ | |
| [data-theme="dark"] .highlight .mh { color: #AE81FF } /* Literal.Number.Hex */ | |
| [data-theme="dark"] .highlight .mi { color: #AE81FF } /* Literal.Number.Integer */ | |
| [data-theme="dark"] .highlight .mo { color: #AE81FF } /* Literal.Number.Oct */ | |
| [data-theme="dark"] .highlight .sa { color: #E6DB74 } /* Literal.String.Affix */ | |
| [data-theme="dark"] .highlight .sb { color: #E6DB74 } /* Literal.String.Backtick */ | |
| [data-theme="dark"] .highlight .sc { color: #E6DB74 } /* Literal.String.Char */ | |
| [data-theme="dark"] .highlight .dl { color: #E6DB74 } /* Literal.String.Delimiter */ | |
| [data-theme="dark"] .highlight .sd { color: #E6DB74 } /* Literal.String.Doc */ | |
| [data-theme="dark"] .highlight .s2 { color: #E6DB74 } /* Literal.String.Double */ | |
| [data-theme="dark"] .highlight .se { color: #AE81FF } /* Literal.String.Escape */ | |
| [data-theme="dark"] .highlight .sh { color: #E6DB74 } /* Literal.String.Heredoc */ | |
| [data-theme="dark"] .highlight .si { color: #E6DB74 } /* Literal.String.Interpol */ | |
| [data-theme="dark"] .highlight .sx { color: #E6DB74 } /* Literal.String.Other */ | |
| [data-theme="dark"] .highlight .sr { color: #E6DB74 } /* Literal.String.Regex */ | |
| [data-theme="dark"] .highlight .s1 { color: #E6DB74 } /* Literal.String.Single */ | |
| [data-theme="dark"] .highlight .ss { color: #E6DB74 } /* Literal.String.Symbol */ | |
| [data-theme="dark"] .highlight .bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */ | |
| [data-theme="dark"] .highlight .fm { color: #A6E22E } /* Name.Function.Magic */ | |
| [data-theme="dark"] .highlight .vc { color: #F8F8F2 } /* Name.Variable.Class */ | |
| [data-theme="dark"] .highlight .vg { color: #F8F8F2 } /* Name.Variable.Global */ | |
| [data-theme="dark"] .highlight .vi { color: #F8F8F2 } /* Name.Variable.Instance */ | |
| [data-theme="dark"] .highlight .vm { color: #F8F8F2 } /* Name.Variable.Magic */ | |
| [data-theme="dark"] .highlight .il { color: #AE81FF } /* Literal.Number.Integer.Long */ | |
| /* Ensure our code metrics override Pygments defaults */ | |
| .highlight pre { | |
| white-space: pre; | |
| margin: 0; | |
| padding: var(--code-pad-y) 0.75rem ; | |
| line-height: var(--code-line-height) ; | |
| font-size: var(--code-font-size) ; | |
| font-family: 'Cascadia Mono', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace ; | |
| border: none; | |
| } | |
| .line-numbers { line-height: var(--code-line-height) ; } | |
| .line-numbers .line-number { line-height: var(--code-line-height) ; } | |
| /* Custom CSS from frontmatter */ | |
| /* Cursor for tools */ | |
| body[data-tool="arrow"] .main-content { | |
| cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>') 12 12, crosshair; | |
| } | |
| body[data-tool="pen"] .main-content { | |
| cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><circle cx="4" cy="20" r="2" fill="%23e53935"/></svg>') 4 20, pointer; | |
| } | |
| body[data-tool="eraser"] .main-content { | |
| cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23e53935" stroke-width="2"><path d="M20 20H7l-7-7 7-7h13v14z"/><path d="M13 13l7-7"/><path d="M13 13L9 9"/></svg>') 12 12, auto; | |
| } | |
| /* Color picker styles */ | |
| .tools-section-title { | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| font-size: 0.65rem; | |
| margin: 0.75rem 0 0.5rem 0; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .color-row { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| gap: 0.25rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .color-swatch { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| position: relative; | |
| } | |
| .color-swatch:hover { | |
| transform: scale(1.1); | |
| border-color: var(--text-secondary); | |
| } | |
| .color-swatch.selected { | |
| border-color: var(--text-primary); | |
| box-shadow: 0 0 0 2px var(--text-link); | |
| } | |
| .color-swatch.selected::after { | |
| content: '✓'; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| font-size: 10px; | |
| font-weight: bold; | |
| text-shadow: 1px 1px 1px black; | |
| } | |
| .color-input { | |
| width: 24px; | |
| height: 24px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 3px; | |
| cursor: pointer; | |
| background: none; | |
| padding: 0; | |
| grid-column: span 2; | |
| justify-self: center; | |
| } | |
| .color-input:hover { | |
| border-color: var(--text-secondary); | |
| } | |
| /* Thickness slider styles */ | |
| .thickness-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| margin-top: 0.75rem; | |
| } | |
| .thickness-slider { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 4px; | |
| background: var(--border-primary); | |
| border-radius: 2px; | |
| outline: none; | |
| opacity: 0.7; | |
| transition: opacity 0.2s; | |
| } | |
| .thickness-slider:hover { | |
| opacity: 1; | |
| } | |
| .thickness-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 12px; | |
| height: 12px; | |
| background: var(--text-link); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .thickness-slider::-moz-range-thumb { | |
| width: 12px; | |
| height: 12px; | |
| background: var(--text-link); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| border: none; | |
| } | |
| .thickness-value { | |
| font-size: 0.7rem; | |
| color: var(--text-secondary); | |
| min-width: 20px; | |
| text-align: right; | |
| } | |
| .highlight { | |
| background: none ; | |
| } | |
| /* Loading animations */ | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid var(--border-primary); | |
| border-radius: 50%; | |
| border-top-color: var(--text-link); | |
| animation: spin 1s linear infinite; | |
| margin-right: 8px; | |
| vertical-align: middle; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-skeleton { | |
| display: inline-block; | |
| background: var(--bg-tertiary); | |
| background: linear-gradient( | |
| 90deg, | |
| var(--bg-tertiary) 25%, | |
| var(--bg-secondary) 50%, | |
| var(--bg-tertiary) 75% | |
| ); | |
| background-size: 200% 100%; | |
| animation: loading-shimmer 2s ease-in-out infinite; | |
| border-radius: 2px; | |
| height: 1em; | |
| width: 80px; | |
| vertical-align: middle; | |
| } | |
| @keyframes loading-shimmer { | |
| 0% { background-position: -200% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| /* Loading state for cell output */ | |
| .cell-output:has(.loading-spinner) { | |
| opacity: 0.7; | |
| background: var(--bg-secondary); | |
| /* border-left: 3px solid var(--text-link); */ | |
| } | |
| </style> | |
| <script> | |
| // --- Drag utilities --- | |
| function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); } | |
| function restorePosition(el, storageKey) { | |
| try { | |
| const raw = localStorage.getItem(storageKey); | |
| if (!raw) return; | |
| const pos = JSON.parse(raw); | |
| if (typeof pos.left === 'number' && typeof pos.top === 'number') { | |
| el.style.left = pos.left + 'px'; | |
| el.style.top = pos.top + 'px'; | |
| el.style.right = 'auto'; | |
| el.style.bottom = 'auto'; | |
| } | |
| } catch (_) {} | |
| } | |
| function savePosition(el, storageKey) { | |
| try { | |
| const left = parseFloat(el.style.left || 'NaN'); | |
| const top = parseFloat(el.style.top || 'NaN'); | |
| if (!Number.isNaN(left) && !Number.isNaN(top)) { | |
| localStorage.setItem(storageKey, JSON.stringify({ left, top })); | |
| } | |
| } catch (_) {} | |
| } | |
| function makeDraggable(el, storageKey, handleEl) { | |
| let dragging = false; | |
| let startX = 0, startY = 0; // cursor | |
| let origLeft = 0, origTop = 0; // element | |
| const onMove = (e) => { | |
| if (!dragging) return; | |
| const clientX = e.touches ? e.touches[0].clientX : e.clientX; | |
| const clientY = e.touches ? e.touches[0].clientY : e.clientY; | |
| const dx = clientX - startX; | |
| const dy = clientY - startY; | |
| const w = el.offsetWidth; | |
| const h = el.offsetHeight; | |
| const maxX = window.innerWidth - w; | |
| const maxY = window.innerHeight - h; | |
| const newLeft = clamp(origLeft + dx, 0, maxX); | |
| const newTop = clamp(origTop + dy, 0, maxY); | |
| el.style.left = newLeft + 'px'; | |
| el.style.top = newTop + 'px'; | |
| el.style.right = 'auto'; | |
| el.style.bottom = 'auto'; | |
| }; | |
| const endDrag = () => { | |
| if (!dragging) return; | |
| dragging = false; | |
| document.removeEventListener('mousemove', onMove); | |
| document.removeEventListener('mouseup', endDrag); | |
| document.removeEventListener('touchmove', onMove); | |
| document.removeEventListener('touchend', endDrag); | |
| handleEl && (handleEl.style.cursor = 'grab'); | |
| savePosition(el, storageKey); | |
| // ensure no-overlap constraint after a drag | |
| try { layoutWidgetsStackedBottomRight(); } catch (_) {} | |
| }; | |
| const startDrag = (e) => { | |
| // Start from element's current on-screen rect | |
| const elRect = el.getBoundingClientRect(); | |
| el.style.left = elRect.left + 'px'; | |
| el.style.top = elRect.top + 'px'; | |
| el.style.right = 'auto'; | |
| el.style.bottom = 'auto'; | |
| dragging = true; | |
| startX = e.touches ? e.touches[0].clientX : e.clientX; | |
| startY = e.touches ? e.touches[0].clientY : e.clientY; | |
| origLeft = elRect.left; | |
| origTop = elRect.top; | |
| document.addEventListener('mousemove', onMove); | |
| document.addEventListener('mouseup', endDrag); | |
| document.addEventListener('touchmove', onMove, { passive: false }); | |
| document.addEventListener('touchend', endDrag); | |
| handleEl && (handleEl.style.cursor = 'grabbing'); | |
| e.preventDefault(); | |
| }; | |
| (handleEl || el).addEventListener('mousedown', startDrag); | |
| (handleEl || el).addEventListener('touchstart', startDrag, { passive: false }); | |
| // Apply any saved position on init | |
| restorePosition(el, storageKey); | |
| } | |
| function toggleCell(cellId) { | |
| const codeElement = document.getElementById('code-' + cellId); | |
| const outputElement = document.getElementById('output-' + cellId); | |
| if (codeElement) { | |
| codeElement.classList.toggle('collapsed'); | |
| } | |
| if (outputElement) { | |
| outputElement.classList.toggle('collapsed'); | |
| } | |
| updateIndicators(cellId); | |
| encodeToolStateToUrl(); | |
| } | |
| function toggleCode(cellId) { | |
| const codeElement = document.getElementById('code-' + cellId); | |
| if (codeElement) { | |
| codeElement.classList.toggle('collapsed'); | |
| updateIndicators(cellId); | |
| encodeToolStateToUrl(); | |
| } | |
| } | |
| function toggleOutput(cellId) { | |
| const outputElement = document.getElementById('output-' + cellId); | |
| if (outputElement) { | |
| outputElement.classList.toggle('collapsed'); | |
| updateIndicators(cellId); | |
| encodeToolStateToUrl(); | |
| } | |
| } | |
| function toggleUvLogs(headerElement) { | |
| const contentElement = headerElement.nextElementSibling; | |
| if (contentElement) { | |
| const isCollapsed = contentElement.style.display === 'none'; | |
| contentElement.style.display = isCollapsed ? 'block' : 'none'; | |
| headerElement.textContent = isCollapsed ? '▼ UV Install Logs' : '▶ UV Install Logs'; | |
| // Update the header indicator if it exists | |
| const uvLogsDiv = headerElement.parentElement; | |
| if (uvLogsDiv && uvLogsDiv.id && uvLogsDiv.id.startsWith('uv-logs-')) { | |
| const cellId = uvLogsDiv.id.replace('uv-logs-', ''); | |
| const indicatorElement = document.getElementById('uv-indicator-' + cellId); | |
| if (indicatorElement) { | |
| indicatorElement.textContent = isCollapsed ? '▼ uv-logs' : '▶ uv-logs'; | |
| } | |
| } | |
| } | |
| } | |
| function toggleUvLogsFromHeader(cellId) { | |
| const uvLogsElement = document.getElementById('uv-logs-' + cellId); | |
| const indicatorElement = document.getElementById('uv-indicator-' + cellId); | |
| if (uvLogsElement) { | |
| const headerElement = uvLogsElement.querySelector('.uv-logs-header'); | |
| const contentElement = uvLogsElement.querySelector('.uv-logs-content'); | |
| if (contentElement && headerElement) { | |
| const isCollapsed = contentElement.style.display === 'none'; | |
| contentElement.style.display = isCollapsed ? 'block' : 'none'; | |
| headerElement.textContent = isCollapsed ? '▼ UV Install Logs' : '▶ UV Install Logs'; | |
| if (indicatorElement) { | |
| indicatorElement.textContent = isCollapsed ? '▼ uv-logs' : '▶ uv-logs'; | |
| } | |
| } | |
| } | |
| } | |
| function updateIndicators(cellId) { | |
| const codeElement = document.getElementById('code-' + cellId); | |
| const outputElement = document.getElementById('output-' + cellId); | |
| const indicators = document.querySelector(`[onclick*="${cellId}"]`)?.closest('.cell-header')?.querySelector('.collapse-indicators'); | |
| if (indicators) { | |
| const codeCollapsed = codeElement && codeElement.classList.contains('collapsed'); | |
| const outputCollapsed = outputElement && outputElement.classList.contains('collapsed'); | |
| const codeIcon = codeCollapsed ? '▶' : '▼'; | |
| const outputIcon = outputCollapsed ? '▶' : '▼'; | |
| const codeSpan = indicators.querySelector('[onclick*="toggleCode"]'); | |
| const outputSpan = indicators.querySelector('[onclick*="toggleOutput"]'); | |
| if (codeSpan) codeSpan.innerHTML = `${codeIcon} code`; | |
| if (outputSpan) outputSpan.innerHTML = `${outputIcon} output`; | |
| } | |
| } | |
| function toggleTheme() { | |
| const html = document.documentElement; | |
| const currentTheme = html.getAttribute('data-theme'); | |
| const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; | |
| html.setAttribute('data-theme', newTheme); | |
| localStorage.setItem('uvnote-theme', newTheme); | |
| updateThemeIcon(); | |
| updateUiDebug(); | |
| } | |
| // Two panel code removed | |
| function updateThemeIcon() { | |
| const theme = document.documentElement.getAttribute('data-theme'); | |
| const toggle = document.querySelector('.theme-toggle'); | |
| if (toggle) { | |
| toggle.textContent = theme === 'dark' ? 'light' : 'dark'; | |
| } | |
| } | |
| function setUiTheme(newUi) { | |
| if (newUi !== 'default' && newUi !== 'none' && newUi !== 'monocolor') return; | |
| const html = document.documentElement; | |
| html.setAttribute('data-ui', newUi); | |
| try { localStorage.setItem('uvnote-ui', newUi); } catch (_) {} | |
| updateUiMenu(); | |
| updateUiDebug(); | |
| } | |
| function updateUiMenu() { | |
| const ui = document.documentElement.getAttribute('data-ui') || 'default'; | |
| const checks = { | |
| default: document.getElementById('checkbox-ui-default'), | |
| none: document.getElementById('checkbox-ui-none'), | |
| monocolor: document.getElementById('checkbox-ui-monocolor') | |
| }; | |
| if (checks.default) checks.default.textContent = ui === 'default' ? '☑' : '☐'; | |
| if (checks.none) checks.none.textContent = ui === 'none' ? '☑' : '☐'; | |
| if (checks.monocolor) checks.monocolor.textContent = ui === 'monocolor' ? '☑' : '☐'; | |
| } | |
| function updateUiDebug() { | |
| const ui = document.documentElement.getAttribute('data-ui') || 'default'; | |
| const color = document.documentElement.getAttribute('data-theme') || 'light'; | |
| const el = document.getElementById('ui-debug'); | |
| if (el) { | |
| el.textContent = `UI: ${ui} | Color: ${color}`; | |
| } | |
| } | |
| // Line selection and deep-linking | |
| function clearLineSelections() { | |
| try { | |
| document.querySelectorAll('.code-line-highlight').forEach(el => { el.style.display = 'none'; }); | |
| document.querySelectorAll('.line-number.selected').forEach(el => el.classList.remove('selected')); | |
| } catch (_) {} | |
| } | |
| let _selection = null; // { cellId, a, b } | |
| function clearSelection(updateUrl) { | |
| clearLineSelections(); | |
| _selection = null; | |
| if (updateUrl) { | |
| try { | |
| const url = new URL(window.location.href); | |
| url.searchParams.delete('cell'); | |
| url.searchParams.delete('line'); | |
| history.replaceState(null, '', url.toString()); | |
| } catch (_) {} | |
| } | |
| updateStateIndicator(); | |
| } | |
| function selectCellLine(cellId, line, updateUrl) { | |
| try { | |
| // Ensure only one selection across the whole document | |
| clearLineSelections(); | |
| const codeBox = document.getElementById(`code-${cellId}`); | |
| if (!codeBox) return; | |
| const pre = codeBox.querySelector('.highlight pre'); | |
| const overlay = document.getElementById(`line-highlight-${cellId}`); | |
| const numbers = document.getElementById(`lines-${cellId}`); | |
| if (!pre || !overlay) return; | |
| // Measure line height directly from computed style | |
| const preStyle = getComputedStyle(pre); | |
| const padTop = parseFloat(preStyle.paddingTop || '0'); | |
| const lh = parseFloat(preStyle.lineHeight || '20'); | |
| // Position overlay | |
| overlay.style.display = 'block'; | |
| overlay.style.height = `${lh}px`; | |
| overlay.style.top = `${pre.offsetTop + padTop + (line - 1) * lh}px`; | |
| // Update selected class in line numbers | |
| if (numbers) { | |
| numbers.querySelectorAll('.line-number').forEach(a => a.classList.remove('selected')); | |
| const sel = numbers.querySelector(`.line-number[data-line="${line}"]`); | |
| if (sel) sel.classList.add('selected'); | |
| } | |
| if (updateUrl) { | |
| const url = new URL(window.location.href); | |
| url.searchParams.set('cell', cellId); | |
| url.searchParams.set('line', String(line)); | |
| history.replaceState(null, '', url.toString()); | |
| } | |
| _selection = { cellId, a: line, b: line }; | |
| updateStateIndicator(); | |
| } catch (e) { console.warn('selectCellLine error', e); } | |
| } | |
| function selectCellLines(cellId, startLine, endLine, updateUrl) { | |
| try { | |
| // normalize order | |
| const a = Math.min(startLine, endLine); | |
| const b = Math.max(startLine, endLine); | |
| clearLineSelections(); | |
| const codeBox = document.getElementById(`code-${cellId}`); | |
| if (!codeBox) return; | |
| const pre = codeBox.querySelector('.highlight pre'); | |
| const overlay = document.getElementById(`line-highlight-${cellId}`); | |
| const numbers = document.getElementById(`lines-${cellId}`); | |
| if (!pre || !overlay) return; | |
| const preStyle = getComputedStyle(pre); | |
| const padTop = parseFloat(preStyle.paddingTop || '0'); | |
| const lh = parseFloat(preStyle.lineHeight || '20'); | |
| overlay.style.display = 'block'; | |
| overlay.style.top = `${pre.offsetTop + padTop + (a - 1) * lh}px`; | |
| overlay.style.height = `${(b - a + 1) * lh}px`; | |
| if (numbers) { | |
| numbers.querySelectorAll('.line-number').forEach(a => a.classList.remove('selected')); | |
| for (let i = a; i <= b; i++) { | |
| const el = numbers.querySelector(`.line-number[data-line="${i}"]`); | |
| if (el) el.classList.add('selected'); | |
| } | |
| } | |
| if (updateUrl) { | |
| const url = new URL(window.location.href); | |
| url.searchParams.set('cell', cellId); | |
| if (a === b) url.searchParams.set('line', String(a)); | |
| else url.searchParams.set('line', `${a}-${b}`); | |
| history.replaceState(null, '', url.toString()); | |
| } | |
| _selection = { cellId, a, b }; | |
| updateStateIndicator(); | |
| } catch (e) { console.warn('selectCellLines error', e); } | |
| } | |
| // Drag-to-select support on line numbers | |
| let _lineDrag = { active: false, cellId: null, start: 0 }; | |
| function onLineNumberMouseDown(e) { | |
| const a = e.target.closest('.line-number'); | |
| if (!a) return; | |
| e.preventDefault(); | |
| const cellId = a.dataset.cell; | |
| const line = parseInt(a.dataset.line || '1', 10) || 1; | |
| // Toggle off if this exact single line is already the only selection | |
| const numbers = document.getElementById(`lines-${cellId}`); | |
| if (numbers) { | |
| const selected = Array.from(numbers.querySelectorAll('.line-number.selected')).map(n => parseInt(n.dataset.line||'0',10)).filter(Boolean); | |
| if (selected.length === 1 && selected[0] === line) { | |
| clearSelection(true); | |
| return; | |
| } | |
| } | |
| _lineDrag.active = true; | |
| _lineDrag.cellId = cellId; | |
| _lineDrag.start = line; | |
| selectCellLines(_lineDrag.cellId, _lineDrag.start, _lineDrag.start, false); | |
| } | |
| function onDocMouseMove(e) { | |
| if (!_lineDrag.active) return; | |
| const el = document.elementFromPoint(e.clientX, e.clientY); | |
| if (!el) return; | |
| const a = el.closest && el.closest('.line-number'); | |
| if (!a) return; | |
| if (a.dataset.cell !== _lineDrag.cellId) return; | |
| const cur = parseInt(a.dataset.line || '1', 10) || 1; | |
| selectCellLines(_lineDrag.cellId, _lineDrag.start, cur, false); | |
| } | |
| function onDocMouseUp(e) { | |
| if (!_lineDrag.active) return; | |
| const last = document.querySelector('.line-number.selected:last-of-type'); | |
| // finalize URL using the current selected range | |
| const numbers = document.getElementById(`lines-${_lineDrag.cellId}`); | |
| if (numbers) { | |
| const selected = Array.from(numbers.querySelectorAll('.line-number.selected')).map(n => parseInt(n.dataset.line||'0',10)).filter(Boolean); | |
| if (selected.length) { | |
| const a = Math.min(...selected); const b = Math.max(...selected); | |
| selectCellLines(_lineDrag.cellId, a, b, true); | |
| } | |
| } | |
| _lineDrag.active = false; _lineDrag.cellId = null; _lineDrag.start = 0; | |
| } | |
| function applyLocationFromUrl() { | |
| try { | |
| const url = new URL(window.location.href); | |
| const cell = url.searchParams.get('cell'); | |
| const lineParam = url.searchParams.get('line'); | |
| if (cell && lineParam) { | |
| if (lineParam.includes('-')) { | |
| const [a, b] = lineParam.split('-').map(x => parseInt(x, 10)); | |
| if (!Number.isNaN(a) && !Number.isNaN(b)) selectCellLines(cell, a, b, false); | |
| } else { | |
| const l = parseInt(lineParam, 10); | |
| if (!Number.isNaN(l)) selectCellLine(cell, l, false); | |
| } | |
| } | |
| // Apply tool parameters from URL | |
| applyToolsFromUrl(url.searchParams); | |
| // Cell states will be applied later in DOMContentLoaded with proper timing | |
| const encodedCellStates = url.searchParams.get('cells'); | |
| console.log('Encoded cell states from URL:', encodedCellStates); | |
| } catch (_) {} | |
| } | |
| function applyToolsFromUrl(params) { | |
| try { | |
| // Check if tools widget should be shown | |
| const showTools = params.get('tools'); | |
| if (showTools === '1') { | |
| // Mark that tool was loaded from URL | |
| _urlLoadedTool = true; | |
| // Apply color | |
| const color = params.get('color'); | |
| if (color && /^[0-9a-fA-F]{6}$/.test(color)) { | |
| setStoredArrowColor('#' + color); | |
| } | |
| // Apply thickness | |
| const thickness = params.get('thickness'); | |
| if (thickness) { | |
| const value = parseInt(thickness, 10); | |
| if (value >= 1 && value <= 10) { | |
| setStoredLineThickness(value); | |
| } | |
| } | |
| // Don't override fadeout time for URL-loaded tools - let individual shapes decide | |
| // Load shapes from URL | |
| const encodedShapes = params.get('shapes'); | |
| if (encodedShapes) { | |
| const decodedShapes = decodeShapesFromUrl(encodedShapes); | |
| if (decodedShapes.length > 0) { | |
| _shapes = decodedShapes; | |
| saveShapes(); | |
| // Trigger render after overlay is initialized | |
| setTimeout(() => { | |
| renderOverlay(); | |
| }, 300); | |
| } | |
| } | |
| // Wait for widgets to be initialized before showing tools | |
| setTimeout(() => { | |
| const toolsWidget = document.querySelector('.tools-widget'); | |
| const checkbox = document.getElementById('checkbox-tools'); | |
| if (toolsWidget && checkbox) { | |
| toolsWidget.style.display = 'block'; | |
| checkbox.textContent = '☑'; | |
| localStorage.setItem('uvnote-widget-tools', 'visible'); | |
| } | |
| // Apply active tool | |
| const activeTool = params.get('tool'); | |
| if (activeTool && ['arrow', 'pen', 'eraser', 'spotlight'].includes(activeTool)) { | |
| const toolBtn = Array.from(document.querySelectorAll('.tool-button')).find(btn => btn.textContent === activeTool); | |
| if (toolBtn) { | |
| toolBtn.click(); | |
| } | |
| } | |
| // Re-layout widgets after showing tools | |
| layoutWidgetsStackedBottomRight(); | |
| }, 200); | |
| } | |
| } catch (_) {} | |
| } | |
| function captureInitialCellStates() { | |
| const cells = document.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| const cellId = cell.id.replace('cell-', ''); | |
| const codeEl = document.getElementById('code-' + cellId); | |
| const outputEl = document.getElementById('output-' + cellId); | |
| if (codeEl || outputEl) { | |
| const state = {}; | |
| if (codeEl) { | |
| state.c = codeEl.classList.contains('collapsed') ? 0 : 1; | |
| } | |
| if (outputEl) { | |
| state.o = outputEl.classList.contains('collapsed') ? 0 : 1; | |
| } | |
| _initialCellStates[cellId] = state; | |
| } | |
| }); | |
| console.log('Captured initial cell states:', _initialCellStates); | |
| } | |
| function encodeCellStatesToUrl() { | |
| // Get all cells and their collapse states | |
| const cells = document.querySelectorAll('.cell'); | |
| const cellStates = {}; | |
| console.log('Found cells:', cells.length); | |
| cells.forEach(cell => { | |
| const cellId = cell.id.replace('cell-', ''); | |
| const codeEl = document.getElementById('code-' + cellId); | |
| const outputEl = document.getElementById('output-' + cellId); | |
| const initialState = _initialCellStates[cellId] || {}; | |
| console.log(`Encoding cell ${cellId}:`, { | |
| codeEl: !!codeEl, | |
| outputEl: !!outputEl, | |
| codeCollapsed: codeEl ? codeEl.classList.contains('collapsed') : 'N/A', | |
| outputCollapsed: outputEl ? outputEl.classList.contains('collapsed') : 'N/A', | |
| initialState: initialState | |
| }); | |
| if (codeEl || outputEl) { | |
| const state = {}; | |
| let hasChanges = false; | |
| if (codeEl) { | |
| const currentCodeState = codeEl.classList.contains('collapsed') ? 0 : 1; | |
| const initialCodeState = initialState.c; | |
| // Only encode if different from initial state | |
| if (initialCodeState !== undefined && currentCodeState !== initialCodeState) { | |
| state.c = currentCodeState; | |
| hasChanges = true; | |
| } | |
| } | |
| if (outputEl) { | |
| const currentOutputState = outputEl.classList.contains('collapsed') ? 0 : 1; | |
| const initialOutputState = initialState.o; | |
| // Only encode if different from initial state | |
| if (initialOutputState !== undefined && currentOutputState !== initialOutputState) { | |
| state.o = currentOutputState; | |
| hasChanges = true; | |
| } | |
| } | |
| // Only include cell if it has changes from initial state | |
| if (hasChanges) { | |
| cellStates[cellId] = state; | |
| console.log(`Added cell ${cellId}:`, state); | |
| } | |
| } | |
| }); | |
| console.log('Final cell states to encode:', cellStates); | |
| // Return empty string if no changed cells | |
| if (Object.keys(cellStates).length === 0) return ''; | |
| // Encode as compact base64 string | |
| const encoded = btoa(JSON.stringify(cellStates)); | |
| console.log('Encoded cell states:', encoded); | |
| return encoded; | |
| } | |
| function decodeCellStatesFromUrl(encodedStates) { | |
| if (!encodedStates) return {}; | |
| try { | |
| return JSON.parse(atob(encodedStates)); | |
| } catch (e) { | |
| console.error('Failed to decode cell states:', e); | |
| return {}; | |
| } | |
| } | |
| function applyCellStatesFromUrl(cellStates) { | |
| console.log('Applying cell states from URL:', cellStates); | |
| Object.entries(cellStates).forEach(([cellId, state]) => { | |
| const codeEl = document.getElementById('code-' + cellId); | |
| const outputEl = document.getElementById('output-' + cellId); | |
| console.log(`Cell ${cellId}:`, { | |
| codeEl: !!codeEl, | |
| outputEl: !!outputEl, | |
| state: state | |
| }); | |
| if (codeEl && state.c !== undefined) { | |
| if (state.c === 0) { | |
| codeEl.classList.add('collapsed'); | |
| console.log(`Collapsed code for cell ${cellId}`, { | |
| hasCollapsedClass: codeEl.classList.contains('collapsed'), | |
| computedDisplay: getComputedStyle(codeEl).display, | |
| classList: Array.from(codeEl.classList), | |
| elementId: codeEl.id | |
| }); | |
| } else { | |
| codeEl.classList.remove('collapsed'); | |
| codeEl.classList.add('expanded'); // Explicitly add expanded class | |
| console.log(`Expanded code for cell ${cellId}`, { | |
| hasCollapsedClass: codeEl.classList.contains('collapsed'), | |
| hasExpandedClass: codeEl.classList.contains('expanded'), | |
| computedDisplay: getComputedStyle(codeEl).display, | |
| classList: Array.from(codeEl.classList), | |
| elementId: codeEl.id | |
| }); | |
| } | |
| } | |
| if (outputEl && state.o !== undefined) { | |
| if (state.o === 0) { | |
| outputEl.classList.add('collapsed'); | |
| console.log(`Collapsed output for cell ${cellId}`); | |
| } else { | |
| outputEl.classList.remove('collapsed'); | |
| console.log(`Expanded output for cell ${cellId}`); | |
| } | |
| } | |
| // Update visual indicators and force style recalculation | |
| try { | |
| updateIndicators(cellId); | |
| // Force browser to recalculate styles | |
| if (codeEl) { | |
| codeEl.offsetHeight; // Force reflow | |
| console.log(`After indicators update - code visible: ${getComputedStyle(codeEl).display !== 'none'}`); | |
| } | |
| if (outputEl) { | |
| outputEl.offsetHeight; // Force reflow | |
| console.log(`After indicators update - output visible: ${getComputedStyle(outputEl).display !== 'none'}`); | |
| } | |
| } catch (e) { | |
| console.error(`Error updating indicators for cell ${cellId}:`, e); | |
| } | |
| }); | |
| } | |
| function encodeShapesToUrl() { | |
| // Encode shapes as compact base64 string | |
| if (_shapes.length === 0) return ''; | |
| const shapeData = _shapes.map(shape => { | |
| const baseData = { | |
| ct: shape.createdAt, // creation timestamp | |
| fo: shape.fadeoutTime || getFadeoutTime() // fadeout time for this shape | |
| }; | |
| if (shape.type === 'arrow') { | |
| return { | |
| ...baseData, | |
| t: 'a', | |
| x1: Math.round(shape.x1), | |
| y1: Math.round(shape.y1), | |
| x2: Math.round(shape.x2), | |
| y2: Math.round(shape.y2), | |
| c: shape.color.substring(1), // remove # | |
| w: shape.width | |
| }; | |
| } else if (shape.type === 'pen') { | |
| return { | |
| ...baseData, | |
| t: 'p', | |
| pts: shape.points.map(p => [Math.round(p.x), Math.round(p.y)]), | |
| c: shape.color.substring(1), | |
| w: shape.width | |
| }; | |
| } else if (shape.type === 'spotlight') { | |
| return { | |
| ...baseData, | |
| t: 's', | |
| x: Math.round(shape.x), | |
| y: Math.round(shape.y), | |
| r: Math.round(shape.radius) | |
| }; | |
| } | |
| }).filter(Boolean); | |
| return btoa(JSON.stringify(shapeData)); | |
| } | |
| function decodeShapesFromUrl(encodedShapes) { | |
| if (!encodedShapes) return []; | |
| try { | |
| const shapeData = JSON.parse(atob(encodedShapes)); | |
| return shapeData.map(data => { | |
| const base = { | |
| createdAt: data.ct || Date.now(), // use encoded timestamp or current time | |
| fadeoutTime: data.fo || 0, // use encoded fadeout time or 0 (never fade) | |
| opacity: 1.0 | |
| }; | |
| if (data.t === 'a') { | |
| return { | |
| ...base, | |
| type: 'arrow', | |
| x1: data.x1, | |
| y1: data.y1, | |
| x2: data.x2, | |
| y2: data.y2, | |
| color: '#' + data.c, | |
| width: data.w | |
| }; | |
| } else if (data.t === 'p') { | |
| return { | |
| ...base, | |
| type: 'pen', | |
| points: data.pts.map(([x, y]) => ({ x, y })), | |
| color: '#' + data.c, | |
| width: data.w | |
| }; | |
| } else if (data.t === 's') { | |
| return { | |
| ...base, | |
| type: 'spotlight', | |
| x: data.x, | |
| y: data.y, | |
| radius: data.r, | |
| color: '#000000' | |
| }; | |
| } | |
| }).filter(Boolean); | |
| } catch (e) { | |
| console.error('Failed to decode shapes:', e); | |
| return []; | |
| } | |
| } | |
| function encodeToolStateToUrl() { | |
| // Don't update URL during initialization | |
| if (_isInitializing) { | |
| return window.location.href; | |
| } | |
| const params = new URLSearchParams(window.location.search); | |
| // Check if tools widget is visible and has an active tool | |
| const toolsWidget = document.querySelector('.tools-widget'); | |
| const activeTool = document.body.dataset.tool; | |
| const hasActiveTool = activeTool && activeTool !== 'none'; | |
| const toolsWidgetVisible = toolsWidget && getComputedStyle(toolsWidget).display !== 'none'; | |
| // Always handle shapes regardless of tool state | |
| const encodedShapes = encodeShapesToUrl(); | |
| if (encodedShapes) { | |
| params.set('shapes', encodedShapes); | |
| } else { | |
| params.delete('shapes'); | |
| } | |
| // Always preserve existing cell states from URL if present | |
| const existingCellStates = params.get('cells'); | |
| if (existingCellStates) { | |
| // Keep existing cell states - don't re-encode from DOM | |
| params.set('cells', existingCellStates); | |
| } else { | |
| // Only encode new cell states if none exist in URL | |
| const encodedCellStates = encodeCellStatesToUrl(); | |
| if (encodedCellStates) { | |
| params.set('cells', encodedCellStates); | |
| } | |
| } | |
| if (toolsWidgetVisible && hasActiveTool) { | |
| // Include tool params when widget is visible AND tool is active | |
| params.set('tools', '1'); | |
| params.set('tool', activeTool); | |
| // Get color (without # prefix) | |
| const color = getArrowColor(); | |
| if (color && color.startsWith('#')) { | |
| params.set('color', color.substring(1)); | |
| } | |
| // Get thickness | |
| const thickness = getLineThickness(); | |
| params.set('thickness', thickness.toString()); | |
| } else { | |
| // Remove tool state params but keep shapes | |
| params.delete('tools'); | |
| params.delete('tool'); | |
| params.delete('color'); | |
| params.delete('thickness'); | |
| params.delete('fadeout'); | |
| } | |
| // Update URL without reloading | |
| const newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '') + window.location.hash; | |
| window.history.replaceState(null, '', newUrl); | |
| return window.location.href; | |
| } | |
| function resetLayout() { | |
| try { | |
| // Clear all uvnote-* keys | |
| const allKeys = Object.keys(localStorage); | |
| const uvnoteKeys = allKeys.filter(key => key.startsWith('uvnote-')); | |
| uvnoteKeys.forEach(k => localStorage.removeItem(k)); | |
| } catch (_) {} | |
| // Clear any active selection and remove URL params | |
| try { clearSelection(true); } catch(_) {} | |
| // Reset active tool if any | |
| try { window.setActiveTool('none'); } catch(_) {} | |
| // Clear shapes | |
| try { _shapes = []; saveShapes(); } catch(_) {} | |
| // Reset URL-loaded tool flag | |
| try { _urlLoadedTool = false; } catch(_) {} | |
| // Reset all cells to expanded state | |
| try { | |
| const cells = document.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| const cellId = cell.id.replace('cell-', ''); | |
| const codeEl = document.getElementById('code-' + cellId); | |
| const outputEl = document.getElementById('output-' + cellId); | |
| if (codeEl) codeEl.classList.remove('collapsed'); | |
| if (outputEl) outputEl.classList.remove('collapsed'); | |
| updateIndicators(cellId); | |
| }); | |
| } catch(_) {} | |
| // Clear ALL URL parameters and reload with clean URL | |
| try { | |
| const cleanUrl = window.location.pathname + window.location.hash; | |
| window.location.href = cleanUrl; // Use window.location.href instead of history.replaceState + reload | |
| } catch (_) { | |
| // Fallback - reload current page | |
| location.reload(); | |
| } | |
| } | |
| function toggleMenu() { | |
| const menuButton = document.querySelector('.menu-button'); | |
| if (menuButton) { | |
| menuButton.classList.toggle('active'); | |
| } | |
| } | |
| function toggleWidget(widgetName) { | |
| let widget; | |
| let checkbox; | |
| // Close the menu first | |
| const menuButton = document.querySelector('.menu-button'); | |
| if (menuButton) { | |
| menuButton.classList.remove('active'); | |
| } | |
| switch(widgetName) { | |
| case 'tools': | |
| widget = document.querySelector('.tools-widget'); | |
| checkbox = document.getElementById('checkbox-tools'); | |
| break; | |
| case 'file-explorer': | |
| widget = document.querySelector('.file-explorer'); | |
| checkbox = document.getElementById('checkbox-file-explorer'); | |
| break; | |
| case 'minimap': | |
| widget = document.querySelector('.minimap'); | |
| checkbox = document.getElementById('checkbox-minimap'); | |
| break; | |
| case 'status': | |
| widget = document.querySelector('.status-widget'); | |
| checkbox = document.getElementById('checkbox-status'); | |
| break; | |
| default: | |
| return; | |
| } | |
| if (widget && checkbox) { | |
| const isVisible = getComputedStyle(widget).display !== 'none'; | |
| widget.style.display = isVisible ? 'none' : 'block'; | |
| checkbox.textContent = isVisible ? '☐' : '☑'; | |
| // Save state to localStorage | |
| try { | |
| localStorage.setItem(`uvnote-widget-${widgetName}`, isVisible ? 'hidden' : 'visible'); | |
| } catch (_) {} | |
| // Re-layout widgets after visibility change | |
| try { | |
| layoutWidgetsStackedBottomRight(); | |
| } catch (_) {} | |
| // Update URL when tools widget visibility changes | |
| if (widgetName === 'tools') { | |
| encodeToolStateToUrl(); | |
| } | |
| } | |
| } | |
| function initializeWidgetVisibility() { | |
| const widgets = [ | |
| { name: 'tools', selector: '.tools-widget' }, | |
| { name: 'file-explorer', selector: '.file-explorer' }, | |
| { name: 'minimap', selector: '.minimap' }, | |
| { name: 'status', selector: '.status-widget' } | |
| ]; | |
| widgets.forEach(({ name, selector }) => { | |
| const defaultState = name === 'status' ? 'visible' : 'hidden'; | |
| const savedState = localStorage.getItem(`uvnote-widget-${name}`) || defaultState; | |
| const widget = document.querySelector(selector); | |
| const checkbox = document.getElementById(`checkbox-${name}`); | |
| if (widget && checkbox) { | |
| const isVisible = savedState === 'visible'; | |
| widget.style.display = isVisible ? 'block' : 'none'; | |
| checkbox.textContent = isVisible ? '☑' : '☐'; | |
| } | |
| }); | |
| } | |
| // Close menu when clicking outside | |
| document.addEventListener('click', function(event) { | |
| const menuButton = document.querySelector('.menu-button'); | |
| // Don't close if clicking on a menu item (let the item handler close it) | |
| if (menuButton && !menuButton.contains(event.target)) { | |
| menuButton.classList.remove('active'); | |
| } | |
| }); | |
| // Layout: stack widgets bottom-right and equalize widths | |
| function hasCustomWidgetPositions() { | |
| try { | |
| return ( | |
| localStorage.getItem('uvnote-minimap-pos') || | |
| localStorage.getItem('uvnote-file-explorer-pos') || | |
| localStorage.getItem('uvnote-tools-pos') | |
| ); | |
| } catch (_) { return false; } | |
| } | |
| function rectsOverlap(r1, r2) { | |
| return !(r1.right <= r2.left || r2.right <= r1.left || r1.bottom <= r2.top || r2.bottom <= r1.top); | |
| } | |
| function widgetsOverlap(widgets) { | |
| for (let i = 0; i < widgets.length; i++) { | |
| const a = widgets[i]; | |
| const ra = a.getBoundingClientRect(); | |
| for (let j = i + 1; j < widgets.length; j++) { | |
| const b = widgets[j]; | |
| const rb = b.getBoundingClientRect(); | |
| if (rectsOverlap(ra, rb)) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function applyStackLayout(widgets, order) { | |
| if (!widgets.length) return; | |
| // Fixed equal width | |
| const fixedWidth = 220; | |
| widgets.forEach(el => { el.style.width = fixedWidth + 'px'; }); | |
| // Fit heights if needed to avoid overflow | |
| const gap = 12; | |
| const available = Math.max(0, window.innerHeight - 40 - gap * (order.length - 1)); | |
| const eachMax = Math.floor(available / order.length); | |
| order.forEach(el => { | |
| el.style.maxHeight = eachMax + 'px'; | |
| el.style.overflowY = 'auto'; | |
| }); | |
| // Stack bottom-up in the requested order | |
| let bottomOffset = 20; // base gutter | |
| order.forEach(el => { | |
| el.style.left = 'auto'; | |
| el.style.top = 'auto'; | |
| el.style.right = '20px'; | |
| el.style.bottom = bottomOffset + 'px'; | |
| bottomOffset += el.offsetHeight + gap; | |
| }); | |
| } | |
| function layoutWidgetsStackedBottomRight() { | |
| const minimap = document.querySelector('.minimap'); | |
| const fileExplorer = document.querySelector('.file-explorer'); | |
| const tools = document.querySelector('.tools-widget'); | |
| const status = document.querySelector('.status-widget'); | |
| const widgets = [minimap, fileExplorer, tools, status].filter(el => el && getComputedStyle(el).display !== 'none'); | |
| if (!widgets.length) return; | |
| const order = [minimap, fileExplorer, tools, status].filter(Boolean).filter(el => getComputedStyle(el).display !== 'none'); | |
| // If user placed custom positions and there is no overlap, respect them. | |
| if (hasCustomWidgetPositions() && !widgetsOverlap(widgets)) return; | |
| applyStackLayout(widgets, order); | |
| } | |
| // Panel icon removed | |
| let _minimapScrollContainer = null; | |
| let _minimapScrollHandler = null; | |
| function initMinimap() { | |
| // Generate minimap content | |
| const minimap = createMinimap(); | |
| document.body.appendChild(minimap); | |
| // Make draggable (use title as handle) | |
| const mTitle = minimap.querySelector('.minimap-title'); | |
| makeDraggable(minimap, 'uvnote-minimap-pos', mTitle); | |
| // Attach scroll listener to window (two-panel removed) | |
| _minimapScrollContainer = window; | |
| if (_minimapScrollContainer) { | |
| _minimapScrollHandler = () => updateMinimapActive(); | |
| if (_minimapScrollContainer === window) { | |
| window.addEventListener('scroll', _minimapScrollHandler); | |
| } else { | |
| _minimapScrollContainer.addEventListener('scroll', _minimapScrollHandler); | |
| } | |
| } | |
| updateMinimapActive(); | |
| } | |
| function teardownMinimap() { | |
| const minimap = document.querySelector('.minimap'); | |
| if (minimap && minimap.parentNode) minimap.parentNode.removeChild(minimap); | |
| if (_minimapScrollContainer && _minimapScrollHandler) { | |
| if (_minimapScrollContainer === window) { | |
| window.removeEventListener('scroll', _minimapScrollHandler); | |
| } else { | |
| _minimapScrollContainer.removeEventListener('scroll', _minimapScrollHandler); | |
| } | |
| } | |
| _minimapScrollContainer = null; | |
| _minimapScrollHandler = null; | |
| } | |
| function initFileExplorer() { | |
| // Generate file explorer content | |
| const fileExplorer = createFileExplorer(); | |
| document.body.appendChild(fileExplorer); | |
| } | |
| function createMinimap() { | |
| const minimap = document.createElement('div'); | |
| minimap.className = 'minimap'; | |
| const title = document.createElement('div'); | |
| title.className = 'minimap-title'; | |
| title.textContent = 'navigation'; | |
| minimap.appendChild(title); | |
| // Find all headings and cells | |
| const root = document.querySelector('.main-content') || document; | |
| const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6'); | |
| const cells = root.querySelectorAll('.cell'); | |
| // Combine and sort by position | |
| const items = []; | |
| headings.forEach(heading => { | |
| const id = heading.id || generateId(heading.textContent); | |
| if (!heading.id) heading.id = id; | |
| items.push({ | |
| element: heading, | |
| type: 'heading', | |
| level: parseInt(heading.tagName.charAt(1)), | |
| text: heading.textContent.trim(), | |
| id: id, | |
| position: heading.getBoundingClientRect().top + window.scrollY | |
| }); | |
| }); | |
| cells.forEach(cell => { | |
| const header = cell.querySelector('.cell-header'); | |
| if (header) { | |
| const id = cell.id || `cell-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| if (!cell.id) cell.id = id; | |
| items.push({ | |
| element: cell, | |
| type: 'cell', | |
| text: header.textContent.trim(), | |
| id: id, | |
| position: cell.getBoundingClientRect().top + window.scrollY | |
| }); | |
| } | |
| }); | |
| // Sort by position | |
| items.sort((a, b) => a.position - b.position); | |
| // Create minimap items | |
| items.forEach(item => { | |
| const link = document.createElement('a'); | |
| link.className = `minimap-item ${item.type === 'heading' ? 'minimap-heading' : 'minimap-cell'}`; | |
| if (item.type === 'heading') { | |
| link.classList.add(`h${item.level}`); | |
| } | |
| link.textContent = item.text.length > 25 ? item.text.substring(0, 22) + '...' : item.text; | |
| link.href = `#${item.id}`; | |
| link.onclick = function(e) { | |
| e.preventDefault(); | |
| item.element.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }; | |
| minimap.appendChild(link); | |
| }); | |
| return minimap; | |
| } | |
| function generateId(text) { | |
| return text.toLowerCase() | |
| .replace(/[^a-z0-9]+/g, '-') | |
| .replace(/^-+|-+$/g, '') | |
| .substring(0, 20); | |
| } | |
| function updateMinimapActive() { | |
| const minimapItems = document.querySelectorAll('.minimap-item'); | |
| const container = _minimapScrollContainer || window; | |
| const containerRect = container === window ? null : container.getBoundingClientRect(); | |
| const scrollPos = (container === window ? window.scrollY : container.scrollTop) + 100; // Offset for better detection | |
| let activeItem = null; | |
| minimapItems.forEach(item => { | |
| const targetId = item.getAttribute('href').substring(1); | |
| const target = document.getElementById(targetId); | |
| if (target) { | |
| const rectTop = target.getBoundingClientRect().top; | |
| const targetPos = (container === window) | |
| ? rectTop + window.scrollY | |
| : rectTop - containerRect.top + container.scrollTop; | |
| if (targetPos <= scrollPos) { | |
| activeItem = item; | |
| } | |
| } | |
| item.classList.remove('active'); | |
| }); | |
| if (activeItem) { | |
| activeItem.classList.add('active'); | |
| } | |
| } | |
| function createFileExplorer() { | |
| const fileExplorer = document.createElement('div'); | |
| fileExplorer.className = 'file-explorer'; | |
| const title = document.createElement('div'); | |
| title.className = 'file-explorer-title'; | |
| title.textContent = 'files'; | |
| fileExplorer.appendChild(title); | |
| // Make draggable (use title as handle) | |
| makeDraggable(fileExplorer, 'uvnote-file-explorer-pos', title); | |
| // Scripts section | |
| const scriptsSection = document.createElement('div'); | |
| scriptsSection.className = 'file-explorer-section'; | |
| const scriptsTitle = document.createElement('div'); | |
| scriptsTitle.className = 'file-explorer-section-title'; | |
| scriptsTitle.textContent = 'scripts'; | |
| scriptsSection.appendChild(scriptsTitle); | |
| // Find all cells and list their script files (single panel) | |
| const root = document.querySelector('.main-content') || document; | |
| const cells = root.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| const header = cell.querySelector('.cell-header'); | |
| if (header) { | |
| const cellText = header.textContent.trim(); | |
| const cellMatch = cellText.match(/Cell: ([a-zA-Z_][a-zA-Z0-9_]*)/); | |
| if (cellMatch) { | |
| const cellId = cellMatch[1]; | |
| const scriptItem = document.createElement('div'); | |
| scriptItem.className = 'file-explorer-item script'; | |
| scriptItem.textContent = `${cellId}.py`; | |
| scriptItem.onclick = function() { | |
| cell.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }; | |
| scriptsSection.appendChild(scriptItem); | |
| } | |
| } | |
| }); | |
| fileExplorer.appendChild(scriptsSection); | |
| // Artifacts section | |
| const artifactsSection = document.createElement('div'); | |
| artifactsSection.className = 'file-explorer-section'; | |
| const artifactsTitle = document.createElement('div'); | |
| artifactsTitle.className = 'file-explorer-section-title'; | |
| artifactsTitle.textContent = 'artifacts'; | |
| artifactsSection.appendChild(artifactsTitle); | |
| // Find all artifact links (single panel) | |
| const artifactsRoot = document.querySelector('.main-content') || document; | |
| const artifacts = artifactsRoot.querySelectorAll('.artifact'); | |
| if (artifacts.length === 0) { | |
| const noArtifacts = document.createElement('div'); | |
| noArtifacts.className = 'file-explorer-item artifact'; | |
| noArtifacts.textContent = '(none)'; | |
| noArtifacts.style.opacity = '0.5'; | |
| artifactsSection.appendChild(noArtifacts); | |
| } else { | |
| artifacts.forEach(artifact => { | |
| const artifactItem = document.createElement('div'); | |
| artifactItem.className = 'file-explorer-item artifact'; | |
| artifactItem.textContent = artifact.textContent; | |
| artifactItem.onclick = function() { | |
| artifact.click(); | |
| }; | |
| artifactsSection.appendChild(artifactItem); | |
| }); | |
| } | |
| fileExplorer.appendChild(artifactsSection); | |
| return fileExplorer; | |
| } | |
| function initStatusWidget() { | |
| let el = document.querySelector('.status-widget'); | |
| if (!el) { | |
| el = document.createElement('div'); | |
| el.className = 'status-widget'; | |
| el.id = 'status-widget'; | |
| el.textContent = 'ready — Esc'; | |
| document.body.appendChild(el); | |
| } | |
| } | |
| // Tools widget | |
| let _cursorX = 0; | |
| let _cursorY = 0; | |
| let _cursorVisible = false; | |
| function setActiveTool(tool) { | |
| if (!tool || tool === 'none') { | |
| document.body.dataset.tool = 'none'; | |
| localStorage.setItem('uvnote-active-tool', 'none'); | |
| setOverlayActive(false); | |
| _cursorVisible = false; | |
| // Remove active class from all tool buttons when deactivating | |
| const toolButtons = document.querySelectorAll('.tools-widget .tool-button'); | |
| toolButtons.forEach(btn => btn.classList.remove('active')); | |
| updateStateIndicator(); | |
| encodeToolStateToUrl(); | |
| return; | |
| } | |
| document.body.dataset.tool = tool; | |
| localStorage.setItem('uvnote-active-tool', tool); | |
| setOverlayActive(true); | |
| _cursorVisible = true; | |
| updateStateIndicator(); | |
| encodeToolStateToUrl(); | |
| } | |
| // Make setActiveTool globally accessible for ESC key handler | |
| window.setActiveTool = setActiveTool; | |
| function getArrowColor() { | |
| const saved = localStorage.getItem('uvnote-arrow-color'); | |
| if (saved) return saved; | |
| return '#e53935'; // Default red color | |
| } | |
| function setStoredArrowColor(color) { | |
| try { localStorage.setItem('uvnote-arrow-color', color); } catch (_) {} | |
| } | |
| function getLineThickness() { | |
| const saved = localStorage.getItem('uvnote-line-thickness'); | |
| if (saved) return parseInt(saved, 10); | |
| return 6; // default thickness | |
| } | |
| function setStoredLineThickness(thickness) { | |
| try { localStorage.setItem('uvnote-line-thickness', thickness); } catch (_) {} | |
| } | |
| function getFadeoutTime() { | |
| const saved = localStorage.getItem('uvnote-fadeout-time'); | |
| if (saved) return parseInt(saved, 10); | |
| return 5; // default 5 seconds | |
| } | |
| function setStoredFadeoutTime(seconds) { | |
| try { localStorage.setItem('uvnote-fadeout-time', seconds); } catch (_) {} | |
| } | |
| function createToolsWidget() { | |
| const tools = document.createElement('div'); | |
| tools.className = 'tools-widget'; | |
| const title = document.createElement('div'); | |
| title.className = 'tools-title'; | |
| title.textContent = 'tools'; | |
| tools.appendChild(title); | |
| const row = document.createElement('div'); | |
| row.className = 'tools-row'; | |
| tools.appendChild(row); | |
| // Arrow tool | |
| const arrowBtn = document.createElement('div'); | |
| arrowBtn.className = 'tool-button'; | |
| arrowBtn.textContent = 'arrow'; | |
| arrowBtn.onclick = function() { | |
| const isActive = arrowBtn.classList.contains('active'); | |
| if (isActive) { | |
| arrowBtn.classList.remove('active'); | |
| setActiveTool('none'); | |
| } else { | |
| tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); | |
| arrowBtn.classList.add('active'); | |
| setActiveTool('arrow'); | |
| } | |
| }; | |
| row.appendChild(arrowBtn); | |
| // Pen tool | |
| const penBtn = document.createElement('div'); | |
| penBtn.className = 'tool-button'; | |
| penBtn.textContent = 'pen'; | |
| penBtn.onclick = function() { | |
| const isActive = penBtn.classList.contains('active'); | |
| if (isActive) { | |
| penBtn.classList.remove('active'); | |
| setActiveTool('none'); | |
| } else { | |
| tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); | |
| penBtn.classList.add('active'); | |
| setActiveTool('pen'); | |
| } | |
| }; | |
| row.appendChild(penBtn); | |
| // Eraser tool | |
| const eraseBtn = document.createElement('div'); | |
| eraseBtn.className = 'tool-button'; | |
| eraseBtn.textContent = 'eraser'; | |
| eraseBtn.onclick = function() { | |
| const isActive = eraseBtn.classList.contains('active'); | |
| if (isActive) { | |
| eraseBtn.classList.remove('active'); | |
| setActiveTool('none'); | |
| } else { | |
| tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); | |
| eraseBtn.classList.add('active'); | |
| setActiveTool('eraser'); | |
| } | |
| }; | |
| row.appendChild(eraseBtn); | |
| // Spotlight tool | |
| const spotlightBtn = document.createElement('div'); | |
| spotlightBtn.className = 'tool-button'; | |
| spotlightBtn.textContent = 'spotlight'; | |
| spotlightBtn.onclick = function() { | |
| const isActive = spotlightBtn.classList.contains('active'); | |
| if (isActive) { | |
| spotlightBtn.classList.remove('active'); | |
| setActiveTool('none'); | |
| } else { | |
| tools.querySelectorAll('.tool-button').forEach(b => b.classList.remove('active')); | |
| spotlightBtn.classList.add('active'); | |
| setActiveTool('spotlight'); | |
| } | |
| }; | |
| row.appendChild(spotlightBtn); | |
| // Clear all | |
| const clearBtn = document.createElement('div'); | |
| clearBtn.className = 'tool-button'; | |
| clearBtn.textContent = 'clear'; | |
| clearBtn.onclick = function() { | |
| _shapes = []; | |
| saveShapes(); | |
| renderOverlay(); | |
| }; | |
| row.appendChild(clearBtn); | |
| // We'll add the copy button at the end of the widget | |
| // Restore active state from storage | |
| const saved = localStorage.getItem('uvnote-active-tool') || 'none'; | |
| if (saved === 'arrow') { | |
| arrowBtn.classList.add('active'); | |
| setActiveTool('arrow'); | |
| } else if (saved === 'pen') { | |
| penBtn.classList.add('active'); | |
| setActiveTool('pen'); | |
| } else if (saved === 'eraser') { | |
| eraseBtn.classList.add('active'); | |
| setActiveTool('eraser'); | |
| } else if (saved === 'spotlight') { | |
| spotlightBtn.classList.add('active'); | |
| setActiveTool('spotlight'); | |
| } | |
| // Color selector | |
| const colorTitle = document.createElement('div'); | |
| colorTitle.className = 'tools-section-title'; | |
| colorTitle.textContent = 'color'; | |
| tools.appendChild(colorTitle); | |
| const colorRow = document.createElement('div'); | |
| colorRow.className = 'tools-row color-row'; | |
| tools.appendChild(colorRow); | |
| const swatchColors = [ | |
| // Primary colors | |
| '#e53935', '#fb8c00', '#fdd835', '#43a047', '#1e88e5', '#8e24aa', | |
| // Additional useful colors | |
| '#ff5722', '#795548', '#607d8b', '#9c27b0', | |
| // Grayscale | |
| '#000000', '#424242', '#9e9e9e', '#ffffff' | |
| ]; | |
| const swatches = []; | |
| swatchColors.forEach(c => { | |
| const s = document.createElement('div'); | |
| s.className = 'color-swatch'; | |
| s.style.backgroundColor = c; | |
| s.title = c; | |
| s.onclick = () => { | |
| setStoredArrowColor(c); | |
| refreshColorUI(c); | |
| if (_cursorVisible) renderOverlay(); | |
| encodeToolStateToUrl(); | |
| }; | |
| colorRow.appendChild(s); | |
| swatches.push(s); | |
| }); | |
| const colorInput = document.createElement('input'); | |
| colorInput.type = 'color'; | |
| colorInput.className = 'color-input'; | |
| colorInput.oninput = () => { | |
| setStoredArrowColor(colorInput.value); | |
| refreshColorUI(colorInput.value); | |
| if (_cursorVisible) renderOverlay(); | |
| encodeToolStateToUrl(); | |
| }; | |
| colorRow.appendChild(colorInput); | |
| function refreshColorUI(selected) { | |
| const selectedHex = selected.startsWith('#') ? selected.toLowerCase() : rgbToHex(selected); | |
| swatches.forEach((s, i) => { | |
| const swatchHex = swatchColors[i].toLowerCase(); | |
| if (swatchHex === selectedHex) { | |
| s.classList.add('selected'); | |
| } else { | |
| s.classList.remove('selected'); | |
| } | |
| }); | |
| try { | |
| colorInput.value = selectedHex; | |
| } catch (_) {} | |
| } | |
| function rgbToHex(rgb) { | |
| const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)\)/i); | |
| if (!m) return '#000000'; | |
| const r = parseInt(m[1]).toString(16).padStart(2, '0'); | |
| const g = parseInt(m[2]).toString(16).padStart(2, '0'); | |
| const b = parseInt(m[3]).toString(16).padStart(2, '0'); | |
| return `#${r}${g}${b}`; | |
| } | |
| // Restore color selection | |
| refreshColorUI(getArrowColor()); | |
| // Thickness slider | |
| const thicknessTitle = document.createElement('div'); | |
| thicknessTitle.className = 'tools-section-title'; | |
| thicknessTitle.textContent = 'thickness'; | |
| tools.appendChild(thicknessTitle); | |
| const thicknessRow = document.createElement('div'); | |
| thicknessRow.className = 'thickness-row'; | |
| tools.appendChild(thicknessRow); | |
| const thicknessSlider = document.createElement('input'); | |
| thicknessSlider.type = 'range'; | |
| thicknessSlider.className = 'thickness-slider'; | |
| thicknessSlider.min = '1'; | |
| thicknessSlider.max = '10'; | |
| thicknessSlider.value = getLineThickness(); | |
| const thicknessValue = document.createElement('span'); | |
| thicknessValue.className = 'thickness-value'; | |
| thicknessValue.textContent = thicknessSlider.value + 'px'; | |
| thicknessSlider.oninput = function() { | |
| const value = parseInt(thicknessSlider.value, 10); | |
| setStoredLineThickness(value); | |
| thicknessValue.textContent = value + 'px'; | |
| if (_cursorVisible) renderOverlay(); | |
| encodeToolStateToUrl(); | |
| }; | |
| thicknessRow.appendChild(thicknessSlider); | |
| thicknessRow.appendChild(thicknessValue); | |
| // Fadeout time slider | |
| const fadeoutTitle = document.createElement('div'); | |
| fadeoutTitle.className = 'tools-section-title'; | |
| fadeoutTitle.textContent = 'fadeout time'; | |
| tools.appendChild(fadeoutTitle); | |
| const fadeoutRow = document.createElement('div'); | |
| fadeoutRow.className = 'thickness-row'; | |
| tools.appendChild(fadeoutRow); | |
| const fadeoutSlider = document.createElement('input'); | |
| fadeoutSlider.type = 'range'; | |
| fadeoutSlider.className = 'thickness-slider'; | |
| fadeoutSlider.min = '0'; | |
| fadeoutSlider.max = '30'; | |
| fadeoutSlider.value = getFadeoutTime(); | |
| const fadeoutValue = document.createElement('span'); | |
| fadeoutValue.className = 'thickness-value'; | |
| fadeoutValue.textContent = fadeoutSlider.value === '0' ? 'never' : fadeoutSlider.value + 's'; | |
| fadeoutSlider.oninput = function() { | |
| const value = parseInt(fadeoutSlider.value, 10); | |
| setStoredFadeoutTime(value); | |
| fadeoutValue.textContent = value === 0 ? 'never' : value + 's'; | |
| encodeToolStateToUrl(); | |
| }; | |
| fadeoutRow.appendChild(fadeoutSlider); | |
| fadeoutRow.appendChild(fadeoutValue); | |
| // Draggable behavior | |
| makeDraggable(tools, 'uvnote-tools-pos', title); | |
| return tools; | |
| } | |
| function initTools() { | |
| const widget = createToolsWidget(); | |
| document.body.appendChild(widget); | |
| } | |
| function teardownTools() { | |
| const w = document.querySelector('.tools-widget'); | |
| if (w && w.parentNode) w.parentNode.removeChild(w); | |
| } | |
| // --- Canvas overlay for tools --- | |
| let _overlay = null; | |
| let _overlayCtx = null; | |
| let _overlayContainer = null; // window | |
| let _overlayMode = 'single'; | |
| let _overlayResizeHandler = null; | |
| let _overlayScrollHandler = null; | |
| let _drawing = null; // current in-progress arrow {x1,y1,x2,y2} | |
| let _shapes = []; // committed shapes for current mode | |
| let _fadeTimer = null; // timer for fade animation | |
| let _urlLoadedTool = false; // track if tool was loaded from URL | |
| let _isInitializing = true; // prevent URL updates during initialization | |
| let _initialCellStates = {}; // track initial cell states from page load | |
| function getOverlayStorageKey() { return 'uvnote-shapes'; } | |
| function loadShapes() { | |
| try { | |
| const raw = localStorage.getItem(getOverlayStorageKey()); | |
| _shapes = raw ? JSON.parse(raw) : []; | |
| } catch (_) { _shapes = []; } | |
| } | |
| function saveShapes() { | |
| try { | |
| localStorage.setItem(getOverlayStorageKey(), JSON.stringify(_shapes)); | |
| // Always update URL when shapes change | |
| encodeToolStateToUrl(); | |
| } catch (_) {} | |
| } | |
| function updateShapesFade() { | |
| const now = Date.now(); | |
| let needsUpdate = false; | |
| for (let i = _shapes.length - 1; i >= 0; i--) { | |
| const shape = _shapes[i]; | |
| if (!shape.createdAt) continue; // Skip old shapes without timestamps | |
| // Use individual shape's fadeout time, or global if not set | |
| const shapesFadeoutSeconds = shape.fadeoutTime !== undefined ? shape.fadeoutTime : getFadeoutTime(); | |
| // Skip fading if fadeout is disabled for this shape | |
| if (shapesFadeoutSeconds === 0) continue; | |
| const fadeStartTime = Math.max(0, (shapesFadeoutSeconds - 2) * 1000); // Start fading 2s before end | |
| const fadeEndTime = shapesFadeoutSeconds * 1000; // Fully gone after specified time | |
| const age = now - shape.createdAt; | |
| if (age >= fadeEndTime) { | |
| // Remove completely faded shapes | |
| _shapes.splice(i, 1); | |
| needsUpdate = true; | |
| } else if (age >= fadeStartTime) { | |
| // Update opacity for fading shapes | |
| const fadeProgress = (age - fadeStartTime) / (fadeEndTime - fadeStartTime); | |
| const newOpacity = 1 - fadeProgress; | |
| if (Math.abs(shape.opacity - newOpacity) > 0.01) { | |
| shape.opacity = newOpacity; | |
| needsUpdate = true; | |
| } | |
| } | |
| } | |
| if (needsUpdate) { | |
| saveShapes(); | |
| renderOverlay(); | |
| // Update URL to remove faded shapes | |
| encodeToolStateToUrl(); | |
| } | |
| } | |
| function getContentContainer() { return window; } | |
| function updateOverlayModeAndContainer() { | |
| _overlayContainer = window; | |
| _overlayMode = 'single'; | |
| } | |
| function updateOverlayBounds() { | |
| if (!_overlay) return; | |
| if (_overlayContainer === window) { | |
| _overlay.style.position = 'fixed'; | |
| _overlay.style.left = '0px'; | |
| _overlay.style.top = '0px'; | |
| _overlay.width = window.innerWidth; | |
| _overlay.height = window.innerHeight; | |
| } else { | |
| const rect = _overlayContainer.getBoundingClientRect(); | |
| _overlay.style.position = 'fixed'; | |
| _overlay.style.left = rect.left + 'px'; | |
| _overlay.style.top = rect.top + 'px'; | |
| _overlay.width = Math.max(0, Math.floor(rect.width)); | |
| _overlay.height = Math.max(0, Math.floor(rect.height)); | |
| } | |
| renderOverlay(); | |
| } | |
| function containerScrollLeft() { | |
| return (_overlayContainer === window) ? (window.scrollX || 0) : (_overlayContainer.scrollLeft || 0); | |
| } | |
| function containerScrollTop() { | |
| return (_overlayContainer === window) ? (window.scrollY || 0) : (_overlayContainer.scrollTop || 0); | |
| } | |
| function toCanvasCoords(clientX, clientY) { | |
| const rect = _overlay.getBoundingClientRect(); | |
| return { x: clientX - rect.left, y: clientY - rect.top }; | |
| } | |
| function onPointerDown(e) { | |
| const tool = document.body.dataset.tool; | |
| if (tool === 'arrow') { | |
| startDrawArrow(e); | |
| } else if (tool === 'pen') { | |
| startDrawPen(e); | |
| } else if (tool === 'eraser') { | |
| eraseAt(e); | |
| } else if (tool === 'spotlight') { | |
| startDrawSpotlight(e); | |
| } | |
| } | |
| function onPointerMove(e) { | |
| // Update cursor position | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _cursorX = pt.x; | |
| _cursorY = pt.y; | |
| if (!_drawing) { | |
| // Just update cursor position and re-render | |
| if (_cursorVisible) { | |
| renderOverlay(); | |
| } | |
| return; | |
| } | |
| if (_drawing.type === 'pen') { | |
| moveDrawPen(e); | |
| } else if (_drawing.type === 'spotlight') { | |
| moveDrawSpotlight(e); | |
| } else { | |
| moveDrawArrow(e); | |
| } | |
| } | |
| function onPointerEnter(e) { | |
| _cursorVisible = document.body.dataset.tool !== 'none'; | |
| if (_cursorVisible) { | |
| renderOverlay(); | |
| } | |
| } | |
| function onPointerLeave(e) { | |
| _cursorVisible = false; | |
| renderOverlay(); | |
| } | |
| function onPointerUp(e) { | |
| if (!_drawing) return; | |
| if (_drawing.type === 'pen') { | |
| endDrawPen(); | |
| } else if (_drawing.type === 'spotlight') { | |
| endDrawSpotlight(); | |
| } else { | |
| endDrawArrow(); | |
| } | |
| } | |
| function startDrawArrow(e) { | |
| if (document.body.dataset.tool !== 'arrow') return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _drawing = { | |
| x1: pt.x + containerScrollLeft(), | |
| y1: pt.y + containerScrollTop(), | |
| x2: pt.x + containerScrollLeft(), | |
| y2: pt.y + containerScrollTop(), | |
| color: getArrowColor(), | |
| width: getLineThickness() | |
| }; | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function moveDrawArrow(e) { | |
| if (!_drawing) return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _drawing.x2 = pt.x + containerScrollLeft(); | |
| _drawing.y2 = pt.y + containerScrollTop(); | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function endDrawArrow() { | |
| if (!_drawing) return; | |
| _shapes.push({ | |
| type: 'arrow', | |
| ..._drawing, | |
| createdAt: Date.now(), | |
| fadeoutTime: getFadeoutTime(), | |
| opacity: 1.0 | |
| }); | |
| _drawing = null; | |
| saveShapes(); | |
| renderOverlay(); | |
| } | |
| function startDrawPen(e) { | |
| if (document.body.dataset.tool !== 'pen') return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _drawing = { | |
| type: 'pen', | |
| points: [{ | |
| x: pt.x + containerScrollLeft(), | |
| y: pt.y + containerScrollTop() | |
| }], | |
| color: getArrowColor(), | |
| width: getLineThickness() | |
| }; | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function moveDrawPen(e) { | |
| if (!_drawing || _drawing.type !== 'pen') return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _drawing.points.push({ | |
| x: pt.x + containerScrollLeft(), | |
| y: pt.y + containerScrollTop() | |
| }); | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function endDrawPen() { | |
| if (!_drawing || _drawing.type !== 'pen') return; | |
| if (_drawing.points.length > 1) { | |
| _shapes.push({ | |
| ..._drawing, | |
| createdAt: Date.now(), | |
| fadeoutTime: getFadeoutTime(), | |
| opacity: 1.0 | |
| }); | |
| } | |
| _drawing = null; | |
| saveShapes(); | |
| renderOverlay(); | |
| } | |
| function startDrawSpotlight(e) { | |
| if (document.body.dataset.tool !== 'spotlight') return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| _drawing = { | |
| type: 'spotlight', | |
| x: pt.x + containerScrollLeft(), | |
| y: pt.y + containerScrollTop(), | |
| radius: getLineThickness() * 20, // Use thickness to control spotlight size (bigger default) | |
| color: getArrowColor() | |
| }; | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function moveDrawSpotlight(e) { | |
| if (!_drawing || _drawing.type !== 'spotlight') return; | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| const dx = pt.x + containerScrollLeft() - _drawing.x; | |
| const dy = pt.y + containerScrollTop() - _drawing.y; | |
| _drawing.radius = Math.max(20, Math.sqrt(dx * dx + dy * dy)); // Minimum radius of 20 | |
| renderOverlay(); | |
| e.preventDefault(); | |
| } | |
| function endDrawSpotlight() { | |
| if (!_drawing || _drawing.type !== 'spotlight') return; | |
| _shapes.push({ | |
| ..._drawing, | |
| createdAt: Date.now(), | |
| fadeoutTime: getFadeoutTime(), | |
| opacity: 1.0 | |
| }); | |
| _drawing = null; | |
| saveShapes(); | |
| renderOverlay(); | |
| } | |
| function distPointToSegment(px, py, x1, y1, x2, y2) { | |
| const dx = x2 - x1, dy = y2 - y1; | |
| if (dx === 0 && dy === 0) return Math.hypot(px - x1, py - y1); | |
| const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / (dx*dx + dy*dy))); | |
| const cx = x1 + t * dx, cy = y1 + t * dy; | |
| return Math.hypot(px - cx, py - cy); | |
| } | |
| function eraseAt(e) { | |
| const pt = toCanvasCoords(e.touches ? e.touches[0].clientX : e.clientX, e.touches ? e.touches[0].clientY : e.clientY); | |
| const x = pt.x + containerScrollLeft(); | |
| const y = pt.y + containerScrollTop(); | |
| const threshold = 10; // pixels | |
| for (let i = _shapes.length - 1; i >= 0; i--) { | |
| const s = _shapes[i]; | |
| if (s.type === 'arrow') { | |
| const d = distPointToSegment(x, y, s.x1, s.y1, s.x2, s.y2); | |
| if (d <= threshold) { | |
| _shapes.splice(i, 1); | |
| saveShapes(); | |
| renderOverlay(); | |
| break; | |
| } | |
| } else if (s.type === 'pen' && s.points) { | |
| // Check if click is near any line segment in the pen stroke | |
| let minDist = Infinity; | |
| for (let j = 1; j < s.points.length; j++) { | |
| const d = distPointToSegment(x, y, s.points[j-1].x, s.points[j-1].y, s.points[j].x, s.points[j].y); | |
| minDist = Math.min(minDist, d); | |
| } | |
| if (minDist <= threshold) { | |
| _shapes.splice(i, 1); | |
| saveShapes(); | |
| renderOverlay(); | |
| break; | |
| } | |
| } | |
| } | |
| e.preventDefault(); | |
| } | |
| function drawArrow(ctx, x1, y1, x2, y2, color, width, opacity = 1.0) { | |
| // Set opacity | |
| const oldAlpha = ctx.globalAlpha; | |
| ctx.globalAlpha = opacity; | |
| ctx.strokeStyle = color; | |
| ctx.fillStyle = color; | |
| ctx.lineWidth = width; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| // Check if points are too close (initial state) | |
| const dx = x2 - x1; | |
| const dy = y2 - y1; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 5) { | |
| // Draw just a small arrowhead pointing down-right when first clicked | |
| const defaultAngle = Math.PI / 4; // 45 degrees (down-right) | |
| const headLength = Math.min(15 + width * 1.5, 25); | |
| const headAngle = Math.PI / 6; | |
| // Calculate arrowhead points | |
| const hx1 = x1 + headLength * Math.cos(defaultAngle - headAngle); | |
| const hy1 = y1 + headLength * Math.sin(defaultAngle - headAngle); | |
| const hx2 = x1 + headLength * Math.cos(defaultAngle + headAngle); | |
| const hy2 = y1 + headLength * Math.sin(defaultAngle + headAngle); | |
| // Draw arrowhead only | |
| ctx.beginPath(); | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(hx1, hy1); | |
| ctx.lineTo(hx2, hy2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } else { | |
| // Normal arrow drawing - head at x1,y1, tail at x2,y2 | |
| const angle = Math.atan2(y1 - y2, x1 - x2); | |
| const headLength = Math.min(15 + width * 1.5, 25); | |
| const headAngle = Math.PI / 6; | |
| // Calculate where the line should end (before the arrowhead) | |
| const lineEndX = x1 - headLength * 0.8 * Math.cos(angle); | |
| const lineEndY = y1 - headLength * 0.8 * Math.sin(angle); | |
| // Draw the line from tail to near the head | |
| ctx.beginPath(); | |
| ctx.moveTo(x2, y2); | |
| ctx.lineTo(lineEndX, lineEndY); | |
| ctx.stroke(); | |
| // Calculate arrowhead points | |
| const hx1 = x1 - headLength * Math.cos(angle - headAngle); | |
| const hy1 = y1 - headLength * Math.sin(angle - headAngle); | |
| const hx2 = x1 - headLength * Math.cos(angle + headAngle); | |
| const hy2 = y1 - headLength * Math.sin(angle + headAngle); | |
| // Draw arrowhead | |
| ctx.beginPath(); | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(hx1, hy1); | |
| ctx.lineTo(hx2, hy2); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| // Restore opacity | |
| ctx.globalAlpha = oldAlpha; | |
| } | |
| function drawPen(ctx, points, color, width, offX, offY, opacity = 1.0) { | |
| if (!points || points.length < 2) return; | |
| // Set opacity | |
| const oldAlpha = ctx.globalAlpha; | |
| ctx.globalAlpha = opacity; | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = width; | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| ctx.beginPath(); | |
| ctx.moveTo(points[0].x - offX, points[0].y - offY); | |
| for (let i = 1; i < points.length; i++) { | |
| ctx.lineTo(points[i].x - offX, points[i].y - offY); | |
| } | |
| ctx.stroke(); | |
| // Restore opacity | |
| ctx.globalAlpha = oldAlpha; | |
| } | |
| function drawAllSpotlights(ctx, spotlights, offX, offY) { | |
| if (!spotlights || spotlights.length === 0) return; | |
| ctx.save(); | |
| // Calculate the overall opacity based on all spotlights | |
| const maxOpacity = Math.max(...spotlights.map(s => s.opacity || 1.0)); | |
| // Fill entire canvas with dark overlay | |
| ctx.fillStyle = `rgba(0, 0, 0, ${0.7 * maxOpacity})`; | |
| ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); | |
| // Cut out completely transparent holes for all spotlights | |
| ctx.globalCompositeOperation = 'destination-out'; | |
| ctx.fillStyle = 'rgba(0, 0, 0, 1)'; // Solid black to ensure complete removal | |
| for (const spotlight of spotlights) { | |
| ctx.beginPath(); | |
| ctx.arc(spotlight.x - offX, spotlight.y - offY, spotlight.radius, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| function renderOverlay() { | |
| if (!_overlay || !_overlayCtx) return; | |
| _overlayCtx.clearRect(0, 0, _overlay.width, _overlay.height); | |
| const offX = containerScrollLeft(); | |
| const offY = containerScrollTop(); | |
| // Draw non-spotlight shapes first | |
| for (const s of _shapes) { | |
| const opacity = s.opacity !== undefined ? s.opacity : 1.0; | |
| if (s.type === 'arrow') { | |
| drawArrow(_overlayCtx, s.x1 - offX, s.y1 - offY, s.x2 - offX, s.y2 - offY, s.color || '#f00', s.width || 2, opacity); | |
| } else if (s.type === 'pen') { | |
| drawPen(_overlayCtx, s.points, s.color || '#f00', s.width || 2, offX, offY, opacity); | |
| } | |
| } | |
| // Draw current drawing (non-spotlight) | |
| if (_drawing) { | |
| if (_drawing.type === 'pen') { | |
| drawPen(_overlayCtx, _drawing.points, _drawing.color, _drawing.width, offX, offY); | |
| } else if (_drawing.type !== 'spotlight') { | |
| drawArrow(_overlayCtx, _drawing.x1 - offX, _drawing.y1 - offY, _drawing.x2 - offX, _drawing.y2 - offY, _drawing.color, _drawing.width); | |
| } | |
| } | |
| // Collect all spotlights (existing + current drawing + cursor preview) | |
| const spotlights = []; | |
| // Add existing spotlight shapes | |
| for (const s of _shapes) { | |
| if (s.type === 'spotlight') { | |
| spotlights.push({ | |
| x: s.x, | |
| y: s.y, | |
| radius: s.radius, | |
| opacity: s.opacity !== undefined ? s.opacity : 1.0 | |
| }); | |
| } | |
| } | |
| // Add current spotlight being drawn | |
| if (_drawing && _drawing.type === 'spotlight') { | |
| spotlights.push({ | |
| x: _drawing.x, | |
| y: _drawing.y, | |
| radius: _drawing.radius, | |
| opacity: 1.0 | |
| }); | |
| } | |
| // Add cursor preview spotlight if tool is active | |
| if (_cursorVisible && !_drawing) { | |
| const tool = document.body.dataset.tool; | |
| if (tool === 'spotlight') { | |
| const thickness = getLineThickness(); | |
| const radius = thickness * 20; | |
| const cursorWorldX = _cursorX + containerScrollLeft(); | |
| const cursorWorldY = _cursorY + containerScrollTop(); | |
| spotlights.push({ | |
| x: cursorWorldX, | |
| y: cursorWorldY, | |
| radius: radius, | |
| opacity: 0.8 | |
| }); | |
| } | |
| } | |
| // Draw all spotlights as a single overlay with multiple holes | |
| drawAllSpotlights(_overlayCtx, spotlights, offX, offY); | |
| // Draw cursor indicators for non-spotlight tools | |
| if (_cursorVisible && !_drawing) { | |
| const tool = document.body.dataset.tool; | |
| const color = getArrowColor(); | |
| const thickness = getLineThickness(); | |
| if (tool !== 'spotlight') { | |
| _overlayCtx.save(); | |
| _overlayCtx.fillStyle = color; | |
| _overlayCtx.globalAlpha = 0.7; | |
| if (tool === 'eraser') { | |
| // Draw eraser indicator | |
| _overlayCtx.strokeStyle = color; | |
| _overlayCtx.lineWidth = 2; | |
| _overlayCtx.beginPath(); | |
| _overlayCtx.arc(_cursorX, _cursorY, 10, 0, 2 * Math.PI); | |
| _overlayCtx.stroke(); | |
| } else { | |
| // Draw dot for pen/arrow | |
| _overlayCtx.beginPath(); | |
| _overlayCtx.arc(_cursorX, _cursorY, thickness / 2, 0, 2 * Math.PI); | |
| _overlayCtx.fill(); | |
| } | |
| _overlayCtx.restore(); | |
| } | |
| } | |
| } | |
| function setOverlayActive(active) { | |
| if (!_overlay) initOverlay(); | |
| _overlay.style.pointerEvents = active ? 'auto' : 'none'; | |
| _overlay.style.cursor = active ? 'none' : 'auto'; | |
| // Re-render to ensure visibility aligns with content | |
| renderOverlay(); | |
| } | |
| function initOverlay() { | |
| if (_overlay) return; | |
| updateOverlayModeAndContainer(); | |
| _overlay = document.createElement('canvas'); | |
| _overlay.className = 'draw-overlay'; | |
| _overlayCtx = _overlay.getContext('2d'); | |
| document.body.appendChild(_overlay); | |
| updateOverlayBounds(); | |
| loadShapes(); | |
| renderOverlay(); | |
| // Events | |
| _overlay.addEventListener('mousedown', onPointerDown); | |
| _overlay.addEventListener('mousemove', onPointerMove); | |
| _overlay.addEventListener('mouseenter', onPointerEnter); | |
| _overlay.addEventListener('mouseleave', onPointerLeave); | |
| document.addEventListener('mouseup', onPointerUp); | |
| _overlay.addEventListener('touchstart', onPointerDown, { passive: false }); | |
| _overlay.addEventListener('touchmove', onPointerMove, { passive: false }); | |
| document.addEventListener('touchend', onPointerUp); | |
| _overlayResizeHandler = () => updateOverlayBounds(); | |
| window.addEventListener('resize', _overlayResizeHandler); | |
| _overlayScrollHandler = () => renderOverlay(); | |
| window.addEventListener('scroll', _overlayScrollHandler); | |
| // Start fade animation timer | |
| _fadeTimer = setInterval(updateShapesFade, 100); // Update every 100ms for smooth fade | |
| } | |
| function rebindOverlayContainer() { | |
| if (!_overlay) return; | |
| // Remove old scroll handler | |
| if (_overlayScrollHandler) { window.removeEventListener('scroll', _overlayScrollHandler); } | |
| updateOverlayModeAndContainer(); | |
| updateOverlayBounds(); | |
| loadShapes(); | |
| renderOverlay(); | |
| _overlayScrollHandler = () => renderOverlay(); | |
| window.addEventListener('scroll', _overlayScrollHandler); | |
| } | |
| function teardownOverlay() { | |
| if (!_overlay) return; | |
| _overlay.removeEventListener('mousedown', onPointerDown); | |
| _overlay.removeEventListener('mousemove', onPointerMove); | |
| _overlay.removeEventListener('mouseenter', onPointerEnter); | |
| _overlay.removeEventListener('mouseleave', onPointerLeave); | |
| document.removeEventListener('mouseup', onPointerUp); | |
| _overlay.removeEventListener('touchstart', onPointerDown); | |
| _overlay.removeEventListener('touchmove', onPointerMove); | |
| document.removeEventListener('touchend', onPointerUp); | |
| if (_overlayResizeHandler) window.removeEventListener('resize', _overlayResizeHandler); | |
| if (_overlayScrollHandler) { | |
| if (_overlayContainer === window) { | |
| window.removeEventListener('scroll', _overlayScrollHandler); | |
| } else if (_overlayContainer) { | |
| _overlayContainer.removeEventListener('scroll', _overlayScrollHandler); | |
| } | |
| } | |
| if (_fadeTimer) { | |
| clearInterval(_fadeTimer); | |
| _fadeTimer = null; | |
| } | |
| if (_overlay.parentNode) _overlay.parentNode.removeChild(_overlay); | |
| _overlay = null; _overlayCtx = null; _overlayContainer = null; _overlayResizeHandler = null; _overlayScrollHandler = null; _drawing = null; | |
| } | |
| function teardownFileExplorer() { | |
| const fe = document.querySelector('.file-explorer'); | |
| if (fe && fe.parentNode) fe.parentNode.removeChild(fe); | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function runCell(cellId){ | |
| const btn=document.querySelector('.run-btn[onclick*="'+cellId+'"]'); | |
| const output=document.getElementById('output-'+cellId); | |
| if(btn){btn.textContent='⏳ running...';btn.disabled=true;} | |
| if(output){output.classList.add('output-stale');} | |
| fetch('/run/'+cellId,{method:'POST'}).then(r=>r.json()).then(data=>{ | |
| if(output){ | |
| output.classList.remove('output-stale'); | |
| let html=''; | |
| if (data.stdout) { | |
| html += '<div class="cell-stdout"><pre class="stdout-text">' | |
| + escapeHtml(data.stdout) | |
| + '</pre></div>'; | |
| } | |
| console.log('UV Logs:', data); | |
| if(data.stderr) { | |
| // Split UV logs from regular stderr | |
| const lines = data.stderr.split('\\n'); | |
| let uvLogs = []; | |
| let regularLogs = []; | |
| let inUvSection = true; | |
| for (const line of lines) { | |
| if (inUvSection) { | |
| uvLogs.push(line); | |
| if (line.startsWith('Installed ')) { | |
| inUvSection = false; | |
| } | |
| } else { | |
| regularLogs.push(line); | |
| } | |
| } | |
| // If we never found "Installed", treat it all as regular stderr | |
| if (inUvSection) { | |
| html+='<div class="cell-stderr">'+escapeHtml(data.stderr)+'</div>'; | |
| } else { | |
| const uvLogsStr = uvLogs.join('\\n'); | |
| const regularLogsStr = regularLogs.join('\\n').trim(); | |
| if (uvLogsStr) { | |
| html+='<div class="uv-install-logs">'; | |
| html+='<div class="uv-logs-header" onclick="toggleUvLogs(this)">▶ UV Install Logs</div>'; | |
| html+='<div class="uv-logs-content" style="display: none;">'+escapeHtml(uvLogsStr)+'</div>'; | |
| html+='</div>'; | |
| } | |
| if (regularLogsStr) { | |
| html+='<div class="cell-stderr">'+escapeHtml(regularLogsStr)+'</div>'; | |
| } | |
| } | |
| } | |
| output.innerHTML=html; | |
| } | |
| if(btn){btn.textContent='▶ run';btn.disabled=false;} | |
| }).catch(e=>{ | |
| console.error('Run failed:',e); | |
| if(output){output.classList.remove('output-stale');} | |
| if(btn){btn.textContent='▶ run';btn.disabled=false;} | |
| }); | |
| } | |
| function copyCell(cellId){ | |
| // Try multiple selectors to find the code element | |
| // Pygments generates .highlight > pre with spans, not wrapped in <code> | |
| let codeElement = document.querySelector('#code-'+cellId+' .highlight pre'); | |
| if (!codeElement) { | |
| codeElement = document.querySelector('#code-'+cellId+' pre'); | |
| } | |
| if (!codeElement) { | |
| codeElement = document.querySelector('#code-'+cellId+' code'); | |
| } | |
| if (!codeElement) { | |
| // Fallback to the code div itself | |
| codeElement = document.getElementById('code-'+cellId); | |
| } | |
| const btn = document.querySelector('.copy-btn[onclick*="'+cellId+'"]'); | |
| if (!codeElement) { | |
| console.error('Code element not found for cell:', cellId); | |
| return; | |
| } | |
| if (!btn) { | |
| console.error('Copy button not found for cell:', cellId); | |
| return; | |
| } | |
| const codeText = codeElement.textContent; | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(codeText).then(function() { | |
| console.log('Clipboard copy successful'); | |
| btn.textContent = '✓ Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(function() { | |
| btn.textContent = 'Copy'; | |
| btn.classList.remove('copied'); | |
| }, 2000); | |
| }).catch(function(err) { | |
| console.warn('Clipboard copy failed:', err); | |
| fallbackCopy(); | |
| }); | |
| } else { | |
| console.log('Using fallback copy method'); | |
| fallbackCopy(); | |
| } | |
| function fallbackCopy() { | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = codeText; | |
| textarea.style.position = 'absolute'; | |
| textarea.style.left = '-9999px'; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| try { | |
| const success = document.execCommand('copy'); | |
| console.log('Fallback copy success:', success); | |
| btn.textContent = '✓ Copied!'; | |
| btn.classList.add('copied'); | |
| setTimeout(function() { | |
| btn.textContent = 'Copy'; | |
| btn.classList.remove('copied'); | |
| }, 2000); | |
| } catch (err) { | |
| console.error('Fallback copy failed:', err); | |
| btn.textContent = 'Copy failed'; | |
| setTimeout(function() { | |
| btn.textContent = 'Copy'; | |
| }, 2000); | |
| } | |
| document.body.removeChild(textarea); | |
| } | |
| } | |
| // // Live reload functionality (robust SSE handling) | |
| // (function(){ | |
| // if (!('EventSource' in window)) { | |
| // console.warn('SSE not supported in this browser'); | |
| // return; | |
| // } | |
| // let source = new EventSource('/events'); | |
| // let isOpen = false; | |
| // source.onopen = function(){ isOpen = true; console.log('SSE connected'); }; | |
| // source.onmessage = function(e){ | |
| // const msg=(e.data||'').trim(); if(!msg) return; | |
| // console.log('SSE message:', msg); | |
| // if (msg==='reload' || msg==='incremental') { location.reload(); } | |
| // // Ignore 'loading' to avoid premature reload loops | |
| // }; | |
| // source.onerror = function(e){ | |
| // // Let EventSource auto-reconnect instead of forcing a reload | |
| // if (isOpen) console.warn('SSE error after open, retrying...', e); | |
| // }; | |
| // window.addEventListener('beforeunload', function(){ try{source.close();}catch(_){} }); | |
| // })(); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Capture initial cell states before any modifications | |
| captureInitialCellStates(); | |
| updateThemeIcon(); | |
| updateUiMenu(); | |
| updateUiDebug(); | |
| const widgetsEnabled = (document.documentElement.getAttribute('data-widgets') || 'on') === 'on'; | |
| if (widgetsEnabled) { | |
| initMinimap(); | |
| initFileExplorer(); | |
| initTools(); | |
| initOverlay(); | |
| initStatusWidget(); | |
| initializeWidgetVisibility(); | |
| layoutWidgetsStackedBottomRight(); | |
| window.addEventListener('resize', layoutWidgetsStackedBottomRight); | |
| } | |
| // Apply deep-link selection if present | |
| applyLocationFromUrl(); | |
| updateStateIndicator(); | |
| // Apply cell states from URL immediately | |
| const url = new URL(window.location.href); | |
| const encodedCellStates = url.searchParams.get('cells'); | |
| if (encodedCellStates) { | |
| console.log('Applying cell states from URL...'); | |
| const cellStates = decodeCellStatesFromUrl(encodedCellStates); | |
| // Use requestAnimationFrame to ensure DOM is ready | |
| requestAnimationFrame(() => { | |
| applyCellStatesFromUrl(cellStates); | |
| // Clear initialization flag after cell states are applied | |
| if (typeof _isInitializing !== 'undefined') { | |
| _isInitializing = false; | |
| } | |
| }); | |
| } else { | |
| // Clear initialization flag even if no cell states | |
| if (typeof _isInitializing !== 'undefined') { | |
| requestAnimationFrame(() => { | |
| _isInitializing = false; | |
| }); | |
| } | |
| } | |
| // Bind drag selection on line numbers | |
| document.addEventListener('mousedown', onLineNumberMouseDown); | |
| document.addEventListener('mousemove', onDocMouseMove); | |
| document.addEventListener('mouseup', onDocMouseUp); | |
| // Add ESC key handler to exit tools | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape' || e.keyCode === 27) { | |
| const currentTool = document.body.dataset.tool; | |
| if (currentTool && currentTool !== 'none') { | |
| // Deactivate the current tool | |
| window.setActiveTool('none'); | |
| } | |
| // Also clear any active line selection | |
| clearSelection(true); | |
| } | |
| }); | |
| }); | |
| function updateStateIndicator() { | |
| try { | |
| const el = document.getElementById('status-widget'); | |
| if (!el) return; | |
| const tool = document.body.dataset.tool || 'none'; | |
| if (tool && tool !== 'none') { | |
| el.textContent = `tool: ${tool} — Esc`; | |
| return; | |
| } | |
| if (_selection) { | |
| const t = _selection.a === _selection.b ? `L${_selection.a}` : `L${_selection.a}-${_selection.b}`; | |
| el.textContent = `selected: ${t} — Esc`; | |
| return; | |
| } | |
| el.textContent = 'ready — Esc'; | |
| } catch (_) {} | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div class="controls"> | |
| <div class="controls-buttons"> | |
| <a href="index.html" class="back-button">← back</a> | |
| <div class="theme-toggle" onclick="toggleTheme()">light</div> | |
| <div class="reset-toggle" onclick="resetLayout()">reset</div> | |
| <div class="menu-button" onclick="toggleMenu()"> | |
| menu ▼ | |
| <div class="menu-dropdown"> | |
| <div class="menu-item" onclick="setUiTheme('default')"> | |
| <span class="menu-checkbox" id="checkbox-ui-default">☑</span> Theme: default | |
| </div> | |
| <div class="menu-item" onclick="setUiTheme('none')"> | |
| <span class="menu-checkbox" id="checkbox-ui-none">☐</span> Theme: none | |
| </div> | |
| <div class="menu-item" onclick="setUiTheme('monocolor')"> | |
| <span class="menu-checkbox" id="checkbox-ui-monocolor">☐</span> Theme: monocolor | |
| </div> | |
| <div class="menu-item" onclick="toggleWidget('tools')"> | |
| <span class="menu-checkbox" id="checkbox-tools">☐</span> Tools | |
| </div> | |
| <div class="menu-item" onclick="toggleWidget('file-explorer')"> | |
| <span class="menu-checkbox" id="checkbox-file-explorer">☐</span> File Explorer | |
| </div> | |
| <div class="menu-item" onclick="toggleWidget('minimap')"> | |
| <span class="menu-checkbox" id="checkbox-minimap">☐</span> Table of Contents | |
| </div> | |
| <div class="menu-item" onclick="toggleWidget('status')"> | |
| <span class="menu-checkbox" id="checkbox-status">☑</span> Status Indicator | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="system-info"> | |
| <div class="system-info-header">Generated on:</div> | |
| <div class="system-info-content"> | |
| Linux x86_64 | Linux-5.10.244-240.970.amzn2.x86_64-x86_64-with-glibc2.35 | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <h1>All Benchmarks Aggregated Report</h1> | |
| <h2><a href="layer_norm/">Layer Norm</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="layer_norm/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="layer_norm/impls/hf_kernels_layer_norm.html">HF Kernels Layer Norm</a></td> | |
| <td>HuggingFace kernels implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="layer_norm/impls/torch_layer_norm.html">PyTorch Layer Norm</a></td> | |
| <td>PyTorch native implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h2><a href="rotary/">Rotary Position Embeddings</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="rotary/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="rotary/impls/hf_kernels_rotary.html">HF Kernels Rotary</a></td> | |
| <td>HuggingFace kernels implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="rotary/impls/torch_rotary.html">PyTorch Rotary</a></td> | |
| <td>PyTorch native implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h2><a href="flash_attn/">Flash Attention</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="flash_attn/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="flash_attn/impls/flash_attention.html">Flash Attention</a></td> | |
| <td>Flash Attention implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="flash_attn/impls/hf_kernels_flash_attn.html">HF Kernels Flash Attention</a></td> | |
| <td>HuggingFace kernels Flash Attention</td> | |
| </tr> | |
| <tr> | |
| <td><a href="flash_attn/impls/hf_kernels_flash_attn3.html">HF Kernels Flash Attention 3</a></td> | |
| <td>HuggingFace kernels Flash Attention 3</td> | |
| </tr> | |
| <tr> | |
| <td><a href="flash_attn/impls/mem_efficient_attention.html">Memory Efficient Attention</a></td> | |
| <td>Memory efficient attention implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="flash_attn/impls/sage_attention.html">Sage Attention</a></td> | |
| <td>Sage attention implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="flash_attn/impls/xformers.html">xFormers</a></td> | |
| <td>xFormers attention implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h2><a href="causal_conv1d/">Causal Conv1D</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="causal_conv1d/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="causal_conv1d/impls/hf_kernels_causal_conv1d.html">HF Kernels Causal Conv1D</a></td> | |
| <td>HuggingFace kernels implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="causal_conv1d/impls/torch_causal_conv1d.html">PyTorch Causal Conv1D</a></td> | |
| <td>PyTorch native implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h2><a href="activation/">Activation</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="activation/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="activation/impls/hf_kernels_swiglu.html">HF Kernels SwiGLU</a></td> | |
| <td>HuggingFace kernels SwiGLU implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="activation/impls/torch_swiglu.html">PyTorch SwiGLU</a></td> | |
| <td>PyTorch native SwiGLU implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <h2><a href="relu/">ReLU</a></h2> | |
| <div class="artifact-preview"> | |
| <object data="relu/results/artifacts/combine/latency.svg" type="image/svg+xml" width="800"> | |
| </object> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Implementation</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><a href="relu/impls/hf_kernels_relu.html">HF Kernels ReLU</a></td> | |
| <td>HuggingFace kernels ReLU implementation</td> | |
| </tr> | |
| <tr> | |
| <td><a href="relu/impls/torch_relu.html">PyTorch ReLU</a></td> | |
| <td>PyTorch native ReLU implementation</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </body> | |
| </html> |