feat: hidden flag, description placeholders, drag handle contrast fix
- Unit and Group get hidden?: boolean — filtered server-side before render
- Hidden group also suppresses all its units from the converter
- New resolveDescription() utility in src/lib/description.ts
supports {id}, {id:label}, {id:plural}, {id:symbol} placeholders
- ConverterCard pre-resolves descriptions and passes result to ConversionResult
- ConversionResult accepts description prop instead of reading unit.description
- Admin forms: Hidden checkbox added for both units and groups
- drag-handle color overridden to --pico-primary-inverse on selected/drag-over rows
(white in light/dark themes, black on Dan Mode yellow)
This commit is contained in:
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fuckton",
|
"id": "fuckton",
|
||||||
"label": "Fuckton",
|
"label": "Fuck ton",
|
||||||
"labelPlural": "Fucktons",
|
"labelPlural": "Fucktons",
|
||||||
"symbol": "fcktn",
|
"symbol": "fcktn",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"labelPlural": "Shit tons",
|
"labelPlural": "Shit tons",
|
||||||
"symbol": "shttn",
|
"symbol": "shttn",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 25.3333333333
|
"toBase": 38
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
let {
|
let {
|
||||||
value,
|
value,
|
||||||
unit,
|
unit,
|
||||||
|
description = '',
|
||||||
highlighted,
|
highlighted,
|
||||||
isInputUnit = false,
|
isInputUnit = false,
|
||||||
isYolo = false,
|
isYolo = false,
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
}: {
|
}: {
|
||||||
value: Big | null;
|
value: Big | null;
|
||||||
unit: Unit;
|
unit: Unit;
|
||||||
|
description?: string;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
isInputUnit?: boolean;
|
isInputUnit?: boolean;
|
||||||
isYolo?: boolean;
|
isYolo?: boolean;
|
||||||
@@ -72,7 +74,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<!-- Line 2: label + symbol -->
|
<!-- Line 2: label + symbol -->
|
||||||
<span class="result-label" use:tooltip={unit.description ?? ''}>{unit.labelPlural} ({unit.symbol})</span>
|
<span class="result-label" use:tooltip={description}>{unit.labelPlural} ({unit.symbol})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { Unit, Group, ResultItem } from '$lib/types';
|
import type { Unit, Group, ResultItem } from '$lib/types';
|
||||||
import type { Big } from 'big.js';
|
import type { Big } from 'big.js';
|
||||||
import { uniformFontSize } from '$lib/actions/uniformFontSize';
|
import { uniformFontSize } from '$lib/actions/uniformFontSize';
|
||||||
|
import { resolveDescription } from '$lib/description';
|
||||||
import UnitInput from './UnitInput.svelte';
|
import UnitInput from './UnitInput.svelte';
|
||||||
import UnitDropdown from './UnitDropdown.svelte';
|
import UnitDropdown from './UnitDropdown.svelte';
|
||||||
import ConversionResult from './ConversionResult.svelte';
|
import ConversionResult from './ConversionResult.svelte';
|
||||||
@@ -33,6 +34,11 @@
|
|||||||
onsetinput: (id: string) => void;
|
onsetinput: (id: string) => void;
|
||||||
onctrlclick: (id: string, value: Big) => void;
|
onctrlclick: (id: string, value: Big) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
// Pre-resolve all unit descriptions so placeholders are expanded at render time.
|
||||||
|
let resolvedDescriptions = $derived(
|
||||||
|
new Map(units.map((u) => [u.id, resolveDescription(u.description, units, groups)]))
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
@@ -51,6 +57,7 @@
|
|||||||
value={item.convertedValue}
|
value={item.convertedValue}
|
||||||
isYolo={item.isYolo}
|
isYolo={item.isYolo}
|
||||||
unit={item.unit}
|
unit={item.unit}
|
||||||
|
description={resolvedDescriptions.get(item.unit.id) ?? ''}
|
||||||
highlighted={item.unit.id === highlightedUnitId}
|
highlighted={item.unit.id === highlightedUnitId}
|
||||||
isInputUnit={item.unit.id === fromUnitId}
|
isInputUnit={item.unit.id === fromUnitId}
|
||||||
onclick={() => onhighlight(item.unit.id)}
|
onclick={() => onhighlight(item.unit.id)}
|
||||||
|
|||||||
52
src/lib/description.ts
Normal file
52
src/lib/description.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Unit, Group } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves placeholder tokens in a description string.
|
||||||
|
*
|
||||||
|
* Supported formats (using the unit/group's ID):
|
||||||
|
* {id} → "Label (symbol)" e.g. "Shit Ton (shttn)"
|
||||||
|
* {id:label} → singular label e.g. "Shit Ton"
|
||||||
|
* {id:plural} → plural label e.g. "Shit Tons"
|
||||||
|
* {id:symbol} → symbol e.g. "shttn"
|
||||||
|
* {id:glabel} → group label e.g. "Mass"
|
||||||
|
*
|
||||||
|
* Unknown IDs or fields are left as-is.
|
||||||
|
*/
|
||||||
|
export function resolveDescription(
|
||||||
|
description: string | undefined,
|
||||||
|
units: Unit[],
|
||||||
|
groups: Group[]
|
||||||
|
): string {
|
||||||
|
if (!description) return '';
|
||||||
|
|
||||||
|
const unitMap = new Map(units.map((u) => [u.id, u]));
|
||||||
|
const groupMap = new Map(groups.map((g) => [g.id, g]));
|
||||||
|
|
||||||
|
return description.replace(/\{([^}]+)\}/g, (match, token: string) => {
|
||||||
|
const colonIdx = token.indexOf(':');
|
||||||
|
const id = colonIdx === -1 ? token : token.slice(0, colonIdx);
|
||||||
|
const field = colonIdx === -1 ? '' : token.slice(colonIdx + 1);
|
||||||
|
|
||||||
|
const unit = unitMap.get(id);
|
||||||
|
if (unit) {
|
||||||
|
switch (field) {
|
||||||
|
case '': return `${unit.label} (${unit.symbol})`;
|
||||||
|
case 'label': return unit.label;
|
||||||
|
case 'plural': return unit.labelPlural;
|
||||||
|
case 'symbol': return unit.symbol;
|
||||||
|
default: return match; // unknown field — leave as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupMap.get(id);
|
||||||
|
if (group) {
|
||||||
|
switch (field) {
|
||||||
|
case '': return group.label;
|
||||||
|
case 'label': return group.label;
|
||||||
|
default: return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match; // unknown ID — leave as-is
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -27,6 +27,9 @@ export interface Unit {
|
|||||||
|
|
||||||
/** Optional human-readable description shown as a tooltip in the converter. */
|
/** Optional human-readable description shown as a tooltip in the converter. */
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
/** If true, this unit is excluded from automatic rendering in the converter. */
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A group of units sharing a base unit and a universal scale factor. */
|
/** A group of units sharing a base unit and a universal scale factor. */
|
||||||
@@ -39,6 +42,8 @@ export interface Group {
|
|||||||
toUniversal: number;
|
toUniversal: number;
|
||||||
/** Display order for units in the converter. Defaults to 'defined'. */
|
/** Display order for units in the converter. Defaults to 'defined'. */
|
||||||
sortOrder?: 'defined' | 'alpha';
|
sortOrder?: 'defined' | 'alpha';
|
||||||
|
/** If true, this group (and all its units) is excluded from automatic rendering. */
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full data payload for units and groups. */
|
/** Full data payload for units and groups. */
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import type { PageServerLoad } from './$types';
|
|||||||
export const load: PageServerLoad = () => {
|
export const load: PageServerLoad = () => {
|
||||||
const data = loadData();
|
const data = loadData();
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const hiddenGroupIds = new Set(data.groups.filter((g) => g.hidden).map((g) => g.id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
units: data.units,
|
units: data.units.filter((u) => !u.hidden && !hiddenGroupIds.has(u.group ?? '')),
|
||||||
groups: data.groups,
|
groups: data.groups.filter((g) => !g.hidden),
|
||||||
yoloLabel: config.yoloLabel ?? 'YOLO mode',
|
yoloLabel: config.yoloLabel ?? 'YOLO mode',
|
||||||
yoloDescription: config.yoloDescription ?? '(cross-group conversions)'
|
yoloDescription: config.yoloDescription ?? '(cross-group conversions)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -861,6 +861,10 @@
|
|||||||
Description <small class="muted">(optional — shown as tooltip in converter)</small>
|
Description <small class="muted">(optional — shown as tooltip in converter)</small>
|
||||||
<textarea bind:value={unitForm.description} placeholder="e.g. A unit based on…" rows="2"></textarea>
|
<textarea bind:value={unitForm.description} placeholder="e.g. A unit based on…" rows="2"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" bind:checked={unitForm.hidden} />
|
||||||
|
Hidden <small class="muted">— exclude from converter rendering</small>
|
||||||
|
</label>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button onclick={saveUnit}>{unitFormNew ? 'Create' : 'Save'}</button>
|
<button onclick={saveUnit}>{unitFormNew ? 'Create' : 'Save'}</button>
|
||||||
{#if !unitFormNew}
|
{#if !unitFormNew}
|
||||||
@@ -906,6 +910,10 @@
|
|||||||
<option value="alpha">Alphabetical</option>
|
<option value="alpha">Alphabetical</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" bind:checked={groupForm.hidden} />
|
||||||
|
Hidden <small class="muted">— exclude group and all its units from converter rendering</small>
|
||||||
|
</label>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button onclick={saveGroup}>{groupFormNew ? 'Create' : 'Save'}</button>
|
<button onclick={saveGroup}>{groupFormNew ? 'Create' : 'Save'}</button>
|
||||||
{#if !groupFormNew}
|
{#if !groupFormNew}
|
||||||
@@ -1219,6 +1227,19 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.checkbox-label input[type=checkbox] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.form-msg {
|
.form-msg {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: var(--pico-border-radius);
|
border-radius: var(--pico-border-radius);
|
||||||
@@ -1289,6 +1310,12 @@
|
|||||||
.drag-handle:active {
|
.drag-handle:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
/* When the row has a primary-colored background, muted grey/orange disappears.
|
||||||
|
Force the handle to the primary inverse (white in normal themes, black in Dan). */
|
||||||
|
.tree-unit-row.selected .drag-handle,
|
||||||
|
.tree-unit-row.drag-over-unit .drag-handle {
|
||||||
|
color: var(--pico-primary-inverse);
|
||||||
|
}
|
||||||
.drag-over-unit {
|
.drag-over-unit {
|
||||||
border-top: 2px solid var(--pico-primary);
|
border-top: 2px solid var(--pico-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user