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

View File

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

View File

@@ -1,17 +1,21 @@
<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.
*/
let {
units,
groups = [],
value,
onchange
}: {
units: Unit[];
groups?: Group[];
value: string;
onchange: (id: string) => void;
} = $props();
@@ -20,10 +24,48 @@
const select = e.currentTarget as HTMLSelectElement;
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>
<select value={value} onchange={handleChange} aria-label="From unit">
{#each units as unit (unit.id)}
<option value={unit.id}>{unit.label} ({unit.symbol})</option>
<select {value} onchange={handleChange} aria-label="From unit">
{#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>
{/each}
</optgroup>
{:else}
{#each section.units as unit (unit.id)}
<option value={unit.id}>{unit.label} ({unit.symbol})</option>
{/each}
{/if}
{/each}
</select>