@poodle64/ui 2026.8.17 → 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 +67 -3
- 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/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/styles.css +131 -10
- package/package.json +1 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -940,15 +940,25 @@ unnoticed through a full app migration (#3).
|
|
|
940
940
|
Second, what the variable layer does not cover: the `text-display` / `text-body` /
|
|
941
941
|
`text-stat` / `tracking-eyebrow` scale keys, the `.ds-edge` card treatment, the
|
|
942
942
|
`.ds-dialog-section` divider rule, the `.ds-chip` / `.ds-dot` / `.ds-ink` status
|
|
943
|
-
classes,
|
|
944
|
-
|
|
945
|
-
|
|
943
|
+
classes, the `--ds-control-*` geometry the density option turns (below), and the
|
|
944
|
+
two things that make this package's overlays animate at all — a
|
|
945
|
+
`@custom-variant` per `data-state` value (bits-ui emits `data-state="open"`, so
|
|
946
|
+
a bare `data-open:` utility matches nothing without one) and an import of
|
|
946
947
|
`tw-animate-css`, which defines `animate-in`, `fade-in-0`, `zoom-in-95` and
|
|
947
948
|
`slide-in-from-*`. Both used to be the app's job, and an app that never did the
|
|
948
949
|
job got dialogues, dropdowns, popovers and selects that opened with no
|
|
949
950
|
transition, silently. A consuming app now declares neither; one that already
|
|
950
951
|
declares them loses nothing, since the definitions are identical.
|
|
951
952
|
|
|
953
|
+
The `data-state` set covers `open`/`closed`, `checked`/`unchecked`/
|
|
954
|
+
`indeterminate` and `active`/`inactive`, so `data-checked:` and `data-active:`
|
|
955
|
+
are usable shorthands in an app's own components too. Only `open`/`closed` were
|
|
956
|
+
declared until 2026.9.0, and the gap was not theoretical: in one app every
|
|
957
|
+
checked checkbox painted no primary fill and every tab strip rendered its
|
|
958
|
+
selected trigger identically to the rest, on eleven routes, for months. Nothing
|
|
959
|
+
fails when a variant is missing — the class is in the DOM, and only the rule is
|
|
960
|
+
absent.
|
|
961
|
+
|
|
952
962
|
It holds no palette. Every value resolves through a `--ds-*` token, so choosing a
|
|
953
963
|
palette stays a matter of overriding `--ds-color-*` in your own `app.css`, and
|
|
954
964
|
`.dark` flows through untouched. Any single name can still be overridden by
|
|
@@ -964,6 +974,60 @@ differ on. An app using them registers them itself.
|
|
|
964
974
|
source scan misses them. Without it the components render unstyled (no build
|
|
965
975
|
error, no lint hit; the classes just never reach the compiled CSS).
|
|
966
976
|
|
|
977
|
+
### Control density
|
|
978
|
+
|
|
979
|
+
An app that runs its controls denser than 40px used to have exactly one move
|
|
980
|
+
available: fork `Button`. Its heights were hard-coded Tailwind classes, so no
|
|
981
|
+
token could reach them — and forking Button takes `dialog`, `alert-dialog`,
|
|
982
|
+
`command`, `input-group` and `form` with it, because each of those imports it.
|
|
983
|
+
One app carried all six for that reason, five of them otherwise identical to
|
|
984
|
+
this package's, and pinned a height on 75 call sites across 20 files.
|
|
985
|
+
|
|
986
|
+
The knob is one attribute, on any ancestor — ordinarily `<html>`, in
|
|
987
|
+
`app.html`:
|
|
988
|
+
|
|
989
|
+
```html
|
|
990
|
+
<html lang="en-AU" data-ds-density="compact"></html>
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Two named values, `comfortable` (the default) and `compact`:
|
|
994
|
+
|
|
995
|
+
| Size | comfortable | compact |
|
|
996
|
+
| --------- | ----------- | ------- |
|
|
997
|
+
| `xs` | 28px | 24px |
|
|
998
|
+
| `sm` | 36px | 28px |
|
|
999
|
+
| `default` | 40px | 32px |
|
|
1000
|
+
| `lg` | 44px | 36px |
|
|
1001
|
+
|
|
1002
|
+
Inline padding and the trim beside an icon move with the height; the `icon-*`
|
|
1003
|
+
sizes stay squares of the same heights.
|
|
1004
|
+
|
|
1005
|
+
`compact` is not a picked scale. This package's `Input` already renders at 32px
|
|
1006
|
+
and its `Select` trigger at 32/28px, so a default-size `Button` beside an
|
|
1007
|
+
`Input` has always been 8px taller than it. At `compact` they are the same
|
|
1008
|
+
height, which is the alignment the forking app was hand-pinning.
|
|
1009
|
+
|
|
1010
|
+
The attribute is honoured wherever it appears, so a dense toolbar can carry it
|
|
1011
|
+
without the page doing so — and `data-ds-density="comfortable"` on a subtree
|
|
1012
|
+
returns that subtree to the default inside a compact page.
|
|
1013
|
+
|
|
1014
|
+
Nothing moves for an app that names no density: the `--ds-control-*` defaults
|
|
1015
|
+
reproduce the previous hard-coded classes exactly, measured in a real browser
|
|
1016
|
+
(`harness/drive.md` §"The control density ramp") rather than asserted here.
|
|
1017
|
+
Hand-tuning an individual rung remains possible and remains a deviation — the
|
|
1018
|
+
sanctioned move is choosing a named density, as choosing a palette rather than a
|
|
1019
|
+
hex value is the sanctioned move for colour.
|
|
1020
|
+
|
|
1021
|
+
### Checkbox and Switch
|
|
1022
|
+
|
|
1023
|
+
Both carry a transparent `::after` skirt that lifts the pointer target over WCAG
|
|
1024
|
+
2.5.8's 24px minimum without moving a painted pixel — a 16px checkbox has a 29px
|
|
1025
|
+
target, a 36x20px switch track a 35x31px one. Both take `aria-invalid`, which
|
|
1026
|
+
paints the destructive ring the inputs already use, so a Formsnap-wired field
|
|
1027
|
+
marks itself. `Checkbox` binds `indeterminate` and now paints the same fill as a
|
|
1028
|
+
checked box for it, rather than a dash on a transparent ground. `Switch` takes
|
|
1029
|
+
`size="sm"`, a 28x16px track that lines up with a `size="sm"` control row.
|
|
1030
|
+
|
|
967
1031
|
## Australian value formatters
|
|
968
1032
|
|
|
969
1033
|
```ts
|
|
@@ -18,18 +18,23 @@
|
|
|
18
18
|
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
|
|
19
19
|
link: 'text-primary underline-offset-4 hover:underline'
|
|
20
20
|
},
|
|
21
|
+
// Every geometry value here is a `--ds-control-*` token whose default is
|
|
22
|
+
// the class it replaced (`h-10` is 2.5rem, `px-4` is 1rem, …), so a
|
|
23
|
+
// consumer that names no density renders unchanged. The tokens, the two
|
|
24
|
+
// named densities and why the ramp is a table rather than a ratio are in
|
|
25
|
+
// styles.css §"Control geometry".
|
|
21
26
|
size: {
|
|
22
27
|
default:
|
|
23
|
-
'h-
|
|
24
|
-
xs: "h-
|
|
25
|
-
sm: "h-
|
|
26
|
-
lg: 'h-
|
|
27
|
-
icon: 'size-
|
|
28
|
+
'h-(--ds-control-height-md) gap-1.5 px-(--ds-control-pad-md) has-data-[icon=inline-end]:pr-(--ds-control-pad-icon-md) has-data-[icon=inline-start]:pl-(--ds-control-pad-icon-md)',
|
|
29
|
+
xs: "h-(--ds-control-height-xs) gap-1 rounded-[min(var(--radius-md),10px)] px-(--ds-control-pad-xs) text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-(--ds-control-pad-icon-xs) has-data-[icon=inline-start]:pl-(--ds-control-pad-icon-xs) [&_svg:not([class*='size-'])]:size-3",
|
|
30
|
+
sm: "h-(--ds-control-height-sm) gap-1.5 rounded-[min(var(--radius-md),12px)] px-(--ds-control-pad-sm) text-[0.85rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-(--ds-control-pad-icon-sm) has-data-[icon=inline-start]:pl-(--ds-control-pad-icon-sm) [&_svg:not([class*='size-'])]:size-4",
|
|
31
|
+
lg: 'h-(--ds-control-height-lg) gap-1.5 px-(--ds-control-pad-lg) has-data-[icon=inline-end]:pr-(--ds-control-pad-icon-lg) has-data-[icon=inline-start]:pl-(--ds-control-pad-icon-lg)',
|
|
32
|
+
icon: 'size-(--ds-control-height-md)',
|
|
28
33
|
'icon-xs':
|
|
29
|
-
"size-
|
|
34
|
+
"size-(--ds-control-height-xs) rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
30
35
|
'icon-sm':
|
|
31
|
-
'size-
|
|
32
|
-
'icon-lg': 'size-
|
|
36
|
+
'size-(--ds-control-height-sm) rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
|
37
|
+
'icon-lg': 'size-(--ds-control-height-lg)'
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
defaultVariants: {
|
|
@@ -13,13 +13,35 @@
|
|
|
13
13
|
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & { class?: string } = $props();
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
|
+
<!--
|
|
17
|
+
The box is 16px, which is what a checkbox has always looked like and is also
|
|
18
|
+
well under WCAG 2.5.8's 24px minimum target. The `::after` overlay is the
|
|
19
|
+
standard answer: a transparent skirt on every side takes the POINTER target to
|
|
20
|
+
a measured 29px without moving a pixel of the control, so the tick still sits where a
|
|
21
|
+
reader expects it and the row's rhythm is unchanged. `harness/drive.mjs`
|
|
22
|
+
measures that by walking outward from the centre until the hit test stops
|
|
23
|
+
returning this element, rather than by trusting the arithmetic here.
|
|
24
|
+
|
|
25
|
+
The inset is 2 rather than 1.5 for margin, not neatness: 1.5 measured 25px,
|
|
26
|
+
which clears the 24px floor at a 16px root and misses it at a 14px one — and
|
|
27
|
+
the floor is in absolute CSS pixels while the skirt is in rem. It stops 1px
|
|
28
|
+
short of a `gap-2` label, and a click that lands on the label toggles the box
|
|
29
|
+
through the label's own `for` anyway, so the enlarged area can never steal an
|
|
30
|
+
interaction from something else.
|
|
31
|
+
|
|
32
|
+
`data-indeterminate` paints the same fill as `data-checked`. It carried none
|
|
33
|
+
until now, so a tri-state checkbox rendered its dash in the FOREGROUND ink on
|
|
34
|
+
a transparent ground — the identical defect the `data-checked` mapping was
|
|
35
|
+
fixed for, one value along, and invisible for the same reason. See styles.css
|
|
36
|
+
§"The bits-ui `data-state` variants".
|
|
37
|
+
-->
|
|
16
38
|
<CheckboxPrimitive.Root
|
|
17
39
|
bind:ref
|
|
18
40
|
bind:checked
|
|
19
41
|
bind:indeterminate
|
|
20
42
|
data-slot="checkbox"
|
|
21
43
|
class={cn(
|
|
22
|
-
'peer border-border focus-visible:ring-ring data-
|
|
44
|
+
'peer border-border focus-visible:ring-ring data-checked:bg-primary data-checked:text-primary-foreground data-checked:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground data-indeterminate:border-primary aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 relative size-4 shrink-0 rounded-sm border shadow-none transition-shadow after:absolute after:-inset-2 focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
|
|
23
45
|
className
|
|
24
46
|
)}
|
|
25
47
|
{...restProps}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
{sideOffset}
|
|
24
24
|
{align}
|
|
25
25
|
class={cn(
|
|
26
|
-
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--bits-dropdown-menu-content-available-height) min-w-44 overflow-x-hidden overflow-y-auto rounded-lg border border-
|
|
26
|
+
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--bits-dropdown-menu-content-available-height) min-w-44 overflow-x-hidden overflow-y-auto rounded-lg border border-border-strong p-1 whitespace-nowrap shadow-lg duration-100 outline-none data-closed:overflow-hidden',
|
|
27
27
|
className
|
|
28
28
|
)}
|
|
29
29
|
{...restProps}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import Root from './switch.svelte';
|
|
2
|
-
export { Root, Root as Switch };
|
|
1
|
+
import Root, { type SwitchSize } from './switch.svelte';
|
|
2
|
+
export { Root, Root as Switch, type SwitchSize };
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** The two track sizes. `sm` lines up with a `size="sm"` control row. */
|
|
3
|
+
export type SwitchSize = 'default' | 'sm';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
1
6
|
<script lang="ts">
|
|
2
7
|
import { Switch as SwitchPrimitive } from 'bits-ui';
|
|
3
8
|
import { cn, type WithoutChildrenOrChild } from '../../../utils.js';
|
|
@@ -5,9 +10,23 @@
|
|
|
5
10
|
let {
|
|
6
11
|
ref = $bindable(null),
|
|
7
12
|
checked = $bindable(false),
|
|
13
|
+
size = 'default',
|
|
8
14
|
class: className,
|
|
9
15
|
...restProps
|
|
10
|
-
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
|
16
|
+
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> & { size?: SwitchSize } = $props();
|
|
17
|
+
|
|
18
|
+
// Track, thumb and travel are one decision, so they are one table rather
|
|
19
|
+
// than three prop reads at three call sites. The travel is not a picked
|
|
20
|
+
// number: the track carries a 2px transparent border, so the thumb's run is
|
|
21
|
+
// (width - 4) - thumb, which is 16px on the default and 12px on `sm`.
|
|
22
|
+
const TRACK = {
|
|
23
|
+
default: 'h-5 w-9',
|
|
24
|
+
sm: 'h-4 w-7'
|
|
25
|
+
} as const;
|
|
26
|
+
const THUMB = {
|
|
27
|
+
default: 'size-4 data-checked:translate-x-4',
|
|
28
|
+
sm: 'size-3 data-checked:translate-x-3'
|
|
29
|
+
} as const;
|
|
11
30
|
</script>
|
|
12
31
|
|
|
13
32
|
<!--
|
|
@@ -17,23 +36,37 @@
|
|
|
17
36
|
ball stays light on the dark-first theme. The track colour comes from the
|
|
18
37
|
project tokens. An app with a non-standard root font-size (a dense console
|
|
19
38
|
running an 18px root, say) compensates in its own override layer, not here.
|
|
39
|
+
|
|
40
|
+
The track is 20px tall (16px at `sm`), under WCAG 2.5.8's 24px minimum, so a
|
|
41
|
+
transparent `::after` skirt lifts the pointer target over it. It grows the
|
|
42
|
+
BLOCK axis only: the track is already 36px wide, so there is nothing to win
|
|
43
|
+
horizontally and an inline skirt would reach into the label beside it.
|
|
44
|
+
|
|
45
|
+
The skirt is inset from the PADDING box, and this track carries a 2px
|
|
46
|
+
transparent border, so its real reach is the inset minus 2 on each side —
|
|
47
|
+
which is why the number here is not the number in the target. The target is
|
|
48
|
+
measured in `harness/drive.mjs`, not calculated.
|
|
20
49
|
-->
|
|
21
50
|
<SwitchPrimitive.Root
|
|
22
51
|
bind:ref
|
|
23
52
|
bind:checked
|
|
24
53
|
class={cn(
|
|
25
|
-
'peer inline-flex
|
|
54
|
+
'peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors',
|
|
55
|
+
'after:absolute after:inset-x-0 after:-inset-y-2',
|
|
26
56
|
'focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
|
57
|
+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 aria-invalid:ring-3',
|
|
27
58
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
28
|
-
'data-
|
|
59
|
+
'data-checked:bg-primary data-unchecked:bg-input',
|
|
60
|
+
TRACK[size],
|
|
29
61
|
className
|
|
30
62
|
)}
|
|
31
63
|
{...restProps}
|
|
32
64
|
>
|
|
33
65
|
<SwitchPrimitive.Thumb
|
|
34
66
|
class={cn(
|
|
35
|
-
'bg-background dark:bg-foreground pointer-events-none block
|
|
36
|
-
'data-
|
|
67
|
+
'bg-background dark:bg-foreground pointer-events-none block rounded-full shadow-lg ring-0 transition-transform',
|
|
68
|
+
'data-unchecked:translate-x-0',
|
|
69
|
+
THUMB[size]
|
|
37
70
|
)}
|
|
38
71
|
/>
|
|
39
72
|
</SwitchPrimitive.Root>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/** The two track sizes. `sm` lines up with a `size="sm"` control row. */
|
|
2
|
+
export type SwitchSize = 'default' | 'sm';
|
|
1
3
|
import { Switch as SwitchPrimitive } from 'bits-ui';
|
|
2
|
-
|
|
4
|
+
import { type WithoutChildrenOrChild } from '../../../utils.js';
|
|
5
|
+
type $$ComponentProps = WithoutChildrenOrChild<SwitchPrimitive.RootProps> & {
|
|
6
|
+
size?: SwitchSize;
|
|
7
|
+
};
|
|
8
|
+
declare const Switch: import("svelte").Component<$$ComponentProps, {}, "ref" | "checked">;
|
|
3
9
|
type Switch = ReturnType<typeof Switch>;
|
|
4
10
|
export default Switch;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
bind:ref
|
|
10
10
|
data-slot="tabs-trigger"
|
|
11
11
|
class={cn(
|
|
12
|
-
"data-
|
|
12
|
+
"data-active:bg-background dark:data-active:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 dark:data-active:border-input dark:data-active:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-active:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
13
13
|
className
|
|
14
14
|
)}
|
|
15
15
|
{...restProps}
|
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.
|
|
@@ -312,6 +352,87 @@
|
|
|
312
352
|
--ds-shell-measure-wide: 120rem;
|
|
313
353
|
}
|
|
314
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
|
+
|
|
315
436
|
@layer base {
|
|
316
437
|
/**
|
|
317
438
|
* The milled edge. Cards read as having depth from a hairline border plus a
|
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",
|
|
@@ -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
|
|