@poodle64/ui 2026.10.8 → 2026.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/components/ui/app-shell/app-shell.svelte +8 -91
- package/dist/components/ui/ledger/ledger.svelte +1 -1
- package/dist/components/ui/ledger/types.d.ts +5 -1
- package/dist/components/ui/record-list/record-list.svelte +1 -1
- package/dist/components/ui/resize-handle/index.d.ts +2 -0
- package/dist/components/ui/resize-handle/index.js +4 -0
- package/dist/components/ui/resize-handle/resize-handle.svelte +140 -0
- package/dist/components/ui/resize-handle/resize-handle.svelte.d.ts +36 -0
- package/dist/styles.css +18 -22
- package/package.json +1 -1
- package/registry/component-map.json +17 -3
- package/registry/component-map.md +9 -1
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ rather than rebuilding it:
|
|
|
118
118
|
| `document-detail` | One document's surface: identity fields, locations, tags and collection memberships, each section present exactly when its data is. Membership links are the app's own via `collectionHref`. |
|
|
119
119
|
| `search-results` | A ranked retrieval answer: title, the matched passage with accent-tinted highlights, source chip, mapped state and a mono relevance figure per hit; plus the before-any-search and matched-nothing empties. |
|
|
120
120
|
| `ledger` | The one ledger every module draws: dated rows of money, coins or units, two lines a row, grouped by the viewer's period, the viewer's columns and running balance. See [The ledger](#the-ledger). |
|
|
121
|
+
| `resize-handle` | The drag edge of a side panel: pointer, arrow keys, double-click reset, remembered width, ceiling measured against the content beside it. AppShell's rail uses it; an app with its own `<aside>` renders `<ResizeHandle bind:width storageKey="…" />` as the aside's last child (positioned) and sets the aside's width from `width`. |
|
|
121
122
|
|
|
122
123
|
**The application shell** (`app-shell`, `command-palette`). Page chrome is not
|
|
123
124
|
what makes an app feel like an app either — the shell is. Five household
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
import { activeNavLabel, matchesPrefix, toItems, type NavSource } from './types.js';
|
|
95
95
|
import { provideShellControls, type ShellControlsSlot } from './controls.js';
|
|
96
96
|
import { cn } from '../../../utils.js';
|
|
97
|
+
import ResizeHandle from '../resize-handle/resize-handle.svelte';
|
|
97
98
|
|
|
98
99
|
let {
|
|
99
100
|
nav,
|
|
@@ -254,81 +255,8 @@
|
|
|
254
255
|
|
|
255
256
|
// Rail width is a personal preference: localStorage, one key across apps.
|
|
256
257
|
// A drag below RAIL_SNAP collapses; the width survives and returns on expand.
|
|
257
|
-
const RAIL_KEY = 'ds-shell-rail-width';
|
|
258
|
-
const RAIL_MIN = 200;
|
|
259
|
-
const RAIL_MAX = 420;
|
|
260
|
-
const RAIL_SNAP = 170;
|
|
261
|
-
const RAIL_STEP = 16;
|
|
262
258
|
let railWidth = $state<number | null>(null);
|
|
263
259
|
let resizing = $state(false);
|
|
264
|
-
$effect(() => {
|
|
265
|
-
try {
|
|
266
|
-
const stored = Number(localStorage.getItem(RAIL_KEY));
|
|
267
|
-
if (stored >= RAIL_MIN && stored <= RAIL_MAX) railWidth = stored;
|
|
268
|
-
} catch {
|
|
269
|
-
/* storage unavailable: the default width stands */
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
function clampRail(next: number) {
|
|
273
|
-
return Math.min(RAIL_MAX, Math.max(RAIL_MIN, Math.round(next)));
|
|
274
|
-
}
|
|
275
|
-
function setRailWidth(next: number) {
|
|
276
|
-
railWidth = clampRail(next);
|
|
277
|
-
try {
|
|
278
|
-
localStorage.setItem(RAIL_KEY, String(railWidth));
|
|
279
|
-
} catch {
|
|
280
|
-
/* storage unavailable: the width lasts the session */
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function startResize(event: PointerEvent) {
|
|
284
|
-
const handle = event.currentTarget as HTMLElement;
|
|
285
|
-
const rail = handle.parentElement as HTMLElement;
|
|
286
|
-
const startX = event.clientX;
|
|
287
|
-
const startWidth = rail.getBoundingClientRect().width;
|
|
288
|
-
const before = railWidth;
|
|
289
|
-
resizing = true;
|
|
290
|
-
handle.setPointerCapture(event.pointerId);
|
|
291
|
-
const move = (e: PointerEvent) => {
|
|
292
|
-
const next = startWidth + (e.clientX - startX);
|
|
293
|
-
if (next < RAIL_SNAP) {
|
|
294
|
-
// A collapse keeps the width it had; only a resize stores one.
|
|
295
|
-
railWidth = before;
|
|
296
|
-
collapsed = true;
|
|
297
|
-
stop();
|
|
298
|
-
} else {
|
|
299
|
-
railWidth = clampRail(next);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
const stop = () => {
|
|
303
|
-
resizing = false;
|
|
304
|
-
if (!collapsed && railWidth !== null) setRailWidth(railWidth);
|
|
305
|
-
handle.removeEventListener('pointermove', move);
|
|
306
|
-
handle.removeEventListener('pointerup', stop);
|
|
307
|
-
handle.removeEventListener('pointercancel', stop);
|
|
308
|
-
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
|
|
309
|
-
};
|
|
310
|
-
handle.addEventListener('pointermove', move);
|
|
311
|
-
handle.addEventListener('pointerup', stop);
|
|
312
|
-
handle.addEventListener('pointercancel', stop);
|
|
313
|
-
}
|
|
314
|
-
function resizeByKey(event: KeyboardEvent) {
|
|
315
|
-
const current = railWidth ?? RAIL_MIN + (RAIL_MAX - RAIL_MIN) / 4;
|
|
316
|
-
if (event.key === 'ArrowLeft') setRailWidth(current - RAIL_STEP);
|
|
317
|
-
else if (event.key === 'ArrowRight') setRailWidth(current + RAIL_STEP);
|
|
318
|
-
else if (event.key === 'Home') resetRailWidth();
|
|
319
|
-
else if (event.key === 'End') setRailWidth(RAIL_MAX);
|
|
320
|
-
else return;
|
|
321
|
-
event.preventDefault();
|
|
322
|
-
}
|
|
323
|
-
/** Back to the package default (Home, or a double-click on the handle). */
|
|
324
|
-
function resetRailWidth() {
|
|
325
|
-
railWidth = null;
|
|
326
|
-
try {
|
|
327
|
-
localStorage.removeItem(RAIL_KEY);
|
|
328
|
-
} catch {
|
|
329
|
-
/* storage unavailable */
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
260
|
|
|
333
261
|
// The measure is resolved ONCE, and the class and the attribute both read
|
|
334
262
|
// that one answer. Two independently-worded conditionals looked symmetric
|
|
@@ -696,25 +624,14 @@
|
|
|
696
624
|
</div>
|
|
697
625
|
{/if}
|
|
698
626
|
{#if collapsible && !railCollapsed && !mobileNavOpen}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
role="separator"
|
|
706
|
-
aria-orientation="vertical"
|
|
707
|
-
aria-label="Resize sidebar"
|
|
708
|
-
aria-valuenow={railWidth ?? undefined}
|
|
709
|
-
aria-valuemin={RAIL_MIN}
|
|
710
|
-
aria-valuemax={RAIL_MAX}
|
|
711
|
-
tabindex="0"
|
|
712
|
-
class="ds-shell-rail-handle"
|
|
713
|
-
onpointerdown={startResize}
|
|
714
|
-
onkeydown={resizeByKey}
|
|
715
|
-
ondblclick={resetRailWidth}
|
|
627
|
+
<ResizeHandle
|
|
628
|
+
bind:width={railWidth}
|
|
629
|
+
bind:dragging={resizing}
|
|
630
|
+
storageKey="ds-shell-rail-width"
|
|
631
|
+
snap={170}
|
|
632
|
+
onsnap={() => (collapsed = true)}
|
|
716
633
|
data-testid="ds-rail-resize"
|
|
717
|
-
|
|
634
|
+
/>
|
|
718
635
|
{/if}
|
|
719
636
|
</aside>
|
|
720
637
|
{/if}
|
|
@@ -547,7 +547,7 @@
|
|
|
547
547
|
>
|
|
548
548
|
{:else if c.cell}
|
|
549
549
|
<span class={cn(CELL, through, 'flex items-center self-center')} {style}
|
|
550
|
-
>{@render c.cell(r)}
|
|
550
|
+
>{@render c.cell(r, c)}
|
|
551
551
|
{#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}</span
|
|
552
552
|
>
|
|
553
553
|
{:else}
|
|
@@ -77,8 +77,12 @@ export interface ListColumn<R> {
|
|
|
77
77
|
/**
|
|
78
78
|
* A cell of the module's own markup. On a row that opens, a click passes
|
|
79
79
|
* through the cell to the row; a control inside takes `pointer-events-auto`.
|
|
80
|
+
* It gets the column too, so one snippet can serve columns built at runtime,
|
|
81
|
+
* telling them apart by `key`. Read the row's figures in the snippet's
|
|
82
|
+
* markup, which follows the row when it changes; a `createRawSnippet` draws
|
|
83
|
+
* once at mount and goes stale.
|
|
80
84
|
*/
|
|
81
|
-
cell?: Snippet<[R]>;
|
|
85
|
+
cell?: Snippet<[R, ListColumn<R>]>;
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
84
88
|
* A column the module offers the ledger. The keys `date`, `title`, `amount`
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
</script>
|
|
221
221
|
|
|
222
222
|
{#snippet value(c: (typeof shown)[number], r: R)}
|
|
223
|
-
{#if c.cell}{@render c.cell(r)}{:else}{c.text?.(r) ?? ''}{/if}
|
|
223
|
+
{#if c.cell}{@render c.cell(r, c)}{:else}{c.text?.(r) ?? ''}{/if}
|
|
224
224
|
{/snippet}
|
|
225
225
|
|
|
226
226
|
{#snippet flags(r: R, all: boolean)}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The drag edge of a resizable side panel: the one implementation behind
|
|
4
|
+
* AppShell's rail, for an app whose sidebar is its own element.
|
|
5
|
+
*
|
|
6
|
+
* Render it as the LAST child of the panel, and give the panel
|
|
7
|
+
* `position: relative` (or sticky) and `width: <width>px` while `width` is
|
|
8
|
+
* set. It owns the pointer drag, the keyboard (arrows nudge, Home resets,
|
|
9
|
+
* End takes the maximum), the double-click reset and persistence; the panel
|
|
10
|
+
* owns how the number is applied.
|
|
11
|
+
*
|
|
12
|
+
* The ceiling is measured against the CONTENT beside the panel, not the
|
|
13
|
+
* window: the panel's parent is the row, so the panel can never grow past
|
|
14
|
+
* `row width - minContent`, whatever the screen.
|
|
15
|
+
*/
|
|
16
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
17
|
+
import { cn } from '../../../utils.js';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
width = $bindable(null),
|
|
21
|
+
dragging = $bindable(false),
|
|
22
|
+
min = 200,
|
|
23
|
+
max = 420,
|
|
24
|
+
minContent = 480,
|
|
25
|
+
step = 16,
|
|
26
|
+
snap,
|
|
27
|
+
onsnap,
|
|
28
|
+
storageKey,
|
|
29
|
+
label = 'Resize sidebar',
|
|
30
|
+
class: className,
|
|
31
|
+
...rest
|
|
32
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, 'role'> & {
|
|
33
|
+
/** Panel width in px; `null` is the panel's own default. */
|
|
34
|
+
width?: number | null;
|
|
35
|
+
/** True during a pointer drag: switch the panel's width transition off. */
|
|
36
|
+
dragging?: boolean;
|
|
37
|
+
min?: number;
|
|
38
|
+
max?: number;
|
|
39
|
+
/** Least width the content beside the panel keeps. */
|
|
40
|
+
minContent?: number;
|
|
41
|
+
/** Arrow-key nudge in px. */
|
|
42
|
+
step?: number;
|
|
43
|
+
/** A drag below this calls `onsnap` (collapse) and keeps the old width. */
|
|
44
|
+
snap?: number;
|
|
45
|
+
onsnap?: () => void;
|
|
46
|
+
/** localStorage key; one per app. Omitted, nothing is stored. */
|
|
47
|
+
storageKey?: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
} = $props();
|
|
50
|
+
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (!storageKey) return;
|
|
53
|
+
try {
|
|
54
|
+
const stored = Number(localStorage.getItem(storageKey));
|
|
55
|
+
if (stored >= min && stored <= max) width = stored;
|
|
56
|
+
} catch {
|
|
57
|
+
/* storage unavailable: the default width stands */
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let handle: HTMLElement;
|
|
62
|
+
const panel = () => handle.parentElement as HTMLElement;
|
|
63
|
+
const clamp = (next: number) => {
|
|
64
|
+
const row = panel().parentElement?.getBoundingClientRect().width || Infinity;
|
|
65
|
+
const ceiling = Math.max(min, Math.min(max, row - minContent));
|
|
66
|
+
return Math.min(ceiling, Math.max(min, Math.round(next)));
|
|
67
|
+
};
|
|
68
|
+
const store = (value: number | null) => {
|
|
69
|
+
if (!storageKey) return;
|
|
70
|
+
try {
|
|
71
|
+
if (value === null) localStorage.removeItem(storageKey);
|
|
72
|
+
else localStorage.setItem(storageKey, String(value));
|
|
73
|
+
} catch {
|
|
74
|
+
/* storage unavailable: the width lasts the session */
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const set = (next: number) => {
|
|
78
|
+
width = clamp(next);
|
|
79
|
+
store(width);
|
|
80
|
+
};
|
|
81
|
+
const reset = () => {
|
|
82
|
+
width = null;
|
|
83
|
+
store(null);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let drag: { x: number; start: number; before: number | null } | null = null;
|
|
87
|
+
function down(event: PointerEvent) {
|
|
88
|
+
drag = { x: event.clientX, start: panel().getBoundingClientRect().width, before: width };
|
|
89
|
+
dragging = true;
|
|
90
|
+
handle.setPointerCapture(event.pointerId);
|
|
91
|
+
}
|
|
92
|
+
function move(event: PointerEvent) {
|
|
93
|
+
if (!drag) return;
|
|
94
|
+
const next = drag.start + event.clientX - drag.x;
|
|
95
|
+
if (snap !== undefined && next < snap) {
|
|
96
|
+
width = drag.before;
|
|
97
|
+
end(event);
|
|
98
|
+
onsnap?.();
|
|
99
|
+
} else width = clamp(next);
|
|
100
|
+
}
|
|
101
|
+
function end(event: PointerEvent) {
|
|
102
|
+
if (!drag) return;
|
|
103
|
+
const moved = width !== drag.before;
|
|
104
|
+
drag = null;
|
|
105
|
+
dragging = false;
|
|
106
|
+
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
|
|
107
|
+
if (moved && width !== null) store(width);
|
|
108
|
+
}
|
|
109
|
+
function key(event: KeyboardEvent) {
|
|
110
|
+
const current = width ?? panel().getBoundingClientRect().width;
|
|
111
|
+
if (event.key === 'ArrowLeft') set(current - step);
|
|
112
|
+
else if (event.key === 'ArrowRight') set(current + step);
|
|
113
|
+
else if (event.key === 'Home') reset();
|
|
114
|
+
else if (event.key === 'End') set(max);
|
|
115
|
+
else return;
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<!-- A focusable separator is the ARIA window-splitter pattern. -->
|
|
121
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
122
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
123
|
+
<div
|
|
124
|
+
bind:this={handle}
|
|
125
|
+
role="separator"
|
|
126
|
+
aria-orientation="vertical"
|
|
127
|
+
aria-label={label}
|
|
128
|
+
aria-valuenow={width ?? undefined}
|
|
129
|
+
aria-valuemin={min}
|
|
130
|
+
aria-valuemax={max}
|
|
131
|
+
tabindex="0"
|
|
132
|
+
class={cn('ds-resize-handle', className)}
|
|
133
|
+
onpointerdown={down}
|
|
134
|
+
onpointermove={move}
|
|
135
|
+
onpointerup={end}
|
|
136
|
+
onpointercancel={end}
|
|
137
|
+
onkeydown={key}
|
|
138
|
+
ondblclick={reset}
|
|
139
|
+
{...rest}
|
|
140
|
+
></div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drag edge of a resizable side panel: the one implementation behind
|
|
3
|
+
* AppShell's rail, for an app whose sidebar is its own element.
|
|
4
|
+
*
|
|
5
|
+
* Render it as the LAST child of the panel, and give the panel
|
|
6
|
+
* `position: relative` (or sticky) and `width: <width>px` while `width` is
|
|
7
|
+
* set. It owns the pointer drag, the keyboard (arrows nudge, Home resets,
|
|
8
|
+
* End takes the maximum), the double-click reset and persistence; the panel
|
|
9
|
+
* owns how the number is applied.
|
|
10
|
+
*
|
|
11
|
+
* The ceiling is measured against the CONTENT beside the panel, not the
|
|
12
|
+
* window: the panel's parent is the row, so the panel can never grow past
|
|
13
|
+
* `row width - minContent`, whatever the screen.
|
|
14
|
+
*/
|
|
15
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
16
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, 'role'> & {
|
|
17
|
+
/** Panel width in px; `null` is the panel's own default. */
|
|
18
|
+
width?: number | null;
|
|
19
|
+
/** True during a pointer drag: switch the panel's width transition off. */
|
|
20
|
+
dragging?: boolean;
|
|
21
|
+
min?: number;
|
|
22
|
+
max?: number;
|
|
23
|
+
/** Least width the content beside the panel keeps. */
|
|
24
|
+
minContent?: number;
|
|
25
|
+
/** Arrow-key nudge in px. */
|
|
26
|
+
step?: number;
|
|
27
|
+
/** A drag below this calls `onsnap` (collapse) and keeps the old width. */
|
|
28
|
+
snap?: number;
|
|
29
|
+
onsnap?: () => void;
|
|
30
|
+
/** localStorage key; one per app. Omitted, nothing is stored. */
|
|
31
|
+
storageKey?: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
};
|
|
34
|
+
declare const ResizeHandle: import("svelte").Component<$$ComponentProps, {}, "width" | "dragging">;
|
|
35
|
+
type ResizeHandle = ReturnType<typeof ResizeHandle>;
|
|
36
|
+
export default ResizeHandle;
|
package/dist/styles.css
CHANGED
|
@@ -968,6 +968,7 @@
|
|
|
968
968
|
top: 0;
|
|
969
969
|
height: 100dvh;
|
|
970
970
|
width: var(--ds-shell-rail-width);
|
|
971
|
+
max-width: 50%;
|
|
971
972
|
transition: width 200ms ease;
|
|
972
973
|
}
|
|
973
974
|
|
|
@@ -1038,28 +1039,23 @@
|
|
|
1038
1039
|
transition: none;
|
|
1039
1040
|
}
|
|
1040
1041
|
|
|
1041
|
-
/* The resize handle: a 6px grab strip astride the
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
.ds-shell-rail-handle:focus-visible {
|
|
1059
|
-
background: var(--ds-color-primary);
|
|
1060
|
-
opacity: 0.4;
|
|
1061
|
-
outline: none;
|
|
1062
|
-
}
|
|
1042
|
+
/* The resize handle (<ResizeHandle>): a 6px grab strip astride the right
|
|
1043
|
+
edge of the panel it is the last child of, showing the primary colour
|
|
1044
|
+
under the pointer and keyboard focus. The panel must be positioned. */
|
|
1045
|
+
.ds-resize-handle {
|
|
1046
|
+
position: absolute;
|
|
1047
|
+
inset-block: 0;
|
|
1048
|
+
right: -3px;
|
|
1049
|
+
width: 6px;
|
|
1050
|
+
cursor: col-resize;
|
|
1051
|
+
z-index: 1;
|
|
1052
|
+
touch-action: none;
|
|
1053
|
+
}
|
|
1054
|
+
.ds-resize-handle:hover,
|
|
1055
|
+
.ds-resize-handle:focus-visible {
|
|
1056
|
+
background: var(--ds-color-primary);
|
|
1057
|
+
opacity: 0.4;
|
|
1058
|
+
outline: none;
|
|
1063
1059
|
}
|
|
1064
1060
|
|
|
1065
1061
|
/* The content measure: the shell's content box is ONE element in four
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.10",
|
|
4
4
|
"description": "Household shared component layer: shadcn-svelte primitives (bits-ui) plus the composed page chrome every app builds its routes from, restyled by each app's @poodle64/design-tokens alias layer. One fix reaches every app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.10.
|
|
4
|
+
"version": "2026.10.10",
|
|
5
5
|
"generatedBy": "scripts/generate-registry.mjs",
|
|
6
6
|
"source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
|
|
7
|
-
"componentCount":
|
|
8
|
-
"situationCount":
|
|
7
|
+
"componentCount": 68,
|
|
8
|
+
"situationCount": 19
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
11
|
+
{
|
|
12
|
+
"key": "resizable-panel",
|
|
13
|
+
"title": "A side panel the user can widen or narrow",
|
|
14
|
+
"description": "An app's own sidebar or list pane that needs a drag edge: pointer, arrow keys, double-click reset, remembered width. AppShell's rail already has it.",
|
|
15
|
+
"components": [
|
|
16
|
+
{
|
|
17
|
+
"name": "ResizeHandle",
|
|
18
|
+
"dir": "resize-handle",
|
|
19
|
+
"import": "@poodle64/ui/resize-handle",
|
|
20
|
+
"props": "width?, dragging?, min?, max?, minContent?, step?, snap?, onsnap?, storageKey?, label?",
|
|
21
|
+
"insteadOf": "a hand-written pointer-drag and localStorage width on your own <aside>"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
11
25
|
{
|
|
12
26
|
"key": "single-figure",
|
|
13
27
|
"title": "A single labelled figure",
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# @poodle64/ui — situation → component map
|
|
2
2
|
|
|
3
3
|
<!-- GENERATED by scripts/generate-registry.mjs from scripts/situations.json + package source. DO NOT EDIT. -->
|
|
4
|
-
Generated from `@poodle64/ui@2026.10.
|
|
4
|
+
Generated from `@poodle64/ui@2026.10.10`. 68 components, 19 situations.
|
|
5
5
|
|
|
6
6
|
**Read this before writing a `<div>`.** Find the SITUATION you are in below, then compose the component named for it — do not hand-build it from raw `Card` or utility classes. Import is `import { Name } from '<import path>'`. Props marked `?` are optional. This map is the retrieval step the [`frontend-design` skill] makes mandatory; the [CHI 2026 study] measured composing-from-a-registry at 95% design-system compliance against 71% for writing the CSS from a prose style guide.
|
|
7
7
|
|
|
8
8
|
[`frontend-design` skill]: the household rule that loads before any UI code.
|
|
9
9
|
[CHI 2026 study]: 10.1145/3772363.3798616
|
|
10
10
|
|
|
11
|
+
## A side panel the user can widen or narrow
|
|
12
|
+
|
|
13
|
+
_An app's own sidebar or list pane that needs a drag edge: pointer, arrow keys, double-click reset, remembered width. AppShell's rail already has it._
|
|
14
|
+
|
|
15
|
+
| Component | Import | Key props | Reach for it instead of |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `ResizeHandle` | `@poodle64/ui/resize-handle` | `width?, dragging?, min?, max?, minContent?, step?, snap?, onsnap?, storageKey?, label?` | a hand-written pointer-drag and localStorage width on your own <aside> |
|
|
18
|
+
|
|
11
19
|
## A single labelled figure
|
|
12
20
|
|
|
13
21
|
_One headline metric that earns a tile — a figure with a label, an optional unit, and the state of the thing it measures._
|