- shift+click now only sets fromUnitId; inputValue is unchanged so all outputs recalculate correctly from the original user input - Ctrl+click is a new gesture that sets both fromUnitId and inputValue to the tile's displayed converted value (the "pivot" action) - highlighted-result block always renders with min-height:8rem so the page below it never jumps when a highlight is added or cleared - hotkey hint updated to document all three click gestures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
4.8 KiB
Svelte
134 lines
4.8 KiB
Svelte
<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 in definition order ─────────────────────
|
||
// convert.ts already handles the same-unit identity (from.id === to.id → return value),
|
||
// so no special-casing needed here for the from-unit.
|
||
let results = $derived<ResultItem[]>(
|
||
units.map((u) => ({
|
||
unit: u,
|
||
convertedValue: convertById(inputValue, fromUnitId, u.id, units)
|
||
}))
|
||
);
|
||
|
||
// ── Derived: highlighted result (unit + value) sourced from full results ────
|
||
// Results now includes all units, so from-unit can be highlighted too.
|
||
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.
|
||
// inputValue is NOT changed — outputs recalculate from the same inputValue with the new fromUnitId.
|
||
// highlightedUnitId is NOT cleared — the two states are independent.
|
||
fromUnitId = id;
|
||
}
|
||
|
||
function handleCtrlClick(id: string, convertedValue: import('big.js').Big) {
|
||
// Ctrl+click: adopt this tile's displayed value as the new inputValue and promote it to from-unit.
|
||
// This is the "pivot" gesture.
|
||
inputValue = parseFloat(convertedValue.toFixed(FRACTIONAL_DIGITS));
|
||
fromUnitId = id;
|
||
}
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Humor Units — Converter</title>
|
||
</svelte:head>
|
||
|
||
<div class="highlighted-result" aria-live="polite">
|
||
{#if highlightedResult !== null}
|
||
<button
|
||
class="highlight-dismiss"
|
||
aria-label="Remove highlight"
|
||
onclick={() => (highlightedUnitId = null)}
|
||
>×</button>
|
||
<span class="highlighted-value">{formatBig(highlightedResult.convertedValue)}</span>
|
||
<span class="highlighted-label">{highlightedResult.unit.labelPlural}</span>
|
||
<span class="highlighted-symbol">{highlightedResult.unit.symbol}</span>
|
||
{/if}
|
||
</div>
|
||
|
||
<ConverterCard
|
||
{units}
|
||
{inputValue}
|
||
{fromUnitId}
|
||
{highlightedUnitId}
|
||
{results}
|
||
oninputchange={(v) => (inputValue = v)}
|
||
onunitchange={handleUnitChange}
|
||
onhighlight={handleHighlight}
|
||
onsetinput={handleSetInput}
|
||
onctrlclick={handleCtrlClick}
|
||
/>
|