Phase 2: core converter, live conversion, URL state, highlight interaction

This commit is contained in:
Falkan
2026-03-16 21:34:16 -04:00
parent 65f8bc1b9f
commit 8f835c35d2
9 changed files with 430 additions and 1 deletions

View File

@@ -1 +1,122 @@
<h1>Humor Units</h1>
<script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { florpUnits } from '$lib/units/definitions';
import { convertById } from '$lib/convert';
import { FRACTIONAL_DIGITS } from '$lib/config';
import { formatBig } from '$lib/format';
import type { Unit, ResultItem } from '$lib/types';
import ConverterCard from '../components/ConverterCard.svelte';
/**
* Converter page — owns all reactive state.
* Phase 2: live conversion, URL state, highlight interaction.
* Phase 3 will add NL input.
*/
const units: Unit[] = florpUnits;
// ── Reactive state ──────────────────────────────────────────────────────────
let inputValue = $state<number>(1);
let fromUnitId = $state<string>(units[0].id);
let highlightedUnitId = $state<string | null>(null);
// ── Initialize from URL params on first client-side load ───────────────────
if (browser) {
const params = page.url.searchParams;
const pValue = params.get('value');
const pFrom = params.get('from');
const pHighlight = params.get('highlight');
if (pValue !== null) {
const n = parseFloat(pValue);
if (!isNaN(n)) inputValue = n;
}
if (pFrom !== null && units.some((u) => u.id === pFrom)) {
fromUnitId = pFrom;
}
if (pHighlight !== null && units.some((u) => u.id === pHighlight)) {
highlightedUnitId = pHighlight;
}
}
// ── Derived: results for all units except fromUnit ─────────────────────────
let results = $derived<ResultItem[]>(
units
.filter((u) => u.id !== fromUnitId)
.map((u) => ({
unit: u,
convertedValue: convertById(inputValue, fromUnitId, u.id, units)
}))
);
// ── Derived: highlighted result (unit + value) sourced from results array ──
// The highlighted unit is always a non-from unit, so it's always in results.
let highlightedResult = $derived(
highlightedUnitId !== null
? (results.find((r) => r.unit.id === highlightedUnitId) ?? null)
: null
);
// ── URL sync: update URL only when serialized params actually change ────────
$effect(() => {
if (!browser) return;
const params = new URLSearchParams();
params.set('value', String(inputValue));
params.set('from', fromUnitId);
if (highlightedUnitId !== null) {
params.set('highlight', highlightedUnitId);
}
const next = params.toString();
if (next !== window.location.search.slice(1)) {
goto(`?${next}`, { replaceState: true, noScroll: true });
}
});
// ── Event handlers ──────────────────────────────────────────────────────────
function handleUnitChange(id: string) {
fromUnitId = id;
// Clear highlight if the selected unit becomes the new from-unit.
if (highlightedUnitId === id) {
highlightedUnitId = null;
}
}
function handleHighlight(id: string) {
highlightedUnitId = id === highlightedUnitId ? null : id;
}
function handleSetInput(id: string) {
// Shift+click: promote result unit to from-unit.
// Re-express the current value in terms of the new from-unit.
const convertedValue = convertById(inputValue, fromUnitId, id, units);
inputValue = parseFloat(convertedValue.toFixed(FRACTIONAL_DIGITS));
fromUnitId = id;
highlightedUnitId = null;
}
</script>
<svelte:head>
<title>Humor Units — Converter</title>
</svelte:head>
{#if highlightedResult !== null}
<div class="highlighted-result">
<span class="highlighted-value">{formatBig(highlightedResult.convertedValue)}</span>
<span class="highlighted-label">{highlightedResult.unit.labelPlural}</span>
<span class="highlighted-symbol">{highlightedResult.unit.symbol}</span>
</div>
{/if}
<ConverterCard
{units}
{inputValue}
{fromUnitId}
{highlightedUnitId}
{results}
oninputchange={(v) => (inputValue = v)}
onunitchange={handleUnitChange}
onhighlight={handleHighlight}
onsetinput={handleSetInput}
/>