Files
dickloads/src/routes/+page.svelte
Falkan af5f10a4ec Fix: shift+click preserves input value, Ctrl+click adopts value, stable highlight layout
- 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>
2026-03-16 22:21:22 -04:00

134 lines
4.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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}
/>