@codenhub/styles 0.5.0 → 0.6.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.
Files changed (59) hide show
  1. package/README.md +7 -7
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -1
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +39 -39
  15. package/dist/tw/aesthetics/cyber.css +16 -11
  16. package/dist/tw/aesthetics/glass.css +235 -10
  17. package/dist/tw/aesthetics/index.css +2 -1
  18. package/dist/tw/aesthetics/neobrutalism.css +23 -11
  19. package/dist/tw/aesthetics/pixel.css +18 -8
  20. package/dist/tw/aesthetics/sketch.css +14 -7
  21. package/dist/tw/box.css +49 -27
  22. package/dist/tw/components/button.css +35 -14
  23. package/dist/tw/components/feedback.css +59 -56
  24. package/dist/tw/components/form.css +200 -126
  25. package/dist/tw/components/index.css +3 -7
  26. package/dist/tw/components/loader.css +4 -5
  27. package/dist/tw/components/surface.css +71 -68
  28. package/dist/tw/components/tooltip.css +75 -41
  29. package/dist/tw/elevation.css +14 -22
  30. package/dist/tw/index.css +15 -19
  31. package/dist/tw/intent.css +31 -24
  32. package/dist/tw/native.css +15 -5
  33. package/dist/tw/palette.css +8 -5
  34. package/dist/tw/presentation.css +16 -30
  35. package/dist/tw/reset.css +21 -28
  36. package/dist/tw/surface.css +11 -12
  37. package/dist/tw/theme.css +58 -86
  38. package/dist/tw/utilities/content.css +268 -199
  39. package/dist/tw/utilities/layout.css +15 -25
  40. package/docs/accessibility.md +50 -33
  41. package/docs/changelog/0.6.0.md +52 -0
  42. package/docs/changelog/index.md +1 -0
  43. package/docs/concepts.md +6 -6
  44. package/docs/index.md +9 -0
  45. package/docs/integrating/nextjs.md +1 -1
  46. package/docs/integrating/tailwind.md +14 -6
  47. package/docs/setup.md +6 -6
  48. package/docs/usage/aesthetics.md +35 -8
  49. package/docs/usage/buttons.md +1 -1
  50. package/docs/usage/composing.md +1 -1
  51. package/docs/usage/content-and-layout.md +12 -11
  52. package/docs/usage/customizing.md +8 -9
  53. package/docs/usage/feedback.md +5 -3
  54. package/docs/usage/forms.md +51 -35
  55. package/docs/usage/theming.md +39 -40
  56. package/docs/usage/tooltips.md +37 -17
  57. package/llms-full.txt +331 -183
  58. package/llms.txt +6 -0
  59. package/package.json +5 -5
@@ -34,43 +34,32 @@
34
34
  --surface-padding: 0;
35
35
  }
36
36
 
37
- /* `.soft` rests untinted for no named intent, rather than mixing its 12% --
38
- measured on a neutral card, 12% of `--color-text` onto any of
39
- `--color-background`, `--color-foreground`, or `--color-surface` lands
40
- within a few units of the others (a near-black ink dominates the result
41
- regardless of which near-white ground it is mixed onto), which is why
42
- swapping the ground token alone does not read as quieter. A resting
43
- no-intent `.card.soft` is instead `--color-foreground` with no ink mixed
44
- in at all -- a plain step up from the page that marks a different
45
- container without drawing attention to it, the way a card that is not
46
- currently interesting should.
47
-
48
- A named intent keeps its ordinary 12% wash rather than being untinted
49
- unconditionally: 12% of an actual hue does not read as a slab the way
50
- 12% of near-black ink does -- measured, `.card.soft.destructive` at 12%
51
- is a gentle `249, 225, 225`, nothing like the `217, 217, 217` a neutral
52
- surface mixing the same 12% of its own ink used to read as -- and
53
- untinting it anyway would silence a `.card.soft.destructive` at rest
54
- rather than merely quiet it, the same reason `.panel`'s own carve-out
55
- (below) is scoped the same way. `:not(...)` names every intent class for
56
- that reason; a version of this fix that untinted every intent shipped
57
- first and was corrected once `.panel` needed the identical, narrower
58
- carve-out to keep a resting `.panel.destructive` legible.
59
-
60
- `--_fill-cap: 0%` (box.css's override slot, not `--ui-fill` directly --
61
- see the comment there) composed with `box-hover`'s own formula
62
- (`min(100%, resting + 14%)`) is what reveals the intent on hover without
63
- a second fill declaration, for the no-intent case: resting at 0% means
64
- hover lands at exactly the hover step, 14%, in `--intent-hover` as
65
- always. The ground steps to `--color-surface` at the same moment,
66
- through `--_d-surface-ground`, the default `surface` reads beneath the
67
- public `--ui-surface-ground` -- so a consumer's own override, or glass's
68
- ground, still wins over both states. Scoped to `.interactive` and `.hoverable`,
69
- the two classes that already opt a card into `box-hover` -- a plain
70
- `.card.soft` with neither has no hover state to reveal into and stays at
71
- the quiet, untinted rest. A named intent needs neither special case: it
72
- already rests tinted, so the same generic `box-hover` formula deepens it
73
- on hover (12% to 26%) with no ground shift required. */
37
+ /* `.soft` rests untinted for no named intent instead of mixing its 12%: 12%
38
+ of `--color-text` onto any of `--color-background`, `--color-foreground`,
39
+ or `--color-surface` lands within a few units of the others (a near-black
40
+ ink dominates the result), so swapping the ground token alone does not read
41
+ as quieter. A resting no-intent `.card.soft` is `--color-foreground` with
42
+ no ink mixed in -- a plain step up from the page that marks a different
43
+ container without drawing attention to it.
44
+
45
+ A named intent keeps its ordinary 12% wash: 12% of an actual hue does not
46
+ read as a slab the way 12% of near-black ink does (`.card.soft.destructive`
47
+ is a gentle `249, 225, 225`), and untinting it would silence it rather than
48
+ quiet it. `:not(...)` names every intent class for that reason; `.panel`'s
49
+ carve-out below is scoped the same way.
50
+
51
+ `--_fill-cap: 0%` (box.css's override slot, not `--ui-fill` directly -- see
52
+ the comment there) composed with `box-hover`'s formula (`min(100%, resting
53
+ + 14%)`) reveals the intent on hover with no second fill declaration for
54
+ the no-intent case: resting at 0% means hover lands at exactly the hover
55
+ step, 14%, in `--intent-hover`. The ground steps to `--color-surface` at
56
+ the same moment, through `--_d-surface-ground`, the default `surface` reads
57
+ beneath the public `--ui-surface-ground`, so a consumer's override, or
58
+ glass's ground, wins over both states. Scoped to `.interactive` and
59
+ `.hoverable`, the two classes that opt a card into `box-hover`: a plain
60
+ `.card.soft` has no hover state to reveal into and stays at the quiet rest.
61
+ A named intent needs neither special case: it rests tinted, so the generic
62
+ `box-hover` formula deepens it on hover (12% to 26%). */
74
63
  &.soft:not(.primary, .secondary, .success, .warning, .destructive, .info) {
75
64
  --_fill-cap: 0%;
76
65
  --_d-surface-ground: var(--color-foreground);
@@ -134,39 +123,53 @@
134
123
  --surface-padding: 0;
135
124
  }
136
125
 
137
- /* Neutral panels are the common case -- most are plain layout wells,
138
- sidebars, and toolbars with no named intent -- and 12% of `--color-text`
139
- (near-black) mixed onto the page is a far more assertive move than 12%
140
- of an actual hue: measured, `217, 217, 217` against a `250, 250, 250`
141
- page, the same "neutral ink reads louder" effect `.card.soft`'s fix
142
- already answered, and the same reason `--intent-fill-max` stops
143
- neutral's fill short everywhere else in the model.
144
-
145
- Scoped to no named intent rather than untinted unconditionally the way
146
- `data-table` is: a panel is not interactive, so there is no hover to
147
- reveal a colored intent back into later, and it is edgeless by default,
148
- so the fill is the only channel a `.primary` bulk-actions bar or a
149
- `.destructive` danger-zone panel has to say what it is. Untinting those
150
- unconditionally would silence them, not just quiet them -- measured, a
151
- resting `.panel.destructive` is a gentle `249, 225, 225` warm wash,
152
- nothing like the neutral slab above. `.card.soft` carries the identical
153
- no-named-intent scoping now, corrected to match once this carve-out
154
- shipped -- see the comment on `.card`'s own `&.soft`, above. `:not(...)`
155
- names every intent class for that reason, plus `.solid`/`.ghost` so an
156
- explicit presentation on the element is never clamped to a cap it did
157
- not ask for -- the same guard `data-table`'s own `:not(.solid, .ghost)`
158
- uses.
159
-
160
- Grounds on `--color-foreground` through `--_d-surface-ground`, the same
161
- default `.card.soft` rests on -- so a bare `.panel` rests at exactly the
162
- plate a `.card.soft` does (measured: `245, 245, 245` light, `23, 23, 23`
163
- dark, identical both ways), and a consumer's `--ui-surface-ground` or
164
- glass's translucent ground still wins over it: a neutral panel under
165
- `.glass` is glass, like every other surface there. Permanent rather than
166
- hover-revealed: neither a panel nor `data-table` has an interaction
167
- state to reveal a tint into, so both rest quiet rather than reveal. */
126
+ /* Neutral panels are the common case -- layout wells, sidebars, toolbars with
127
+ no named intent -- and 12% of `--color-text` (near-black) mixed onto the
128
+ page is far more assertive than 12% of a hue (`217, 217, 217` against a
129
+ `250, 250, 250` page): the same "neutral ink reads louder" effect
130
+ `.card.soft` answers, and the reason `--intent-fill-max` stops neutral's
131
+ fill short everywhere else.
132
+
133
+ Scoped to no named intent rather than untinted unconditionally, as
134
+ `data-table` is: a panel is not interactive, so there is no hover to reveal
135
+ a colored intent into, and it is edgeless by default, so the fill is the
136
+ only channel a `.primary` bulk-actions bar or a `.destructive` danger-zone
137
+ panel has to say what it is (a resting `.panel.destructive` is a gentle
138
+ `249, 225, 225` wash). `.card.soft` carries the same scoping. `:not(...)`
139
+ names every intent class, plus `.solid`/`.ghost` so an explicit
140
+ presentation on the element is never clamped to a cap it did not ask for --
141
+ the guard `data-table`'s `:not(.solid, .ghost)` also uses.
142
+
143
+ Grounds on `--color-foreground` through `--_d-surface-ground`, the default
144
+ `.card.soft` rests on, so a bare `.panel` rests at exactly the plate a
145
+ `.card.soft` does, and a consumer's `--ui-surface-ground` or glass's
146
+ translucent ground wins over it: a neutral panel under `.glass` is glass.
147
+ Permanent rather than hover-revealed: neither a panel nor `data-table` has
148
+ an interaction state to reveal a tint into. */
168
149
  &:not(.primary, .secondary, .success, .warning, .destructive, .info, .solid, .ghost) {
169
150
  --_fill-cap: 0%;
170
151
  --_d-surface-ground: var(--color-foreground);
171
152
  }
172
153
  }
154
+
155
+ /* The user agent already promises both of these for a native `<dialog>` --
156
+ `dialog:not([open]) { display: none }` and a centred `margin: auto` on the
157
+ open box -- but both are user-agent origin, which loses to any author rule
158
+ at all. `.card`'s own `display: block` beats the first outright, so a
159
+ `<dialog class="card">` rendered open with no `open` attribute and no
160
+ `.showModal()` call, and a margin reset beats the second, so an opened one
161
+ pinned to the top-left. The registry has no dialog component, and these
162
+ three are the containers a consumer reaches for.
163
+
164
+ Here rather than in the reset, so every entrypoint that ships `.card` ships
165
+ the fix, `/components` included. Unlayered, because a behaviour override has
166
+ to win over any layered rule whatever its specificity, and scoped to the
167
+ package's own containers, so a dialog the package does not style keeps
168
+ whatever margin its author gives it. */
169
+ dialog:is(.card, .panel, .surface):not([open]) {
170
+ display: none;
171
+ }
172
+
173
+ dialog:is(.card, .panel, .surface)[open] {
174
+ margin: auto;
175
+ }
@@ -4,21 +4,24 @@
4
4
  @reference "../surface.css";
5
5
 
6
6
  /* The host is a positioning wrapper and nothing else -- it paints no fill, no
7
- edge, no material of its own. The bubble is a real element beside the
8
- trigger rather than a pseudo-element attached to it, so intent,
9
- presentation, aesthetic, and elevation classes go on `.tooltip-bubble`, the
10
- thing actually shown, not on the wrapper -- the same rule every other
11
- component in this package follows (theming.md: "Intent does not cascade").
12
-
13
- Composing the bubble as a real element fixes a real gap, not just a styling
14
- one: `content: attr(data-tooltip)` text is not reliably exposed to
15
- assistive tech and cannot be the target of an `aria-describedby`, since that
16
- needs a real element with a real `id`. A real bubble can carry
17
- `role="tooltip"` and be referenced by the trigger's own `aria-describedby`;
18
- wiring that association is the consumer's, this package only styles the
19
- bubble. See docs/usage/tooltips.md. */
7
+ edge, no material of its own. The bubble is a real element beside the trigger
8
+ rather than a pseudo-element, so intent, presentation, aesthetic, and
9
+ elevation classes go on `.tooltip-bubble`, the thing actually shown, not on
10
+ the wrapper -- the same rule every other component follows (theming.md:
11
+ "Intent does not cascade").
12
+
13
+ A real element is also what makes the tooltip accessible: `content:
14
+ attr(data-tooltip)` text is not reliably exposed to assistive tech and cannot
15
+ be the target of an `aria-describedby`, which needs a real element with a
16
+ real `id`. A real bubble can carry `role="tooltip"` and be referenced by the
17
+ trigger's own `aria-describedby`; wiring that association is the consumer's,
18
+ this package only styles the bubble. See docs/usage/tooltips.md. */
20
19
  @utility tooltip {
21
20
  --tooltip-gap: 0.5rem;
21
+ /* How long the bubble stays after the pointer leaves the trigger: long
22
+ enough to cross `--tooltip-gap` onto the bubble, which keeps it open
23
+ (WCAG 1.4.13). */
24
+ --tooltip-linger: 300ms;
22
25
  --tooltip-top: calc(var(--tooltip-gap) * -1);
23
26
  --tooltip-right: auto;
24
27
  --tooltip-bottom: auto;
@@ -52,24 +55,31 @@
52
55
  }
53
56
 
54
57
  /* The bubble. It is a surface, so an aesthetic that blurs surfaces blurs it
55
- too, and it rests on a ground of its own, edged, and flat -- depth is
56
- opt-in through its own `.raised`/`.floating`, like any other component, now
57
- that it is one. No proxy inheritance is needed to carry elevation across a
58
- host-to-pseudo-element gap: there is no gap, `.tooltip-bubble.raised` reads
59
- `--ui-elevation` the ordinary way every other component does. See
58
+ too, and it rests on a ground of its own, edged, and flat -- depth is opt-in
59
+ through its own `.raised`/`.floating`, like any other component. See
60
60
  model.md#elevation. */
61
61
  @utility tooltip-bubble {
62
- @apply surface pointer-events-none absolute z-(--z-popover) w-max max-w-72 px-2.5 py-2 text-sm font-normal whitespace-normal opacity-0 transition-[opacity,transform] duration-(--motion-duration-normal) ease-(--motion-ease);
62
+ @apply surface absolute z-(--z-popover) w-max max-w-72 px-2.5 py-2 text-sm font-normal whitespace-normal opacity-0;
63
+
64
+ /* Hidden rather than merely transparent, so a closed bubble is nothing
65
+ under the pointer, and it lingers before it goes: the host stays hovered
66
+ once the pointer is over the bubble, so crossing the gap in
67
+ `--tooltip-linger` keeps it open. Visibility switches at the end of the
68
+ fade, so the bubble is there to land on for the whole of it. */
69
+ visibility: hidden;
70
+ transition-property: opacity, transform, visibility;
71
+ transition-duration: var(--motion-duration-normal);
72
+ transition-timing-function: var(--motion-ease);
73
+ transition-delay: var(--tooltip-linger, 0s);
63
74
 
64
75
  /* Filled, over a ground of its own. An intent floods the fill
65
76
  (`.tooltip-bubble.primary` is the primary colour); with no intent
66
77
  `intent.css` resolves the bubble onto `--color-tooltip` and lifts the
67
- neutral cap, a plate stated per theme because the derived version was a
68
- mid-grey slab in light and lifted in dark. The ground stays the surface
69
- tone, not the plate -- pointed at both, the fill axis went inert and
70
- `axes.spec.ts` caught it. An opaque ground is also what keeps a cascaded
71
- `.ghost` from leaving the bubble boundaryless. `--ui-surface-ground` is
72
- read first so a glass tooltip stays glass. */
78
+ neutral cap. The ground stays the surface tone, not the plate: pointed at
79
+ both, the fill axis goes inert (`axes.spec.ts` catches it). An opaque
80
+ ground is also what keeps a cascaded `.ghost` from leaving the bubble
81
+ boundaryless. `--ui-surface-ground` is read first so a glass tooltip stays
82
+ glass. */
73
83
  --_d-fill: 100%;
74
84
  --_d-fg-on-fill: 100%;
75
85
  --_d-border: 100%;
@@ -105,40 +115,58 @@
105
115
  &.edgeless {
106
116
  --_edge-floor: 0%;
107
117
  }
118
+
119
+ /* The bubble appears without growing under reduced motion, for the entries
120
+ without the reset; see `box.css`. */
121
+ @media (prefers-reduced-motion: reduce) {
122
+ transition-duration: 0.01ms;
123
+ }
108
124
  }
109
125
 
110
126
  /* Shown on hover or focus anywhere in the host, or pinned open with
111
127
  `data-state="open"`. `:focus-within` rather than the trigger's own
112
- `:focus-visible`, the same substitution `.input-group` makes for the
113
- identical reason: the bubble is a sibling, not a pseudo-element, of the
114
- trigger, so nothing but the shared ancestor can see both at once. The cost
115
- is the one `.input-group` already accepts -- the bubble also shows on a
116
- mouse click, which the old `:focus-visible`-only version did not.
117
-
118
- In `utilities`, where the bubble's own `@utility` is, winning on
119
- specificity -- see the seams note in `form.css`. */
128
+ `:focus-visible`, the same substitution `.input-group` makes: the bubble is a
129
+ sibling of the trigger, so only the shared ancestor can see both at once. The
130
+ cost is the one `.input-group` accepts -- the bubble also shows on a mouse
131
+ click.
132
+
133
+ In `utilities`, where the bubble's own `@utility` is, winning on specificity
134
+ -- see the seams note in `form.css`. */
120
135
  @layer utilities {
121
136
  .tooltip:hover .tooltip-bubble,
122
137
  .tooltip:focus-within .tooltip-bubble,
123
138
  .tooltip[data-state="open"] .tooltip-bubble {
124
139
  opacity: 1;
125
140
  transform: var(--tooltip-transform) scale(1);
141
+ visibility: visible;
142
+ transition-delay: 0s;
143
+ }
144
+
145
+ /* Dismissed: a hover message has to close without the pointer moving
146
+ (WCAG 1.4.13), and CSS cannot hear Escape, so the consumer's handler
147
+ writes `data-state="closed"` and clears it when the pointer and focus
148
+ leave. Later and as specific as the rule above, so it wins over hover and
149
+ focus alike. */
150
+ .tooltip[data-state="closed"] .tooltip-bubble {
151
+ opacity: 0;
152
+ transform: var(--tooltip-transform) scale(0);
153
+ visibility: hidden;
154
+ transition-delay: 0s;
126
155
  }
127
156
  }
128
157
 
129
158
  /* The trigger badge, for a message with no other element to hang it on. Quiet
130
159
  -- ghost, edged, on `--intent-subtle`, a key cap's resting shape -- because
131
160
  it marks that there is a message, it is not one. The line earns its keep:
132
- `--intent-subtle` is a near-page tint that reads the same across families,
133
- so a plate alone left three of eight intents indistinguishable;
161
+ `--intent-subtle` is a near-page tint that reads the same across families, so
162
+ a plate alone leaves three of eight intents indistinguishable;
134
163
  `--intent-border` is the slot that differs.
135
164
 
136
- `clip-path: none` used to exist to protect the bubble, a pseudo-element of
137
- this same host, from being clipped away along with it under an aesthetic
138
- like `.pixel`. The bubble is independent now, so that reason is gone, but
139
- the declaration stays: the docs call this a circular fixed identity, not a
140
- shape a container's aesthetic reaches, the same way `.radio` keeps its own
141
- circle instead of taking `.pixel`'s stepped silhouette. */
165
+ Its corner is the aesthetic's, through the pill corner and then the control
166
+ corner: round by default, a diamond under `.cyber`, square under
167
+ `.neobrutalism` and `.pixel`, whose corner is zero. Only the silhouette
168
+ clip is refused -- `clip-path: none`, as `.radio` has it -- because
169
+ `.pixel`'s stepped polygon would bite a 20px chip rather than shape it. */
142
170
  @utility tooltip-icon {
143
171
  @apply box inline-flex size-5 cursor-default items-center justify-center text-sm font-bold;
144
172
 
@@ -148,6 +176,12 @@
148
176
  --_d-elevation: 0;
149
177
  --_d-ground: var(--intent-subtle);
150
178
 
179
+ /* The trigger is best a real `<button>`, which is a `.btn` under `/native`
180
+ and carries the user agent's padding under `/components`; either would
181
+ stretch the chip past its own size. */
182
+ min-width: 0;
183
+ min-height: 0;
184
+ padding: 0;
151
185
  border-radius: var(--ui-radius-pill, var(--ui-radius, var(--ui-corner, calc(infinity * 1px))));
152
186
  clip-path: none;
153
187
  }
@@ -3,31 +3,23 @@
3
3
  docs/internal/model.md#cascade-layers. */
4
4
 
5
5
  /* Elevation is a modifier, not an axis: depth is a per-element decision (raised
6
- tiles beside a flat panel in one aesthetic), so one unitless number multiplies
7
- the aesthetic's shadow geometry where the component composes it. `.flat`
8
- removes a part-based slab or blur without a conditional (complete surface
9
- shadows like glass remain). See model.md#elevation.
6
+ tiles beside a flat panel in one aesthetic), so one unitless number
7
+ multiplies the aesthetic's shadow geometry where the component composes it.
8
+ `.flat` removes a part-based slab or blur without a conditional (complete
9
+ surface shadows like glass remain). See model.md#elevation.
10
10
 
11
11
  `--ui-elevation`, and the plain-page fallback geometry (`--elevation-y`,
12
12
  `--elevation-blur`) below, are registered non-inheriting: a class sets them
13
- on the element it is written on and nothing else picks them up. They used to
14
- be ordinary inherited custom properties, on the reasoning that a container
15
- could flatten or lift a whole region in one class and any element inside
16
- could opt back out -- but the same mechanism let a `.floating` dialog's
17
- ambient ×2 reach its own unclassed "Cancel"/"Remove" buttons, doubling their
18
- shadow with nothing in the markup asking for that. Depth is not the same
19
- kind of thing as color or material: a button inside a raised card is not
20
- itself more raised, it is just resting normally inside an already-raised
21
- surface. `--ui-shadow-*` -- what an aesthetic supplies -- is unaffected and
22
- still cascades normally; that is a different mechanism (the material tokens
23
- in `src/aesthetics/*.css`), and an aesthetic still has to reach every nested
24
- component uniformly the way it always did. Registering these three is what
25
- scopes the change to the elevation modifier alone.
13
+ on the element it is written on and nothing else picks them up. Inherited, a
14
+ `.floating` dialog's ambient x2 would reach its own unclassed buttons and
15
+ double their shadow with nothing in the markup asking for it. Depth is not
16
+ the same kind of thing as color or material: a button inside a raised card is
17
+ not itself more raised. `--ui-shadow-*` -- what an aesthetic supplies -- is a
18
+ different mechanism and still cascades normally, so an aesthetic reaches
19
+ every nested component uniformly.
26
20
 
27
21
  `.flat`/`.raised`/`.floating` name what they do rather than a scale step:
28
- bare `sm`/`md`/`lg` would collide with the size modifier, and the
29
- `.elevation-*` spellings, which borrowed the raw `--elevation-*` shadows'
30
- names for a different mechanism, were removed in `0.5.0`. */
22
+ bare `sm`/`md`/`lg` would collide with the size modifier. */
31
23
 
32
24
  /* `syntax: "*"` with no `initial-value` is deliberate: a numeric syntax would
33
25
  require one (CSS Properties and Values API), and any initial-value at all
@@ -36,8 +28,8 @@
36
28
  1))` in `box.css` needs to fall through to a component's own default. With
37
29
  no initial-value and `inherits: false`, an element that does not set the
38
30
  property directly computes it as guaranteed-invalid, the same as if it had
39
- never been registered at all -- `var()` takes its fallback precisely where
40
- inheritance used to hand it an ambient value instead. */
31
+ never been registered at all -- `var()` takes its fallback where an
32
+ inherited value would otherwise be handed in. */
41
33
  @property --ui-elevation {
42
34
  syntax: "*";
43
35
  inherits: false;
package/dist/tw/index.css CHANGED
@@ -1,27 +1,23 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  /* The build has no markup to scan, so Tailwind's automatic content detection
4
- sees only this package's own files. The `src/` scan is load-bearing -- it is
4
+ sees only this package's own files. The `src/` scan is load-bearing: it is
5
5
  how the composition utilities (`box`, `surface`, `text-control`,
6
- `input-group`, `loader-mask`) get emitted as standalone rules, since nothing
7
- `@source inline`s them -- so `source(none)` is not an option. A handful of
8
- Tailwind utility names also collide with a CSS property or a word this
9
- file's own comments use (`backdrop-filter`, `filter`, `transition`,
10
- `transform`, `container`), which reads like a risk of the same scan --
11
- verified against Tailwind 4.3.2, none of them emit.
12
- `tests/integration/exports.test.ts` guards it, so a scanner change that
13
- starts emitting one is caught rather than shipping silently.
6
+ `input-group`, `loader-mask`) are emitted as standalone rules, so
7
+ `source(none)` is not an option. Tailwind utility names that collide with a
8
+ CSS property or a word these comments use (`backdrop-filter`, `filter`,
9
+ `transition`, `transform`, `container`) do not emit, verified against
10
+ Tailwind 4.3.2; `tests/integration/exports.test.ts` guards it.
14
11
 
15
- The `@source not` lines below keep everything that is not `src/` out of that
16
- scan: the sibling app and fixture directories, the `docs/` tree, the build
17
- scripts, and the root prose and data files (`README.md`, `llms*.txt`,
18
- `registry*.json`, `package.json`). Without them a class name or `--color-*`
19
- reference that appears only in a documentation example ships as real CSS --
20
- the violet ramp in `docs/usage/theming.md`'s custom-intent example was
21
- compiling into `dist/index.css` and `dist/native.css`, and any utility in a
22
- docs code fence did the same. `tests/integration/exports.test.ts` guards it.
23
- `scripts/build-tw.mjs` strips these lines from the `dist/tw/` copy, where they
24
- resolve to nothing and the `/tw` consumer scans their own tree anyway. */
12
+ The `@source not` lines keep everything that is not `src/` out of the scan:
13
+ the sibling app and fixture directories, the `docs/` tree, the build scripts,
14
+ and the root prose, data, config, and license files. Without them a class
15
+ name or `--color-*` reference that appears only in a documentation example
16
+ ships as real CSS -- the license's "contents" once shipped as
17
+ `.contents{display:contents}`.
18
+ `exports.test.ts` guards it. `scripts/build-tw.mjs` strips these lines from
19
+ the `dist/tw/` copy, where they resolve to nothing and the `/tw` consumer
20
+ scans their own tree. */
25
21
 
26
22
  @import "./theme.css";
27
23
  @import "./typography.css";
@@ -33,12 +33,11 @@
33
33
  }
34
34
 
35
35
  /* Neutral stops its fill short: its colour is the page's ink, so a full fill is
36
- a slab and `.neutral.solid` was pixel-for-pixel `.primary.solid`. 20% is
36
+ a slab and `.neutral.solid` would be pixel-for-pixel `.primary.solid`. 20% is
37
37
  chosen against `.soft`'s 12% -- a step past the tint, not a repeat.
38
38
  `--intent-contrast` stays the page colour; a `.neutral.solid` badge's ink is
39
39
  `box`'s question, answered by the same `--ui-fg-on-fill` cap. Pointing this
40
- slot at `--color-text` instead rendered a checked neutral checkbox
41
- black-on-black. */
40
+ slot at `--color-text` renders a checked neutral checkbox black-on-black. */
42
41
  .neutral {
43
42
  --intent-color: var(--color-text);
44
43
  --intent-contrast: var(--color-text-contrast);
@@ -171,34 +170,42 @@
171
170
  `--ui-ink`: the theme keeps a control border apart from a surface border, and
172
171
  an aesthetic whose neutral line suits a pane but not a control's boundary --
173
172
  `.glass`'s white hairline, `.chunky-tile`'s tile grey -- names a control line
174
- of its own. See docs/internal/model.md#presentation. */
175
- :where(.text-control, .ipt, .textarea, .select, .input-group, .checkbox, .radio, .switch) {
173
+ of its own. See docs/internal/model.md#presentation.
174
+
175
+ The second selector is the tooltip bubble's below, for the same reason:
176
+ `.neutral` carries 0-1-0 and beat `:where()`, so a `.neutral` field under
177
+ `.glass` drew the pane's hairline while an unclassed one drew the control
178
+ line, and writing the intent out changed the box. */
179
+ :where(.text-control, .ipt, .textarea, .select, .input-group, .checkbox, .radio, .switch),
180
+ :is(.text-control, .ipt, .textarea, .select, .input-group, .checkbox, .radio, .switch).neutral {
176
181
  --intent-border: var(--ui-control-ink, var(--ui-ink, var(--color-control-border)));
177
182
  }
178
183
 
184
+ /* A table's lines -- its frame, head rule, and row rules -- are drawn on a
185
+ plate, not at a pane's edge against the backdrop, so an aesthetic whose
186
+ neutral line is a highlight names a rule line of its own: glass's white
187
+ hairline vanishes on the table's light plate. Neutral only, and `.neutral`
188
+ included, the way the control line above is. */
189
+ :where(.data-table),
190
+ .data-table.neutral {
191
+ --intent-border: var(--ui-rule-ink, var(--ui-ink, var(--color-border)));
192
+ }
193
+
179
194
  /* The bubble a tooltip draws is the one plate in the package that is chosen
180
195
  rather than composed, so it resolves the no-intent case onto its own pair
181
- instead of onto the shared neutral. Composed, it was 20% of the page's
182
- ink over `--color-surface`, which lifts off a dark page and sinks into a light
183
- one -- see `--color-tooltip` in `theme.css` for the measurements.
196
+ instead of the shared neutral -- see `--color-tooltip` in `theme.css`.
184
197
 
185
198
  The cap goes with it. Neutral stops its fill at 20% because its colour is the
186
- page's ink and a full fill of that is a slab; `--color-tooltip` is a plate
187
- rather than an ink, so a full fill of it is the plate and there is nothing to
188
- stop short of.
189
-
190
- `--intent-subtle` stays the surface token, the same tone every other
191
- unclassed component sits on. `.tooltip-icon`, the trigger badge, is a
192
- separate element now and does not read this block at all -- it never fills
193
- past `0%`, so the generic neutral reset above it is enough, and it keeps its
194
- own resting ground and line independent of whatever intent the bubble shows.
195
-
196
- Two selectors, because `none` and `neutral` have to agree here. `:where()`
197
- loses to any intent class, so `.tooltip-bubble.primary` still floods with
198
- primary; `.tooltip-bubble.neutral` carries 0-2-0 and beats `.neutral`, so
199
- writing the intent out gives the same bubble as writing nothing. Without the
200
- second selector the two would differ on a component where they mean the
201
- same thing. */
199
+ page's ink; `--color-tooltip` is a plate rather than an ink, so a full fill
200
+ of it is the plate and there is nothing to stop short of. `--intent-subtle`
201
+ stays the surface token, the tone every other unclassed component sits on.
202
+ `.tooltip-icon` is a separate element that never fills past `0%`, so the
203
+ generic neutral reset above is enough for it.
204
+
205
+ Two selectors, so `none` and `neutral` agree. `:where()` loses to any intent
206
+ class, so `.tooltip-bubble.primary` still floods with primary;
207
+ `.tooltip-bubble.neutral` carries 0-2-0 and beats `.neutral`, so writing the
208
+ intent out gives the same bubble as writing nothing. */
202
209
  :where(.tooltip-bubble),
203
210
  .tooltip-bubble.neutral {
204
211
  --intent-color: var(--color-tooltip);
@@ -26,11 +26,13 @@
26
26
  --intent-fill-max: 20%;
27
27
  }
28
28
 
29
- /* Native text controls need the same stronger neutral boundary as their classed
30
- counterparts, without changing buttons, checkboxes, radios, or other inputs --
29
+ /* Native text controls and toggles take the same stronger neutral boundary as
30
+ their classed counterparts, without changing buttons or other inputs --
31
31
  control ink included, as `intent.css`'s class reset reads it. */
32
32
  :where(
33
33
  input:not([type]),
34
+ input[type="checkbox"],
35
+ input[type="radio"],
34
36
  input[type="text"],
35
37
  input[type="number"],
36
38
  input[type="email"],
@@ -49,6 +51,12 @@
49
51
  --intent-border: var(--ui-control-ink, var(--ui-ink, var(--color-control-border)));
50
52
  }
51
53
 
54
+ /* A bare table's lines take the rule line, as `.data-table`'s do in
55
+ `intent.css`. */
56
+ :where(table) {
57
+ --intent-border: var(--ui-rule-ink, var(--ui-ink, var(--color-border)));
58
+ }
59
+
52
60
  :where(hr) {
53
61
  --intent-color: currentColor;
54
62
  --intent-border: currentColor;
@@ -80,9 +88,10 @@
80
88
  @apply text-label;
81
89
  }
82
90
 
91
+ /* Links are left out: a link takes the text it sits in, so one inside a
92
+ heading stays heading-sized. */
83
93
  p,
84
- li,
85
- a {
94
+ li {
86
95
  @apply text-body;
87
96
  }
88
97
 
@@ -173,7 +182,8 @@
173
182
  transition: none;
174
183
  }
175
184
 
176
- input[type="checkbox"]:checked {
185
+ input[type="checkbox"]:checked,
186
+ input[type="checkbox"]:indeterminate {
177
187
  background-color: Highlight;
178
188
  border-color: Highlight;
179
189
 
@@ -7,11 +7,14 @@
7
7
  @layer theme, base, components, utilities;
8
8
 
9
9
  @layer theme {
10
- :root {
11
- --palette-neutral-solid-bg: oklab(0.145 0 0 / 0.2);
10
+ :root,
11
+ .light,
12
+ .theme-light,
13
+ [data-theme="light"] {
14
+ --palette-neutral-solid-bg: oklab(0.817 0 0);
12
15
  --palette-neutral-solid-fg: oklab(0.205 0 0);
13
16
  --palette-neutral-solid-edge: oklab(0 0 0 / 0);
14
- --palette-neutral-solid-bg-hover: oklab(0.371 0 0 / 0.34);
17
+ --palette-neutral-solid-bg-hover: oklab(0.77624 0 0);
15
18
  --palette-neutral-solid-edge-hover: oklab(0 0 0 / 0);
16
19
  --palette-neutral-soft-fg: oklab(0.205 0 0);
17
20
  --palette-neutral-soft-bg: oklab(0.145 0 0 / 0.12);
@@ -233,10 +236,10 @@
233
236
  .dark,
234
237
  .theme-dark,
235
238
  [data-theme="dark"] {
236
- --palette-neutral-solid-bg: oklab(0.985 0 0 / 0.2);
239
+ --palette-neutral-solid-bg: oklab(0.313 0 0);
237
240
  --palette-neutral-solid-fg: oklab(0.97 0 0);
238
241
  --palette-neutral-solid-edge: oklab(0 0 0 / 0);
239
- --palette-neutral-solid-bg-hover: oklab(0.922 0 0 / 0.34);
242
+ --palette-neutral-solid-bg-hover: oklab(0.40918 0 0);
240
243
  --palette-neutral-solid-edge-hover: oklab(0 0 0 / 0);
241
244
  --palette-neutral-soft-fg: oklab(0.97 0 0);
242
245
  --palette-neutral-soft-bg: oklab(0.985 0 0 / 0.12);