From 739a64e7121a7ab8e5171215b31d6af3087054e6 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Wed, 7 Oct 2026 19:38:27 +0200 Subject: [PATCH 01/11] ndg-templates: synchronize theme color transitions --- crates/ndg-templates/templates/default.css | 302 +++++++++----------- crates/ndg-templates/templates/main.js | 12 + crates/ndg-templates/templates/main.test.js | 55 +++- 3 files changed, 208 insertions(+), 161 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index 98c1b9374..af89c900b 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -124,6 +124,13 @@ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); --transition: 250ms cubic-bezier(0.4, 0, 0.2, 1); + /* Color transitions compose with component movement and hover timing. */ + --theme-colors: + color var(--theme-transition, var(--transition)), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)), + box-shadow var(--theme-transition, var(--transition)); + /* Fonts */ --font-mono: "JetBrains Mono", monospace; } @@ -212,6 +219,22 @@ --admonition-caution-color: #f97316; } +/* Keep the built-in themed surfaces in step while switching themes. */ +:where(body, header, .header-nav, .site-title a, footer, .sidebar, .sidebar-toggle, .sidebar-section > summary, .sidebar-dir-group, .toc-list, .toc-list ul, .toc-list ul > li::before, .toc-list a, .toc-list summary, .toc-list summary::before, .page-toc-nav h3, .page-toc-list a, .content h1, .content h2, .content h3, .content h4, .content h5, .content h6, a, code, pre, blockquote, table, th, td, section.footnotes, .theme-toggle, .search-container input, .search-results, .search-result-item, .search-result-anchor::before, .search-result-preview, .search-keyboard-hints, .search-keyboard-hints kbd, .search-error, .search-error button, .search-error a, .mobile-search-popup, .mobile-search-container, .mobile-search-results, .mobile-search-header, .mobile-search-close, #mobile-search-input, .mobile-sidebar-container, .mobile-sidebar-header, .mobile-sidebar-close, #search-page-input, #options-filter, #lib-filter, .filter-clear, .filter-results, .option, .option-type, .options-chunk-status, .options-chunk-error, .options-chunk-noscript, .lib-entry, .lib-entry-signature, .lib-entry-arguments dd, .lib-entry-deprecated, .lib-entry-warnings, .admonition, .admonition-title, mark, .copy-link, .copy-feedback) { + transition: var(--theme-colors); +} + +:root.theme-changing { + --theme-transition: var(--transition); +} + +@media (prefers-reduced-motion: reduce) { + :root { + --transition: 0s; + --transition-fast: 0s; + } +} + /* Smooth scrolling */ html { scroll-behavior: smooth; @@ -239,9 +262,8 @@ body { background-color: var(--bg-color); margin: 0; padding: 0; - transition: - background-color var(--transition), - color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), color var(--theme-transition, var(--transition)); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -301,9 +323,8 @@ header { border-bottom: 1px solid var(--header-border); z-index: 100; top: 0px; - transition: - background-color var(--transition), - border-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, var(--transition)); backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: saturate(180%) blur(12px); height: var(--header-height); @@ -325,7 +346,7 @@ header { a { color: var(--header-text); text-decoration: none; - transition: opacity var(--transition-fast); + transition: var(--theme-colors), opacity var(--transition-fast); } a:hover { @@ -359,7 +380,8 @@ header { font-size: 0.95rem; font-weight: 500; padding: var(--space-2) 0; - transition: color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)); display: block; } @@ -417,7 +439,7 @@ header { overflow-y: auto; z-index: 10; box-sizing: border-box; - transition: transform var(--transition); + transition: var(--theme-colors), transform var(--transition); font-size: 0.875rem; scrollbar-gutter: stable; @@ -429,7 +451,8 @@ header { margin-bottom: var(--space-2); color: var(--text-muted); font-weight: 600; - transition: color var(--transition); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition)); } ul { @@ -453,9 +476,9 @@ header { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: - color var(--transition-fast), - background-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), background-color var(--theme-transition, + var(--transition-fast)); font-size: 0.875rem; &:hover { @@ -499,7 +522,7 @@ header { border-radius: 0 var(--radius-md) var(--radius-md) 0; cursor: pointer; z-index: 20; - transition: left var(--transition); + transition: var(--theme-colors), left var(--transition); display: flex; align-items: center; justify-content: center; @@ -520,7 +543,7 @@ header { width: 16px; height: 16px; fill: var(--text-color); - transition: transform var(--transition); + transition: var(--theme-colors), transform var(--transition); } .sidebar-collapsed &, @@ -543,9 +566,8 @@ footer { color: var(--text-muted); margin-top: auto; border-top: 1px solid var(--border-color); - transition: - background-color var(--transition), - border-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, var(--transition)); } .content { @@ -562,7 +584,8 @@ footer { position: relative; cursor: pointer; font-weight: 600; - transition: color var(--transition); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition)); letter-spacing: -0.015em; } @@ -580,12 +603,14 @@ footer { border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; margin-top: 1em; - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); } h2 { font-size: 1.75rem; - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); } h3 { @@ -616,9 +641,9 @@ footer { a { color: var(--link-color); text-decoration: none; - transition: - color var(--transition-fast), - text-decoration-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), text-decoration-color var(--theme-transition, + var(--transition-fast)); text-underline-offset: 0.15em; } @@ -646,7 +671,8 @@ code { border-radius: var(--radius-sm); padding: 0.2em 0.4em; font-size: 0.875em; - transition: background-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)); letter-spacing: -0.025em; border: 1px solid var(--code-border); } @@ -658,9 +684,8 @@ pre { overflow-x: auto; margin: 1.5em 0; border: 1px solid var(--border-color); - transition: - background-color var(--transition), - border-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, var(--transition)); line-height: 1.6; position: relative; } @@ -686,9 +711,9 @@ blockquote { border-radius: 0 var(--radius-md) var(--radius-md) 0; color: var(--text-color); font-style: italic; - transition: - border-color var(--transition), - background-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)), background-color var(--theme-transition, + var(--transition)); } blockquote p:first-of-type { @@ -714,13 +739,15 @@ td { border: 1px solid var(--border-color); padding: var(--space-2) var(--space-3); text-align: left; - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); } th { background-color: var(--sidebar-bg); font-weight: 600; - transition: background-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)); border-bottom-width: 2px; } @@ -795,9 +822,9 @@ img { border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer; - transition: - color var(--transition-fast), - border-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), border-color var(--theme-transition, + var(--transition-fast)); &:hover { color: var(--header-text); @@ -859,11 +886,10 @@ img { font-size: 0.875rem; background-color: var(--bg-surface); color: var(--text-color); - transition: - background-color var(--transition-fast), - border-color var(--transition-fast), - box-shadow var(--transition-fast), - width var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition-fast)), border-color var(--theme-transition, + var(--transition-fast)), box-shadow var(--theme-transition, + var(--transition-fast)), width var(--transition); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: 0.75rem center; @@ -898,15 +924,15 @@ img { box-shadow: var(--shadow-lg); display: none; z-index: 1000; - transition: - background-color var(--transition), - border-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, var(--transition)); } .search-result-item { padding: var(--space-4); border-bottom: 1px solid var(--border-color); - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); min-height: 44px; display: flex; align-items: center; @@ -1093,9 +1119,9 @@ img { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: - border-color var(--transition-fast), - background-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition-fast)), background-color var(--theme-transition, + var(--transition)); box-sizing: border-box; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -1185,10 +1211,9 @@ img { border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--sidebar-bg); - transition: - transform var(--transition-fast), - border-color var(--transition-fast), - box-shadow var(--transition-fast); + transition: var(--theme-colors), transform var(--transition-fast), + border-color var(--theme-transition, var(--transition-fast)), box-shadow + var(--theme-transition, var(--transition-fast)); } .search-page-results .search-result-item:hover { @@ -1254,9 +1279,8 @@ mark { background-color: var(--sidebar-bg); overflow: hidden; box-shadow: var(--shadow-md); - transition: - background-color var(--transition), - box-shadow var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), box-shadow var(--theme-transition, var(--transition)); border: 1px solid var(--border-color); } @@ -1268,7 +1292,8 @@ mark { align-items: center; font-size: 1rem !important; border-bottom: 1px solid var(--border-color); - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); } .admonition > :not(.admonition-title) { @@ -1398,51 +1423,35 @@ mark { /* Dark mode SVG icon adjustments */ @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) - .admonition.note - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.note .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2360a5fa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.tip - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.tip .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2334d399' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.warning - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fbbf24' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.danger - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.danger .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f87171' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.info - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.info .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.important - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.example - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2322d3ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) - .admonition.caution - .admonition-title::before { + :where(:root:not([data-theme="light"])) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fb923c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1467,9 +1476,7 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) - .admonition.important - .admonition-title::before { +:where(:root[data-theme="dark"]) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } @@ -1483,51 +1490,35 @@ mark { /* Light mode SVG icon adjustments */ @media (prefers-color-scheme: light) { - :where(:root:not([data-theme="dark"])) - .admonition.note - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.note .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.tip - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.tip .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.warning - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.danger - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.danger .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.info - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.info .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.important - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.example - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) - .admonition.caution - .admonition-title::before { + :where(:root:not([data-theme="dark"])) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1540,9 +1531,7 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) - .admonition.warning - .admonition-title::before { +:where(:root[data-theme="light"]) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } @@ -1554,21 +1543,15 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) - .admonition.important - .admonition-title::before { +:where(:root[data-theme="light"]) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) - .admonition.example - .admonition-title::before { +:where(:root[data-theme="light"]) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) - .admonition.caution - .admonition-title::before { +:where(:root[data-theme="light"]) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } @@ -1603,9 +1586,8 @@ mark { background-position: center; cursor: pointer; opacity: 0; - transition: - opacity var(--transition-fast), - background-color var(--transition-fast); + transition: var(--theme-colors), opacity var(--transition-fast), + background-color var(--theme-transition, var(--transition-fast)); vertical-align: middle; border-radius: var(--radius-sm); } @@ -1671,7 +1653,8 @@ h6:hover .copy-link { ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; - transition: background-color var(--transition); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)); border: 2px solid var(--bg-color); } @@ -1785,9 +1768,9 @@ h6:hover .copy-link { background-size: 18px; background-repeat: no-repeat; border: 1px solid rgba(255, 255, 255, 0.1); - transition: - background-color var(--transition-fast), - border-color var(--transition-fast); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition-fast)), border-color var(--theme-transition, + var(--transition-fast)); } .search-container #search-input::placeholder { @@ -1894,7 +1877,8 @@ h6:hover .copy-link { list-style-type: none; padding-left: 0; margin: var(--space-6) 0; - transition: border-color var(--transition); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition)); background-color: transparent; padding: var(--space-2) 0; } @@ -1927,9 +1911,9 @@ h6:hover .copy-link { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: - color var(--transition-fast), - background-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), background-color var(--theme-transition, + var(--transition-fast)); font-size: 0.875rem; white-space: nowrap; overflow: hidden; @@ -1967,10 +1951,9 @@ h6:hover .copy-link { text-overflow: ellipsis; list-style: none; border-radius: var(--radius-md); - transition: - transform var(--transition-fast), - color var(--transition-fast), - background-color var(--transition-fast); + transition: var(--theme-colors), transform var(--transition-fast), color + var(--theme-transition, var(--transition-fast)), background-color + var(--theme-transition, var(--transition-fast)); } .toc-list details summary:hover { @@ -1987,9 +1970,8 @@ h6:hover .copy-link { font-size: 0.65rem; flex-shrink: 0; color: var(--text-muted); - transition: - transform var(--transition-fast), - color var(--transition-fast); + transition: var(--theme-colors), transform var(--transition-fast), color + var(--theme-transition, var(--transition-fast)); } .toc-list details summary span:first-of-type { @@ -2064,10 +2046,10 @@ h6:hover .copy-link { border-radius: var(--radius-lg); margin: var(--space-6) 0; background-color: var(--bg-surface); - transition: - background-color var(--transition), - border-color var(--transition-fast), - box-shadow var(--transition-fast); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, + var(--transition-fast)), box-shadow var(--theme-transition, + var(--transition-fast)); box-shadow: var(--shadow-sm); min-width: 0; } @@ -2232,11 +2214,9 @@ h6:hover .copy-link { border-radius: var(--radius-md); margin: var(--space-6) 0; background-color: var(--sidebar-bg); - transition: - opacity var(--transition-fast), - transform var(--transition-fast), - background-color var(--transition), - border-color var(--transition); + transition: var(--theme-colors), opacity var(--transition-fast), transform + var(--transition-fast), background-color var(--theme-transition, + var(--transition)), border-color var(--theme-transition, var(--transition)); box-shadow: var(--shadow-sm); } @@ -2388,7 +2368,8 @@ h6:hover .copy-link { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: border-color var(--transition-fast); + transition: var(--theme-colors), border-color var(--theme-transition, + var(--transition-fast)); box-sizing: border-box; } @@ -2563,9 +2544,9 @@ h6:hover .copy-link { align-items: center; gap: var(--space-2); border-radius: var(--radius-md); - transition: - color var(--transition-fast), - background-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), background-color var(--theme-transition, + var(--transition-fast)); } .sidebar-section > summary:hover { @@ -2580,7 +2561,7 @@ h6:hover .copy-link { .sidebar-section > summary::before { content: "▶"; font-size: 0.625rem; - transition: transform var(--transition-fast); + transition: var(--theme-colors), transform var(--transition-fast); flex-shrink: 0; } @@ -2598,7 +2579,7 @@ h6:hover .copy-link { display: grid; grid-template-rows: 0fr; overflow: hidden; - transition: grid-template-rows var(--transition); + transition: var(--theme-colors), grid-template-rows var(--transition); } .sidebar-section[open] .sidebar-section-content { @@ -2684,9 +2665,9 @@ h6:hover .copy-link { padding: var(--space-1) var(--space-3); color: var(--text-muted); text-decoration: none; - transition: - color var(--transition-fast), - border-color var(--transition-fast); + transition: var(--theme-colors), color var(--theme-transition, + var(--transition-fast)), border-color var(--theme-transition, + var(--transition-fast)); border-left: 2px solid transparent; } @@ -2744,7 +2725,8 @@ h6:hover .copy-link { cursor: pointer; text-decoration: none; font-size: 0.875rem; - transition: background-color var(--transition-fast); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition-fast)); } .search-error button:hover, @@ -2849,9 +2831,9 @@ h6:hover .copy-link { text-transform: uppercase; letter-spacing: 0.04em; list-style: none; - transition: - background-color var(--transition-fast), - color var(--transition-fast); + transition: var(--theme-colors), background-color var(--theme-transition, + var(--transition-fast)), color var(--theme-transition, + var(--transition-fast)); &::-webkit-details-marker { display: none; @@ -2860,7 +2842,7 @@ h6:hover .copy-link { &::before { content: "▶"; font-size: 0.625rem; - transition: transform var(--transition-fast); + transition: var(--theme-colors), transform var(--transition-fast); flex-shrink: 0; } diff --git a/crates/ndg-templates/templates/main.js b/crates/ndg-templates/templates/main.js index 43c05ae85..44b52f54b 100644 --- a/crates/ndg-templates/templates/main.js +++ b/crates/ndg-templates/templates/main.js @@ -569,6 +569,8 @@ function setupOptionChunkLoading(signal) { }) .then((html) => { chunk.innerHTML = html; + // FIXME: Sync themed pictures in this chunk with currentTheme(); images + // loaded after initializePage currently keep the system color scheme. chunk.removeAttribute("data-src"); loadedChunks.add(index); updateStatus(); @@ -1442,7 +1444,17 @@ function syncThemeToggles(mode) { }); } +let themeTransitionFrame; + function applyTheme(mode) { + // Use one color duration for the switch without changing normal hover timing. + cancelAnimationFrame(themeTransitionFrame); + document.documentElement.classList.add("theme-changing"); + themeTransitionFrame = requestAnimationFrame(() => { + themeTransitionFrame = requestAnimationFrame(() => { + document.documentElement.classList.remove("theme-changing"); + }); + }); if (mode === "auto") { delete document.documentElement.dataset.theme; } else { diff --git a/crates/ndg-templates/templates/main.test.js b/crates/ndg-templates/templates/main.test.js index f7cb6621c..e79d74079 100644 --- a/crates/ndg-templates/templates/main.test.js +++ b/crates/ndg-templates/templates/main.test.js @@ -14,14 +14,16 @@ function loadMain(requestAnimationFrame, browser = {}) { "window", "document", "requestAnimationFrame", + "cancelAnimationFrame", "fetch", "DOMParser", "IntersectionObserver", - `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme, getFilterMatches, setFilteredItemVisibility };`, + `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme, applyTheme, getFilterMatches, setFilteredItemVisibility };`, )( window, document, requestAnimationFrame, + browser.cancelAnimationFrame, browser.fetch, browser.DOMParser, browser.IntersectionObserver, @@ -385,3 +387,54 @@ Deno.test("the theme toggle goes back to the system theme", () => { assert(nextTheme("light", "light") === "dark", "a pinned light selects dark"); assert(nextTheme("light", "dark") === "auto", "light goes back to auto"); }); + +Deno.test("rapid theme changes keep transition timing until the next paint", () => { + const frames = new Map(); + let nextFrame = 0; + const classes = new Set(); + const root = { + dataset: {}, + classList: { + add: (name) => classes.add(name), + remove: (name) => classes.delete(name), + }, + }; + const events = []; + const browser = themeBrowser(false); + browser.cancelAnimationFrame = (id) => frames.delete(id); + browser.document = { + addEventListener() {}, + documentElement: root, + querySelectorAll: () => [], + dispatchEvent: (event) => events.push(event.detail), + }; + const { applyTheme } = loadMain((callback) => { + frames.set(++nextFrame, callback); + return nextFrame; + }, browser); + const paint = () => { + const pending = [...frames.values()]; + frames.clear(); + pending.forEach((callback) => callback()); + }; + + applyTheme("dark"); + paint(); + applyTheme("light"); + paint(); + assert( + classes.has("theme-changing"), + "an older cleanup must not end the new switch", + ); + assert(root.dataset.theme === "light", "the latest selection wins"); + paint(); + assert( + !classes.has("theme-changing"), + "normal hover timing resumes after paint", + ); + assert(frames.size === 0, "no animation frames remain pending"); + assert( + events.length === 2 && events[1].resolved === "light", + "theme events follow each selection", + ); +}); From 6a62889dbfa3c6b86ea4ad88e193cb43c487c77d Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Wed, 7 Oct 2026 20:24:53 +0200 Subject: [PATCH 02/11] ndg-templates: consolidate theme palette with light-dark() --- crates/ndg-templates/templates/default.css | 277 ++++----------------- 1 file changed, 46 insertions(+), 231 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index af89c900b..f60c245b1 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -1,4 +1,5 @@ :root { + color-scheme: light dark; /* Palettes created with: https://www.tints.dev/?output=oklch&version=4 */ --neutral-50: oklch(0.958 0.004 315.23); --neutral-100: oklch(0.915 0.009 314.98); @@ -60,35 +61,36 @@ --accent-900: var(--orange-900); --accent-950: var(--orange-950); - /* Light Mode Colors */ - --bg-color: #ffffff; - --bg-surface: #f8fafc; - --bg-surface-2: #f1f5f9; - --text-color: #334155; - --text-muted: #64748b; - --heading-color: #0f172a; - --link-color: #3b82f6; - --link-hover-color: #2563eb; + /* Theme colors follow color-scheme, including the system preference. */ + --bg-color: light-dark(#ffffff, var(--neutral-950)); + --bg-surface: light-dark(#f8fafc, var(--neutral-900)); + --bg-surface-2: light-dark(#f1f5f9, #334155); + --text-color: light-dark(#334155, #e2e8f0); + --text-muted: light-dark(#64748b, #94a3b8); + --heading-color: light-dark(#0f172a, var(--neutral-100)); + --link-color: light-dark(#3b82f6, #60a5fa); + --link-hover-color: light-dark(#2563eb, #93c5fd); /* Accent */ --brand: var(--link-color); --brand-hover: var(--link-hover-color); - --focus-ring: rgba(59, 130, 246, 0.35); - --border-strong: #cbd5e1; - - --border-color: #e2e8f0; - --table-border-color: #cbd5e1; - --code-bg: #f1f5f9; - --header-bg: rgba(255, 255, 255, 0.8); - --header-text: #1e293b; - --header-muted: #64748b; - --header-border: #e2e8f0; - --sidebar-bg: #f8fafc; - --sidebar-hover: #eef1f6; - --sidebar-active: #e6eaf1; - --footer-bg: #f1f5f9; - --shadow-color: rgba(15, 23, 42, 0.08); - --shadow-color-darker: rgba(15, 23, 42, 0.12); + --focus-ring: light-dark(rgba(59, 130, 246, 0.35), rgba(96, 165, 250, 0.4)); + --border-strong: light-dark(#cbd5e1, var(--neutral-700)); + + --border-color: light-dark(#e2e8f0, var(--neutral-800)); + --table-border-color: light-dark(#cbd5e1, var(--neutral-800)); + --code-bg: light-dark(#f1f5f9, #111827); + --code-border: light-dark(transparent, var(--neutral-800)); + --header-bg: light-dark(rgba(255, 255, 255, 0.8), color-mix(in oklch, var(--neutral-950) 80%, transparent)); + --header-text: light-dark(#1e293b, var(--neutral-100)); + --header-muted: light-dark(#64748b, var(--neutral-400)); + --header-border: light-dark(#e2e8f0, var(--neutral-800)); + --sidebar-bg: light-dark(#f8fafc, var(--neutral-900)); + --sidebar-hover: light-dark(#eef1f6, var(--neutral-800)); + --sidebar-active: light-dark(#e6eaf1, var(--neutral-700)); + --footer-bg: light-dark(#f1f5f9, var(--neutral-900)); + --shadow-color: light-dark(rgba(15, 23, 42, 0.08), rgba(0, 0, 0, 0.25)); + --shadow-color-darker: light-dark(rgba(15, 23, 42, 0.12), rgba(0, 0, 0, 0.35)); /* Admonition colors */ --admonition-note-color: #3b82f6; @@ -135,88 +137,12 @@ --font-mono: "JetBrains Mono", monospace; } -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - /* Dark Mode Colors */ - --bg-color: var(--neutral-950); - --bg-surface: var(--neutral-900); - --bg-surface-2: #334155; - --text-color: #e2e8f0; - --text-muted: #94a3b8; - --heading-color: var(--neutral-100); - --link-color: #60a5fa; - --link-hover-color: #93c5fd; - - --brand: var(--link-color); - --brand-hover: var(--link-hover-color); - --focus-ring: rgba(96, 165, 250, 0.4); - --border-strong: var(--neutral-700); - - --border-color: var(--neutral-800); - --code-bg: #111827; - --code-border: var(--neutral-800); - --header-bg: color-mix(in oklch, var(--neutral-950) 80%, transparent); - --header-text: var(--neutral-100); - --header-muted: var(--neutral-400); - --header-border: var(--neutral-800); - --sidebar-bg: var(--neutral-900); - --sidebar-hover: var(--neutral-800); - --sidebar-active: var(--neutral-700); - --footer-bg: var(--neutral-900); - --shadow-color: rgba(0, 0, 0, 0.25); - --shadow-color-darker: rgba(0, 0, 0, 0.35); - - /* Admonition Colors */ - --admonition-note-color: #3b82f6; - --admonition-tip-color: #10b981; - --admonition-warning-color: #f59e0b; - --admonition-danger-color: #ef4444; - --admonition-info-color: #6366f1; - --admonition-important-color: #8b5cf6; - --admonition-example-color: #06b6d4; - --admonition-caution-color: #f97316; - } +:root[data-theme="light"] { + color-scheme: light; } :root[data-theme="dark"] { - /* Dark Mode Colors */ - --bg-color: var(--neutral-950); - --bg-surface: var(--neutral-900); - --bg-surface-2: #334155; - --text-color: #e2e8f0; - --text-muted: #94a3b8; - --heading-color: var(--neutral-100); - --link-color: #60a5fa; - --link-hover-color: #93c5fd; - - --brand: var(--link-color); - --brand-hover: var(--link-hover-color); - --focus-ring: rgba(96, 165, 250, 0.4); - --border-strong: var(--neutral-700); - - --border-color: var(--neutral-800); - --code-bg: #111827; - --code-border: var(--neutral-800); - --header-bg: color-mix(in oklch, var(--neutral-950) 80%, transparent); - --header-text: var(--neutral-100); - --header-muted: var(--neutral-400); - --header-border: var(--neutral-800); - --sidebar-bg: var(--neutral-900); - --sidebar-hover: var(--neutral-800); - --sidebar-active: var(--neutral-700); - --footer-bg: var(--neutral-900); - --shadow-color: rgba(0, 0, 0, 0.25); - --shadow-color-darker: rgba(0, 0, 0, 0.35); - - /* Admonition Colors */ - --admonition-note-color: #3b82f6; - --admonition-tip-color: #10b981; - --admonition-warning-color: #f59e0b; - --admonition-danger-color: #ef4444; - --admonition-info-color: #6366f1; - --admonition-important-color: #8b5cf6; - --admonition-example-color: #06b6d4; - --admonition-caution-color: #f97316; + color-scheme: dark; } /* Keep the built-in themed surfaces in step while switching themes. */ @@ -736,7 +662,7 @@ table { th, td { - border: 1px solid var(--border-color); + border: 1px solid var(--table-border-color); padding: var(--space-2) var(--space-3); text-align: left; transition: var(--theme-colors), border-color var(--theme-transition, @@ -751,46 +677,6 @@ th { border-bottom-width: 2px; } -@media (prefers-color-scheme: light) { - :root:not([data-theme="dark"]) { - --table-border-color: #cbd5e1; - } - - :where(:root:not([data-theme="dark"])) th, - :where(:root:not([data-theme="dark"])) td { - border-color: var(--table-border-color); - } -} - -:root[data-theme="light"] { - --table-border-color: #cbd5e1; -} - -:where(:root[data-theme="light"]) th, -:where(:root[data-theme="light"]) td { - border-color: var(--table-border-color); -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - --table-border-color: var(--neutral-800); - } - - :where(:root:not([data-theme="light"])) th, - :where(:root:not([data-theme="light"])) td { - border-color: var(--table-border-color); - } -} - -:root[data-theme="dark"] { - --table-border-color: var(--neutral-800); -} - -:where(:root[data-theme="dark"]) th, -:where(:root[data-theme="dark"]) td { - border-color: var(--table-border-color); -} - /* Images */ img { max-width: 100%; @@ -978,7 +864,7 @@ img { left: 0; width: 100%; height: 100%; - background-color: rgba(15, 23, 42, 0.5); + background-color: light-dark(rgba(15, 23, 42, 0.5), rgba(0, 0, 0, 0.7)); z-index: 1000; align-items: flex-start; justify-content: center; @@ -986,16 +872,6 @@ img { backdrop-filter: blur(4px); } -@media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .mobile-search-popup { - background-color: rgba(0, 0, 0, 0.7); - } -} - -:where(:root[data-theme="dark"]) .mobile-search-popup { - background-color: rgba(0, 0, 0, 0.7); -} - .mobile-search-popup.active { display: flex; } @@ -1168,27 +1044,13 @@ img { padding: 0.125rem 0.375rem; font-size: 0.75rem; font-family: ui-monospace, monospace; - background-color: var(--code-bg); + background-color: light-dark(var(--code-bg), var(--sidebar-bg)); border: 1px solid var(--border-color); border-radius: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.2)); color: var(--text-color); } -@media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .search-keyboard-hints kbd { - background-color: var(--sidebar-bg); - border-color: var(--border-color); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - } -} - -:where(:root[data-theme="dark"]) .search-keyboard-hints kbd { - background-color: var(--sidebar-bg); - border-color: var(--border-color); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); -} - @media (max-width: 800px) { .search-keyboard-hints { display: none; @@ -1243,25 +1105,14 @@ img { /* Search highlighting */ mark { - background-color: rgba(59, 130, 246, 0.2); + background-color: light-dark(rgba(59, 130, 246, 0.2), rgba(59, 130, 246, + 0.3)); color: var(--link-color); font-weight: 600; padding: 0 2px; border-radius: 2px; } -@media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) mark { - background-color: rgba(59, 130, 246, 0.3); - color: #60a5fa; - } -} - -:where(:root[data-theme="dark"]) mark { - background-color: rgba(59, 130, 246, 0.3); - color: #60a5fa; -} - @media (max-width: 800px) { .search-page-results { margin-top: var(--space-4); @@ -2740,7 +2591,7 @@ h6:hover .copy-link { /* Search result highlighting on target pages */ .search-highlight { - background-color: rgba(255, 235, 59, 0.4); + background-color: light-dark(rgba(255, 235, 59, 0.4), rgba(255, 193, 7, 0.3)); color: inherit; font-weight: inherit; padding: 2px 0; @@ -2749,68 +2600,32 @@ h6:hover .copy-link { } .search-highlight-active { - background-color: rgba(255, 193, 7, 0.6); + background-color: light-dark(rgba(255, 193, 7, 0.6), rgba(255, 152, 0, 0.5)); animation: highlight-pulse-active 1s ease-in-out infinite alternate; } @keyframes highlight-fade { 0% { - background-color: rgba(255, 235, 59, 0.6); + background-color: light-dark(rgba(255, 235, 59, 0.6), rgba(255, 193, 7, + 0.4)); } 100% { - background-color: rgba(255, 235, 59, 0.2); + background-color: light-dark(rgba(255, 235, 59, 0.2), rgba(255, 193, 7, + 0.15)); } } @keyframes highlight-pulse-active { 0% { - background-color: rgba(255, 193, 7, 0.5); - } - 100% { - background-color: rgba(255, 193, 7, 0.7); - } -} - -@keyframes highlight-fade-dark { - 0% { - background-color: rgba(255, 193, 7, 0.4); - } - 100% { - background-color: rgba(255, 193, 7, 0.15); - } -} - -@keyframes highlight-pulse-active-dark { - 0% { - background-color: rgba(255, 152, 0, 0.4); + background-color: light-dark(rgba(255, 193, 7, 0.5), rgba(255, 152, 0, + 0.4)); } 100% { - background-color: rgba(255, 152, 0, 0.6); + background-color: light-dark(rgba(255, 193, 7, 0.7), rgba(255, 152, 0, + 0.6)); } } -@media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .search-highlight { - background-color: rgba(255, 193, 7, 0.3); - animation: highlight-fade-dark 3s ease-out forwards; - } - - :where(:root:not([data-theme="light"])) .search-highlight-active { - background-color: rgba(255, 152, 0, 0.5); - animation: highlight-pulse-active-dark 1s ease-in-out infinite alternate; - } -} - -:where(:root[data-theme="dark"]) .search-highlight { - background-color: rgba(255, 193, 7, 0.3); - animation: highlight-fade-dark 3s ease-out forwards; -} - -:where(:root[data-theme="dark"]) .search-highlight-active { - background-color: rgba(255, 152, 0, 0.5); - animation: highlight-pulse-active-dark 1s ease-in-out infinite alternate; -} - /* Sidebar directory groups */ .sidebar-dir-group { list-style: none; From 8bded7db4c3b93d11ecaf4bb98187c22e807ce80 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Wed, 7 Oct 2026 21:05:43 +0200 Subject: [PATCH 03/11] docs: update unreleased theme toggle entry --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f86485c1d..f35f8db07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,9 @@ changes. theme is the same as the system theme, the page follows the system theme again. The toggle also selects the matching `` of a `` with a `prefers-color-scheme` media query. The page sends an `ndg:themechange` event - on `document` when the theme changes. + on `document` when the theme changes. Colors transition consistently across + page elements while preserving normal hover timing, and native browser + controls follow the selected theme through `color-scheme`. ### Changed From e92e5ed2608e35302f4323155f787138375eb55b Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Wed, 7 Oct 2026 21:27:56 +0200 Subject: [PATCH 04/11] ndg-templates: scope theme transitions with a shared page class --- crates/ndg-templates/templates/default.css | 391 ++++++++++++------ crates/ndg-templates/templates/default.html | 2 +- crates/ndg-templates/templates/lib.html | 2 +- crates/ndg-templates/templates/main.test.js | 101 ++--- .../templates/mobile-sidebar.css | 9 +- .../ndg-templates/templates/options-pages.css | 5 +- crates/ndg-templates/templates/options.html | 2 +- crates/ndg-templates/templates/search.html | 2 +- 8 files changed, 324 insertions(+), 190 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index f60c245b1..8204fefd6 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -81,7 +81,10 @@ --table-border-color: light-dark(#cbd5e1, var(--neutral-800)); --code-bg: light-dark(#f1f5f9, #111827); --code-border: light-dark(transparent, var(--neutral-800)); - --header-bg: light-dark(rgba(255, 255, 255, 0.8), color-mix(in oklch, var(--neutral-950) 80%, transparent)); + --header-bg: light-dark( + rgba(255, 255, 255, 0.8), + color-mix(in oklch, var(--neutral-950) 80%, transparent) + ); --header-text: light-dark(#1e293b, var(--neutral-100)); --header-muted: light-dark(#64748b, var(--neutral-400)); --header-border: light-dark(#e2e8f0, var(--neutral-800)); @@ -90,7 +93,10 @@ --sidebar-active: light-dark(#e6eaf1, var(--neutral-700)); --footer-bg: light-dark(#f1f5f9, var(--neutral-900)); --shadow-color: light-dark(rgba(15, 23, 42, 0.08), rgba(0, 0, 0, 0.25)); - --shadow-color-darker: light-dark(rgba(15, 23, 42, 0.12), rgba(0, 0, 0, 0.35)); + --shadow-color-darker: light-dark( + rgba(15, 23, 42, 0.12), + rgba(0, 0, 0, 0.35) + ); /* Admonition colors */ --admonition-note-color: #3b82f6; @@ -145,8 +151,13 @@ color-scheme: dark; } -/* Keep the built-in themed surfaces in step while switching themes. */ -:where(body, header, .header-nav, .site-title a, footer, .sidebar, .sidebar-toggle, .sidebar-section > summary, .sidebar-dir-group, .toc-list, .toc-list ul, .toc-list ul > li::before, .toc-list a, .toc-list summary, .toc-list summary::before, .page-toc-nav h3, .page-toc-list a, .content h1, .content h2, .content h3, .content h4, .content h5, .content h6, a, code, pre, blockquote, table, th, td, section.footnotes, .theme-toggle, .search-container input, .search-results, .search-result-item, .search-result-anchor::before, .search-result-preview, .search-keyboard-hints, .search-keyboard-hints kbd, .search-error, .search-error button, .search-error a, .mobile-search-popup, .mobile-search-container, .mobile-search-results, .mobile-search-header, .mobile-search-close, #mobile-search-input, .mobile-sidebar-container, .mobile-sidebar-header, .mobile-sidebar-close, #search-page-input, #options-filter, #lib-filter, .filter-clear, .filter-results, .option, .option-type, .options-chunk-status, .options-chunk-error, .options-chunk-noscript, .lib-entry, .lib-entry-signature, .lib-entry-arguments dd, .lib-entry-deprecated, .lib-entry-warnings, .admonition, .admonition-title, mark, .copy-link, .copy-feedback) { +/* Default color transitions for the built-in theme. */ +:where(.ndg-theme, .ndg-theme *) { + transition: var(--theme-colors); +} + +:where(.ndg-theme, .ndg-theme *)::before, +:where(.ndg-theme, .ndg-theme *)::after { transition: var(--theme-colors); } @@ -188,8 +199,10 @@ body { background-color: var(--bg-color); margin: 0; padding: 0; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + color var(--theme-transition, var(--transition)); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -249,8 +262,10 @@ header { border-bottom: 1px solid var(--header-border); z-index: 100; top: 0px; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)); backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: saturate(180%) blur(12px); height: var(--header-height); @@ -272,7 +287,9 @@ header { a { color: var(--header-text); text-decoration: none; - transition: var(--theme-colors), opacity var(--transition-fast); + transition: + var(--theme-colors), + opacity var(--transition-fast); } a:hover { @@ -306,8 +323,9 @@ header { font-size: 0.95rem; font-weight: 500; padding: var(--space-2) 0; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)); display: block; } @@ -365,7 +383,9 @@ header { overflow-y: auto; z-index: 10; box-sizing: border-box; - transition: var(--theme-colors), transform var(--transition); + transition: + var(--theme-colors), + transform var(--transition); font-size: 0.875rem; scrollbar-gutter: stable; @@ -377,8 +397,9 @@ header { margin-bottom: var(--space-2); color: var(--text-muted); font-weight: 600; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition)); } ul { @@ -402,9 +423,10 @@ header { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), background-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); font-size: 0.875rem; &:hover { @@ -448,7 +470,9 @@ header { border-radius: 0 var(--radius-md) var(--radius-md) 0; cursor: pointer; z-index: 20; - transition: var(--theme-colors), left var(--transition); + transition: + var(--theme-colors), + left var(--transition); display: flex; align-items: center; justify-content: center; @@ -469,7 +493,9 @@ header { width: 16px; height: 16px; fill: var(--text-color); - transition: var(--theme-colors), transform var(--transition); + transition: + var(--theme-colors), + transform var(--transition); } .sidebar-collapsed &, @@ -492,8 +518,10 @@ footer { color: var(--text-muted); margin-top: auto; border-top: 1px solid var(--border-color); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)); } .content { @@ -510,8 +538,9 @@ footer { position: relative; cursor: pointer; font-weight: 600; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition)); letter-spacing: -0.015em; } @@ -529,14 +558,16 @@ footer { border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; margin-top: 1em; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); } h2 { font-size: 1.75rem; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); } h3 { @@ -567,9 +598,10 @@ footer { a { color: var(--link-color); text-decoration: none; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), text-decoration-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + text-decoration-color var(--theme-transition, var(--transition-fast)); text-underline-offset: 0.15em; } @@ -597,8 +629,9 @@ code { border-radius: var(--radius-sm); padding: 0.2em 0.4em; font-size: 0.875em; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)); letter-spacing: -0.025em; border: 1px solid var(--code-border); } @@ -610,8 +643,10 @@ pre { overflow-x: auto; margin: 1.5em 0; border: 1px solid var(--border-color); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)); line-height: 1.6; position: relative; } @@ -637,9 +672,10 @@ blockquote { border-radius: 0 var(--radius-md) var(--radius-md) 0; color: var(--text-color); font-style: italic; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)), background-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)), + background-color var(--theme-transition, var(--transition)); } blockquote p:first-of-type { @@ -665,15 +701,17 @@ td { border: 1px solid var(--table-border-color); padding: var(--space-2) var(--space-3); text-align: left; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); } th { background-color: var(--sidebar-bg); font-weight: 600; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)); border-bottom-width: 2px; } @@ -708,9 +746,10 @@ img { border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), border-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); &:hover { color: var(--header-text); @@ -772,10 +811,12 @@ img { font-size: 0.875rem; background-color: var(--bg-surface); color: var(--text-color); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition-fast)), border-color var(--theme-transition, - var(--transition-fast)), box-shadow var(--theme-transition, - var(--transition-fast)), width var(--transition); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)), + box-shadow var(--theme-transition, var(--transition-fast)), + width var(--transition); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: 0.75rem center; @@ -810,15 +851,18 @@ img { box-shadow: var(--shadow-lg); display: none; z-index: 1000; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)); } .search-result-item { padding: var(--space-4); border-bottom: 1px solid var(--border-color); - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); min-height: 44px; display: flex; align-items: center; @@ -995,9 +1039,10 @@ img { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition-fast)), background-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition)); box-sizing: border-box; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -1073,9 +1118,11 @@ img { border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--sidebar-bg); - transition: var(--theme-colors), transform var(--transition-fast), - border-color var(--theme-transition, var(--transition-fast)), box-shadow - var(--theme-transition, var(--transition-fast)); + transition: + var(--theme-colors), + transform var(--transition-fast), + border-color var(--theme-transition, var(--transition-fast)), + box-shadow var(--theme-transition, var(--transition-fast)); } .search-page-results .search-result-item:hover { @@ -1105,8 +1152,10 @@ img { /* Search highlighting */ mark { - background-color: light-dark(rgba(59, 130, 246, 0.2), rgba(59, 130, 246, - 0.3)); + background-color: light-dark( + rgba(59, 130, 246, 0.2), + rgba(59, 130, 246, 0.3) + ); color: var(--link-color); font-weight: 600; padding: 0 2px; @@ -1130,8 +1179,10 @@ mark { background-color: var(--sidebar-bg); overflow: hidden; box-shadow: var(--shadow-md); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), box-shadow var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + box-shadow var(--theme-transition, var(--transition)); border: 1px solid var(--border-color); } @@ -1143,8 +1194,9 @@ mark { align-items: center; font-size: 1rem !important; border-bottom: 1px solid var(--border-color); - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); } .admonition > :not(.admonition-title) { @@ -1274,35 +1326,51 @@ mark { /* Dark mode SVG icon adjustments */ @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .admonition.note .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2360a5fa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.tip .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2334d399' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.warning .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fbbf24' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.danger .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f87171' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.info .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.important .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.example .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2322d3ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.caution .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fb923c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1327,7 +1395,9 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.important .admonition-title::before { +:where(:root[data-theme="dark"]) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } @@ -1341,35 +1411,51 @@ mark { /* Light mode SVG icon adjustments */ @media (prefers-color-scheme: light) { - :where(:root:not([data-theme="dark"])) .admonition.note .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.tip .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.warning .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.danger .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.info .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.important .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.example .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.caution .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1382,7 +1468,9 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.warning .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } @@ -1394,15 +1482,21 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.important .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.example .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.caution .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } @@ -1437,7 +1531,9 @@ mark { background-position: center; cursor: pointer; opacity: 0; - transition: var(--theme-colors), opacity var(--transition-fast), + transition: + var(--theme-colors), + opacity var(--transition-fast), background-color var(--theme-transition, var(--transition-fast)); vertical-align: middle; border-radius: var(--radius-sm); @@ -1504,8 +1600,9 @@ h6:hover .copy-link { ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)); border: 2px solid var(--bg-color); } @@ -1619,9 +1716,10 @@ h6:hover .copy-link { background-size: 18px; background-repeat: no-repeat; border: 1px solid rgba(255, 255, 255, 0.1); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition-fast)), border-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); } .search-container #search-input::placeholder { @@ -1728,8 +1826,9 @@ h6:hover .copy-link { list-style-type: none; padding-left: 0; margin: var(--space-6) 0; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition)); background-color: transparent; padding: var(--space-2) 0; } @@ -1762,9 +1861,10 @@ h6:hover .copy-link { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), background-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); font-size: 0.875rem; white-space: nowrap; overflow: hidden; @@ -1802,9 +1902,11 @@ h6:hover .copy-link { text-overflow: ellipsis; list-style: none; border-radius: var(--radius-md); - transition: var(--theme-colors), transform var(--transition-fast), color - var(--theme-transition, var(--transition-fast)), background-color - var(--theme-transition, var(--transition-fast)); + transition: + var(--theme-colors), + transform var(--transition-fast), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); } .toc-list details summary:hover { @@ -1821,8 +1923,10 @@ h6:hover .copy-link { font-size: 0.65rem; flex-shrink: 0; color: var(--text-muted); - transition: var(--theme-colors), transform var(--transition-fast), color - var(--theme-transition, var(--transition-fast)); + transition: + var(--theme-colors), + transform var(--transition-fast), + color var(--theme-transition, var(--transition-fast)); } .toc-list details summary span:first-of-type { @@ -1897,10 +2001,11 @@ h6:hover .copy-link { border-radius: var(--radius-lg); margin: var(--space-6) 0; background-color: var(--bg-surface); - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, - var(--transition-fast)), box-shadow var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition-fast)), + box-shadow var(--theme-transition, var(--transition-fast)); box-shadow: var(--shadow-sm); min-width: 0; } @@ -2065,9 +2170,12 @@ h6:hover .copy-link { border-radius: var(--radius-md); margin: var(--space-6) 0; background-color: var(--sidebar-bg); - transition: var(--theme-colors), opacity var(--transition-fast), transform - var(--transition-fast), background-color var(--theme-transition, - var(--transition)), border-color var(--theme-transition, var(--transition)); + transition: + var(--theme-colors), + opacity var(--transition-fast), + transform var(--transition-fast), + background-color var(--theme-transition, var(--transition)), + border-color var(--theme-transition, var(--transition)); box-shadow: var(--shadow-sm); } @@ -2219,8 +2327,9 @@ h6:hover .copy-link { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: var(--theme-colors), border-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition-fast)); box-sizing: border-box; } @@ -2395,9 +2504,10 @@ h6:hover .copy-link { align-items: center; gap: var(--space-2); border-radius: var(--radius-md); - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), background-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); } .sidebar-section > summary:hover { @@ -2412,7 +2522,9 @@ h6:hover .copy-link { .sidebar-section > summary::before { content: "▶"; font-size: 0.625rem; - transition: var(--theme-colors), transform var(--transition-fast); + transition: + var(--theme-colors), + transform var(--transition-fast); flex-shrink: 0; } @@ -2430,7 +2542,9 @@ h6:hover .copy-link { display: grid; grid-template-rows: 0fr; overflow: hidden; - transition: var(--theme-colors), grid-template-rows var(--transition); + transition: + var(--theme-colors), + grid-template-rows var(--transition); } .sidebar-section[open] .sidebar-section-content { @@ -2516,9 +2630,10 @@ h6:hover .copy-link { padding: var(--space-1) var(--space-3); color: var(--text-muted); text-decoration: none; - transition: var(--theme-colors), color var(--theme-transition, - var(--transition-fast)), border-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); border-left: 2px solid transparent; } @@ -2576,8 +2691,9 @@ h6:hover .copy-link { cursor: pointer; text-decoration: none; font-size: 0.875rem; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition-fast)); } .search-error button:hover, @@ -2606,23 +2722,31 @@ h6:hover .copy-link { @keyframes highlight-fade { 0% { - background-color: light-dark(rgba(255, 235, 59, 0.6), rgba(255, 193, 7, - 0.4)); + background-color: light-dark( + rgba(255, 235, 59, 0.6), + rgba(255, 193, 7, 0.4) + ); } 100% { - background-color: light-dark(rgba(255, 235, 59, 0.2), rgba(255, 193, 7, - 0.15)); + background-color: light-dark( + rgba(255, 235, 59, 0.2), + rgba(255, 193, 7, 0.15) + ); } } @keyframes highlight-pulse-active { 0% { - background-color: light-dark(rgba(255, 193, 7, 0.5), rgba(255, 152, 0, - 0.4)); + background-color: light-dark( + rgba(255, 193, 7, 0.5), + rgba(255, 152, 0, 0.4) + ); } 100% { - background-color: light-dark(rgba(255, 193, 7, 0.7), rgba(255, 152, 0, - 0.6)); + background-color: light-dark( + rgba(255, 193, 7, 0.7), + rgba(255, 152, 0, 0.6) + ); } } @@ -2646,9 +2770,10 @@ h6:hover .copy-link { text-transform: uppercase; letter-spacing: 0.04em; list-style: none; - transition: var(--theme-colors), background-color var(--theme-transition, - var(--transition-fast)), color var(--theme-transition, - var(--transition-fast)); + transition: + var(--theme-colors), + background-color var(--theme-transition, var(--transition-fast)), + color var(--theme-transition, var(--transition-fast)); &::-webkit-details-marker { display: none; @@ -2657,7 +2782,9 @@ h6:hover .copy-link { &::before { content: "▶"; font-size: 0.625rem; - transition: var(--theme-colors), transform var(--transition-fast); + transition: + var(--theme-colors), + transform var(--transition-fast); flex-shrink: 0; } diff --git a/crates/ndg-templates/templates/default.html b/crates/ndg-templates/templates/default.html index c1768114c..eaf73346f 100644 --- a/crates/ndg-templates/templates/default.html +++ b/crates/ndg-templates/templates/default.html @@ -37,7 +37,7 @@ {% endif %} - +
diff --git a/crates/ndg-templates/templates/lib.html b/crates/ndg-templates/templates/lib.html index 4f1599583..d53aa35c1 100644 --- a/crates/ndg-templates/templates/lib.html +++ b/crates/ndg-templates/templates/lib.html @@ -43,7 +43,7 @@ {% endif %} - +
diff --git a/crates/ndg-templates/templates/main.test.js b/crates/ndg-templates/templates/main.test.js index e79d74079..617007f77 100644 --- a/crates/ndg-templates/templates/main.test.js +++ b/crates/ndg-templates/templates/main.test.js @@ -388,53 +388,56 @@ Deno.test("the theme toggle goes back to the system theme", () => { assert(nextTheme("light", "dark") === "auto", "light goes back to auto"); }); -Deno.test("rapid theme changes keep transition timing until the next paint", () => { - const frames = new Map(); - let nextFrame = 0; - const classes = new Set(); - const root = { - dataset: {}, - classList: { - add: (name) => classes.add(name), - remove: (name) => classes.delete(name), - }, - }; - const events = []; - const browser = themeBrowser(false); - browser.cancelAnimationFrame = (id) => frames.delete(id); - browser.document = { - addEventListener() {}, - documentElement: root, - querySelectorAll: () => [], - dispatchEvent: (event) => events.push(event.detail), - }; - const { applyTheme } = loadMain((callback) => { - frames.set(++nextFrame, callback); - return nextFrame; - }, browser); - const paint = () => { - const pending = [...frames.values()]; - frames.clear(); - pending.forEach((callback) => callback()); - }; +Deno.test( + "rapid theme changes keep transition timing until the next paint", + () => { + const frames = new Map(); + let nextFrame = 0; + const classes = new Set(); + const root = { + dataset: {}, + classList: { + add: (name) => classes.add(name), + remove: (name) => classes.delete(name), + }, + }; + const events = []; + const browser = themeBrowser(false); + browser.cancelAnimationFrame = (id) => frames.delete(id); + browser.document = { + addEventListener() {}, + documentElement: root, + querySelectorAll: () => [], + dispatchEvent: (event) => events.push(event.detail), + }; + const { applyTheme } = loadMain((callback) => { + frames.set(++nextFrame, callback); + return nextFrame; + }, browser); + const paint = () => { + const pending = [...frames.values()]; + frames.clear(); + pending.forEach((callback) => callback()); + }; - applyTheme("dark"); - paint(); - applyTheme("light"); - paint(); - assert( - classes.has("theme-changing"), - "an older cleanup must not end the new switch", - ); - assert(root.dataset.theme === "light", "the latest selection wins"); - paint(); - assert( - !classes.has("theme-changing"), - "normal hover timing resumes after paint", - ); - assert(frames.size === 0, "no animation frames remain pending"); - assert( - events.length === 2 && events[1].resolved === "light", - "theme events follow each selection", - ); -}); + applyTheme("dark"); + paint(); + applyTheme("light"); + paint(); + assert( + classes.has("theme-changing"), + "an older cleanup must not end the new switch", + ); + assert(root.dataset.theme === "light", "the latest selection wins"); + paint(); + assert( + !classes.has("theme-changing"), + "normal hover timing resumes after paint", + ); + assert(frames.size === 0, "no animation frames remain pending"); + assert( + events.length === 2 && events[1].resolved === "light", + "theme events follow each selection", + ); + }, +); diff --git a/crates/ndg-templates/templates/mobile-sidebar.css b/crates/ndg-templates/templates/mobile-sidebar.css index 8e89d5363..6f812392b 100644 --- a/crates/ndg-templates/templates/mobile-sidebar.css +++ b/crates/ndg-templates/templates/mobile-sidebar.css @@ -33,7 +33,9 @@ box-shadow: var(--shadow-lg); z-index: 1000; transform: translateX(-100%); - transition: transform var(--transition); + transition: + var(--theme-colors), + transform var(--transition); flex-direction: column; } @@ -188,8 +190,9 @@ body.mobile-sidebar-open { font: inherit; cursor: pointer; transition: - background-color var(--transition-fast), - border-color var(--transition-fast); + var(--theme-colors), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); } .mobile-sidebar-toggle:hover { diff --git a/crates/ndg-templates/templates/options-pages.css b/crates/ndg-templates/templates/options-pages.css index 7f674666d..f2e577b3e 100644 --- a/crates/ndg-templates/templates/options-pages.css +++ b/crates/ndg-templates/templates/options-pages.css @@ -32,10 +32,11 @@ color: var(--text-color); text-decoration: none; transition: + var(--theme-colors), opacity var(--transition-fast), transform var(--transition-fast), - color var(--transition-fast), - background-color var(--transition-fast); + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); } .filter-entering, diff --git a/crates/ndg-templates/templates/options.html b/crates/ndg-templates/templates/options.html index 434ce79ae..bb0d3f8c7 100644 --- a/crates/ndg-templates/templates/options.html +++ b/crates/ndg-templates/templates/options.html @@ -37,7 +37,7 @@ {% endif %} - +
diff --git a/crates/ndg-templates/templates/search.html b/crates/ndg-templates/templates/search.html index bf73697c7..d7fbbf5ad 100644 --- a/crates/ndg-templates/templates/search.html +++ b/crates/ndg-templates/templates/search.html @@ -35,7 +35,7 @@ - +
From ab5301558221fc008733a7bbd560478b0ed6015e Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 11 Oct 2026 01:33:55 +0200 Subject: [PATCH 05/11] ndg-templates: remove redundant theme color transitions --- crates/ndg-templates/templates/default.css | 87 +++++----------------- 1 file changed, 20 insertions(+), 67 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index 8204fefd6..e559798eb 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -199,10 +199,7 @@ body { background-color: var(--bg-color); margin: 0; padding: 0; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -262,10 +259,7 @@ header { border-bottom: 1px solid var(--header-border); z-index: 100; top: 0px; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: saturate(180%) blur(12px); height: var(--header-height); @@ -397,9 +391,7 @@ header { margin-bottom: var(--space-2); color: var(--text-muted); font-weight: 600; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } ul { @@ -518,10 +510,7 @@ footer { color: var(--text-muted); margin-top: auto; border-top: 1px solid var(--border-color); - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } .content { @@ -538,9 +527,7 @@ footer { position: relative; cursor: pointer; font-weight: 600; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); letter-spacing: -0.015em; } @@ -558,16 +545,12 @@ footer { border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; margin-top: 1em; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } h2 { font-size: 1.75rem; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } h3 { @@ -629,9 +612,7 @@ code { border-radius: var(--radius-sm); padding: 0.2em 0.4em; font-size: 0.875em; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); letter-spacing: -0.025em; border: 1px solid var(--code-border); } @@ -643,10 +624,7 @@ pre { overflow-x: auto; margin: 1.5em 0; border: 1px solid var(--border-color); - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); line-height: 1.6; position: relative; } @@ -672,10 +650,7 @@ blockquote { border-radius: 0 var(--radius-md) var(--radius-md) 0; color: var(--text-color); font-style: italic; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)), - background-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } blockquote p:first-of-type { @@ -701,17 +676,13 @@ td { border: 1px solid var(--table-border-color); padding: var(--space-2) var(--space-3); text-align: left; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } th { background-color: var(--sidebar-bg); font-weight: 600; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); border-bottom-width: 2px; } @@ -851,18 +822,13 @@ img { box-shadow: var(--shadow-lg); display: none; z-index: 1000; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } .search-result-item { padding: var(--space-4); border-bottom: 1px solid var(--border-color); - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); min-height: 44px; display: flex; align-items: center; @@ -1041,8 +1007,7 @@ img { font-size: 1rem; transition: var(--theme-colors), - border-color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition)); + border-color var(--theme-transition, var(--transition-fast)); box-sizing: border-box; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -1179,10 +1144,7 @@ mark { background-color: var(--sidebar-bg); overflow: hidden; box-shadow: var(--shadow-md); - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)), - box-shadow var(--theme-transition, var(--transition)); + transition: var(--theme-colors); border: 1px solid var(--border-color); } @@ -1194,9 +1156,7 @@ mark { align-items: center; font-size: 1rem !important; border-bottom: 1px solid var(--border-color); - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); } .admonition > :not(.admonition-title) { @@ -1600,9 +1560,7 @@ h6:hover .copy-link { ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); border: 2px solid var(--bg-color); } @@ -1826,9 +1784,7 @@ h6:hover .copy-link { list-style-type: none; padding-left: 0; margin: var(--space-6) 0; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition)); + transition: var(--theme-colors); background-color: transparent; padding: var(--space-2) 0; } @@ -2003,7 +1959,6 @@ h6:hover .copy-link { background-color: var(--bg-surface); transition: var(--theme-colors), - background-color var(--theme-transition, var(--transition)), border-color var(--theme-transition, var(--transition-fast)), box-shadow var(--theme-transition, var(--transition-fast)); box-shadow: var(--shadow-sm); @@ -2173,9 +2128,7 @@ h6:hover .copy-link { transition: var(--theme-colors), opacity var(--transition-fast), - transform var(--transition-fast), - background-color var(--theme-transition, var(--transition)), - border-color var(--theme-transition, var(--transition)); + transform var(--transition-fast); box-shadow: var(--shadow-sm); } From 3f486868911953be9833f3cc0d4396d59b6fe159 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 11 Oct 2026 12:02:02 +0200 Subject: [PATCH 06/11] ndg-templates: synchronize automatic theme transitions --- crates/ndg-templates/templates/default.css | 107 ++++++++----- crates/ndg-templates/templates/main.js | 76 +++++---- crates/ndg-templates/templates/main.test.js | 162 +++++++++++++++++++- 3 files changed, 275 insertions(+), 70 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index e559798eb..01947916d 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -143,11 +143,11 @@ --font-mono: "JetBrains Mono", monospace; } -:root[data-theme="light"] { +:root:is([data-theme="light"], [data-theme-resolved="light"]) { color-scheme: light; } -:root[data-theme="dark"] { +:root:is([data-theme="dark"], [data-theme-resolved="dark"]) { color-scheme: dark; } @@ -751,7 +751,8 @@ img { } @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .theme-toggle { + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) + .theme-toggle { .theme-icon-moon { display: none; } @@ -762,7 +763,8 @@ img { } } -:where(:root[data-theme="dark"]) .theme-toggle { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .theme-toggle { .theme-icon-moon { display: none; } @@ -1016,12 +1018,14 @@ img { } @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) #search-page-input { + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) + #search-page-input { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); } } -:where(:root[data-theme="dark"]) #search-page-input { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + #search-page-input { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); } @@ -1286,175 +1290,197 @@ mark { /* Dark mode SVG icon adjustments */ @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.note .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2360a5fa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.tip .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2334d399' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fbbf24' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.danger .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f87171' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.info .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2322d3ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fb923c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } -:where(:root[data-theme="dark"]) .admonition.note .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2360a5fa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.tip .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2334d399' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.warning .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fbbf24' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.danger .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f87171' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.info .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.example .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2322d3ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.caution .admonition-title::before { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fb923c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } /* Light mode SVG icon adjustments */ @media (prefers-color-scheme: light) { - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.note .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.tip .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.danger .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.info .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) + :where(:root:not([data-theme="dark"], [data-theme-resolved="dark"])) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } -:where(:root[data-theme="light"]) .admonition.note .admonition-title::before { +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.tip .admonition-title::before { +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) .admonition.warning .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.danger .admonition-title::before { +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.info .admonition-title::before { +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) .admonition.important .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) .admonition.example .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) +:where(:root:is([data-theme="light"], [data-theme-resolved="light"])) .admonition.caution .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); @@ -1500,12 +1526,13 @@ mark { } @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .copy-link { + :where(:root:not([data-theme="light"], [data-theme-resolved="light"])) + .copy-link { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'%3E%3C/path%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'%3E%3C/path%3E%3C/svg%3E"); } } -:where(:root[data-theme="dark"]) .copy-link { +:where(:root:is([data-theme="dark"], [data-theme-resolved="dark"])) .copy-link { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'%3E%3C/path%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'%3E%3C/path%3E%3C/svg%3E"); } diff --git a/crates/ndg-templates/templates/main.js b/crates/ndg-templates/templates/main.js index 44b52f54b..5a288a9b3 100644 --- a/crates/ndg-templates/templates/main.js +++ b/crates/ndg-templates/templates/main.js @@ -1388,9 +1388,9 @@ function initializePage() { // Light and dark theme. The inline script of each page applies the stored // theme to `` before the first paint. Without `data-theme`, -// the theme is `auto` and follows the color scheme of the system. The -// stylesheet reads `data-theme`, so this code only changes it, stores it, -// updates the images and labels, and dispatches `ndg:themechange` on +// the theme is `auto`. In auto mode, `data-theme-resolved` holds the system +// scheme so CSS waits for applyTheme before starting color transitions. +// Changes also update images and labels and dispatch `ndg:themechange` on // `document` with the `mode` and the `resolved` theme. const THEME_STORAGE_KEY = "theme"; const DARK_SCHEME_QUERY = "(prefers-color-scheme: dark)"; @@ -1408,13 +1408,35 @@ function resolveTheme(mode) { return mode === "auto" ? systemTheme() : mode; } +function syncThemeAttributes(mode, resolved) { + const { dataset } = document.documentElement; + if (mode === "auto") { + dataset.themeResolved = resolved; + delete dataset.theme; + } else { + dataset.theme = mode; + delete dataset.themeResolved; + } +} + +function saveThemePreference(mode) { + try { + if (mode === "auto") { + localStorage.removeItem(THEME_STORAGE_KEY); + } else { + localStorage.setItem(THEME_STORAGE_KEY, mode); + } + } catch { + // localStorage unavailable: the theme applies to this page only + } +} + // A `` of a `` follows // only the system. For a selected theme, replace each color scheme feature // with a feature that always matches or never matches. A browser shows the // page on a bitmap screen, thus `(grid: 0)` always matches and `(grid: 1)` // never matches. The `data-scheme-media` attribute keeps the original query. function syncThemedImages(mode, root = document) { - const resolved = resolveTheme(mode); root .querySelectorAll( "picture > source[media*='prefers-color-scheme'], picture > source[data-scheme-media]", @@ -1427,15 +1449,15 @@ function syncThemedImages(mode, root = document) { : source.dataset.schemeMedia.replace( /\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)/gi, (_, scheme) => - scheme.toLowerCase() === resolved ? "(grid: 0)" : "(grid: 1)", + scheme.toLowerCase() === mode ? "(grid: 0)" : "(grid: 1)", ); }); } // The label tells what a click does, as the icon does. -function syncThemeToggles(mode) { +function syncThemeToggles(resolved) { const label = - resolveTheme(mode) === "dark" + resolved === "dark" ? "Switch to the light theme" : "Switch to the dark theme"; document.querySelectorAll("[data-theme-toggle]").forEach((toggle) => { @@ -1446,25 +1468,27 @@ function syncThemeToggles(mode) { let themeTransitionFrame; -function applyTheme(mode) { +function startThemeTransition() { // Use one color duration for the switch without changing normal hover timing. + const root = document.documentElement; cancelAnimationFrame(themeTransitionFrame); - document.documentElement.classList.add("theme-changing"); + root.classList.add("theme-changing"); themeTransitionFrame = requestAnimationFrame(() => { themeTransitionFrame = requestAnimationFrame(() => { - document.documentElement.classList.remove("theme-changing"); + root.classList.remove("theme-changing"); }); }); - if (mode === "auto") { - delete document.documentElement.dataset.theme; - } else { - document.documentElement.dataset.theme = mode; - } - syncThemeToggles(mode); +} + +function applyTheme(mode) { + startThemeTransition(); + const resolved = resolveTheme(mode); + syncThemeAttributes(mode, resolved); + syncThemeToggles(resolved); syncThemedImages(mode); document.dispatchEvent( new CustomEvent("ndg:themechange", { - detail: { mode, resolved: resolveTheme(mode) }, + detail: { mode, resolved }, }), ); } @@ -1478,25 +1502,19 @@ function nextTheme(mode, system) { } function setupThemeToggle() { - syncThemeToggles(currentTheme()); + const mode = currentTheme(); + const resolved = resolveTheme(mode); + syncThemeAttributes(mode, resolved); + syncThemeToggles(resolved); document.addEventListener("click", (event) => { if (!event.target.closest?.("[data-theme-toggle]")) return; const mode = nextTheme(currentTheme(), systemTheme()); - try { - if (mode === "auto") { - localStorage.removeItem(THEME_STORAGE_KEY); - } else { - localStorage.setItem(THEME_STORAGE_KEY, mode); - } - } catch { - // localStorage unavailable: the theme applies to this page only - } + saveThemePreference(mode); applyTheme(mode); }); - // In `auto`, the stylesheet follows the system, but other scripts can need - // the event. + // Change the resolved scheme and transition timing together in auto mode. window.matchMedia(DARK_SCHEME_QUERY).addEventListener("change", () => { if (currentTheme() === "auto") applyTheme("auto"); }); diff --git a/crates/ndg-templates/templates/main.test.js b/crates/ndg-templates/templates/main.test.js index 617007f77..232380988 100644 --- a/crates/ndg-templates/templates/main.test.js +++ b/crates/ndg-templates/templates/main.test.js @@ -18,7 +18,8 @@ function loadMain(requestAnimationFrame, browser = {}) { "fetch", "DOMParser", "IntersectionObserver", - `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme, applyTheme, getFilterMatches, setFilteredItemVisibility };`, + "localStorage", + `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme, applyTheme, setupThemeToggle, currentTheme, getFilterMatches, setFilteredItemVisibility };`, )( window, document, @@ -27,6 +28,7 @@ function loadMain(requestAnimationFrame, browser = {}) { browser.fetch, browser.DOMParser, browser.IntersectionObserver, + browser.localStorage, ); } @@ -388,6 +390,164 @@ Deno.test("the theme toggle goes back to the system theme", () => { assert(nextTheme("light", "dark") === "auto", "light goes back to auto"); }); +function themeToggleFixture(dark, mode = "auto") { + const classes = new Set(); + const schemeChanges = []; + const dataset = new Proxy(mode === "auto" ? {} : { theme: mode }, { + set(target, key, value) { + if (key === "themeResolved") { + schemeChanges.push({ + value, + transitioning: classes.has("theme-changing"), + }); + } + target[key] = value; + return true; + }, + }); + const root = { + dataset, + classList: { + add: (name) => classes.add(name), + remove: (name) => classes.delete(name), + }, + }; + const events = []; + const labels = {}; + const listeners = {}; + const mediaListeners = {}; + const media = { + matches: dark, + addEventListener: (type, listener) => (mediaListeners[type] = listener), + }; + const stored = new Map(mode === "auto" ? [] : [["theme", mode]]); + const browser = themeBrowser(dark); + browser.window.matchMedia = () => media; + browser.cancelAnimationFrame = () => {}; + browser.localStorage = { + setItem: (key, value) => stored.set(key, value), + removeItem: (key) => stored.delete(key), + }; + browser.document = { + documentElement: root, + addEventListener: (type, listener) => (listeners[type] = listener), + querySelectorAll: (selector) => + selector === "[data-theme-toggle]" + ? [{ setAttribute: (key, value) => (labels[key] = value) }] + : [], + dispatchEvent: (event) => events.push(event.detail), + }; + const theme = loadMain(() => {}, browser); + theme.setupThemeToggle(); + return { + ...theme, + root, + classes, + events, + labels, + schemeChanges, + stored, + media, + reportSystemChange: () => mediaListeners.change(), + click: () => listeners.click({ target: { closest: () => true } }), + }; +} + +Deno.test("auto mode waits for the OS listener to change the palette", () => { + for (const dark of [false, true]) { + const fixture = themeToggleFixture(dark); + assert( + fixture.root.dataset.themeResolved === (dark ? "dark" : "light"), + "initialization pins the current system scheme", + ); + assert(fixture.events.length === 0, "initialization sends no theme event"); + assert(fixture.classes.size === 0, "initialization starts no transition"); + + for (const nextDark of [!dark, dark]) { + const previous = fixture.root.dataset.themeResolved; + fixture.media.matches = nextDark; + assert( + fixture.root.dataset.themeResolved === previous, + "the palette stays on the old scheme before event delivery", + ); + fixture.reportSystemChange(); + const resolved = nextDark ? "dark" : "light"; + assert(fixture.currentTheme() === "auto", "OS changes keep auto mode"); + assert( + fixture.root.dataset.themeResolved === resolved && + fixture.schemeChanges.at(-1).transitioning, + "the new palette is applied with shared transition timing", + ); + const event = fixture.events.at(-1); + assert( + event.mode === "auto" && event.resolved === resolved, + "events report auto mode and the new system scheme", + ); + assert( + fixture.labels["aria-label"] === + `Switch to the ${nextDark ? "light" : "dark"} theme`, + "the toggle label follows the resolved scheme", + ); + assert( + !fixture.stored.has("theme"), + "OS changes save no explicit choice", + ); + } + } +}); + +Deno.test("explicit choices ignore OS changes and can return to auto", () => { + for (const mode of ["light", "dark"]) { + const fixture = themeToggleFixture(mode !== "dark", mode); + fixture.media.matches = mode === "dark"; + fixture.reportSystemChange(); + assert( + fixture.currentTheme() === mode, + "the explicit choice remains active", + ); + assert(fixture.events.length === 0, "OS changes send no override event"); + assert( + fixture.classes.size === 0, + "OS changes start no override transition", + ); + + fixture.click(); + const other = mode === "dark" ? "light" : "dark"; + assert( + fixture.stored.get("theme") === other, + "a click saves the other theme", + ); + assert( + !fixture.root.dataset.themeResolved, + "explicit choices use data-theme without an auto scheme", + ); + fixture.click(); + assert( + fixture.currentTheme() === "auto", + "the second click returns to auto", + ); + assert( + !fixture.stored.has("theme"), + "returning to auto clears the saved choice", + ); + assert( + fixture.root.dataset.themeResolved === mode, + "auto uses the system scheme", + ); + + fixture.media.matches = mode !== "dark"; + fixture.reportSystemChange(); + assert( + fixture.root.dataset.themeResolved === other, + "auto follows later OS changes", + ); + assert( + fixture.currentTheme() === "auto", + "the resolved scheme is not a choice", + ); + } +}); + Deno.test( "rapid theme changes keep transition timing until the next paint", () => { From f09f338313d1dcf9041299bf4c9062cfd8ebb1b8 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 11 Oct 2026 12:09:01 +0200 Subject: [PATCH 07/11] docs: clarify automatic theme transition timing --- CHANGELOG.md | 7 ++++--- crates/ndg-templates/templates/main.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35f8db07..735cbd17e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,9 +49,10 @@ changes. theme is the same as the system theme, the page follows the system theme again. The toggle also selects the matching `` of a `` with a `prefers-color-scheme` media query. The page sends an `ndg:themechange` event - on `document` when the theme changes. Colors transition consistently across - page elements while preserving normal hover timing, and native browser - controls follow the selected theme through `color-scheme`. + on `document` when the theme changes. Colors use consistent transition timing + across page elements, including system theme changes in auto mode, while + preserving normal hover timing. Native browser controls follow the selected + theme through `color-scheme`. ### Changed diff --git a/crates/ndg-templates/templates/main.js b/crates/ndg-templates/templates/main.js index 5a288a9b3..b650df6e2 100644 --- a/crates/ndg-templates/templates/main.js +++ b/crates/ndg-templates/templates/main.js @@ -1473,6 +1473,7 @@ function startThemeTransition() { const root = document.documentElement; cancelAnimationFrame(themeTransitionFrame); root.classList.add("theme-changing"); + // Keep the class through a paint; running transitions retain their timing. themeTransitionFrame = requestAnimationFrame(() => { themeTransitionFrame = requestAnimationFrame(() => { root.classList.remove("theme-changing"); From 4fae19b4e7d8520006885e454d56628770639831 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 17 Oct 2026 08:34:52 +0200 Subject: [PATCH 08/11] ndg-templates: restore fast highlight timing on search results and mobile controls --- crates/ndg-templates/templates/default.css | 11 ++++++++++- crates/ndg-templates/templates/mobile-sidebar.css | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index 01947916d..5fa8cb5d2 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -830,7 +830,10 @@ img { .search-result-item { padding: var(--space-4); border-bottom: 1px solid var(--border-color); - transition: var(--theme-colors); + transition: + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); min-height: 44px; display: flex; align-items: center; @@ -914,6 +917,10 @@ img { border-radius: 4px; background-color: var(--bg-color); color: var(--text-color); + transition: + var(--theme-colors), + border-color var(--theme-transition, var(--transition-fast)), + box-shadow var(--theme-transition, var(--transition-fast)); } #mobile-search-input:focus { @@ -936,6 +943,7 @@ img { display: flex; align-items: center; justify-content: center; + transition: color var(--theme-transition, var(--transition-fast)); } .close-mobile-search:hover { @@ -2428,6 +2436,7 @@ h6:hover .copy-link { left: var(--space-4); color: var(--text-muted); font-size: 1em; + transition: color var(--theme-transition, var(--transition-fast)); } .search-result-anchor a { diff --git a/crates/ndg-templates/templates/mobile-sidebar.css b/crates/ndg-templates/templates/mobile-sidebar.css index 6f812392b..a1a42d5eb 100644 --- a/crates/ndg-templates/templates/mobile-sidebar.css +++ b/crates/ndg-templates/templates/mobile-sidebar.css @@ -77,6 +77,10 @@ cursor: pointer; font-size: 1.25rem; line-height: 1; + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); } .mobile-sidebar-close:hover { @@ -113,6 +117,11 @@ text-decoration: none; font-size: 0.875rem; font-weight: 600; + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)); } .mobile-sidebar-site-nav a:hover, @@ -150,6 +159,10 @@ border-radius: var(--radius-md); color: var(--text-color); text-decoration: none; + transition: + var(--theme-colors), + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)); } .mobile-sidebar-content From 3dd362ccf223da5ba965e7a3548bd6402362efe8 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 17 Oct 2026 09:46:19 +0200 Subject: [PATCH 09/11] ndg-templates: theme the mobile sidebar backdrop with light-dark() --- crates/ndg-templates/templates/mobile-sidebar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ndg-templates/templates/mobile-sidebar.css b/crates/ndg-templates/templates/mobile-sidebar.css index a1a42d5eb..f0157e60d 100644 --- a/crates/ndg-templates/templates/mobile-sidebar.css +++ b/crates/ndg-templates/templates/mobile-sidebar.css @@ -7,7 +7,7 @@ position: fixed; inset: 0; z-index: 900; - background-color: rgba(15, 23, 42, 0.4); + background-color: light-dark(rgba(15, 23, 42, 0.4), rgba(0, 0, 0, 0.6)); opacity: 0; transition: opacity var(--transition-fast); } From 3a1924bd96e0419d6f0e0bcf2d425aa53edf883d Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 17 Oct 2026 11:03:37 +0200 Subject: [PATCH 10/11] ndg-templates: collect fast theme color transitions into a variable --- crates/ndg-templates/templates/default.css | 108 ++++++------------ .../templates/mobile-sidebar.css | 21 +--- .../ndg-templates/templates/options-pages.css | 6 +- 3 files changed, 39 insertions(+), 96 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index 5fa8cb5d2..578736a09 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -132,12 +132,18 @@ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); --transition: 250ms cubic-bezier(0.4, 0, 0.2, 1); - /* Color transitions compose with component movement and hover timing. */ + /* Color transitions compose with component movement and hover timing. + The fast variant keeps state changes of interactive elements quick. */ --theme-colors: color var(--theme-transition, var(--transition)), background-color var(--theme-transition, var(--transition)), border-color var(--theme-transition, var(--transition)), box-shadow var(--theme-transition, var(--transition)); + --theme-colors-fast: + color var(--theme-transition, var(--transition-fast)), + background-color var(--theme-transition, var(--transition-fast)), + border-color var(--theme-transition, var(--transition-fast)), + box-shadow var(--theme-transition, var(--transition-fast)); /* Fonts */ --font-mono: "JetBrains Mono", monospace; @@ -317,9 +323,7 @@ header { font-size: 0.95rem; font-weight: 500; padding: var(--space-2) 0; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); display: block; } @@ -415,10 +419,7 @@ header { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); font-size: 0.875rem; &:hover { @@ -582,8 +583,7 @@ a { color: var(--link-color); text-decoration: none; transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), + var(--theme-colors-fast), text-decoration-color var(--theme-transition, var(--transition-fast)); text-underline-offset: 0.15em; } @@ -717,10 +717,7 @@ img { border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); &:hover { color: var(--header-text); @@ -785,10 +782,7 @@ img { background-color: var(--bg-surface); color: var(--text-color); transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)), - box-shadow var(--theme-transition, var(--transition-fast)), + var(--theme-colors-fast), width var(--transition); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -830,10 +824,7 @@ img { .search-result-item { padding: var(--space-4); border-bottom: 1px solid var(--border-color); - transition: - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); min-height: 44px; display: flex; align-items: center; @@ -917,10 +908,7 @@ img { border-radius: 4px; background-color: var(--bg-color); color: var(--text-color); - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition-fast)), - box-shadow var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } #mobile-search-input:focus { @@ -943,7 +931,7 @@ img { display: flex; align-items: center; justify-content: center; - transition: color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .close-mobile-search:hover { @@ -1015,9 +1003,7 @@ img { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); box-sizing: border-box; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -1096,10 +1082,8 @@ img { border-radius: var(--radius-md); background-color: var(--sidebar-bg); transition: - var(--theme-colors), - transform var(--transition-fast), - border-color var(--theme-transition, var(--transition-fast)), - box-shadow var(--theme-transition, var(--transition-fast)); + var(--theme-colors-fast), + transform var(--transition-fast); } .search-page-results .search-result-item:hover { @@ -1526,9 +1510,8 @@ mark { cursor: pointer; opacity: 0; transition: - var(--theme-colors), - opacity var(--transition-fast), - background-color var(--theme-transition, var(--transition-fast)); + var(--theme-colors-fast), + opacity var(--transition-fast); vertical-align: middle; border-radius: var(--radius-sm); } @@ -1709,10 +1692,7 @@ h6:hover .copy-link { background-size: 18px; background-repeat: no-repeat; border: 1px solid rgba(255, 255, 255, 0.1); - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .search-container #search-input::placeholder { @@ -1852,10 +1832,7 @@ h6:hover .copy-link { display: block; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md); - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); font-size: 0.875rem; white-space: nowrap; overflow: hidden; @@ -1894,10 +1871,8 @@ h6:hover .copy-link { list-style: none; border-radius: var(--radius-md); transition: - var(--theme-colors), - transform var(--transition-fast), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + var(--theme-colors-fast), + transform var(--transition-fast); } .toc-list details summary:hover { @@ -1915,9 +1890,8 @@ h6:hover .copy-link { flex-shrink: 0; color: var(--text-muted); transition: - var(--theme-colors), - transform var(--transition-fast), - color var(--theme-transition, var(--transition-fast)); + var(--theme-colors-fast), + transform var(--transition-fast); } .toc-list details summary span:first-of-type { @@ -1992,10 +1966,7 @@ h6:hover .copy-link { border-radius: var(--radius-lg); margin: var(--space-6) 0; background-color: var(--bg-surface); - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition-fast)), - box-shadow var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); box-shadow: var(--shadow-sm); min-width: 0; } @@ -2315,9 +2286,7 @@ h6:hover .copy-link { background-color: var(--sidebar-bg); color: var(--text-color); font-size: 1rem; - transition: - var(--theme-colors), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); box-sizing: border-box; } @@ -2436,7 +2405,7 @@ h6:hover .copy-link { left: var(--space-4); color: var(--text-muted); font-size: 1em; - transition: color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .search-result-anchor a { @@ -2493,10 +2462,7 @@ h6:hover .copy-link { align-items: center; gap: var(--space-2); border-radius: var(--radius-md); - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .sidebar-section > summary:hover { @@ -2619,10 +2585,7 @@ h6:hover .copy-link { padding: var(--space-1) var(--space-3); color: var(--text-muted); text-decoration: none; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); border-left: 2px solid transparent; } @@ -2680,9 +2643,7 @@ h6:hover .copy-link { cursor: pointer; text-decoration: none; font-size: 0.875rem; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .search-error button:hover, @@ -2759,10 +2720,7 @@ h6:hover .copy-link { text-transform: uppercase; letter-spacing: 0.04em; list-style: none; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition-fast)), - color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); &::-webkit-details-marker { display: none; diff --git a/crates/ndg-templates/templates/mobile-sidebar.css b/crates/ndg-templates/templates/mobile-sidebar.css index f0157e60d..e06417679 100644 --- a/crates/ndg-templates/templates/mobile-sidebar.css +++ b/crates/ndg-templates/templates/mobile-sidebar.css @@ -77,10 +77,7 @@ cursor: pointer; font-size: 1.25rem; line-height: 1; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .mobile-sidebar-close:hover { @@ -117,11 +114,7 @@ text-decoration: none; font-size: 0.875rem; font-weight: 600; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .mobile-sidebar-site-nav a:hover, @@ -159,10 +152,7 @@ border-radius: var(--radius-md); color: var(--text-color); text-decoration: none; - transition: - var(--theme-colors), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .mobile-sidebar-content @@ -202,10 +192,7 @@ body.mobile-sidebar-open { color: var(--header-text); font: inherit; cursor: pointer; - transition: - var(--theme-colors), - background-color var(--theme-transition, var(--transition-fast)), - border-color var(--theme-transition, var(--transition-fast)); + transition: var(--theme-colors-fast); } .mobile-sidebar-toggle:hover { diff --git a/crates/ndg-templates/templates/options-pages.css b/crates/ndg-templates/templates/options-pages.css index f2e577b3e..3875f9d42 100644 --- a/crates/ndg-templates/templates/options-pages.css +++ b/crates/ndg-templates/templates/options-pages.css @@ -32,11 +32,9 @@ color: var(--text-color); text-decoration: none; transition: - var(--theme-colors), + var(--theme-colors-fast), opacity var(--transition-fast), - transform var(--transition-fast), - color var(--theme-transition, var(--transition-fast)), - background-color var(--theme-transition, var(--transition-fast)); + transform var(--transition-fast); } .filter-entering, From 55a31c1e6e77bbff439d1f97684e2010d090f565 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 17 Oct 2026 12:02:58 +0200 Subject: [PATCH 11/11] ndg-templates: drop duplicated search result rules --- crates/ndg-templates/templates/default.css | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index 578736a09..9cf3a02d9 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -845,11 +845,6 @@ img { background-color: var(--sidebar-hover); } -.search-result-item.search-result-active { - background-color: var(--sidebar-active); - border-left: 3px solid var(--link-color); -} - .search-result-title { font-weight: 600; margin-bottom: var(--space-2); @@ -946,14 +941,6 @@ img { /* Reuse desktop search result styling */ .mobile-search-results .search-result-item { padding: var(--space-3) var(--space-4); - min-height: 44px; - display: flex; - align-items: center; - border-bottom: 1px solid var(--border-color); -} - -.mobile-search-results .search-result-item:last-child { - border-bottom: none; } .mobile-search-results .search-result-item a { @@ -966,10 +953,6 @@ img { flex-grow: 1; } -.mobile-search-results .search-result-item:hover { - background-color: var(--sidebar-hover); -} - /* Touch feedback for mobile devices */ @media (hover: none) and (pointer: coarse) { .mobile-search-results .search-result-item:active {