From b020b74c31ea4780395dff72727d58c04f5fb4ab Mon Sep 17 00:00:00 2001 From: Falkan Date: Wed, 18 Mar 2026 20:15:45 -0400 Subject: [PATCH] feat: yoloVisibility config, alwaysShowLabel on groups, site settings panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppConfig gains yoloVisibility: 'auto' | 'never' (default: auto) - Group gains alwaysShowLabel?: boolean - YOLO toggle only renders when showYoloToggle is true (auto + >1 group) - Group divider shows when >1 group OR alwaysShowLabel is set - New /admin/api/config GET+PUT endpoint for yolo settings - Admin: alwaysShowLabel checkbox in group form - Admin: ⚙ Settings panel with YOLO label, description, and visibility controls --- data/units.json | 38 ++++++++---- src/components/ConverterCard.svelte | 4 +- src/lib/server/data.ts | 6 ++ src/lib/types.ts | 2 + src/routes/+page.server.ts | 3 +- src/routes/+page.svelte | 22 +++++-- src/routes/admin/+page.svelte | 82 +++++++++++++++++++++++++- src/routes/admin/api/config/+server.ts | 22 +++++++ 8 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 src/routes/admin/api/config/+server.ts diff --git a/data/units.json b/data/units.json index cc8c922..4c041fe 100644 --- a/data/units.json +++ b/data/units.json @@ -1,14 +1,5 @@ { "units": [ - { - "id": "pound", - "label": "pound", - "labelPlural": "pounds", - "symbol": "lb", - "group": "dickloads", - "toBase": 0.038, - "description": "My description is {dickload:description}" - }, { "id": "dickload", "label": "dickload", @@ -18,6 +9,15 @@ "toBase": 1, "description": "See {pound:description}" }, + { + "id": "pound", + "label": "pound", + "labelPlural": "pounds", + "symbol": "lb", + "group": "dickloads", + "toBase": 0.038, + "description": "My description is {dickload:description}" + }, { "id": "barrel", "label": "barrel", @@ -92,12 +92,30 @@ "symbol": "shttn", "group": "dickloads", "toBase": 38 + }, + { + "id": "asdf", + "label": "asdf", + "labelPlural": "asdf", + "symbol": "asdf", + "group": "another-group", + "toBase": 1, + "hidden": true } ], "groups": [ { "id": "dickloads", - "label": "Dickloads", + "label": "Weights and Masses", + "baseUnitId": "", + "toUniversal": 1, + "sortOrder": "alpha", + "hidden": false, + "alwaysShowLabel": true + }, + { + "id": "another-group", + "label": "Another Group", "baseUnitId": "", "toUniversal": 1 } diff --git a/src/components/ConverterCard.svelte b/src/components/ConverterCard.svelte index 64edf22..a84f7e6 100644 --- a/src/components/ConverterCard.svelte +++ b/src/components/ConverterCard.svelte @@ -27,7 +27,7 @@ fromUnitId: string; highlightedUnitId: string | null; results: ResultItem[]; - orderedResults: { groupLabel: string | null; items: ResultItem[] }[]; + orderedResults: { groupLabel: string | null; alwaysShowLabel: boolean; items: ResultItem[] }[]; oninputchange: (v: number) => void; onunitchange: (id: string) => void; onhighlight: (id: string) => void; @@ -46,7 +46,7 @@ {#each orderedResults as section} - {#if orderedResults.length > 1 && section.groupLabel} + {#if (orderedResults.length > 1 || section.alwaysShowLabel) && section.groupLabel}
{section.groupLabel}
{/if}
diff --git a/src/lib/server/data.ts b/src/lib/server/data.ts index 96b8919..5e4af00 100644 --- a/src/lib/server/data.ts +++ b/src/lib/server/data.ts @@ -15,6 +15,12 @@ export interface AppConfig { yoloLabel?: string; /** Description shown inline after the label. Defaults to "(cross-group conversions)". */ yoloDescription?: string; + /** + * Controls when the YOLO mode toggle is visible to users. + * 'auto' — show only when more than one group is present (default) + * 'never' — always hidden + */ + yoloVisibility?: 'auto' | 'never'; } /** Read units.json synchronously. Returns parsed UnitsData. */ diff --git a/src/lib/types.ts b/src/lib/types.ts index f732f1f..96a8db1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -44,6 +44,8 @@ export interface Group { sortOrder?: 'defined' | 'alpha'; /** If true, this group (and all its units) is excluded from automatic rendering. */ hidden?: boolean; + /** If true, the group label is shown in the converter even when only one group is present. */ + alwaysShowLabel?: boolean; } /** Full data payload for units and groups. */ diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 4badf59..30c149c 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -11,6 +11,7 @@ export const load: PageServerLoad = () => { units: data.units.filter((u) => !u.hidden && !hiddenGroupIds.has(u.group ?? '')), groups: data.groups.filter((g) => !g.hidden), yoloLabel: config.yoloLabel ?? 'YOLO mode', - yoloDescription: config.yoloDescription ?? '(cross-group conversions)' + yoloDescription: config.yoloDescription ?? '(cross-group conversions)', + yoloVisibility: config.yoloVisibility ?? 'auto', }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0f196ad..58de9fd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -14,12 +14,16 @@ * Supports same-group and cross-group (YOLO) conversions. */ - let { data }: { data: { units: Unit[]; groups: Group[]; yoloLabel: string; yoloDescription: string } } = $props(); + let { data }: { data: { units: Unit[]; groups: Group[]; yoloLabel: string; yoloDescription: string; yoloVisibility: 'auto' | 'never' } } = $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); + let yoloVisibility: 'auto' | 'never' = $derived(data.yoloVisibility); + let showYoloToggle: boolean = $derived( + yoloVisibility !== 'never' && groups.length > 1 + ); // ── Reactive state ────────────────────────────────────────────────────────── let inputValue = $state(1); @@ -134,7 +138,7 @@ } // ── Derived: results per group for display — single O(n) pass ────────────── - let orderedResults: { groupLabel: string | null; items: ResultItem[] }[] = $derived.by(() => { + let orderedResults: { groupLabel: string | null; alwaysShowLabel: boolean; items: ResultItem[] }[] = $derived.by(() => { // Build a Map of groupId → items in a single pass over results const byGroup = new Map(); for (const r of results) { @@ -144,18 +148,18 @@ bucket.push(r); } // Emit sections in group-definition order, then orphaned at end - const sections: { groupLabel: string | null; items: ResultItem[] }[] = []; + const sections: { groupLabel: string | null; alwaysShowLabel: boolean; items: ResultItem[] }[] = []; for (const group of groups) { const items = byGroup.get(group.id); if (items?.length) { const sorted = group.sortOrder === 'alpha' ? [...items].sort((a, b) => a.unit.label.localeCompare(b.unit.label)) : items; - sections.push({ groupLabel: group.label, items: sorted }); + sections.push({ groupLabel: group.label, alwaysShowLabel: group.alwaysShowLabel ?? false, items: sorted }); } } const orphaned = byGroup.get(null); - if (orphaned?.length) sections.push({ groupLabel: 'Ungrouped', items: orphaned }); + if (orphaned?.length) sections.push({ groupLabel: 'Ungrouped', alwaysShowLabel: false, items: orphaned }); return sections; }); @@ -182,7 +186,8 @@ {/if}
-
+
+ {#if showYoloToggle} + {/if}
.yolo-toggle-row { margin-bottom: 1rem; + min-height: 0; + } + .yolo-toggle-row.yolo-hidden { + margin-bottom: 0; } .yolo-label { diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index ba1d7e9..3971f4f 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -7,7 +7,7 @@ // ── State ──────────────────────────────────────────────────────────────────── let unitsData = $state({ units: [], groups: [] }); - let selectedItem = $state<{ type: 'unit' | 'group'; id: string } | null>(null); + let selectedItem = $state<{ type: 'unit' | 'group' | 'settings'; id: string } | null>(null); let checkedUnitIds = $state>(new Set()); let bulkAction = $state(''); // '' | 'move' | 'delete' let bulkMoveTargetGroupId = $state(''); @@ -32,6 +32,50 @@ let groupFormSnapshot = $state>({}); let groupFieldError = $state(null); // which field has error + // Site config state + let configYoloLabel = $state('YOLO mode'); + let configYoloDescription = $state('(cross-group conversions)'); + let configYoloVisibility = $state<'auto' | 'never'>('auto'); + let configSaving = $state(false); + let configError = $state(null); + let configSuccess = $state(null); + + async function loadConfig() { + const res = await fetch(`${api}/config`); + if (!res.ok) return; + const c = await res.json(); + configYoloLabel = c.yoloLabel ?? 'YOLO mode'; + configYoloDescription = c.yoloDescription ?? '(cross-group conversions)'; + configYoloVisibility = c.yoloVisibility ?? 'auto'; + } + + async function saveConfig() { + configSaving = true; + configError = null; + configSuccess = null; + try { + const res = await fetch(`${api}/config`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + yoloLabel: configYoloLabel, + yoloDescription: configYoloDescription, + yoloVisibility: configYoloVisibility, + }) + }); + if (!res.ok) { configError = 'Save failed'; } + else { configSuccess = 'Saved.'; } + } catch { configError = 'Network error'; } + configSaving = false; + } + + function onConfigKeydown(e: KeyboardEvent) { + if (e.key === 'Enter' && (e.target as HTMLElement).tagName !== 'TEXTAREA') { + e.preventDefault(); + saveConfig(); + } + } + // ── Auto-ID state ───────────────────────────────────────────────────────────── let unitIdManuallyEdited = $state(false); let groupIdManuallyEdited = $state(false); @@ -195,7 +239,7 @@ expandedGroups = new Set([...expandedGroups, ...groups.map((g) => g.id)]); } - $effect(() => { loadData(); }); + $effect(() => { loadData(); loadConfig(); }); async function extractError(res: Response, fallback: string): Promise { const body = await res.json().catch(() => ({})); @@ -842,6 +886,7 @@ @@ -953,6 +998,10 @@ +
+ {:else if selectedItem?.type === 'settings'} + +
+

Site Settings

+ {#if configError}{/if} + {#if configSuccess}

{configSuccess}

{/if} +
+ YOLO Mode Toggle + + + +
+
+ +
+
+ {:else}

Select a unit or group to edit, or add a new one.

{/if} diff --git a/src/routes/admin/api/config/+server.ts b/src/routes/admin/api/config/+server.ts new file mode 100644 index 0000000..b82a335 --- /dev/null +++ b/src/routes/admin/api/config/+server.ts @@ -0,0 +1,22 @@ +import { json } from '@sveltejs/kit'; +import { loadConfig, saveConfig } from '$lib/server/data'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = () => { + const { yoloLabel, yoloDescription, yoloVisibility } = loadConfig(); + return json({ yoloLabel, yoloDescription, yoloVisibility }); +}; + +export const PUT: RequestHandler = async ({ request }) => { + const body = await request.json(); + const config = loadConfig(); + + if (typeof body.yoloLabel === 'string') config.yoloLabel = body.yoloLabel; + if (typeof body.yoloDescription === 'string') config.yoloDescription = body.yoloDescription; + if (body.yoloVisibility === 'auto' || body.yoloVisibility === 'never') { + config.yoloVisibility = body.yoloVisibility; + } + + saveConfig(config); + return json({ ok: true }); +};