| 1 | --- |
| 2 | // The header's theme button: one icon, for Auto (the system's), Light or |
| 3 | // Dark, and a click moves to the next. The same choice, kept in the same |
| 4 | // place, as Starlight's theme select at the foot of the phone menu, so |
| 5 | // the two stay in step. |
| 6 | import { Monitor, Moon, Sun } from '@lucide/astro'; |
| 7 | --- |
| 8 | |
| 9 | <button type="button" class="g1t-theme" data-g1t-theme aria-label="Theme: Auto"> |
| 10 | <Monitor class="g1t-theme-auto" /> |
| 11 | <Sun class="g1t-theme-light" /> |
| 12 | <Moon class="g1t-theme-dark" /> |
| 13 | </button> |
| 14 | |
| 15 | <script is:inline> |
| 16 | // Before the first paint, so the button never shows the wrong icon. |
| 17 | (() => { |
| 18 | let stored = null; |
| 19 | try { |
| 20 | stored = localStorage.getItem('starlight-theme'); |
| 21 | } catch {} |
| 22 | const choice = stored === 'light' || stored === 'dark' ? stored : 'auto'; |
| 23 | for (const button of document.querySelectorAll('[data-g1t-theme]')) button.dataset.choice = choice; |
| 24 | })(); |
| 25 | </script> |
| 26 | |
| 27 | <script> |
| 28 | type Theme = 'auto' | 'light' | 'dark'; |
| 29 | const NEXT: Record<Theme, Theme> = { auto: 'light', light: 'dark', dark: 'auto' }; |
| 30 | const NAMES: Record<Theme, string> = { auto: 'Auto', light: 'Light', dark: 'Dark' }; |
| 31 | const KEY = 'starlight-theme'; |
| 32 | |
| 33 | const load = (): Theme => { |
| 34 | try { |
| 35 | const stored = localStorage.getItem(KEY); |
| 36 | return stored === 'light' || stored === 'dark' ? stored : 'auto'; |
| 37 | } catch { |
| 38 | return 'auto'; |
| 39 | } |
| 40 | }; |
| 41 | |
| 42 | function show(theme: Theme) { |
| 43 | document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => { |
| 44 | button.dataset.choice = theme; |
| 45 | button.setAttribute('aria-label', `Theme: ${NAMES[theme]}`); |
| 46 | }); |
| 47 | } |
| 48 | |
| 49 | function choose(theme: Theme) { |
| 50 | const system = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; |
| 51 | document.documentElement.dataset.theme = theme === 'auto' ? system : theme; |
| 52 | try { |
| 53 | localStorage.setItem(KEY, theme === 'auto' ? '' : theme); |
| 54 | } catch {} |
| 55 | // Starlight's own select, in the phone menu, shows the same choice. |
| 56 | (window as { StarlightThemeProvider?: { updatePickers(theme: Theme): void } }).StarlightThemeProvider?.updatePickers( |
| 57 | theme, |
| 58 | ); |
| 59 | show(theme); |
| 60 | } |
| 61 | |
| 62 | show(load()); |
| 63 | document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => { |
| 64 | button.addEventListener('click', () => choose(NEXT[load()])); |
| 65 | }); |
| 66 | // A choice made in the phone menu's select. |
| 67 | document.addEventListener('change', (event) => { |
| 68 | if ((event.target as Element).closest('starlight-theme-select')) show(load()); |
| 69 | }); |
| 70 | </script> |
| 71 | |
| 72 | <style> |
| 73 | .g1t-theme { |
| 74 | display: inline-flex; |
| 75 | align-items: center; |
| 76 | justify-content: center; |
| 77 | width: 2rem; |
| 78 | height: 2rem; |
| 79 | border: 0; |
| 80 | border-radius: 0.5rem; |
| 81 | padding: 0; |
| 82 | background: transparent; |
| 83 | color: var(--g1t-muted); |
| 84 | cursor: pointer; |
| 85 | transition: color 0.15s, background 0.15s; |
| 86 | } |
| 87 | .g1t-theme:hover { |
| 88 | background: var(--g1t-raised); |
| 89 | color: var(--g1t-fg); |
| 90 | } |
| 91 | .g1t-theme:focus-visible { |
| 92 | outline: 2px solid var(--g1t-accent); |
| 93 | outline-offset: 1px; |
| 94 | } |
| 95 | .g1t-theme :global(svg) { |
| 96 | display: none; |
| 97 | width: 1rem; |
| 98 | height: 1rem; |
| 99 | } |
| 100 | .g1t-theme[data-choice='auto'] :global(.g1t-theme-auto), |
| 101 | .g1t-theme[data-choice='light'] :global(.g1t-theme-light), |
| 102 | .g1t-theme[data-choice='dark'] :global(.g1t-theme-dark) { |
| 103 | display: block; |
| 104 | } |
| 105 | </style> |