diff --git a/hdyc-svelte/src/app.css b/hdyc-svelte/src/app.css index 1632c6f..2bb3aa9 100644 --- a/hdyc-svelte/src/app.css +++ b/hdyc-svelte/src/app.css @@ -246,6 +246,28 @@ a { a:hover { color: var(--accent-dark); } +a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 6px; +} + +.skip-link { + position: fixed; + top: 0.6rem; + left: 0.6rem; + z-index: 500; + background: var(--bg-elevated); + color: var(--text); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.5rem 0.8rem; + transform: translateY(-160%); + transition: transform 0.2s ease; +} +.skip-link:focus-visible { + transform: translateY(0); +} /* ─── Layout Shell ───────────────────────────────────────── */ @@ -264,6 +286,12 @@ a:hover { border-bottom: 1px solid var(--border); } +.header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + .site-logo { display: flex; align-items: center; @@ -281,15 +309,51 @@ a:hover { .header-right { display: flex; align-items: center; - gap: 1rem; + gap: 0.65rem; +} + +.desktop-header-search { + width: min(420px, 42vw); +} + +.header-icon-btn { + display: none; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: 1px solid var(--border); + border-radius: 50%; + background: var(--input-bg); + color: var(--text); + cursor: pointer; + font-size: 1rem; + line-height: 1; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; +} +.header-icon-btn:hover { + border-color: var(--accent); +} +.header-icon-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.mobile-header-search { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + background: var(--header-bg); +} +.mobile-header-search[hidden] { + display: none !important; } .theme-toggle { display: inline-flex; align-items: center; justify-content: center; - width: 36px; - height: 36px; + width: 44px; + height: 44px; border-radius: 50%; border: 1px solid var(--border); background: var(--input-bg); @@ -395,12 +459,25 @@ a:hover { .hamburger { display: none; - background: none; - border: none; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 50%; color: var(--text); - font-size: 1.4rem; + font-size: 1.2rem; cursor: pointer; - padding: 0.25rem; + padding: 0; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; +} +.hamburger:hover { + border-color: var(--accent); +} +.hamburger:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-glow); } .site-body { @@ -419,6 +496,9 @@ a:hover { padding: clamp(1.5rem, 2vw, 3rem); width: 100%; } +.main-content:focus { + outline: none; +} .site-footer { padding: 2rem; @@ -431,22 +511,35 @@ a:hover { /* ─── Page Utilities ─────────────────────────────────────── */ .breadcrumbs { - display: flex; - align-items: center; - gap: 0.4rem; font-size: 0.82rem; color: var(--text-muted); margin-bottom: 1.5rem; } +.breadcrumbs ol { + list-style: none; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; + margin: 0; + padding: 0; +} +.breadcrumbs li { + display: inline-flex; + align-items: center; + color: var(--text-muted); +} +.breadcrumbs li + li::before { + content: '›'; + opacity: 0.4; + margin-right: 0.4rem; +} .breadcrumbs a { color: var(--text-muted); } .breadcrumbs a:hover { color: var(--accent); } -.breadcrumbs .sep { - opacity: 0.4; -} .page-title { font-size: 1.8rem; @@ -459,6 +552,10 @@ a:hover { background-clip: text; } +.calculator-page-title { + margin-bottom: 1.25rem; +} + .page-subtitle { color: var(--text-muted); font-size: 1rem; @@ -531,6 +628,11 @@ a:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); color: var(--accent); } +.calc-list-item:focus-visible { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} /* ─── Related Converters ─────────────────────────────────── */ @@ -554,6 +656,11 @@ a:hover { color: var(--accent); border-color: var(--accent); } +.related-chip:focus-visible { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} /* ─── SEO Content ────────────────────────────────────────── */ @@ -633,7 +740,7 @@ a:hover { @media (max-width: 1024px) { .hamburger { - display: block; + display: inline-flex; } .site-body { gap: 1rem; @@ -642,9 +749,31 @@ a:hover { } @media (max-width: 768px) { + .site-header { + padding: 0 1rem; + } + .site-logo { + font-size: 1rem; + gap: 0.4rem; + } + .desktop-header-search { + display: none; + } + .header-icon-btn { + display: inline-flex; + } + .mobile-header-search { + padding: 0.65rem 1rem; + } .main-content { padding: 1.25rem; } + .breadcrumbs { + margin-bottom: 1rem; + } + .page-title { + font-size: 1.5rem; + } .hero h1 { font-size: 1.8rem; } @@ -665,6 +794,9 @@ a:hover { .stats-row { gap: 1.5rem; } + .seo-content { + padding: 1.25rem; + } .site-body { gap: 1rem; padding-inline: 1rem; diff --git a/hdyc-svelte/src/hooks.server.ts b/hdyc-svelte/src/hooks.server.ts index e081ac8..1c4a2cb 100644 --- a/hdyc-svelte/src/hooks.server.ts +++ b/hdyc-svelte/src/hooks.server.ts @@ -13,18 +13,38 @@ const MIME_TYPES: Record = { '.otf': 'font/otf' }; +const HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate'; +const ASSET_404_CACHE_CONTROL = 'no-store'; + export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event); - if (event.url.pathname.startsWith('/_app/')) { + const pathname = event.url.pathname; + const contentType = response.headers.get('content-type') ?? ''; + + if (pathname.startsWith('/_app/')) { const existing = response.headers.get('content-type'); const hasValidHeader = existing && existing.trim().length > 0; if (!hasValidHeader) { - const extension = path.extname(event.url.pathname).toLowerCase(); + const extension = path.extname(pathname).toLowerCase(); const mime = extension && MIME_TYPES[extension]; if (mime) { response.headers.set('content-type', mime); } } + + // Missing hashed assets should never be cached; otherwise stale HTML can + // keep pointing to already-rotated files long after a deployment. + if (response.status >= 400) { + response.headers.set('cache-control', ASSET_404_CACHE_CONTROL); + } + + return response; + } + + // HTML documents should revalidate so they can reference the latest client + // bundle hashes after each deployment. + if (contentType.includes('text/html')) { + response.headers.set('cache-control', HTML_CACHE_CONTROL); } return response; diff --git a/hdyc-svelte/src/lib/assets/favicon.svg b/hdyc-svelte/src/lib/assets/favicon.svg index cc5dc66..e45a622 100644 --- a/hdyc-svelte/src/lib/assets/favicon.svg +++ b/hdyc-svelte/src/lib/assets/favicon.svg @@ -1 +1,21 @@ -svelte-logo \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/hdyc-svelte/src/lib/components/Calculator.svelte b/hdyc-svelte/src/lib/components/Calculator.svelte index 74fd21e..cd2fc57 100644 --- a/hdyc-svelte/src/lib/components/Calculator.svelte +++ b/hdyc-svelte/src/lib/components/Calculator.svelte @@ -8,6 +8,7 @@ import QuickConversionTable from '$lib/components/QuickConversionTable.svelte'; export let config: CalculatorDef; + export let showTitle = true; let val1 = ''; let val2 = ''; @@ -70,12 +71,16 @@
-
-

{config.name}

- {#if config.teaser} -

{config.teaser}

- {/if} -
+ {#if showTitle || config.teaser} +
+ {#if showTitle} +

{config.name}

+ {/if} + {#if config.teaser} +

{config.teaser}

+ {/if} +
+ {/if}
@@ -181,6 +186,9 @@ color: rgba(255, 255, 255, 0.85); font-weight: 400; } + .calc-subtitle.no-title { + margin-top: 0; + } .calc-body { display: grid; @@ -261,6 +269,10 @@ background: var(--accent-dark); transform: rotate(180deg); } + .swap-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-glow); + } .calc-footer { display: flex; @@ -284,16 +296,34 @@ color: #fff; border-color: var(--accent); } + .clear-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-glow); + } .formula-hint { font-size: 0.78rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; } + @media (max-width: 900px) { + .calc-footer { + flex-wrap: wrap; + gap: 0.75rem; + } + .formula-hint { + width: 100%; + } + } + @media (max-width: 640px) { + .calc-header { + padding: 1.2rem 1.2rem 0.9rem; + } .calc-body { grid-template-columns: 1fr; gap: 0.75rem; + padding: 1.25rem; } .calc-body.three-col { grid-template-columns: 1fr; @@ -307,5 +337,8 @@ .swap-btn:hover { transform: rotate(270deg); } + .calc-footer { + padding: 0.9rem 1.25rem 1rem; + } } diff --git a/hdyc-svelte/src/lib/components/QuickConversionExample.svelte b/hdyc-svelte/src/lib/components/QuickConversionExample.svelte index 01ec7d8..e75ef49 100644 --- a/hdyc-svelte/src/lib/components/QuickConversionExample.svelte +++ b/hdyc-svelte/src/lib/components/QuickConversionExample.svelte @@ -97,4 +97,11 @@ font-weight: 600; margin-left: 0.35rem; } + + @media (max-width: 640px) { + .example-card { + margin: 0 1.25rem 1.25rem; + padding: 1rem; + } + } diff --git a/hdyc-svelte/src/lib/components/QuickConversionTable.svelte b/hdyc-svelte/src/lib/components/QuickConversionTable.svelte index b2d0dfe..5c0245b 100644 --- a/hdyc-svelte/src/lib/components/QuickConversionTable.svelte +++ b/hdyc-svelte/src/lib/components/QuickConversionTable.svelte @@ -98,4 +98,17 @@ .chart-output-unit { font-variant: petite-caps; } + + @media (max-width: 640px) { + .quick-chart { + margin: 0.75rem 1.25rem 1.25rem; + padding: 0.9rem 1rem; + } + .chart-row { + font-size: 0.9rem; + } + .chart-statement { + line-height: 1.35; + } + } diff --git a/hdyc-svelte/src/lib/components/QuickDefinitionCard.svelte b/hdyc-svelte/src/lib/components/QuickDefinitionCard.svelte index dab4d7c..c5536c1 100644 --- a/hdyc-svelte/src/lib/components/QuickDefinitionCard.svelte +++ b/hdyc-svelte/src/lib/components/QuickDefinitionCard.svelte @@ -68,4 +68,14 @@ font-size: 0.85rem; color: var(--text-muted); } + + @media (max-width: 640px) { + .definition-card { + margin: 0 1.25rem 1.25rem; + padding: 1rem; + } + .definition-grid { + grid-template-columns: 1fr; + } + } diff --git a/hdyc-svelte/src/lib/components/SearchBar.svelte b/hdyc-svelte/src/lib/components/SearchBar.svelte index 69aff7f..40c85a1 100644 --- a/hdyc-svelte/src/lib/components/SearchBar.svelte +++ b/hdyc-svelte/src/lib/components/SearchBar.svelte @@ -2,11 +2,27 @@ import { searchCalculators } from '$lib/data/calculators'; import { goto } from '$app/navigation'; + export let idPrefix = 'search'; + let query = ''; let focused = false; let selectedIndex = -1; + let lastQuery = ''; $: results = query.length >= 2 ? searchCalculators(query).slice(0, 8) : []; + $: listboxId = `${idPrefix}-listbox`; + $: inputId = `${idPrefix}-input`; + $: isOpen = focused && results.length > 0; + $: activeDescendant = selectedIndex >= 0 && isOpen ? `${idPrefix}-option-${selectedIndex}` : undefined; + + $: if (query !== lastQuery) { + selectedIndex = -1; + lastQuery = query; + } + + $: if (selectedIndex >= results.length) { + selectedIndex = results.length - 1; + } function handleKeydown(e: KeyboardEvent) { if (e.key === 'ArrowDown') { @@ -33,12 +49,13 @@ } -
0}> +
(focused = true)} @@ -46,6 +63,12 @@ on:keydown={handleKeydown} placeholder="Search conversions..." aria-label="Search conversions" + role="combobox" + aria-autocomplete="list" + aria-haspopup="listbox" + aria-expanded={isOpen ? 'true' : 'false'} + aria-controls={listboxId} + aria-activedescendant={activeDescendant} /> {#if query}
- {#if focused && results.length > 0} -
    + {#if isOpen} +
      {#each results as result, i}
    • +
-{#if open} - - -
(open = false)}>
+{#if open && !isDesktop} + {/if}