@poodle64/ui 2026.8.15 → 2026.9.0
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 +367 -14
- package/dist/components/ui/button/button.svelte +13 -8
- package/dist/components/ui/checkbox/checkbox.svelte +23 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
- package/dist/components/ui/form/form-button.svelte +7 -0
- package/dist/components/ui/form/form-button.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-description.svelte +18 -0
- package/dist/components/ui/form/form-description.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-element-field.svelte +25 -0
- package/dist/components/ui/form/form-element-field.svelte.d.ts +28 -0
- package/dist/components/ui/form/form-field-errors.svelte +31 -0
- package/dist/components/ui/form/form-field-errors.svelte.d.ts +8 -0
- package/dist/components/ui/form/form-field.svelte +25 -0
- package/dist/components/ui/form/form-field.svelte.d.ts +28 -0
- package/dist/components/ui/form/form-fieldset.svelte +16 -0
- package/dist/components/ui/form/form-fieldset.svelte.d.ts +27 -0
- package/dist/components/ui/form/form-label.svelte +25 -0
- package/dist/components/ui/form/form-label.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-legend.svelte +17 -0
- package/dist/components/ui/form/form-legend.svelte.d.ts +4 -0
- package/dist/components/ui/form/index.d.ts +11 -0
- package/dist/components/ui/form/index.js +13 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/page-header/page-header.svelte +70 -33
- package/dist/components/ui/page-header/page-header.svelte.d.ts +4 -0
- package/dist/components/ui/switch/index.d.ts +2 -2
- package/dist/components/ui/switch/index.js +1 -1
- package/dist/components/ui/switch/switch.svelte +38 -5
- package/dist/components/ui/switch/switch.svelte.d.ts +7 -1
- package/dist/components/ui/tabs/tabs-trigger.svelte +1 -1
- package/dist/format.d.ts +222 -0
- package/dist/format.js +422 -0
- package/dist/styles.css +310 -15
- package/package.json +16 -2
- package/registry/component-map.json +74 -4
- package/registry/component-map.md +18 -2
package/dist/styles.css
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
@custom-variant dark (&:is(.dark *));
|
|
64
64
|
|
|
65
65
|
/* ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
-
The bits-ui
|
|
66
|
+
The bits-ui `data-state` variants.
|
|
67
67
|
|
|
68
68
|
Same defect as `dark` above, one attribute along. This package writes ~47
|
|
69
69
|
`data-open:` / `data-closed:` utilities across the dialogue, alert-dialogue,
|
|
@@ -76,18 +76,58 @@
|
|
|
76
76
|
matches — which is why four of the five adopting apps shipped with dead
|
|
77
77
|
overlay transitions and none of them noticed.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`data-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
`data-state` is ONE attribute carrying a dozen-odd values, and declaring two
|
|
80
|
+
of them left every other value as the same trap one value along.
|
|
81
|
+
`data-checked:` and `data-active:` compile to `&[data-checked]` /
|
|
82
|
+
`&[data-active]`, and bits-ui emits `data-state="checked"` and
|
|
83
|
+
`data-state="active"` — so a checked checkbox painted no fill and a selected
|
|
84
|
+
tab was indistinguishable from its neighbours, in exactly the silence
|
|
85
|
+
described above. Both were found in a consuming app, in a browser, months
|
|
86
|
+
after they shipped.
|
|
87
|
+
|
|
88
|
+
So the whole value set is declared, not the two that had already been
|
|
89
|
+
caught: `open`/`closed` (dialogue, menu, popover, tooltip, select),
|
|
90
|
+
`checked`/`unchecked`/`indeterminate` (checkbox, switch, menu checkbox item)
|
|
91
|
+
and `active`/`inactive` (tabs). That is a table with one rule — one
|
|
92
|
+
declaration per value bits-ui can put in `data-state` on a component this
|
|
93
|
+
package ships — rather than a growing list of exceptions, and it is the
|
|
94
|
+
whole point of fixing this at the mapping: the NEXT component to reach for
|
|
95
|
+
`data-checked:` gets a rule that matches instead of rediscovering this.
|
|
96
|
+
|
|
97
|
+
`data-active` is the one union, and it is not defensiveness. bits-ui emits
|
|
98
|
+
BOTH forms under that name: `data-state="active"` on a tabs trigger, and a
|
|
99
|
+
bare `data-active` on a navigation-menu link, a slider thumb and a pin-input
|
|
100
|
+
cell — and this package's own AppNav writes `data-active="true"` on every
|
|
101
|
+
nav row. A single-selector declaration would therefore have fixed tabs by
|
|
102
|
+
breaking the four surfaces that were already working. The other values have
|
|
103
|
+
no bare form in bits-ui 2.18.x, so they get one selector each.
|
|
104
|
+
|
|
105
|
+
`data-state="delayed-open"` (tooltip) and `data-state="selected"` (this
|
|
106
|
+
package's own table rows) keep the bracketed long form at their call sites
|
|
107
|
+
and get NO shorthand. `delayed-open` has no plausible shorthand spelling,
|
|
108
|
+
and `data-selected` is ALREADY a bare bits-ui attribute on menu and calendar
|
|
109
|
+
items — pointing that name at `[data-state='selected']` would take those
|
|
110
|
+
with it. Where a name means two things, the long form is what says which.
|
|
111
|
+
|
|
112
|
+
The remaining five shorthand data-variants this package writes —
|
|
113
|
+
`data-selected`, `data-highlighted`, `data-disabled`, `data-placeholder`
|
|
114
|
+
(bits-ui, all emitted as empty-string-or-undefined) and `data-inset` (set by
|
|
115
|
+
this package's own menu items) — are BARE attributes, so Tailwind's default
|
|
116
|
+
`&[data-x]` already matches them and a declaration here would only restate
|
|
117
|
+
it. `src/test/data-state-variants.test.ts` is what keeps that distinction
|
|
118
|
+
honest: it enumerates every shorthand data-variant the built package ships
|
|
119
|
+
and fails on any one whose compiled selector is not the attribute the DOM
|
|
120
|
+
actually carries, so a new unowned variant cannot ship. It also mounts a
|
|
121
|
+
real checkbox, switch and tab strip and reads the attributes back, so the
|
|
122
|
+
table above is pinned to bits-ui rather than to this paragraph.
|
|
88
123
|
───────────────────────────────────────────────────────────────────────────── */
|
|
89
124
|
@custom-variant data-open (&[data-state='open']);
|
|
90
125
|
@custom-variant data-closed (&[data-state='closed']);
|
|
126
|
+
@custom-variant data-checked (&[data-state='checked']);
|
|
127
|
+
@custom-variant data-unchecked (&[data-state='unchecked']);
|
|
128
|
+
@custom-variant data-indeterminate (&[data-state='indeterminate']);
|
|
129
|
+
@custom-variant data-active (&[data-state='active'], &[data-active]);
|
|
130
|
+
@custom-variant data-inactive (&[data-state='inactive']);
|
|
91
131
|
|
|
92
132
|
/* ─────────────────────────────────────────────────────────────────────────────
|
|
93
133
|
The shadcn semantic surface.
|
|
@@ -198,7 +238,16 @@
|
|
|
198
238
|
--color-secondary: var(--secondary, var(--ds-color-surface-1));
|
|
199
239
|
--color-secondary-foreground: var(--secondary-foreground, var(--ds-color-foreground));
|
|
200
240
|
--color-muted: var(--muted, var(--ds-color-surface-1));
|
|
201
|
-
|
|
241
|
+
/* A SELECTION tint, not a surface rung (design-system#24). It was
|
|
242
|
+
--ds-color-surface-2 — the same rung --color-card resolves to — which made
|
|
243
|
+
`hover:bg-accent/50` on a card a no-op BY CONSTRUCTION: mixing a colour at
|
|
244
|
+
any opacity over a ground it is identical to cannot change a pixel. It also
|
|
245
|
+
left the menu/select/command highlight, the only thing this package's own
|
|
246
|
+
components use `bg-accent` for, at 1.03:1 against the popover it sits on in
|
|
247
|
+
light mode. A translucent tint of the accent composites over whatever
|
|
248
|
+
ground it lands on, so one value serves both, and it is the idiom this
|
|
249
|
+
package already ships for the SearchResults match highlight. */
|
|
250
|
+
--color-accent: var(--accent, color-mix(in oklch, var(--ds-color-primary) 12%, transparent));
|
|
202
251
|
--color-accent-foreground: var(--accent-foreground, var(--ds-color-foreground));
|
|
203
252
|
--color-input: var(--input, var(--ds-color-border));
|
|
204
253
|
|
|
@@ -303,6 +352,87 @@
|
|
|
303
352
|
--ds-shell-measure-wide: 120rem;
|
|
304
353
|
}
|
|
305
354
|
|
|
355
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
Control geometry — the density option.
|
|
357
|
+
|
|
358
|
+
Button's height and inline padding, as tokens rather than the hard-coded
|
|
359
|
+
`h-10` / `px-4` they were. No token could reach those classes, so an app
|
|
360
|
+
wanting a denser control scale had exactly one move available: fork the
|
|
361
|
+
component. One did, and it cost six directories — `button`, `dialog`,
|
|
362
|
+
`alert-dialog`, `command`, `input-group` and `form` all stayed vendored
|
|
363
|
+
there, five of them byte-identical to this package's but for which Button
|
|
364
|
+
they import. That is the whole defect: a geometry decision with no knob
|
|
365
|
+
turns into a fork of everything downstream of it.
|
|
366
|
+
|
|
367
|
+
The values below reproduce the previous hard-coded classes EXACTLY (2.5rem
|
|
368
|
+
is h-10, 1rem is px-4, and so on down the ramp), so a consumer that names
|
|
369
|
+
nothing renders to the same pixel. `harness/additivity.mjs` is what proves
|
|
370
|
+
that rather than this sentence.
|
|
371
|
+
|
|
372
|
+
An app does not set these by hand. It names a density on any ancestor —
|
|
373
|
+
ordinarily `<html>` — and the preset moves the whole ramp:
|
|
374
|
+
|
|
375
|
+
<html data-ds-density="compact">
|
|
376
|
+
|
|
377
|
+
Two named values, `comfortable` (the default) and `compact`, and the
|
|
378
|
+
attribute is honoured wherever it appears, so a single dense toolbar can
|
|
379
|
+
carry it without the page doing so. Hand-tuning a rung stays possible for
|
|
380
|
+
the app that must, but it is a deviation rather than the affordance: the
|
|
381
|
+
sanctioned move is choosing a named density, exactly as choosing a palette
|
|
382
|
+
rather than a hex value is the sanctioned move for colour.
|
|
383
|
+
|
|
384
|
+
`compact` is not a picked number. This package's Input already renders at
|
|
385
|
+
`h-8` and its Select trigger at `h-8`/`h-7`, so a default-size Button next
|
|
386
|
+
to an Input has been 8px taller than it for as long as both have shipped.
|
|
387
|
+
The compact ramp lands `md` on 2rem and `sm` on 1.75rem — the input and the
|
|
388
|
+
select trigger exactly — so the density an app was hand-pinning on 75 call
|
|
389
|
+
sites is the one that makes a form row line up.
|
|
390
|
+
|
|
391
|
+
`comfortable` shares the `:root` selector rather than restating the ramp;
|
|
392
|
+
it is there so a subtree can opt back OUT of a compact page. Both density
|
|
393
|
+
rules carry the same specificity as `:root` and are declared after it, so a
|
|
394
|
+
page-level attribute wins, and they declare on the element that carries the
|
|
395
|
+
attribute rather than at `:root`, so a scoped one reaches only its subtree.
|
|
396
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
397
|
+
:root,
|
|
398
|
+
[data-ds-density='comfortable'] {
|
|
399
|
+
--ds-control-height-xs: 1.75rem;
|
|
400
|
+
--ds-control-height-sm: 2.25rem;
|
|
401
|
+
--ds-control-height-md: 2.5rem;
|
|
402
|
+
--ds-control-height-lg: 2.75rem;
|
|
403
|
+
|
|
404
|
+
--ds-control-pad-xs: 0.625rem;
|
|
405
|
+
--ds-control-pad-sm: 0.875rem;
|
|
406
|
+
--ds-control-pad-md: 1rem;
|
|
407
|
+
--ds-control-pad-lg: 1.25rem;
|
|
408
|
+
|
|
409
|
+
/* The inline padding on an edge an icon sits against: an icon is already
|
|
410
|
+
optical whitespace, so it needs less room beside it than a glyph does. A
|
|
411
|
+
separate rung rather than a subtraction from the pad above, because the
|
|
412
|
+
trim is not one ratio — sm gives back 0.375rem where md gives back
|
|
413
|
+
0.25rem — and folding it into a calc would have changed sm for every
|
|
414
|
+
consumer to buy a shorter table. */
|
|
415
|
+
--ds-control-pad-icon-xs: 0.375rem;
|
|
416
|
+
--ds-control-pad-icon-sm: 0.5rem;
|
|
417
|
+
--ds-control-pad-icon-md: 0.75rem;
|
|
418
|
+
--ds-control-pad-icon-lg: 0.75rem;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
[data-ds-density='compact'] {
|
|
422
|
+
--ds-control-height-xs: 1.5rem;
|
|
423
|
+
--ds-control-height-sm: 1.75rem;
|
|
424
|
+
--ds-control-height-md: 2rem;
|
|
425
|
+
--ds-control-height-lg: 2.25rem;
|
|
426
|
+
--ds-control-pad-xs: 0.5rem;
|
|
427
|
+
--ds-control-pad-sm: 0.625rem;
|
|
428
|
+
--ds-control-pad-md: 0.75rem;
|
|
429
|
+
--ds-control-pad-lg: 1rem;
|
|
430
|
+
--ds-control-pad-icon-xs: 0.25rem;
|
|
431
|
+
--ds-control-pad-icon-sm: 0.375rem;
|
|
432
|
+
--ds-control-pad-icon-md: 0.5rem;
|
|
433
|
+
--ds-control-pad-icon-lg: 0.625rem;
|
|
434
|
+
}
|
|
435
|
+
|
|
306
436
|
@layer base {
|
|
307
437
|
/**
|
|
308
438
|
* The milled edge. Cards read as having depth from a hairline border plus a
|
|
@@ -788,22 +918,187 @@
|
|
|
788
918
|
the measure rather than outside it. That is the same arithmetic as the
|
|
789
919
|
`mx-auto max-w-4xl px-6` idiom this replaces, and keeping it means one
|
|
790
920
|
element rather than a second wrapper nobody asked for. */
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
921
|
+
/* Two classes, because the MEASURE and the CENTRING are two decisions and
|
|
922
|
+
only the shell's content box wants both (design-system#22).
|
|
923
|
+
|
|
924
|
+
`.ds-measure` caps the width and nothing else. It is for a block INSIDE a
|
|
925
|
+
page — the paragraph of explanatory prose on a route legitimately set to
|
|
926
|
+
`wide`, which would otherwise inherit a 120rem measure and run to ~123
|
|
927
|
+
characters a line. `.ds-shell-measure` is that plus `margin-inline: auto`,
|
|
928
|
+
which is right for the content box and wrong for a block within a page:
|
|
929
|
+
applied to a set of left-anchored paragraphs it indented them ~207px away
|
|
930
|
+
from their own label, measured in a consumer and reverted.
|
|
931
|
+
|
|
932
|
+
Same attribute, same custom properties, so a block retunes with the shell
|
|
933
|
+
rather than drifting from it. That is the whole point: the alternative an
|
|
934
|
+
app reaches for is a local `max-w-prose` or `max-w-[72ch]`, which is a
|
|
935
|
+
number typed once that never hears about a retune — four apps had written
|
|
936
|
+
ten of them between them, and not one matched this package's own 72ch. */
|
|
937
|
+
.ds-measure[data-measure='prose'],
|
|
795
938
|
.ds-shell-measure[data-measure='prose'] {
|
|
796
939
|
max-width: var(--ds-shell-measure-prose);
|
|
797
940
|
}
|
|
798
941
|
|
|
942
|
+
.ds-measure[data-measure='page'],
|
|
799
943
|
.ds-shell-measure[data-measure='page'] {
|
|
800
944
|
max-width: var(--ds-shell-measure-page);
|
|
801
945
|
}
|
|
802
946
|
|
|
947
|
+
.ds-measure[data-measure='wide'],
|
|
803
948
|
.ds-shell-measure[data-measure='wide'] {
|
|
804
949
|
max-width: var(--ds-shell-measure-wide);
|
|
805
950
|
}
|
|
806
951
|
|
|
952
|
+
.ds-shell-measure {
|
|
953
|
+
margin-inline: auto;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* ── The prose face (design-system#32) ────────────────────────────────────
|
|
957
|
+
Styling for HTML the app did NOT author — rendered markdown, an extracted
|
|
958
|
+
document body, a description field that arrives as rich text.
|
|
959
|
+
|
|
960
|
+
Three consumers had built one of these, by three different mechanisms: a
|
|
961
|
+
hand-written token-based block, the Tailwind typography plugin, and a
|
|
962
|
+
second hand-written block that one of them had already duplicated inside
|
|
963
|
+
itself. Every consumer that renders a document body would otherwise derive
|
|
964
|
+
it a fourth time, and typography degrades worse than most things when it
|
|
965
|
+
is re-derived per app.
|
|
966
|
+
|
|
967
|
+
The package styles it and does NOT render it: sanitising untrusted HTML is
|
|
968
|
+
a security boundary, and a package cannot see the inputs the boundary is
|
|
969
|
+
protecting. The app owns the markdown-to-HTML seam and hands the result in.
|
|
970
|
+
|
|
971
|
+
It carries no measure. A reading measure and a reading face are two
|
|
972
|
+
decisions, and an app composes them:
|
|
973
|
+
|
|
974
|
+
<div class="ds-prose ds-measure" data-measure="prose">…</div>
|
|
975
|
+
|
|
976
|
+
Nothing here reaches for a colour or a length that is not a --ds-* token,
|
|
977
|
+
so a palette or a retune moves the whole face with it. */
|
|
978
|
+
.ds-prose {
|
|
979
|
+
color: var(--ds-color-foreground);
|
|
980
|
+
line-height: 1.7;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.ds-prose h1,
|
|
984
|
+
.ds-prose h2,
|
|
985
|
+
.ds-prose h3,
|
|
986
|
+
.ds-prose h4 {
|
|
987
|
+
font-family: var(--ds-font-display);
|
|
988
|
+
font-weight: 600;
|
|
989
|
+
line-height: 1.25;
|
|
990
|
+
margin-block: var(--ds-spacing-lg) var(--ds-spacing-sm);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/* `em` rather than the type scale: these headings sit INSIDE a document, so
|
|
994
|
+
they are relative to the body copy around them, not to the page's own
|
|
995
|
+
heading ramp. An <h1> in rendered content is not the page's <h1>. */
|
|
996
|
+
.ds-prose h1 {
|
|
997
|
+
font-size: 1.5em;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.ds-prose h2 {
|
|
1001
|
+
font-size: 1.25em;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.ds-prose h3,
|
|
1005
|
+
.ds-prose h4 {
|
|
1006
|
+
font-size: 1.1em;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.ds-prose p,
|
|
1010
|
+
.ds-prose ul,
|
|
1011
|
+
.ds-prose ol,
|
|
1012
|
+
.ds-prose blockquote {
|
|
1013
|
+
margin-block: var(--ds-spacing-sm);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.ds-prose ul,
|
|
1017
|
+
.ds-prose ol {
|
|
1018
|
+
padding-inline-start: var(--ds-spacing-lg);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/* Restored explicitly: the preflight reset strips list markers, and rendered
|
|
1022
|
+
content is the one place a bullet carries meaning. */
|
|
1023
|
+
.ds-prose ul {
|
|
1024
|
+
list-style: disc;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.ds-prose ol {
|
|
1028
|
+
list-style: decimal;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.ds-prose a {
|
|
1032
|
+
color: var(--ds-color-primary);
|
|
1033
|
+
text-decoration: underline;
|
|
1034
|
+
text-underline-offset: 2px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.ds-prose strong {
|
|
1038
|
+
font-weight: 600;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.ds-prose code {
|
|
1042
|
+
font-family: var(--ds-font-code);
|
|
1043
|
+
font-size: 0.9em;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.ds-prose pre {
|
|
1047
|
+
background: var(--ds-color-surface-1);
|
|
1048
|
+
border: 1px solid var(--ds-color-border);
|
|
1049
|
+
border-radius: var(--ds-radius-sm);
|
|
1050
|
+
overflow-x: auto;
|
|
1051
|
+
padding: var(--ds-spacing-sm);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.ds-prose blockquote {
|
|
1055
|
+
border-inline-start: 2px solid var(--ds-color-border-strong);
|
|
1056
|
+
color: var(--ds-color-muted-foreground);
|
|
1057
|
+
padding-inline-start: var(--ds-spacing-sm);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.ds-prose hr {
|
|
1061
|
+
border: 0;
|
|
1062
|
+
border-block-start: 1px solid var(--ds-color-border);
|
|
1063
|
+
margin-block: var(--ds-spacing-lg);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* A table in an extracted document is routinely wider than the measure around
|
|
1067
|
+
it. It scrolls in its OWN box rather than pushing the page sideways — the
|
|
1068
|
+
same rule the library surfaces learned at 390px. */
|
|
1069
|
+
.ds-prose table {
|
|
1070
|
+
border-collapse: collapse;
|
|
1071
|
+
display: block;
|
|
1072
|
+
font-size: 0.9em;
|
|
1073
|
+
margin-block: var(--ds-spacing-md);
|
|
1074
|
+
max-width: 100%;
|
|
1075
|
+
overflow-x: auto;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.ds-prose th,
|
|
1079
|
+
.ds-prose td {
|
|
1080
|
+
border: 1px solid var(--ds-color-border);
|
|
1081
|
+
padding: var(--ds-spacing-xs) var(--ds-spacing-sm);
|
|
1082
|
+
text-align: start;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.ds-prose th {
|
|
1086
|
+
color: var(--ds-color-muted-foreground);
|
|
1087
|
+
font-weight: 600;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/* A figure in a document is evidence, so it gets the surface and border a
|
|
1091
|
+
plate deserves, and never exceeds its column. */
|
|
1092
|
+
.ds-prose img {
|
|
1093
|
+
background: var(--ds-color-surface-2);
|
|
1094
|
+
border: 1px solid var(--ds-color-border);
|
|
1095
|
+
border-radius: var(--ds-radius-sm);
|
|
1096
|
+
display: block;
|
|
1097
|
+
height: auto;
|
|
1098
|
+
margin-block: var(--ds-spacing-md);
|
|
1099
|
+
max-width: 100%;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
807
1102
|
/* The content texture: the house atmosphere, painted once by the shell on its
|
|
808
1103
|
own scrolling content region.
|
|
809
1104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.9.0",
|
|
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",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/utils.d.ts",
|
|
21
21
|
"svelte": "./dist/utils.js"
|
|
22
22
|
},
|
|
23
|
+
"./format": {
|
|
24
|
+
"types": "./dist/format.d.ts",
|
|
25
|
+
"svelte": "./dist/format.js"
|
|
26
|
+
},
|
|
23
27
|
"./*": {
|
|
24
28
|
"types": "./dist/components/ui/*/index.d.ts",
|
|
25
29
|
"svelte": "./dist/components/ui/*/index.js"
|
|
@@ -27,9 +31,11 @@
|
|
|
27
31
|
},
|
|
28
32
|
"peerDependencies": {
|
|
29
33
|
"bits-ui": "^2.18.1",
|
|
34
|
+
"formsnap": "^2.0.1",
|
|
30
35
|
"mode-watcher": "^1.1.0",
|
|
31
36
|
"svelte": "^5.33.0",
|
|
32
|
-
"svelte-sonner": "^1.1.1"
|
|
37
|
+
"svelte-sonner": "^1.1.1",
|
|
38
|
+
"sveltekit-superforms": "^2.30.0"
|
|
33
39
|
},
|
|
34
40
|
"peerDependenciesMeta": {
|
|
35
41
|
"mode-watcher": {
|
|
@@ -37,6 +43,12 @@
|
|
|
37
43
|
},
|
|
38
44
|
"svelte-sonner": {
|
|
39
45
|
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"formsnap": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"sveltekit-superforms": {
|
|
51
|
+
"optional": true
|
|
40
52
|
}
|
|
41
53
|
},
|
|
42
54
|
"dependencies": {
|
|
@@ -58,6 +70,7 @@
|
|
|
58
70
|
"@testing-library/svelte": "^5.4.2",
|
|
59
71
|
"@types/node": "^22.20.1",
|
|
60
72
|
"bits-ui": "^2.18.1",
|
|
73
|
+
"formsnap": "^2.0.1",
|
|
61
74
|
"jsdom": "^29.1.1",
|
|
62
75
|
"mode-watcher": "^1.1.0",
|
|
63
76
|
"playwright": "^1.62.0",
|
|
@@ -65,6 +78,7 @@
|
|
|
65
78
|
"svelte": "^5.56.2",
|
|
66
79
|
"svelte-check": "^4.6.0",
|
|
67
80
|
"svelte-sonner": "^1.1.1",
|
|
81
|
+
"sveltekit-superforms": "^2.30.2",
|
|
68
82
|
"tailwindcss": "^4.3.2",
|
|
69
83
|
"typescript": "^6.0.3",
|
|
70
84
|
"vite": "^8.0.16",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.
|
|
4
|
+
"version": "2026.9.0",
|
|
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": 55,
|
|
8
|
+
"situationCount": 15
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
11
11
|
{
|
|
@@ -155,6 +155,76 @@
|
|
|
155
155
|
}
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
"key": "hand-written-form",
|
|
160
|
+
"title": "A form the app itself describes",
|
|
161
|
+
"description": "A form whose fields the app writes out itself, validated by superforms and wired for accessibility by Formsnap. These wrappers put the label/description/error rhythm and the ARIA wiring in one place; the schema-described renderer above is for the other case, where the shape arrives at runtime.",
|
|
162
|
+
"components": [
|
|
163
|
+
{
|
|
164
|
+
"name": "FormField",
|
|
165
|
+
"dir": "form",
|
|
166
|
+
"import": "@poodle64/ui/form",
|
|
167
|
+
"props": "form, name, class?",
|
|
168
|
+
"insteadOf": "a <div class=\"space-y-2\"> you rebuild per field, per app"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "FormControl",
|
|
172
|
+
"dir": "form",
|
|
173
|
+
"import": "@poodle64/ui/form",
|
|
174
|
+
"props": "children, id?",
|
|
175
|
+
"insteadOf": "wiring id/aria-describedby/aria-invalid onto the input by hand"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "FormLabel",
|
|
179
|
+
"dir": "form",
|
|
180
|
+
"import": "@poodle64/ui/form",
|
|
181
|
+
"props": "class?",
|
|
182
|
+
"insteadOf": "a <Label> that does not turn destructive when its field errors"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "FormDescription",
|
|
186
|
+
"dir": "form",
|
|
187
|
+
"import": "@poodle64/ui/form",
|
|
188
|
+
"props": "class?",
|
|
189
|
+
"insteadOf": "helper text the field's aria-describedby never points at"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "FormFieldErrors",
|
|
193
|
+
"dir": "form",
|
|
194
|
+
"import": "@poodle64/ui/form",
|
|
195
|
+
"props": "class?, errorClasses?",
|
|
196
|
+
"insteadOf": "hand-rendered {#each errors} blocks, styled differently in each app"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "FormFieldset",
|
|
200
|
+
"dir": "form",
|
|
201
|
+
"import": "@poodle64/ui/form",
|
|
202
|
+
"props": "form, name, class?",
|
|
203
|
+
"insteadOf": "a bare <fieldset> with no error state"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "FormLegend",
|
|
207
|
+
"dir": "form",
|
|
208
|
+
"import": "@poodle64/ui/form",
|
|
209
|
+
"props": "class?",
|
|
210
|
+
"insteadOf": "a <legend> styled per app"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"name": "FormElementField",
|
|
214
|
+
"dir": "form",
|
|
215
|
+
"import": "@poodle64/ui/form",
|
|
216
|
+
"props": "form, name, class?",
|
|
217
|
+
"insteadOf": "hand-indexing into an array field's errors"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "FormButton",
|
|
221
|
+
"dir": "form",
|
|
222
|
+
"import": "@poodle64/ui/form",
|
|
223
|
+
"props": "children, variant?, size?, disabled?, class?",
|
|
224
|
+
"insteadOf": "remembering type=\"submit\" on every form's Button — it forwards every Button prop and sets the type"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
158
228
|
{
|
|
159
229
|
"key": "server-described-form",
|
|
160
230
|
"title": "A form the server described",
|
|
@@ -220,7 +290,7 @@
|
|
|
220
290
|
"name": "PageHeader",
|
|
221
291
|
"dir": "page-header",
|
|
222
292
|
"import": "@poodle64/ui/page-header",
|
|
223
|
-
"props": "title?, eyebrow?, breadcrumbs?, subtitle?, info?, actions?",
|
|
293
|
+
"props": "title?, eyebrow?, breadcrumbs?, icon?, subtitle?, info?, meta?, actions?",
|
|
224
294
|
"insteadOf": "a hand-written <h1> and title bar (the drift gate fails this)"
|
|
225
295
|
},
|
|
226
296
|
{
|
|
@@ -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.
|
|
4
|
+
Generated from `@poodle64/ui@2026.9.0`. 55 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
|
|
|
@@ -83,6 +83,22 @@ _The generic titled card every route reaches for — an optional header with ico
|
|
|
83
83
|
| --- | --- | --- | --- |
|
|
84
84
|
| `Panel` | `@poodle64/ui/panel` | `children, title?, subtitle?, icon?, action?, pad?` | raw Card + CardHeader + CardTitle assembled by hand on every route |
|
|
85
85
|
|
|
86
|
+
## A form the app itself describes
|
|
87
|
+
|
|
88
|
+
_A form whose fields the app writes out itself, validated by superforms and wired for accessibility by Formsnap. These wrappers put the label/description/error rhythm and the ARIA wiring in one place; the schema-described renderer above is for the other case, where the shape arrives at runtime._
|
|
89
|
+
|
|
90
|
+
| Component | Import | Key props | Reach for it instead of |
|
|
91
|
+
| --- | --- | --- | --- |
|
|
92
|
+
| `FormField` | `@poodle64/ui/form` | `form, name, class?` | a <div class="space-y-2"> you rebuild per field, per app |
|
|
93
|
+
| `FormControl` | `@poodle64/ui/form` | `children, id?` | wiring id/aria-describedby/aria-invalid onto the input by hand |
|
|
94
|
+
| `FormLabel` | `@poodle64/ui/form` | `class?` | a <Label> that does not turn destructive when its field errors |
|
|
95
|
+
| `FormDescription` | `@poodle64/ui/form` | `class?` | helper text the field's aria-describedby never points at |
|
|
96
|
+
| `FormFieldErrors` | `@poodle64/ui/form` | `class?, errorClasses?` | hand-rendered {#each errors} blocks, styled differently in each app |
|
|
97
|
+
| `FormFieldset` | `@poodle64/ui/form` | `form, name, class?` | a bare <fieldset> with no error state |
|
|
98
|
+
| `FormLegend` | `@poodle64/ui/form` | `class?` | a <legend> styled per app |
|
|
99
|
+
| `FormElementField` | `@poodle64/ui/form` | `form, name, class?` | hand-indexing into an array field's errors |
|
|
100
|
+
| `FormButton` | `@poodle64/ui/form` | `children, variant?, size?, disabled?, class?` | remembering type="submit" on every form's Button — it forwards every Button prop and sets the type |
|
|
101
|
+
|
|
86
102
|
## A form the server described
|
|
87
103
|
|
|
88
104
|
_A config object whose shape arrives at runtime as a JSON Schema plus a JSON Forms UI Schema — the renderer walks both and dispatches to this package's widgets. Anything it cannot dispatch renders flagged, never blank._
|
|
@@ -109,7 +125,7 @@ _The standing chrome — the app shell and its nav, the one page-title treatment
|
|
|
109
125
|
| Component | Import | Key props | Reach for it instead of |
|
|
110
126
|
| --- | --- | --- | --- |
|
|
111
127
|
| `AppShell` | `@poodle64/ui/app-shell` | `nav, currentPath, brandTitle?, actions?, context?, children` | a bespoke nav rail + header layout per app |
|
|
112
|
-
| `PageHeader` | `@poodle64/ui/page-header` | `title?, eyebrow?, breadcrumbs?, subtitle?, info?, actions?` | a hand-written <h1> and title bar (the drift gate fails this) |
|
|
128
|
+
| `PageHeader` | `@poodle64/ui/page-header` | `title?, eyebrow?, breadcrumbs?, icon?, subtitle?, info?, meta?, actions?` | a hand-written <h1> and title bar (the drift gate fails this) |
|
|
113
129
|
| `ContextColumn` | `@poodle64/ui/context-column` | `stats, statsTitle?, statsInfo?, detail?, ariaLabel?` | a hand-built right-hand <aside> with a stat card and a detail pane |
|
|
114
130
|
|
|
115
131
|
## A dialog or overlay
|