Hardening responsiveness and SEO

This commit is contained in:
Codex
2026-03-07 23:23:09 +00:00
parent 5a8740722c
commit c1aebbb5e2
16 changed files with 656 additions and 59 deletions

View File

@@ -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 @@
</script>
<div class="calculator-card">
<div class="calc-header">
<h2>{config.name}</h2>
{#if config.teaser}
<p class="calc-subtitle">{config.teaser}</p>
{/if}
</div>
{#if showTitle || config.teaser}
<div class="calc-header">
{#if showTitle}
<h2>{config.name}</h2>
{/if}
{#if config.teaser}
<p class="calc-subtitle" class:no-title={!showTitle}>{config.teaser}</p>
{/if}
</div>
{/if}
<div class="calc-body" class:three-col={has3}>
<div class="input-group">
@@ -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;
}
}
</style>

View File

@@ -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;
}
}
</style>

View File

@@ -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;
}
}
</style>

View File

@@ -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;
}
}
</style>

View File

@@ -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 @@
}
</script>
<div class="search-wrapper" class:active={focused && results.length > 0}>
<div class="search-wrapper" class:active={isOpen}>
<div class="search-input-wrap">
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
id={inputId}
type="text"
bind:value={query}
on:focus={() => (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}
<button
@@ -61,11 +84,12 @@
</button>
{/if}
</div>
{#if focused && results.length > 0}
<ul class="results" role="listbox" aria-label="Conversion suggestions">
{#if isOpen}
<ul class="results" id={listboxId} role="listbox" aria-label="Conversion suggestions">
{#each results as result, i}
<li>
<button
id={`${idPrefix}-option-${i}`}
type="button"
class="result-item"
class:selected={i === selectedIndex}
@@ -162,6 +186,10 @@
.result-item.selected {
background: var(--hover-bg);
}
.result-item:focus-visible {
outline: none;
background: var(--hover-bg);
}
.result-name {
font-weight: 500;
}

View File

@@ -131,12 +131,31 @@
}
export let open = false;
$: isSidebarHidden = !isDesktop && !open;
function closeSidebar() {
open = false;
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && open && !isDesktop) {
closeSidebar();
}
}
</script>
<aside class="sidebar" class:open id="site-navigation" aria-hidden={open ? 'false' : 'true'}>
<svelte:window on:keydown={handleWindowKeydown} />
<aside
class="sidebar"
class:open={open}
id="site-navigation"
aria-hidden={isSidebarHidden ? 'true' : undefined}
inert={isSidebarHidden}
>
<div class="sidebar-header">
<h3>All Converters</h3>
<button class="close-btn" on:click={() => (open = false)} aria-label="Close sidebar"></button>
<button type="button" class="close-btn" on:click={closeSidebar} aria-label="Close sidebar"></button>
</div>
<nav aria-label="Calculator categories">
{#each categoryUnitGroups as group}
@@ -200,10 +219,8 @@
</nav>
</aside>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="overlay" on:click={() => (open = false)}></div>
{#if open && !isDesktop}
<button type="button" class="overlay" aria-label="Close sidebar" on:click={closeSidebar}></button>
{/if}
<style>
@@ -243,6 +260,11 @@
cursor: pointer;
padding: 0.25rem;
}
.close-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--accent-glow);
border-radius: 8px;
}
nav {
padding: 0.5rem 0;
@@ -265,6 +287,10 @@
.cat-toggle:hover {
background: var(--hover-bg);
}
.cat-toggle:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px var(--accent-glow);
}
.cat-toggle.active {
color: var(--accent);
font-weight: 600;
@@ -306,6 +332,11 @@
color: var(--accent);
background: var(--hover-bg);
}
.cat-list li a:focus-visible {
outline: none;
color: var(--accent);
background: var(--hover-bg);
}
.cat-list li a.current {
color: var(--accent);
font-weight: 600;
@@ -337,6 +368,10 @@
.unit-toggle:hover {
background: var(--hover-bg);
}
.unit-toggle:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px var(--accent-glow);
}
.unit-toggle.expanded {
color: var(--accent);
font-weight: 600;
@@ -362,9 +397,15 @@
color: var(--accent);
background: var(--hover-bg);
}
.unit-list li a:focus-visible {
outline: none;
color: var(--accent);
background: var(--hover-bg);
}
.overlay {
display: none;
border: 0;
}
@media (max-width: 1024px) {