feat: YOLO toggle inline layout; yoloLabel/yoloDescription configurable via config.json
This commit is contained in:
@@ -1,16 +1,6 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* UnitInput — uncontrolled numeric input with one-way push to parent.
|
||||
*
|
||||
* The input owns its own DOM value. We only push changes upward via onchange.
|
||||
* We deliberately do NOT set value={value} on the input element — that would
|
||||
* cause Svelte to re-render the input on every parent state update, resetting
|
||||
* the cursor position and stealing focus mid-typing.
|
||||
*
|
||||
* The parent can pass a new `value` prop if it needs to imperatively set the
|
||||
* input (e.g. Ctrl+click adopts a converted value). We detect that via $effect
|
||||
* and update the DOM node directly, only when the value actually differs from
|
||||
* what's currently in the input.
|
||||
* UnitInput — controlled numeric input.
|
||||
*/
|
||||
|
||||
let {
|
||||
@@ -21,18 +11,6 @@
|
||||
onchange: (v: number) => void;
|
||||
} = $props();
|
||||
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Sync parent-driven value changes into the DOM — but only when the parent
|
||||
// is actually setting a new value (e.g. Ctrl+click), not on every keystroke.
|
||||
$effect(() => {
|
||||
if (!inputEl) return;
|
||||
const current = parseFloat(inputEl.value);
|
||||
if (isNaN(current) || current !== value) {
|
||||
inputEl.value = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
const parsed = parseFloat(input.value);
|
||||
@@ -43,9 +21,9 @@
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="number"
|
||||
step="any"
|
||||
value={value}
|
||||
oninput={handleInput}
|
||||
aria-label="Input value"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface AppConfig {
|
||||
adminPath: string;
|
||||
passwordHash: string;
|
||||
sessionSecret: string;
|
||||
/** Label shown next to the YOLO mode toggle. Defaults to "YOLO mode". */
|
||||
yoloLabel?: string;
|
||||
/** Description shown inline after the label. Defaults to "(cross-group conversions)". */
|
||||
yoloDescription?: string;
|
||||
}
|
||||
|
||||
/** Read units.json synchronously. Returns parsed UnitsData. */
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { loadData } from '$lib/server/data';
|
||||
import { loadData, loadConfig } from '$lib/server/data';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = () => {
|
||||
const data = loadData();
|
||||
const config = loadConfig();
|
||||
return {
|
||||
units: data.units,
|
||||
groups: data.groups
|
||||
groups: data.groups,
|
||||
yoloLabel: config.yoloLabel ?? 'YOLO mode',
|
||||
yoloDescription: config.yoloDescription ?? '(cross-group conversions)'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
* Supports same-group and cross-group (YOLO) conversions.
|
||||
*/
|
||||
|
||||
let { data }: { data: { units: Unit[]; groups: Group[] } } = $props();
|
||||
let { data }: { data: { units: Unit[]; groups: Group[]; yoloLabel: string; yoloDescription: string } } = $props();
|
||||
|
||||
let units: Unit[] = $derived(data.units);
|
||||
let groups: Group[] = $derived(data.groups);
|
||||
let yoloLabel: string = $derived(data.yoloLabel);
|
||||
let yoloDescription: string = $derived(data.yoloDescription);
|
||||
|
||||
// ── Reactive state ──────────────────────────────────────────────────────────
|
||||
let inputValue = $state<number>(1);
|
||||
@@ -94,6 +96,8 @@
|
||||
);
|
||||
|
||||
// ── URL sync ────────────────────────────────────────────────────────────────
|
||||
// Use history.replaceState directly — goto() triggers a SvelteKit navigation
|
||||
// which re-renders the page and steals focus from the input on every keystroke.
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const params = new URLSearchParams();
|
||||
@@ -104,7 +108,7 @@
|
||||
}
|
||||
const next = params.toString();
|
||||
if (next !== window.location.search.slice(1)) {
|
||||
goto(`?${next}`, { replaceState: true, noScroll: true });
|
||||
history.replaceState(history.state, '', `?${next}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -176,7 +180,10 @@
|
||||
<div class="yolo-toggle-row">
|
||||
<label class="yolo-label">
|
||||
<input type="checkbox" bind:checked={yoloMode} role="switch" />
|
||||
YOLO mode <small>(cross-group conversions)</small>
|
||||
<span class="yolo-label-text">{yoloLabel}</span>
|
||||
{#if yoloDescription}
|
||||
<small class="yolo-desc">{yoloDescription}</small>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -198,17 +205,24 @@
|
||||
<style>
|
||||
.yolo-toggle-row {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.yolo-label {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.yolo-label-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.yolo-desc {
|
||||
color: var(--pico-muted-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.yolo-marker {
|
||||
|
||||
Reference in New Issue
Block a user