@poodle64/ui 2026.9.2 → 2026.9.4
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 +22 -22
- package/bin/check-colour-surface.mjs +1 -1
- package/dist/components/ui/app-identity/app-identity.svelte +49 -1
- package/dist/components/ui/app-identity/app-identity.svelte.d.ts +13 -0
- package/dist/components/ui/app-shell/app-shell.svelte +22 -7
- package/dist/components/ui/context-column/context-column.svelte +36 -8
- package/dist/styles.css +9 -0
- package/package.json +105 -102
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -83,10 +83,10 @@ rather than rebuilding it:
|
|
|
83
83
|
|
|
84
84
|
| Import | What it is |
|
|
85
85
|
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
-
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar.
|
|
86
|
+
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar. |
|
|
87
87
|
| `panel` | The generic titled card: optional icon, subtitle and trailing actions over a body that can opt out of padding. |
|
|
88
88
|
| `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions. |
|
|
89
|
-
| `context-column` | The persistent right-hand column: a standing `StatList` plus an optional detail that flows in on select.
|
|
89
|
+
| `context-column` | The persistent right-hand column: a standing `StatList` plus an optional detail that flows in on select. Below `xl` (1280px), where the column has nowhere to sit, the same content opens from a floating trigger instead of disappearing. |
|
|
90
90
|
| `app-dialog` | The dialogue frame: titled header, scrollable body, footer action bar, five sizes (`xs`…`xl`), and an `onOpenChange` for the dismissals the caller did not drive. |
|
|
91
91
|
| `dialog-section` | One section of a dialogue body; adjacent sections are divided automatically. |
|
|
92
92
|
| `stat-card` | A single metric that earns its space (label, value, unit, sub, status dot, and `valueTone` to colour the figure itself). |
|
|
@@ -380,10 +380,10 @@ top-right — painted once, by the shell, on the region that scrolls:
|
|
|
380
380
|
</AppShell>
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
| Value | What it paints | Reach for it when
|
|
384
|
-
| ------ | ------------------------------------------------------- |
|
|
385
|
-
| `grid` | a dot-grid floor plus one accent vignette in the corner | the default — the house "instrument-grade" surface
|
|
386
|
-
| `none` | nothing at all | an app arguing a deliberate exception
|
|
383
|
+
| Value | What it paints | Reach for it when |
|
|
384
|
+
| ------ | ------------------------------------------------------- | -------------------------------------------------- |
|
|
385
|
+
| `grid` | a dot-grid floor plus one accent vignette in the corner | the default — the house "instrument-grade" surface |
|
|
386
|
+
| `none` | nothing at all | an app arguing a deliberate exception |
|
|
387
387
|
|
|
388
388
|
`grid` is the default since `2026.8.8`, so a shell that does not mention
|
|
389
389
|
`texture` wears the house atmosphere. It shipped `none`-by-default in `2026.8.4`
|
|
@@ -1038,20 +1038,20 @@ Money, dates and times, percentages and plain numbers, in `en-AU` / AUD /
|
|
|
1038
1038
|
`Australia/Brisbane`. No dependencies and no DOM — it is `Intl` and arithmetic,
|
|
1039
1039
|
so it runs in a load function as happily as in a component.
|
|
1040
1040
|
|
|
1041
|
-
| Function
|
|
1042
|
-
|
|
|
1043
|
-
| `formatCurrency(dollars, opts)`
|
|
1044
|
-
| `formatCurrencyFromCents(cents, opts)`
|
|
1045
|
-
| `formatCurrencyString(str, opts)`
|
|
1046
|
-
| `compactCurrency(value, opts)`
|
|
1047
|
-
| `isNegativeMoney(value)`
|
|
1048
|
-
| `dollarsToCents(dollars)`
|
|
1049
|
-
| `formatNumber(value, opts)`
|
|
1050
|
-
| `formatPercentage(value, opts)`
|
|
1051
|
-
| `formatRatioAsPercentage(value, opts)`
|
|
1052
|
-
| `formatDate(value, opts)`
|
|
1053
|
-
| `formatDateTime(value, opts)`
|
|
1054
|
-
| `parseApiDate(iso)`
|
|
1041
|
+
| Function | What it does |
|
|
1042
|
+
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
1043
|
+
| `formatCurrency(dollars, opts)` | `$1,234.56`. `decimals` (default 2), `currency` (default AUD), `fallback`. |
|
|
1044
|
+
| `formatCurrencyFromCents(cents, opts)` | The same, for money stored as integer cents. |
|
|
1045
|
+
| `formatCurrencyString(str, opts)` | Groups a money STRING without ever parsing it to a float — for a figure that reaches a tax return. |
|
|
1046
|
+
| `compactCurrency(value, opts)` | `$1.1m` / `$12k`, for a chart axis or a dense tile. |
|
|
1047
|
+
| `isNegativeMoney(value)` | The sign, without a parse, for choosing a tone class. |
|
|
1048
|
+
| `dollarsToCents(dollars)` | Whole cents. |
|
|
1049
|
+
| `formatNumber(value, opts)` | `1,234,567`. |
|
|
1050
|
+
| `formatPercentage(value, opts)` | Value is already in percentage POINTS: `4.5` → `4.5%`. |
|
|
1051
|
+
| `formatRatioAsPercentage(value, opts)` | Value is a 0–1 RATIO: `0.045` → `4.5%`. |
|
|
1052
|
+
| `formatDate(value, opts)` | `19 Dec 2024`, `19 December 2024` (`format: 'long'`) or `19/12/2024` (`format: 'numeric'`). |
|
|
1053
|
+
| `formatDateTime(value, opts)` | The same plus a 24-hour time: `19 Dec 2024, 14:05`. |
|
|
1054
|
+
| `parseApiDate(iso)` | Reads an offset-less timestamp as UTC — the naive-UTC backend trap below. |
|
|
1055
1055
|
|
|
1056
1056
|
Four decisions worth knowing before a migration, because each was settled
|
|
1057
1057
|
against an app that had decided it the other way:
|
|
@@ -1109,8 +1109,8 @@ still pass while `bg-card`, `bg-muted`, `bg-accent`, `bg-popover` and
|
|
|
1109
1109
|
`border-input` compile to no rule at all:
|
|
1110
1110
|
|
|
1111
1111
|
```css
|
|
1112
|
-
@import '@poodle64/ui/styles.css';
|
|
1113
|
-
@source '../node_modules/@poodle64/ui/dist';
|
|
1112
|
+
@import '@poodle64/ui/styles.css'; /* registers the surface */
|
|
1113
|
+
@source '../node_modules/@poodle64/ui/dist'; /* puts it in the scan */
|
|
1114
1114
|
```
|
|
1115
1115
|
|
|
1116
1116
|
They fail differently, so the gate asserts them differently: it compiles the
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `lint:css` and `check` all succeed, and the app ships with `bg-card`,
|
|
13
13
|
* `bg-muted`, `bg-accent`, `bg-popover` and `border-input` compiling to no rule
|
|
14
14
|
* at all — no build error, no lint hit, no failing test, just classes in the DOM
|
|
15
|
-
* with nothing behind them. That is
|
|
15
|
+
* with nothing behind them. That is radar-hooves/design-system#3, which reached every
|
|
16
16
|
* app in the estate before anyone saw it: dropdowns with no hover, inputs with no
|
|
17
17
|
* border, cards and popovers with no surface.
|
|
18
18
|
*
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
* baked in here: `onSignOut` and `onAccountSettings` are callbacks. Theme
|
|
24
24
|
* follows AppShell's own `onToggleTheme` pattern — override it, or accept
|
|
25
25
|
* mode-watcher's `toggleMode` by default.
|
|
26
|
+
*
|
|
27
|
+
* `onManageMembers` exists for the caller a workspace switcher has nothing
|
|
28
|
+
* to show: `WorkspaceMenu` (the factory template) renders nothing in the
|
|
29
|
+
* bar for a single-member workspace, and this is where Members lands
|
|
30
|
+
* instead (operator ruling, 07/09/2026, master-project#291's org-of-one
|
|
31
|
+
* case). Wire it whenever the app has a members route, whether or not it
|
|
32
|
+
* also renders a switcher.
|
|
26
33
|
*/
|
|
27
34
|
import { toggleMode } from 'mode-watcher';
|
|
28
35
|
import * as DropdownMenu from '../dropdown-menu/index.js';
|
|
@@ -35,6 +42,7 @@
|
|
|
35
42
|
entitlements = [],
|
|
36
43
|
onSignOut,
|
|
37
44
|
onAccountSettings,
|
|
45
|
+
onManageMembers,
|
|
38
46
|
onSwitchTheme
|
|
39
47
|
}: {
|
|
40
48
|
user: { username: string; display_name: string | null; email: string | null };
|
|
@@ -47,6 +55,19 @@
|
|
|
47
55
|
onSignOut: () => void;
|
|
48
56
|
/** Optional until the app has an account-settings destination to send it to. */
|
|
49
57
|
onAccountSettings?: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Reach the workspace's members surface from here. Ignored while
|
|
60
|
+
* `workspace` is unset.
|
|
61
|
+
*
|
|
62
|
+
* A workspace holding exactly one member has nothing to switch, so the
|
|
63
|
+
* app's own workspace switcher renders nothing for it (operator ruling,
|
|
64
|
+
* 07/09/2026) — and Members was that switcher's one non-switching act,
|
|
65
|
+
* the route an org-of-one needs to make its first grant. This is where
|
|
66
|
+
* that act lives once the switcher is gone; a switcher still showing for
|
|
67
|
+
* several workspaces keeps its own Members entry too, so the two never
|
|
68
|
+
* disagree about where it is.
|
|
69
|
+
*/
|
|
70
|
+
onManageMembers?: () => void;
|
|
50
71
|
/** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
|
|
51
72
|
onSwitchTheme?: () => void;
|
|
52
73
|
} = $props();
|
|
@@ -63,7 +84,31 @@
|
|
|
63
84
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
64
85
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
65
86
|
}
|
|
66
|
-
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initials for a bare HANDLE — no display name to fall back on.
|
|
90
|
+
*
|
|
91
|
+
* A handle has no word boundaries a name has: `initialsOf` slicing the
|
|
92
|
+
* first two characters of one unbroken token produces a pair that reads
|
|
93
|
+
* as real initials while meaning nothing (operator ruling, 07/09/2026).
|
|
94
|
+
* Where the handle itself encodes two parts — `firstname.lastname`,
|
|
95
|
+
* `firstname-lastname` — one letter from each reads the same as a real
|
|
96
|
+
* name's initials; where it does not, only the first letter is honest.
|
|
97
|
+
*/
|
|
98
|
+
function handleInitials(handle: string): string {
|
|
99
|
+
const parts = handle.trim().split(/[.-]+/).filter(Boolean);
|
|
100
|
+
if (parts.length === 0) return '';
|
|
101
|
+
if (parts.length === 1) return parts[0].slice(0, 1).toUpperCase();
|
|
102
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Never text in the bar (operator ruling, 07/09/2026): with no display
|
|
106
|
+
// name, the trigger below still renders only this avatar, initialled off
|
|
107
|
+
// the handle rather than off `shown`, which for this branch just IS the
|
|
108
|
+
// handle.
|
|
109
|
+
const initials = $derived(
|
|
110
|
+
user.display_name ? initialsOf(user.display_name) : handleInitials(user.username)
|
|
111
|
+
);
|
|
67
112
|
|
|
68
113
|
function switchTheme() {
|
|
69
114
|
if (onSwitchTheme) onSwitchTheme();
|
|
@@ -128,6 +173,9 @@
|
|
|
128
173
|
{/each}
|
|
129
174
|
</div>
|
|
130
175
|
{/if}
|
|
176
|
+
{#if onManageMembers}
|
|
177
|
+
<DropdownMenu.Item onSelect={onManageMembers}>Members</DropdownMenu.Item>
|
|
178
|
+
{/if}
|
|
131
179
|
{/if}
|
|
132
180
|
<DropdownMenu.Separator />
|
|
133
181
|
<DropdownMenu.Item onSelect={switchTheme}>Switch theme</DropdownMenu.Item>
|
|
@@ -13,6 +13,19 @@ type $$ComponentProps = {
|
|
|
13
13
|
onSignOut: () => void;
|
|
14
14
|
/** Optional until the app has an account-settings destination to send it to. */
|
|
15
15
|
onAccountSettings?: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Reach the workspace's members surface from here. Ignored while
|
|
18
|
+
* `workspace` is unset.
|
|
19
|
+
*
|
|
20
|
+
* A workspace holding exactly one member has nothing to switch, so the
|
|
21
|
+
* app's own workspace switcher renders nothing for it (operator ruling,
|
|
22
|
+
* 07/09/2026) — and Members was that switcher's one non-switching act,
|
|
23
|
+
* the route an org-of-one needs to make its first grant. This is where
|
|
24
|
+
* that act lives once the switcher is gone; a switcher still showing for
|
|
25
|
+
* several workspaces keeps its own Members entry too, so the two never
|
|
26
|
+
* disagree about where it is.
|
|
27
|
+
*/
|
|
28
|
+
onManageMembers?: () => void;
|
|
16
29
|
/** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
|
|
17
30
|
onSwitchTheme?: () => void;
|
|
18
31
|
};
|
|
@@ -420,16 +420,26 @@
|
|
|
420
420
|
trailing sizes to its clamp without stretching the right-hand group.
|
|
421
421
|
-->
|
|
422
422
|
{#snippet searchButton(sizing: string)}
|
|
423
|
+
<!--
|
|
424
|
+
Below `sm` this is an icon-only square, matching the menu/theme-toggle
|
|
425
|
+
buttons either side of it: a fixed-width pill sharing the row with a
|
|
426
|
+
`context` switcher or a couple of trailing `actions` badges has as little
|
|
427
|
+
as the width of one glyph to shrink into, and a button painting a single
|
|
428
|
+
truncated letter reads as broken rather than as a control. `sizing`
|
|
429
|
+
supplies the width/flex/padding pair at each tier (base then `sm:`), so
|
|
430
|
+
this snippet's own classes carry shape only and never fight it over the
|
|
431
|
+
same property.
|
|
432
|
+
-->
|
|
423
433
|
<button
|
|
424
434
|
onclick={onSearch}
|
|
425
435
|
class={cn(
|
|
426
|
-
'border-border text-shell-muted-foreground hover:text-shell-foreground flex items-center
|
|
436
|
+
'border-border text-shell-muted-foreground hover:text-shell-foreground flex items-center rounded-md border transition-colors sm:text-sm',
|
|
427
437
|
sizing
|
|
428
438
|
)}
|
|
429
439
|
data-testid="ds-shell-search"
|
|
430
440
|
>
|
|
431
441
|
<Search class="size-4 flex-none" />
|
|
432
|
-
<span class="truncate">{searchLabel}</span>
|
|
442
|
+
<span class="hidden truncate sm:inline">{searchLabel}</span>
|
|
433
443
|
{#if searchShortcut}
|
|
434
444
|
<kbd
|
|
435
445
|
class="border-border bg-background text-shell-muted-foreground text-2xs ml-auto hidden rounded border px-1.5 py-0.5 font-mono sm:block"
|
|
@@ -481,7 +491,7 @@
|
|
|
481
491
|
<button
|
|
482
492
|
bind:this={overlayFirstFocus}
|
|
483
493
|
onclick={closeMobileNav}
|
|
484
|
-
class="border-border text-shell-muted-foreground hover:text-shell-foreground absolute top-
|
|
494
|
+
class="border-border text-shell-muted-foreground hover:text-shell-foreground absolute top-[calc(0.75rem+env(safe-area-inset-top))] right-3 z-10 grid size-8 place-items-center rounded-md border md:hidden"
|
|
485
495
|
aria-label="Close menu"
|
|
486
496
|
>
|
|
487
497
|
<X class="size-4" />
|
|
@@ -535,7 +545,7 @@
|
|
|
535
545
|
|
|
536
546
|
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
537
547
|
<header
|
|
538
|
-
class="ds-shell-bar bg-shell/80 text-shell-foreground border-border sticky top-0 z-20 flex h-14 flex-none items-center gap-2 border-b px-3 backdrop-blur sm:gap-3 sm:px-5"
|
|
548
|
+
class="ds-shell-bar bg-shell/80 text-shell-foreground border-border sticky top-0 z-20 flex min-h-14 flex-none items-center gap-2 border-b px-3 pt-[env(safe-area-inset-top)] backdrop-blur sm:gap-3 sm:px-5"
|
|
539
549
|
>
|
|
540
550
|
{#if hasNav}
|
|
541
551
|
<!-- `aria-controls` names the region this toggle opens. It is present
|
|
@@ -568,12 +578,16 @@
|
|
|
568
578
|
{#if context}{@render context()}{/if}
|
|
569
579
|
|
|
570
580
|
{#if onSearch && searchPlacement === 'leading'}
|
|
571
|
-
{@render searchButton(
|
|
581
|
+
{@render searchButton(
|
|
582
|
+
'h-9 w-9 flex-none justify-center px-0 sm:h-auto sm:w-[clamp(200px,32vw,560px)] sm:flex-1 sm:justify-start sm:gap-2 sm:px-3 sm:py-1.5'
|
|
583
|
+
)}
|
|
572
584
|
{/if}
|
|
573
585
|
|
|
574
586
|
<div class="ml-auto flex flex-none items-center gap-2 sm:gap-3">
|
|
575
587
|
{#if onSearch && searchPlacement === 'trailing'}
|
|
576
|
-
{@render searchButton(
|
|
588
|
+
{@render searchButton(
|
|
589
|
+
'h-9 w-9 flex-none justify-center px-0 sm:h-auto sm:w-[clamp(200px,32vw,560px)] sm:justify-start sm:gap-2 sm:px-3 sm:py-1.5'
|
|
590
|
+
)}
|
|
577
591
|
{/if}
|
|
578
592
|
|
|
579
593
|
{#if actions}{@render actions()}{/if}
|
|
@@ -694,7 +708,8 @@
|
|
|
694
708
|
'flex min-h-0 min-w-0 flex-1 flex-col',
|
|
695
709
|
'w-full',
|
|
696
710
|
capped && 'ds-shell-measure',
|
|
697
|
-
padded &&
|
|
711
|
+
padded &&
|
|
712
|
+
'px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12'
|
|
698
713
|
)}
|
|
699
714
|
data-measure={capped ?? undefined}
|
|
700
715
|
>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { cn, type WithElementRef } from '../../../utils.js';
|
|
5
5
|
import StatList from '../stat-list/stat-list.svelte';
|
|
6
6
|
import type { StatItem } from '../stat-list/stat-list.svelte';
|
|
7
|
+
import AppDialog from '../app-dialog/app-dialog.svelte';
|
|
8
|
+
import PanelRight from '@lucide/svelte/icons/panel-right';
|
|
7
9
|
|
|
8
10
|
// The persistent right-hand context column, identical on every route. It holds
|
|
9
11
|
// one standing "At a glance" stat card and, on routes with a selectable table,
|
|
@@ -16,10 +18,11 @@
|
|
|
16
18
|
// the stat card is shrink-0 and any detail takes the remaining height with its
|
|
17
19
|
// own inner scroll.
|
|
18
20
|
//
|
|
19
|
-
// Below xl (1280px)
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// Below xl (1280px) a fixed nav rail plus a ~400px column would crush the
|
|
22
|
+
// primary pane, so the standing `<aside>` stays hidden there — but its content
|
|
23
|
+
// does not vanish with it. A floating trigger opens the identical stats/detail
|
|
24
|
+
// pair in a dialogue instead: the column's shape is a desktop affordance, the
|
|
25
|
+
// information inside it is not.
|
|
23
26
|
let {
|
|
24
27
|
stats,
|
|
25
28
|
statsTitle = 'At a glance',
|
|
@@ -38,8 +41,17 @@
|
|
|
38
41
|
detail?: Snippet;
|
|
39
42
|
ariaLabel?: string;
|
|
40
43
|
} = $props();
|
|
44
|
+
|
|
45
|
+
let sheetOpen = $state(false);
|
|
41
46
|
</script>
|
|
42
47
|
|
|
48
|
+
{#snippet body()}
|
|
49
|
+
<StatList items={stats} title={statsTitle} info={statsInfo} />
|
|
50
|
+
{#if detail}
|
|
51
|
+
{@render detail()}
|
|
52
|
+
{/if}
|
|
53
|
+
{/snippet}
|
|
54
|
+
|
|
43
55
|
<aside
|
|
44
56
|
bind:this={ref}
|
|
45
57
|
aria-label={ariaLabel}
|
|
@@ -49,8 +61,24 @@
|
|
|
49
61
|
)}
|
|
50
62
|
{...restProps}
|
|
51
63
|
>
|
|
52
|
-
|
|
53
|
-
{#if detail}
|
|
54
|
-
{@render detail()}
|
|
55
|
-
{/if}
|
|
64
|
+
{@render body()}
|
|
56
65
|
</aside>
|
|
66
|
+
|
|
67
|
+
<!-- The reachable stand-in below xl. `size-11` (44px) rather than this
|
|
68
|
+
package's usual 36px control row: this is the one control whose entire
|
|
69
|
+
job is making otherwise-unreachable content reachable, so it earns the
|
|
70
|
+
full touch-target minimum rather than the shell's ordinary density. -->
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onclick={() => (sheetOpen = true)}
|
|
74
|
+
class="border-border bg-card text-muted-foreground hover:text-foreground ds-edge fixed right-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-30 grid size-11 flex-none place-items-center rounded-full border xl:hidden"
|
|
75
|
+
aria-label={ariaLabel ? `Open ${ariaLabel}` : `Open ${statsTitle}`}
|
|
76
|
+
>
|
|
77
|
+
<PanelRight class="size-4.5" />
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
<AppDialog bind:open={sheetOpen} title={ariaLabel ?? statsTitle} size="sm">
|
|
81
|
+
<div class="flex flex-col gap-5">
|
|
82
|
+
{@render body()}
|
|
83
|
+
</div>
|
|
84
|
+
</AppDialog>
|
package/dist/styles.css
CHANGED
|
@@ -867,6 +867,11 @@
|
|
|
867
867
|
transition: width 200ms ease;
|
|
868
868
|
}
|
|
869
869
|
|
|
870
|
+
/* Full viewport height (`inset-block: 0`), so the brand row at its top and
|
|
871
|
+
the last nav row at its bottom sit exactly where a notch or a home
|
|
872
|
+
indicator gesture bar would cover them. The rail's own sticky state
|
|
873
|
+
never needs this — it starts below the page's own top edge in normal
|
|
874
|
+
flow, not against the physical screen edge. */
|
|
870
875
|
.ds-shell-rail[data-drawer='true'] {
|
|
871
876
|
display: flex;
|
|
872
877
|
position: fixed;
|
|
@@ -874,6 +879,8 @@
|
|
|
874
879
|
left: 0;
|
|
875
880
|
z-index: 50;
|
|
876
881
|
max-width: 82vw;
|
|
882
|
+
padding-top: env(safe-area-inset-top);
|
|
883
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
877
884
|
box-shadow:
|
|
878
885
|
0 10px 15px -3px oklch(0 0 0 / 0.1),
|
|
879
886
|
0 4px 6px -4px oklch(0 0 0 / 0.1);
|
|
@@ -890,6 +897,8 @@
|
|
|
890
897
|
position: sticky;
|
|
891
898
|
z-index: auto;
|
|
892
899
|
max-width: none;
|
|
900
|
+
padding-top: 0;
|
|
901
|
+
padding-bottom: 0;
|
|
893
902
|
box-shadow: none;
|
|
894
903
|
animation: none;
|
|
895
904
|
}
|
package/package.json
CHANGED
|
@@ -1,104 +1,107 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
2
|
+
"name": "@poodle64/ui",
|
|
3
|
+
"version": "2026.9.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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/radar-hooves/design-system.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"registry": "https://registry.npmjs.org",
|
|
13
|
+
"access": "public",
|
|
14
|
+
"provenance": false
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"dist",
|
|
19
|
+
"registry"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./styles.css": "./dist/styles.css",
|
|
24
|
+
"./utils": {
|
|
25
|
+
"types": "./dist/utils.d.ts",
|
|
26
|
+
"svelte": "./dist/utils.js"
|
|
27
|
+
},
|
|
28
|
+
"./format": {
|
|
29
|
+
"types": "./dist/format.d.ts",
|
|
30
|
+
"svelte": "./dist/format.js"
|
|
31
|
+
},
|
|
32
|
+
"./*": {
|
|
33
|
+
"types": "./dist/components/ui/*/index.d.ts",
|
|
34
|
+
"svelte": "./dist/components/ui/*/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"ds-check-colour-surface": "bin/check-colour-surface.mjs"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"bits-ui": "^2.18.1",
|
|
42
|
+
"formsnap": "^2.0.1",
|
|
43
|
+
"mode-watcher": "^1.1.0",
|
|
44
|
+
"svelte": "^5.33.0",
|
|
45
|
+
"svelte-sonner": "^1.1.1",
|
|
46
|
+
"sveltekit-superforms": "^2.30.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"mode-watcher": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"svelte-sonner": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"formsnap": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"sveltekit-superforms": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@jsonforms/core": "^3.8.0",
|
|
64
|
+
"@lucide/svelte": "^1.25.0",
|
|
65
|
+
"@tanstack/table-core": "^8.21.3",
|
|
66
|
+
"clsx": "^2.1.1",
|
|
67
|
+
"tailwind-merge": "^3.6.0",
|
|
68
|
+
"tailwind-variants": "^3.2.2",
|
|
69
|
+
"tw-animate-css": "^1.4.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@poodle64/design-tokens": "workspace:*",
|
|
73
|
+
"@sveltejs/kit": "^2.63.0",
|
|
74
|
+
"@sveltejs/package": "^2.5.8",
|
|
75
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
76
|
+
"@tailwindcss/cli": "^4.3.2",
|
|
77
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
78
|
+
"@testing-library/svelte": "^5.4.2",
|
|
79
|
+
"@types/node": "^22.20.1",
|
|
80
|
+
"bits-ui": "^2.18.1",
|
|
81
|
+
"formsnap": "^2.0.1",
|
|
82
|
+
"jsdom": "^29.1.1",
|
|
83
|
+
"mode-watcher": "^1.1.0",
|
|
84
|
+
"playwright": "^1.62.0",
|
|
85
|
+
"publint": "^0.3.15",
|
|
86
|
+
"svelte": "^5.56.2",
|
|
87
|
+
"svelte-check": "^4.6.0",
|
|
88
|
+
"svelte-sonner": "^1.1.1",
|
|
89
|
+
"sveltekit-superforms": "^2.30.2",
|
|
90
|
+
"tailwindcss": "^4.3.2",
|
|
91
|
+
"typescript": "^6.0.3",
|
|
92
|
+
"vite": "^8.0.16",
|
|
93
|
+
"vitest": "^4.1.10"
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"build": "node scripts/generate-registry.mjs && svelte-kit sync && svelte-package && publint",
|
|
97
|
+
"registry": "node scripts/generate-registry.mjs",
|
|
98
|
+
"registry:check": "node scripts/generate-registry.mjs --check",
|
|
99
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
100
|
+
"test": "pnpm run build && vitest run",
|
|
101
|
+
"test:browser": "pnpm run harness:build && node harness/drive.mjs",
|
|
102
|
+
"prepublishOnly": "pnpm run build",
|
|
103
|
+
"harness:build": "vite build --config harness/vite.config.ts && tailwindcss -i harness/harness.css -o harness/dist/app.css --minify",
|
|
104
|
+
"harness:serve": "python3 -m http.server 4180 --directory harness/dist"
|
|
105
|
+
},
|
|
106
|
+
"packageManager": "pnpm@10.28.0"
|
|
104
107
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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.9.
|
|
4
|
+
Generated from `@poodle64/ui@2026.9.4`. 56 components, 15 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
|
|