diff --git a/demo/main.ts b/demo/main.ts index 566f84a..2ed231f 100644 --- a/demo/main.ts +++ b/demo/main.ts @@ -23,39 +23,138 @@ function element(id: string): T { const settingsDialog = element('chart-settings'); const settingsButton = element('open-settings'); +const desktopSettings = window.matchMedia('(min-width: 1000px)'); +const chartPanel = element('smith').closest('.chart-panel')!; +const scalesPanel = element('parameter-panel').closest('.scales-panel')!; +const settingsBackdrop = element('settings-backdrop'); +const settingsBackground = document.querySelectorAll( + 'main > header, main > .toolbar, .scales-panel, .workspace > aside, main > footer', +); + function positionSettingsDialog(): void { - if (!settingsDialog.open || !window.matchMedia('(min-width: 1000px)').matches) { + if (!settingsDialog.open || !desktopSettings.matches) { return; } - const anchor = element('parameter-panel').closest('.scales-panel')!; - const bounds = anchor.getBoundingClientRect(); - // Align with the readout panel while reserving at least 320 px for settings on short screens. - const top = Math.max(16, Math.min(bounds.top, window.innerHeight - 336)); - settingsDialog.style.setProperty('--settings-top', `${top}px`); - settingsDialog.style.setProperty('--settings-left', `${bounds.left}px`); - settingsDialog.style.setProperty('--settings-width', `${bounds.width}px`); + const chartBounds = chartPanel.getBoundingClientRect(); + const scalesBounds = scalesPanel.getBoundingClientRect(); + settingsDialog.style.setProperty('--settings-top', `${chartBounds.top}px`); + settingsDialog.style.setProperty('--settings-left', `${scalesBounds.left}px`); + settingsDialog.style.setProperty('--settings-width', `${scalesBounds.width}px`); + settingsDialog.style.setProperty('--settings-height', `${chartBounds.height}px`); +} + +function setDesktopSettingsActive(active: boolean): void { + document.body.classList.toggle('settings-active', active); + settingsBackdrop.hidden = !active; + for (const region of settingsBackground) { + region.inert = active; + } } -settingsButton.addEventListener('click', () => { - settingsDialog.showModal(); + +function showSettings(): void { + setDesktopSettingsActive(desktopSettings.matches); + if (desktopSettings.matches) { + // A non-modal dialog leaves the chart available for zoom and marker interaction. + settingsDialog.show(); + } else { + settingsDialog.showModal(); + } positionSettingsDialog(); -}); +} + +settingsButton.addEventListener('click', showSettings); window.addEventListener('resize', positionSettingsDialog); window.addEventListener('scroll', positionSettingsDialog, { passive: true }); -settingsDialog.addEventListener('close', () => settingsButton.focus({ preventScroll: true })); +const settingsResizeObserver = new ResizeObserver(positionSettingsDialog); +settingsResizeObserver.observe(chartPanel); +settingsResizeObserver.observe(scalesPanel); +desktopSettings.addEventListener('change', () => { + if (settingsDialog.open) { + settingsDialog.close(); + showSettings(); + } +}); + +let settingsPressOutside = false; +function outsideSettings(event: MouseEvent): boolean { + if (event.target === settingsBackdrop) { + return true; + } + const bounds = settingsDialog.getBoundingClientRect(); + return ( + event.target === settingsDialog && + (event.clientX < bounds.left || + event.clientX > bounds.right || + event.clientY < bounds.top || + event.clientY > bounds.bottom) + ); +} +document.addEventListener('pointerdown', (event) => { + settingsPressOutside = settingsDialog.open && event.button === 0 && outsideSettings(event); +}); +document.addEventListener('pointercancel', () => { + settingsPressOutside = false; +}); +document.addEventListener('click', (event) => { + if (settingsPressOutside && outsideSettings(event)) { + settingsDialog.close(); + } + settingsPressOutside = false; +}); +document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && settingsDialog.open && desktopSettings.matches) { + event.preventDefault(); + settingsDialog.close(); + } +}); +settingsDialog.addEventListener('close', () => { + // A breakpoint change closes and immediately reopens the dialog in its new mode. + if (settingsDialog.open) { + return; + } + settingsPressOutside = false; + setDesktopSettingsActive(false); + settingsButton.focus({ preventScroll: true }); +}); + +function demoAppearance(theme: SmithAppearance['theme']): SmithAppearance { + return { + theme, + ...(theme === 'dark' + ? { + overrides: { + background: '#191c20', + fontFamily: 'Arial, sans-serif', + grid: { stroke: '#727984', textColor: '#c7cbd1' }, + boundary: { stroke: '#9da3ad' }, + scales: { + stroke: '#727984', + boundaryColor: '#9da3ad', + textColor: '#b5bcc5', + valueColor: '#e4e7eb', + indicatorColor: '#e9ac59', + indicatorOutline: '#191c20', + }, + cursor: { pointColor: '#a0bce5', impedanceColor: '#a0bce5' }, + }, + } + : {}), + }; +} const smith = new Smith({ - appearance: { theme: 'dark' }, + appearance: demoAppearance('dark'), interaction: { zoom: true, cursor: true }, peripheralScales: { visible: true }, grid: { detail: 'detailed' }, }); smith.draw('#smith'); -const scales = new SmithScales({ theme: 'dark' }); +const scales = new SmithScales(demoAppearance('dark')); scales.draw('#smith-scales'); element('theme').addEventListener('change', (event) => { const theme = (event.target as HTMLSelectElement).value as SmithAppearance['theme']; - const appearance: SmithAppearance = { theme }; + const appearance = demoAppearance(theme); smith.setOptions({ appearance }); scales.setAppearance(appearance); document.documentElement.dataset.theme = theme; diff --git a/demo/style.css b/demo/style.css index 1190d90..239e840 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,16 +1,21 @@ :root { - --text: #24364b; - --page: #f3f6f9; + --text: #25292f; + --page: #f4f5f6; --surface: white; - --muted: #617286; - --accent: #287e76; - --border: #dce4eb; - --input-border: #bbcbd4; - --hover: #edf5f4; + --muted: #626a75; + --accent: #315f94; + --border: #dfe2e6; + --input-border: #b8bec7; + --hover: #e9edf2; --error: #b42318; color-scheme: light; - font-family: Inter, system-ui, sans-serif; + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; color: var(--text); background: var(--page); font-synthesis: none; @@ -29,15 +34,9 @@ main { header { margin-bottom: 28px; } -.eyebrow { - color: var(--accent); - font-size: 11px; - font-weight: 750; - letter-spacing: 0.16em; -} h1 { font-size: 36px; - letter-spacing: -0.04em; + letter-spacing: -0.025em; margin: 6px 0 8px; } p { @@ -62,10 +61,8 @@ nav[aria-label='Resources'] a { align-items: center; justify-content: flex-start; gap: 20px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 12px; - padding: 16px 20px; + border-bottom: 1px solid var(--border); + padding: 0 0 12px; margin-bottom: 20px; } fieldset { @@ -121,7 +118,7 @@ select:focus-visible { .panel { background: var(--surface); border: 1px solid var(--border); - border-radius: 12px; + border-radius: 6px; } .chart-column { min-width: 0; @@ -389,7 +386,7 @@ footer { } .toolbar { gap: 12px; - padding: 8px 16px; + padding: 0 0 12px; margin-bottom: 12px; } .workspace { @@ -398,7 +395,7 @@ footer { } .chart-column { grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: start; + align-items: stretch; gap: 16px; } #smith { @@ -416,7 +413,7 @@ footer { } aside { grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: start; + align-items: stretch; } } @media (max-width: 800px) { @@ -447,15 +444,15 @@ footer { } :root[data-theme='dark'] { - --text: #e2e8f0; - --page: #020617; - --surface: #0f172a; - --muted: #94a3b8; - --accent: #5eead4; - --border: #334155; - --input-border: #475569; - --hover: #1e293b; - --error: #fca5a5; + --text: #e4e7eb; + --page: #111315; + --surface: #191c20; + --muted: #a0a7b2; + --accent: #a0bce5; + --border: #30353c; + --input-border: #4d5560; + --hover: #272c33; + --error: #efa19b; color-scheme: dark; } #chart-settings, @@ -464,26 +461,40 @@ footer { max-height: calc(100dvh - 32px); padding: 24px; border: 1px solid var(--border); - border-radius: 16px; + border-radius: 8px; background: var(--surface); color: var(--text); - box-shadow: 0 16px 64px rgb(0 0 0 / 25%); + box-shadow: 0 12px 32px rgb(0 0 0 / 22%); } #chart-settings::backdrop, #image-export::backdrop { - background: rgb(2 6 23 / 45%); + background: rgb(0 0 0 / 42%); } @media (min-width: 1000px) { #chart-settings { + position: fixed; + z-index: 21; inset: var(--settings-top, 16px) auto auto var(--settings-left, calc(50% + 8px)); margin: 0; width: var(--settings-width, calc(50vw - 32px)); - max-height: calc(100dvh - var(--settings-top, 16px) - 16px); + height: var(--settings-height); + max-height: none; + overflow-y: auto; + overscroll-behavior: contain; } - #chart-settings::backdrop { - background: rgb(2 6 23 / 25%); + .settings-active .chart-panel { + position: relative; + z-index: 21; } } +#settings-backdrop:not([hidden]) { + position: fixed; + inset: 0; + z-index: 20; + background: rgb(10 12 15 / 72%); + backdrop-filter: grayscale(1); + -webkit-backdrop-filter: grayscale(1); +} .settings-heading { display: flex; align-items: center; @@ -667,3 +678,60 @@ select, width: 100%; margin: 6px 0 12px; } + +.header-controls { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 12px 24px; + font-size: 13px; +} +.theme-control { + display: flex; + align-items: center; + gap: 8px; +} +.theme-control label { + color: var(--muted); +} +.theme-control select { + min-height: 36px; + border: 1px solid var(--input-border); + border-radius: 4px; + padding-block: 6px; + padding-left: 10px; + font: inherit; +} +button:disabled, +select:disabled { + opacity: 0.5; + cursor: default; +} +input[type='file']::file-selector-button { + margin-right: 12px; + padding: 7px 10px; + border: 1px solid var(--input-border); + border-radius: 4px; + background: var(--surface); + color: var(--text); + cursor: pointer; +} +input[type='file']::file-selector-button:hover { + background: var(--hover); +} +@media (max-width: 600px) { + .header-heading { + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + .header-controls { + width: 100%; + justify-content: space-between; + gap: 12px; + } + nav[aria-label='Resources'] { + gap: 12px; + } +} diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 30685d8..bae6ac2 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -162,7 +162,11 @@ those individual overrides. Invalid appearance settings throw before making chan SVG exports resolve the appearance without requiring theme CSS. A configured background is included in the exported image; `transparent` preserves transparency. -The **Theme** selector in the demo's **Chart settings** dialog updates both library components and the demo UI. +The **Theme** selector in the demo's top-right header updates both library components and the demo UI. +On desktop, **Chart settings** opens beside the chart at the same height. The chart +remains interactive, including zoom and markers, while the rest of the page is +dimmed and inactive. Click the dimmed area, press Escape, or use Close to dismiss +settings. On smaller screens, settings use a modal dialog. ## Grid layers diff --git a/examples/basic/index.html b/examples/basic/index.html index 8ce5066..5a9c21a 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -3,28 +3,35 @@ - Smithkit integration example + Mount, update, and destroy · Smithkit examples
-