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>
This commit is contained in:
Falkan
2026-03-16 22:21:22 -04:00
parent d9489a5db2
commit af5f10a4ec
4 changed files with 27 additions and 11 deletions

View File

@@ -97,6 +97,7 @@
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
position: relative; position: relative;
min-height: 8rem;
} }
.highlighted-value { .highlighted-value {

View File

@@ -14,7 +14,8 @@
highlighted, highlighted,
isInputUnit = false, isInputUnit = false,
onclick, onclick,
onshiftclick onshiftclick,
onctrlclick
}: { }: {
value: Big; value: Big;
unit: Unit; unit: Unit;
@@ -22,14 +23,18 @@
isInputUnit?: boolean; isInputUnit?: boolean;
onclick: () => void; onclick: () => void;
onshiftclick: () => void; onshiftclick: () => void;
onctrlclick: () => void;
} = $props(); } = $props();
// Compute once per reactive update; shared between aria-label and display span. // Compute once per reactive update; shared between aria-label and display span.
let formattedValue = $derived(formatBig(value)); let formattedValue = $derived(formatBig(value));
function handleClick(e: MouseEvent) { function handleClick(e: MouseEvent) {
if (e.shiftKey) { if (e.ctrlKey) {
// Shift+click promotes this tile to the from-unit. // Ctrl+click adopts this tile's value as the new input value and promotes it to from-unit.
onctrlclick();
} else if (e.shiftKey) {
// Shift+click promotes this tile to the from-unit (inputValue unchanged).
// Guard: if it already IS the from-unit, the action would be a no-op. // Guard: if it already IS the from-unit, the action would be a no-op.
if (!isInputUnit) onshiftclick(); if (!isInputUnit) onshiftclick();
} else { } else {

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Unit, ResultItem } from '$lib/types'; import type { Unit, ResultItem } from '$lib/types';
import type { Big } from 'big.js';
import { MIN_COLUMN_WIDTH, GRID_GAP } from '$lib/config'; import { MIN_COLUMN_WIDTH, GRID_GAP } from '$lib/config';
import UnitInput from './UnitInput.svelte'; import UnitInput from './UnitInput.svelte';
import UnitDropdown from './UnitDropdown.svelte'; import UnitDropdown from './UnitDropdown.svelte';
@@ -19,7 +20,8 @@
oninputchange, oninputchange,
onunitchange, onunitchange,
onhighlight, onhighlight,
onsetinput onsetinput,
onctrlclick
}: { }: {
units: Unit[]; units: Unit[];
inputValue: number; inputValue: number;
@@ -30,6 +32,7 @@
onunitchange: (id: string) => void; onunitchange: (id: string) => void;
onhighlight: (id: string) => void; onhighlight: (id: string) => void;
onsetinput: (id: string) => void; onsetinput: (id: string) => void;
onctrlclick: (id: string, value: Big) => void;
} = $props(); } = $props();
const gridStyle = `--min-col-width: ${MIN_COLUMN_WIDTH}; --grid-gap: ${GRID_GAP};`; const gridStyle = `--min-col-width: ${MIN_COLUMN_WIDTH}; --grid-gap: ${GRID_GAP};`;
@@ -50,11 +53,12 @@
isInputUnit={item.unit.id === fromUnitId} isInputUnit={item.unit.id === fromUnitId}
onclick={() => onhighlight(item.unit.id)} onclick={() => onhighlight(item.unit.id)}
onshiftclick={() => onsetinput(item.unit.id)} onshiftclick={() => onsetinput(item.unit.id)}
onctrlclick={() => onctrlclick(item.unit.id, item.convertedValue)}
/> />
{/each} {/each}
</div> </div>
<p class="hotkey-hint"> <p class="hotkey-hint">
<small>Click a result to highlight it · Shift+click to use it as input</small> <small>Click to highlight · Shift+click to change input unit · Ctrl+click to adopt value as input</small>
</p> </p>
</article> </article>

View File

@@ -89,9 +89,14 @@
function handleSetInput(id: string) { function handleSetInput(id: string) {
// Shift+click: promote result unit to from-unit. // Shift+click: promote result unit to from-unit.
// Re-express the current value in terms of the new 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. // highlightedUnitId is NOT cleared — the two states are independent.
const convertedValue = convertById(inputValue, fromUnitId, id, units); 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)); inputValue = parseFloat(convertedValue.toFixed(FRACTIONAL_DIGITS));
fromUnitId = id; fromUnitId = id;
} }
@@ -101,8 +106,8 @@
<title>Humor Units — Converter</title> <title>Humor Units — Converter</title>
</svelte:head> </svelte:head>
<div class="highlighted-result" aria-live="polite">
{#if highlightedResult !== null} {#if highlightedResult !== null}
<div class="highlighted-result">
<button <button
class="highlight-dismiss" class="highlight-dismiss"
aria-label="Remove highlight" aria-label="Remove highlight"
@@ -111,8 +116,8 @@
<span class="highlighted-value">{formatBig(highlightedResult.convertedValue)}</span> <span class="highlighted-value">{formatBig(highlightedResult.convertedValue)}</span>
<span class="highlighted-label">{highlightedResult.unit.labelPlural}</span> <span class="highlighted-label">{highlightedResult.unit.labelPlural}</span>
<span class="highlighted-symbol">{highlightedResult.unit.symbol}</span> <span class="highlighted-symbol">{highlightedResult.unit.symbol}</span>
</div>
{/if} {/if}
</div>
<ConverterCard <ConverterCard
{units} {units}
@@ -124,4 +129,5 @@
onunitchange={handleUnitChange} onunitchange={handleUnitChange}
onhighlight={handleHighlight} onhighlight={handleHighlight}
onsetinput={handleSetInput} onsetinput={handleSetInput}
onctrlclick={handleCtrlClick}
/> />