feat: optgroup dividers in unit dropdown, matching group order and visibility
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user