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}
{configError}
{/if}
+ {#if configSuccess}
{configSuccess}
{/if}
+
+
+
+
+
+
{: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 });
+};