feat: optgroup dividers in unit dropdown, matching group order and visibility

This commit is contained in:
Falkan
2026-03-18 23:00:07 -04:00
parent a44551d4bc
commit 1dd2f0c5d3
3 changed files with 67 additions and 16 deletions

View File

@@ -98,13 +98,14 @@
"hidden": false "hidden": false
}, },
{ {
"id": "asdf", "id": "mm",
"label": "asdf", "label": "millimeter",
"labelPlural": "asdf", "labelPlural": "millimeters",
"symbol": "asdf", "symbol": "mm",
"group": "another-group", "group": "lengths-and-distance",
"toBase": 1, "toBase": 1,
"hidden": true "hidden": false,
"description": "It's so tiny and kawaii (♡‿♡)."
}, },
{ {
"id": "pound", "id": "pound",
@@ -138,6 +139,14 @@
"symbol": "t", "symbol": "t",
"group": "dickloads", "group": "dickloads",
"toBase": 76 "toBase": 76
},
{
"id": "cm",
"label": "centimeter",
"labelPlural": "centimeters",
"symbol": "cm",
"group": "lengths-and-distance",
"toBase": 10
} }
], ],
"groups": [ "groups": [
@@ -151,12 +160,12 @@
"alwaysShowLabel": true "alwaysShowLabel": true
}, },
{ {
"id": "another-group", "id": "lengths-and-distance",
"label": "Another Group", "label": "Lengths and Distance",
"baseUnitId": "", "baseUnitId": "mm",
"toUniversal": 1, "toUniversal": 1,
"alwaysShowLabel": false, "alwaysShowLabel": false,
"hidden": true "hidden": false
} }
] ]
} }

View File

@@ -42,7 +42,7 @@
<article> <article>
<div class="converter-controls"> <div class="converter-controls">
<UnitInput value={inputValue} onchange={oninputchange} /> <UnitInput value={inputValue} onchange={oninputchange} />
<UnitDropdown {units} value={fromUnitId} onchange={onunitchange} /> <UnitDropdown {units} {groups} value={fromUnitId} onchange={onunitchange} />
</div> </div>
{#each orderedResults as section} {#each orderedResults as section}

View File

@@ -1,17 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { Unit } from '$lib/types'; import type { Unit, Group } from '$lib/types';
/** /**
* UnitDropdown — select element populated from a Unit[] prop. * UnitDropdown — select element populated from Unit[] with optional Group[] dividers.
* Group headers render as non-selectable <optgroup> labels, in group order.
* Units with no group appear at the end under an implicit "Ungrouped" section.
* Emits onchange with the selected unit ID string. * Emits onchange with the selected unit ID string.
*/ */
let { let {
units, units,
groups = [],
value, value,
onchange onchange
}: { }: {
units: Unit[]; units: Unit[];
groups?: Group[];
value: string; value: string;
onchange: (id: string) => void; onchange: (id: string) => void;
} = $props(); } = $props();
@@ -20,10 +24,48 @@
const select = e.currentTarget as HTMLSelectElement; const select = e.currentTarget as HTMLSelectElement;
onchange(select.value); onchange(select.value);
} }
// Build ordered sections matching the converter output order
let sections = $derived.by(() => {
const byGroup = new Map<string | null, Unit[]>();
for (const u of units) {
const key = u.group ?? null;
let bucket = byGroup.get(key);
if (!bucket) { bucket = []; byGroup.set(key, bucket); }
bucket.push(u);
}
const result: { label: string | null; units: Unit[] }[] = [];
if (groups.length > 1) {
// Multi-group: emit in group order with labels
for (const g of groups) {
const items = byGroup.get(g.id);
if (items?.length) result.push({ label: g.label, units: items });
}
const orphans = byGroup.get(null);
if (orphans?.length) result.push({ label: 'Ungrouped', units: orphans });
} else {
// Single group or no groups: flat list, no dividers
result.push({ label: null, units });
}
return result;
});
</script> </script>
<select value={value} onchange={handleChange} aria-label="From unit"> <select {value} onchange={handleChange} aria-label="From unit">
{#each units as unit (unit.id)} {#each sections as section}
{#if section.label}
<optgroup label={section.label}>
{#each section.units as unit (unit.id)}
<option value={unit.id}>{unit.label} ({unit.symbol})</option> <option value={unit.id}>{unit.label} ({unit.symbol})</option>
{/each} {/each}
</optgroup>
{:else}
{#each section.units as unit (unit.id)}
<option value={unit.id}>{unit.label} ({unit.symbol})</option>
{/each}
{/if}
{/each}
</select> </select>