From 8f835c35d2cfe8c2936e37d78c7988439bdd963e Mon Sep 17 00:00:00 2001 From: Falkan Date: Mon, 16 Mar 2026 21:34:16 -0400 Subject: [PATCH] Phase 2: core converter, live conversion, URL state, highlight interaction --- src/app.css | 115 +++++++++++++++++++++++ src/components/ConversionResult.svelte | 52 +++++++++++ src/components/ConverterCard.svelte | 59 ++++++++++++ src/components/UnitDropdown.svelte | 29 ++++++ src/components/UnitInput.svelte | 30 ++++++ src/lib/format.ts | 16 ++++ src/lib/types.ts | 6 ++ src/routes/+layout.svelte | 1 + src/routes/+page.svelte | 123 ++++++++++++++++++++++++- 9 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 src/app.css create mode 100644 src/components/ConversionResult.svelte create mode 100644 src/components/ConverterCard.svelte create mode 100644 src/components/UnitDropdown.svelte create mode 100644 src/components/UnitInput.svelte create mode 100644 src/lib/format.ts diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..516c499 --- /dev/null +++ b/src/app.css @@ -0,0 +1,115 @@ +/* app.css — global styles for humor-units */ + +/* CSS custom properties (set dynamically via inline style from ConverterCard) */ +:root { + --min-col-width: 160px; + --grid-gap: 1rem; +} + +/* ── Results grid ──────────────────────────────────────────────────────────── */ +.results-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--min-col-width), 1fr)); + gap: var(--grid-gap); + margin-top: 1.5rem; +} + +/* ── Result tile ───────────────────────────────────────────────────────────── */ +.result-tile { + cursor: pointer; + padding: 0.75rem 1rem; + border: 1px solid var(--pico-muted-border-color, #ccc); + border-radius: var(--pico-border-radius, 0.25rem); + display: flex; + flex-direction: column; + gap: 0.2rem; + transition: background-color 0.15s ease, border-color 0.15s ease; + user-select: none; +} + +.result-tile:hover { + background-color: var(--pico-secondary-background, rgba(0, 0, 0, 0.04)); + border-color: var(--pico-primary, #1095c1); +} + +.result-tile.highlighted { + border-color: var(--pico-primary, #1095c1); + background-color: var(--pico-primary-background, rgba(16, 149, 193, 0.08)); +} + +.result-value { + font-size: 1rem; + font-weight: 600; + font-variant-numeric: tabular-nums; + word-break: break-all; +} + +.result-label { + font-size: 0.85rem; + color: var(--pico-muted-color, #666); +} + +.result-symbol { + font-size: 0.75rem; + color: var(--pico-muted-color, #666); + font-style: italic; +} + +/* ── Highlighted result (above grid) ──────────────────────────────────────── */ +.highlighted-result { + text-align: center; + padding: 2rem 1rem; + margin-bottom: 1.5rem; + border-radius: var(--pico-border-radius, 0.25rem); + background-color: var(--pico-primary-background, rgba(16, 149, 193, 0.08)); + border: 2px solid var(--pico-primary, #1095c1); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; +} + +.highlighted-value { + font-size: 2.5rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1.1; + word-break: break-all; +} + +.highlighted-label { + font-size: 1.25rem; + color: var(--pico-color, inherit); +} + +.highlighted-symbol { + font-size: 0.9rem; + color: var(--pico-muted-color, #666); + font-style: italic; +} + +/* ── Converter controls ────────────────────────────────────────────────────── */ +.converter-controls { + display: flex; + gap: 1rem; + align-items: center; + flex-wrap: wrap; +} + +.converter-controls input[type='number'] { + flex: 1 1 200px; + min-width: 120px; +} + +.converter-controls select { + flex: 0 1 auto; + min-width: 160px; +} + +/* ── Hotkey hint ───────────────────────────────────────────────────────────── */ +.hotkey-hint { + margin-top: 1rem; + margin-bottom: 0; + color: var(--pico-muted-color, #666); + text-align: center; +} diff --git a/src/components/ConversionResult.svelte b/src/components/ConversionResult.svelte new file mode 100644 index 0000000..f73683a --- /dev/null +++ b/src/components/ConversionResult.svelte @@ -0,0 +1,52 @@ + + + + +
{ if (e.key === 'Enter') onclick(); }} + aria-label="{formattedValue} {unit.labelPlural}" + aria-pressed={highlighted} +> + {formattedValue} + {unit.labelPlural} + {unit.symbol} +
diff --git a/src/components/ConverterCard.svelte b/src/components/ConverterCard.svelte new file mode 100644 index 0000000..64608d5 --- /dev/null +++ b/src/components/ConverterCard.svelte @@ -0,0 +1,59 @@ + + +
+
+ + +
+ +
+ {#each results as item (item.unit.id)} + onhighlight(item.unit.id)} + onshiftclick={() => onsetinput(item.unit.id)} + /> + {/each} +
+ +

+ Click a result to highlight it · Shift+click to use it as input +

+
diff --git a/src/components/UnitDropdown.svelte b/src/components/UnitDropdown.svelte new file mode 100644 index 0000000..5405303 --- /dev/null +++ b/src/components/UnitDropdown.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/components/UnitInput.svelte b/src/components/UnitInput.svelte new file mode 100644 index 0000000..6632c85 --- /dev/null +++ b/src/components/UnitInput.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..c045a42 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,16 @@ +import type { Big } from 'big.js'; +import { FRACTIONAL_DIGITS, INTEGER_DIGITS } from './config'; + +/** + * Format a Big value for display. + * Uses FRACTIONAL_DIGITS decimal places (fixed-point). + * If INTEGER_DIGITS is set, zero-pads the integer portion to that width. + */ +export function formatBig(v: Big): string { + const fixed = v.toFixed(FRACTIONAL_DIGITS); + if (INTEGER_DIGITS === undefined) return fixed; + + const [intPart, fracPart] = fixed.split('.'); + const paddedInt = intPart.padStart(INTEGER_DIGITS, '0'); + return fracPart !== undefined ? `${paddedInt}.${fracPart}` : paddedInt; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 7ea326f..6624ac0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -32,3 +32,9 @@ export interface UnitGroup { label: string; units: Unit[]; } + +/** One computed conversion result — unit plus its converted Big value. */ +export interface ResultItem { + unit: Unit; + convertedValue: import('big.js').Big; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cd92650..cb68cbe 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,6 @@ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3aab346..2ca28a3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1 +1,122 @@ -

Humor Units

+ + + + Humor Units — Converter + + +{#if highlightedResult !== null} +
+ {formatBig(highlightedResult.convertedValue)} + {highlightedResult.unit.labelPlural} + {highlightedResult.unit.symbol} +
+{/if} + + (inputValue = v)} + onunitchange={handleUnitChange} + onhighlight={handleHighlight} + onsetinput={handleSetInput} +/>