actual-css 0.10.0 → 0.11.1

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 (89) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/README.md +6 -2
  3. package/components.json +14 -3
  4. package/dist/actual.full.css +483 -128
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/enhancement-contract.md +16 -7
  8. package/docs/design-notes/platform-alignment.md +8 -4
  9. package/docs/design-notes/rich-text.md +61 -0
  10. package/docs/design-notes/steps.md +2 -2
  11. package/docs/navigation.json +4 -1
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/bleed.md +5 -4
  15. package/docs/pages/components/button.md +1 -1
  16. package/docs/pages/components/chat.md +5 -0
  17. package/docs/pages/components/close.md +9 -3
  18. package/docs/pages/components/dialog.md +145 -13
  19. package/docs/pages/components/meter.md +2 -0
  20. package/docs/pages/components/pagination.md +15 -15
  21. package/docs/pages/components/shimmer.md +57 -0
  22. package/docs/pages/components/skeleton.md +2 -1
  23. package/docs/pages/components/spinner.md +11 -6
  24. package/docs/pages/components/table.md +13 -0
  25. package/docs/pages/enhancements/flyout.md +2 -0
  26. package/docs/pages/enhancements/tabs.md +4 -0
  27. package/docs/pages/examples/overview.md +1 -0
  28. package/docs/pages/forms/choice-cards.md +77 -4
  29. package/docs/pages/forms/combobox.md +16 -1
  30. package/docs/pages/forms/overview.md +4 -1
  31. package/docs/pages/forms/range.md +110 -5
  32. package/docs/pages/forms/rich-text.md +110 -0
  33. package/docs/pages/forms/validation.md +5 -1
  34. package/docs/pages/foundations/theming.md +49 -3
  35. package/docs/pages/foundations/tokens.md +15 -5
  36. package/docs/pages/guides/bootstrap.md +2 -1
  37. package/docs/pages/guides/design-tools.md +36 -0
  38. package/docs/pages/guides/introduction.md +1 -1
  39. package/docs/pages/guides/modular-import.md +1 -1
  40. package/docs/pages/guides/progressive-enhancement.md +13 -14
  41. package/docs/pages/guides/tailwind.md +4 -2
  42. package/llms.txt +4 -3
  43. package/package.json +6 -1
  44. package/reserved-classes.json +1 -0
  45. package/skills/actual-css/references/enhancements.md +35 -0
  46. package/src/cli/actual-css.js +49 -6
  47. package/src/css/components/avatar.css +1 -1
  48. package/src/css/components/bleed.css +5 -5
  49. package/src/css/components/button.css +14 -1
  50. package/src/css/components/close.css +17 -6
  51. package/src/css/components/flyout.css +4 -1
  52. package/src/css/components/meter.css +6 -3
  53. package/src/css/components/modal.css +123 -7
  54. package/src/css/components/pagination.css +3 -2
  55. package/src/css/components/rating.css +1 -1
  56. package/src/css/components/status-bar.css +2 -2
  57. package/src/css/components/steps.css +3 -3
  58. package/src/css/components/tab.css +3 -3
  59. package/src/css/components/table.css +6 -0
  60. package/src/css/components/tooltip.css +1 -1
  61. package/src/css/core/focus.css +14 -4
  62. package/src/css/core/theme.css +8 -0
  63. package/src/css/core/tokens.css +8 -4
  64. package/src/css/core/variants.css +2 -1
  65. package/src/css/effects/aura.css +51 -33
  66. package/src/css/effects/index.css +1 -0
  67. package/src/css/effects/shimmer.css +57 -0
  68. package/src/css/forms/choice-card.css +71 -23
  69. package/src/css/forms/control.css +2 -2
  70. package/src/css/forms/native.css +5 -1
  71. package/src/css/forms/otp.css +9 -1
  72. package/src/css/forms/range.css +64 -21
  73. package/src/js/command.js +61 -32
  74. package/src/js/dialog.js +8 -23
  75. package/src/js/dismiss.js +1 -2
  76. package/src/js/enhance.js +84 -123
  77. package/src/js/full.js +1 -0
  78. package/src/js/password.js +1 -8
  79. package/src/js/range.js +151 -0
  80. package/src/js/scrollspy.js +4 -5
  81. package/src/js/status.js +2 -10
  82. package/src/js/tooltip.js +11 -1
  83. package/src/js/validation.js +4 -10
  84. package/src/tooling/design-components.js +286 -0
  85. package/src/tooling/design-export.js +69 -0
  86. package/src/tooling/design-tokens.js +466 -0
  87. package/src/tooling/penpot-plugin/manifest.json +7 -0
  88. package/src/tooling/penpot-plugin/plugin.js +272 -0
  89. package/src/tooling/penpot-plugin/ui.html +58 -0
@@ -57,10 +57,11 @@ than the registration root, without crossing shadow boundaries.
57
57
 
58
58
  > `data-enhance` is a generic opt-in for when HTML does not already provide an unambiguous one.
59
59
 
60
- - **A token** is for a root controller — a behavior that owns a subtree and manages
61
- descendants (tabs, flyout, scrollspy, validation).
62
- - **A self-describing `data-*`** is for a leaf whose attribute is simultaneously the opt-in
63
- and the configuration (`data-tooltip`, `data-mask`, `data-filter`,
60
+ - **A token** is for a named behavior whose opt-in carries no configuration,
61
+ whether it owns a subtree or enhances one leaf (tabs, flyout, range,
62
+ scrollspy, validation).
63
+ - **A self-describing `data-*`** is for a leaf whose attribute is simultaneously
64
+ the opt-in and the configuration (`data-tooltip`, `data-mask`, `data-filter`,
64
65
  `data-context-menu`).
65
66
  - **Elements that are their own opt-in** need nothing (`<dialog>`).
66
67
 
@@ -72,6 +73,13 @@ than the registration root, without crossing shadow boundaries.
72
73
  on the same root throws; `disconnect()` releases the name; re-registration
73
74
  after disconnect is allowed; the same token on a different root is independent.
74
75
 
76
+ Named registrations share one `[data-enhance]` discovery gate per root: however
77
+ many behaviors are registered, the observer queries that single selector and
78
+ each registration matches its own `[data-enhance~="name"]` against what it finds.
79
+ `enhance()` records share the same observer and lifecycle with their arbitrary
80
+ selectors. Behaviors on one element start in no guaranteed order and must not
81
+ depend on each other.
82
+
75
83
  `enhance()` stays generic and unguarded — registering the same selector twice
76
84
  creates two independent records that both run.
77
85
 
@@ -95,8 +103,8 @@ those registrations explicitly.
95
103
 
96
104
  - Use a command for an immediate trigger-to-current-target action. Commands
97
105
  resolve the DOM at event time and do not own element lifecycle.
98
- - Use `registerEnhancement()` for an opted-in controller that owns listeners or
99
- state for a subtree.
106
+ - Use `registerEnhancement()` for a named, opted-in behavior that owns listeners
107
+ or state for a subtree or leaf.
100
108
  - Use `enhance()` for a self-describing leaf behavior that needs connection and
101
109
  cleanup lifecycle without a named token.
102
110
  - Prefer delegation when no persistent per-element state is required.
@@ -145,7 +153,8 @@ of the contract itself.
145
153
  | ------------ | ------------------------------------------------ | ------------------- |
146
154
  | `tabs` | Tab panel switching, arrow-key navigation | `.tabs` |
147
155
  | `flyout` | Trigger → panel positioning, open/close | `.flyout` |
156
+ | `range` | Fill share and named-value synchronization | `.range` |
148
157
  | `scrollspy` | Scroll-driven nav highlighting | `.scrollspy` |
149
158
  | `validation` | Form validation, focus management, server errors | `.needs-validation` |
150
159
 
151
- Four tokens. Everything else stays self-describing.
160
+ Five tokens. Everything else stays self-describing.
@@ -386,12 +386,16 @@ Prefer native controls and progressive styling.
386
386
 
387
387
  The concrete signal to watch is `appearance: base-slider` with `::slider-track`, `::slider-fill`, and `::slider-thumb`. Checked on Chrome 152: `CSS.supports('appearance', 'base-slider')` and `CSS.supports('selector(::slider-fill)')` are both false, so there is nothing to enhance against yet.
388
388
 
389
- Until then, `.range` intentionally omits filled tracks and tick marks. Both are reachable today only through techniques Actual should not take on:
389
+ Until then, `.range` intentionally omits tick marks, and its filled track comes
390
+ from the `range` enhancement (one `--range-progress` share driving a
391
+ track gradient) rather than engine-specific pseudo-elements:
390
392
 
391
- - The Chromium filled-track trick needs `overflow: hidden` on the runnable track plus a large `box-shadow` on the thumb. It collides twice with `range.css`: the thumb is wider than the track and would be clipped, and the focus ring already owns the thumb's `box-shadow`.
392
- - CSS-only tick marks need a step count duplicated out of `min`/`max`/`step` into a custom property, so markup carries the same contract twice and the two can diverge. `datalist` cannot carry it either: its native ticks are part of the appearance that `appearance: none` removes, and no engine renders `<option label>` for a range.
393
+ - The Chromium filled-track trick needs `overflow: hidden` on the runnable track plus a large `box-shadow` on the thumb. It collides twice with `range.css`: the thumb is wider than the track and would be clipped, and the thumb's `box-shadow` is already owned by the focus ring.
394
+ - CSS-only tick marks need a step count duplicated out of `min`/`max`/`step` into a custom property, so markup carries the same contract twice and the two can diverge. `datalist` cannot carry it either: its native ticks are part of the appearance that `appearance: none` removes, and no engine renders `<option label>` for a range — Actual reads the datalist as data only.
393
395
 
394
- When the base-slider parts become interoperable, this becomes a `@supports` enhancement in the shape of `custom-select.css` — less engine-specific CSS, not more.
396
+ When the base-slider parts become interoperable, the `--range-progress`
397
+ calculation becomes a `@supports` enhancement in the shape of
398
+ `custom-select.css` — less engine-specific CSS, not more.
395
399
 
396
400
  ### Navigation
397
401
 
@@ -0,0 +1,61 @@
1
+ # Rich text: three levels and why a package demos level 2
2
+
3
+ Editing text and editing a document are different problems with different
4
+ costs. Actual draws the line at the document boundary, in three levels:
5
+
6
+ ```text
7
+ 1. TEXTAREA (native)
8
+ text, autosize, actions row, plain-text caret helpers
9
+ → the composer pattern: demo/templates/composer.html
10
+
11
+ 2. HOSTED RICH-TEXT PACKAGE (integration)
12
+ bold / italic / lists / links / quote, undo, paste cleanup,
13
+ structured @mentions, / snippets
14
+ → the rich-text recipe: demo/templates/rich-text.html + @lekoala/rich-text
15
+
16
+ 3. STRUCTURED DOCUMENT ENGINE (integration, heavier)
17
+ custom document nodes, annotations, collaborative state
18
+ → no demo; the bridge contract from level 2 applies unchanged
19
+ ```
20
+
21
+ Level 1 stays a real `<textarea>`: form value, IME, undo, mobile,
22
+ accessibility, validation. `contenteditable="plaintext-only"` buys nothing over
23
+ it and loses the native form control, so the framework does not use it. The
24
+ Markdown round-trip some engines offer is still beta upstream and is not the
25
+ storage contract.
26
+
27
+ ## Why a package for the level-2 demo
28
+
29
+ The demo exists to prove the bridge (tokens, pressed states, focus, invalid,
30
+ composer chrome), not the engine. Owning the toolbar, sanitizer, roving focus
31
+ and lifecycle inside the demo meant Actual did involuntarily much of the
32
+ editor's job. Extracting that into `@lekoala/rich-text` restored the boundary:
33
+
34
+ ```text
35
+ Actual CSS presentation, tokens, composer chrome, dialogs, validation
36
+ @lekoala/rich-text textarea enhancement, Squire, sanitization, toolbar
37
+ semantics + keyboard, links, mentions, suggestions,
38
+ form/reset/readonly/disabled, lifecycle
39
+ application people lookup, templates, attachments, dictation, send
40
+ ```
41
+
42
+ `@lekoala/rich-text` owns the editor UI semantics; Actual owns its visual
43
+ language. Squire is an implementation detail of the integration package: it
44
+ owns selection, editing, paste normalisation and undo/redo, while the package
45
+ owns the product-facing contract around it (progressive enhancement of a real
46
+ `<textarea>`, mandatory sanitization, accessible generated UI, lifecycle and
47
+ cleanup). HTML stays the source of truth, which keeps the storage decoupled
48
+ and the engine choice reversible.
49
+
50
+ ## What was deliberately not demoed
51
+
52
+ - A second engine demo would prove nothing more about the bridge, and one
53
+ candidate changes fundamental choices between minor versions — a pinned demo
54
+ would rot. Spikes belong in `tmp/`, not `demo/`.
55
+ - Plain-text caret helpers stay a textarea-level improvement at level 1.
56
+ Structured `@mentions` live at level 2 because they are sanitized, atomic
57
+ entities inserted as HTML — the demo's Snippet/Mention buttons reuse the
58
+ same suggestion seam as `/` and `@`, so one capability keeps two surfaces
59
+ without duplicating its logic.
60
+ - Syntax highlighting inside inputs is a search-DSL concern, not a composer
61
+ concern; layering it onto a textarea rebuilds a fake rich editor.
@@ -630,8 +630,8 @@ so an author who lowers `--step-min` below `--step-size` narrows the scroll
630
630
  budget without collapsing items behind their own markers.
631
631
 
632
632
  The cost of scrolling is that clipping happens at the padding edge, and a focus
633
- ring is drawn outside its element's border box — `--focus-outline` is two
634
- border-widths wide at `--focus-outline-offset`, so 4px past a label that sits
633
+ ring is drawn outside its element's border box — `--focus-outline` is
634
+ `--focus-ring-width` wide at `--focus-outline-offset`, so 4px past a label that sits
635
635
  flush with the bottom of a stacked row. A navigable row therefore reserves
636
636
  exactly that much `padding-block`, gated on `.steps-horizontal:has(a[href])`. An
637
637
  informational stepper — the common case, with no ring to protect — keeps its
@@ -9,7 +9,8 @@
9
9
  "modular-import",
10
10
  "progressive-enhancement",
11
11
  "bootstrap",
12
- "tailwind"
12
+ "tailwind",
13
+ "design-tools"
13
14
  ]
14
15
  },
15
16
  {
@@ -67,6 +68,7 @@
67
68
  "pagination",
68
69
  "progress",
69
70
  "rating",
71
+ "shimmer",
70
72
  "skeleton",
71
73
  "spinner",
72
74
  "steps",
@@ -80,6 +82,7 @@
80
82
  "overview",
81
83
  "controls",
82
84
  "combobox",
85
+ "rich-text",
83
86
  "labels",
84
87
  "floating-field",
85
88
  "states-and-sizes",
@@ -84,22 +84,22 @@ The frame color comes from an intent such as `.primary`, `.success`, or
84
84
  a neutral intent produces a neutral aura. Use a chromatic intent when you want
85
85
  a visibly luminous effect.
86
86
 
87
- Two hooks tune how loud the effect is. `--aura-intensity` scales both halos at
88
- once, resting and breathing alike, so the breath stays proportional at every
89
- setting; `--aura-duration` sets one full rotation of the accent, or one breath
90
- of the glow.
87
+ The default is a thin lit line with a faint halo. Three hooks tune how loud it
88
+ is: `--aura-width` sets the line, `--aura-intensity` scales both halos at once,
89
+ resting and breathing alike, and `--aura-duration` sets one full rotation of
90
+ the accent, or one breath of the glow.
91
91
 
92
92
  ```html demo
93
93
  <div class="cluster" style="gap: 2rem">
94
- <span class="aura aura-glow primary" style="--aura-intensity: 0.4">
95
- <span class="card" style="inline-size: 8rem">Muted</span>
94
+ <span class="aura aura-glow primary" style="--aura-intensity: 0">
95
+ <span class="card" style="inline-size: 8rem">Line only</span>
96
96
  </span>
97
97
 
98
98
  <span class="aura aura-glow primary">
99
99
  <span class="card" style="inline-size: 8rem">Default</span>
100
100
  </span>
101
101
 
102
- <span class="aura aura-glow primary" style="--aura-intensity: 2">
102
+ <span class="aura aura-glow primary" style="--aura-width: 3px; --aura-intensity: 1">
103
103
  <span class="card" style="inline-size: 8rem">Loud</span>
104
104
  </span>
105
105
 
@@ -113,11 +113,10 @@ Keep exactly one direct child and set `--aura-radius` when its shape differs
113
113
  from the common button, card, alert, and badge shapes detected as a visual
114
114
  enhancement.
115
115
 
116
- The child must be opaque. The frame paints across the whole box and relies on
117
- the child to cover all but its edge, so a transparent child — an `.outline`
118
- alert, a `.ghost` button — is filled edge to edge with the frame color instead
119
- of framed by it. Give such a child a surface, or move the aura to an opaque
120
- wrapper around it.
116
+ A transparent or translucent child — an `.outline` alert, a `.ghost` button, a
117
+ glass surface — shows `--surface-opaque` beneath it, the theme's opaque
118
+ neutral, rather than the frame color. On a differently painted band, give the
119
+ child its own surface so it matches the band.
121
120
 
122
121
  ## CSS hooks
123
122
 
@@ -125,7 +124,7 @@ wrapper around it.
125
124
  - `--aura-width` — frame thickness.
126
125
  - `--aura-blur` — near halo blur; the far halo derives its own blur and its
127
126
  spread beyond the frame from this value, so one hook scales both.
128
- - `--aura-intensity` — multiplies both halo opacities, resting and breathing.
127
+ - `--aura-intensity` — multiplies both halo strengths, resting and breathing.
129
128
  It does not touch the frame itself, which stays a crisp line at any setting.
130
129
  - `--aura-radius` — child corner radius used to derive the outer radius.
131
130
  - `--aura-duration` — one rotation of the accent, or one breath of the glow.
@@ -2,6 +2,12 @@
2
2
 
3
3
  > Compact label for counts, status, or category tags, with shared intents and variants.
4
4
 
5
+ A badge is **information, never a control**: counts, status, categories. It is
6
+ non-interactive by nature — for a selectable option use
7
+ [`.choice-card.chip`](../forms/choice-cards.md#chips); for a value produced by
8
+ a widget that can be removed (combobox multi-value), the widget owns the
9
+ removable tag (see [combobox](../forms/combobox.md)).
10
+
5
11
  - Supports intent colors.
6
12
  - Soft by default. Use `.solid` for counters and notification badges, or `.outline` for quieter emphasis.
7
13
  - Empty badges are solid status dots: visual variants such as `.soft` and `.outline` do not change their fill.
@@ -180,6 +186,10 @@ Override the existing hooks when the three-step scale does not fit the content.
180
186
  Use `.badge soft` for tag visuals. Add a direct [`.close`](close.md) button only when the tag can actually be removed. There is no separate chip component.
181
187
  For a compact action or filter, use `.btn.sm` instead; badges describe content
182
188
  and must not be turned into toggle controls merely to obtain a compact shape.
189
+ Never put a `.close` inside a `.choice-card.chip` label: the label is already
190
+ an interactive target driving its checkbox/radio, and a nested button would mix
191
+ two interactive targets. A removable tag next to a selectable chip is a sibling
192
+ (a widget tag or a `.badge` + `.close`), never a child of the label.
183
193
 
184
194
  Inside a badge the close follows the pill's size, keeps a 24px pointer target,
185
195
  and tucks into the padding.
@@ -3,10 +3,11 @@
3
3
  `.bleed` lets a direct child of a padded surface reach its edges. Use it for a
4
4
  full-width image, a colored header or footer, or a notice band.
5
5
 
6
- It works inside `.card`, `.drawer`, and an accordion panel. The accordion panel
7
- owns the padding, so place `.bleed` directly inside the panel, after the summary.
8
- A deeper descendant stays within its wrapper. A modular build imports
9
- `actual-css/css/components/bleed` alongside the surface component.
6
+ It works inside `.card`, `.drawer`, a modal's direct `<form>` or `.stack`, and
7
+ an accordion panel. The accordion panel owns the padding, so place `.bleed`
8
+ directly inside the panel, after the summary. A deeper descendant stays within
9
+ its wrapper. A modular build imports `actual-css/css/components/bleed`
10
+ alongside the surface component.
10
11
 
11
12
  ```html demo
12
13
  <article class="card stack" style="--card-max-inline-size: 24rem">
@@ -89,7 +89,7 @@ every variant keeps working — `.outline` and `.ghost` take the solid ink:
89
89
  ```css
90
90
  .btn.app-dark {
91
91
  --intent: var(--surface-solid);
92
- --intent-fg: var(--surface);
92
+ --intent-fg: var(--surface-solid-fg);
93
93
  }
94
94
  ```
95
95
 
@@ -34,6 +34,11 @@ For a live conversation, put an appropriate live-region or `role="log"` on the
34
34
  conversation container according to the product's announcement needs. Do not
35
35
  put `role="log"` on individual messages.
36
36
 
37
+ For the message input itself, keep a native `<textarea>` inside a surface with
38
+ an actions row — see the [composer template](../../demo/templates/composer.html).
39
+ Formatting needs a hosted editor instead — see the
40
+ [rich-text template](../../demo/templates/rich-text.html).
41
+
37
42
  ## Class reference
38
43
 
39
44
  | Class | Description |
@@ -28,7 +28,9 @@
28
28
  In a `.modal` or a `.drawer`, the close sits out of the content flow at the
29
29
  panel's top inline-end corner. Place it as a direct child of the dialog, of its
30
30
  top-level `form` or `header`, or of a `form` inside that header; a `.close`
31
- deeper in the content keeps its own place. See [Dialog](dialog.md) and
31
+ deeper in the content keeps its own place, so wrapping it in a `.cluster` keeps
32
+ it in the header's row. A header that hosts the corner close reserves inline
33
+ room so a title never runs under it. See [Dialog](dialog.md) and
32
34
  [Drawer](drawer.md).
33
35
 
34
36
  ```html
@@ -43,6 +45,10 @@ deeper in the content keeps its own place. See [Dialog](dialog.md) and
43
45
 
44
46
  - `--close-size` — inline and block size of the button; `--control-size` unless the container sets it.
45
47
  - `--close-icon-size` — size of the X.
48
+ - `--close-bg` — background of the dialog corner close; `--surface-subtle` unless the dialog or a painted band sets it, e.g. `transparent` for a bare X.
49
+ - `--close-color` — ink of the X; `--text-muted` at the dialog corner, otherwise inherited from the context.
50
+ - `--close-hover-color` — ink of the dialog corner close on hover; `--text` unless a band sets its own foreground.
51
+ - `--close-hover-overlay` — hover and active wash that tints the fill the context gave the control; the dialog corner sets it `transparent` so a hover changes only the ink.
46
52
 
47
- Alerts and badges declare both hooks on themselves, so set them on the container
48
- or on the button.
53
+ Alerts and badges declare both size hooks on themselves, so set them on the
54
+ container or on the button.
@@ -14,15 +14,44 @@
14
14
  | `.dialog-icon` | Component | Circular intent-aware icon well. |
15
15
  | `.close` | Composition | Icon-only close button, at the top end. |
16
16
 
17
- A [`.close`](close.md) placed as a direct child of the dialog or of its form
18
- sits outside the content flow, so it does not take part in the dialog's own
19
- layout.
17
+ ## Anatomy
18
+
19
+ `.modal` is the outer surface and deliberately has no inner padding. Wrap its
20
+ content in a direct `<form>` or `.stack`; that wrapper owns the padding, rhythm,
21
+ and overflow. Put `.stack` on the wrapper, never on `dialog.modal` itself.
22
+
23
+ For a header band, place `<header>` directly under `dialog.modal`, before the
24
+ body wrapper. Put a flexible `.media` title region first and a `.cluster` of
25
+ controls second. The title has the close control's minimum height; its leading
26
+ icon and every trailing control share the same center axis. A wrapped title
27
+ simply makes the row taller.
28
+
29
+ A direct `dialog > header` is dialog chrome and forms a distinct header band.
30
+ Put an ordinary content heading inside the padded `form` or `.stack` instead.
31
+
32
+ Keep a corner [`.close`](close.md) as a direct child of the dialog and put the
33
+ title in the wrapper's direct `<header>`. The button then stays outside the
34
+ content flow while the header reserves enough inline space to keep its title
35
+ clear.
36
+
37
+ A `.close` placed as a direct child of the chrome header is still the corner
38
+ overlay. That header reserves the same inline room for it, never below its own
39
+ `--modal-pad`, so a wrapping title stays clear either way.
40
+
41
+ The corner close overlays the panel edge, so a body tall enough to scroll would
42
+ run its scrollbar under the button. Give such a dialog the `scrollable` anatomy:
43
+ the close then sits over the fixed header and the scrollbar starts below it.
44
+
45
+ Add `.bleed` to that header when it should form a full-width band. It reaches
46
+ the dialog edges, keeps the modal's inner padding, and can take a surface token
47
+ such as `background: var(--surface-subtle)`.
20
48
 
21
49
  ## Usage
22
50
 
23
51
  Modals use the platform-native `<dialog class="modal">` element with `commandfor` and `command` buttons.
24
52
 
25
- Use `command="show-modal"` to open a modal dialog.
53
+ Use `command="show-modal"` to open a modal dialog, or `command="--show"` to open
54
+ it non-modally.
26
55
 
27
56
  Add `aria-haspopup="dialog"` and `aria-controls="<id>"` to opening buttons so
28
57
  their semantics are present before either native or framework JavaScript runs.
@@ -84,7 +113,8 @@ For a compact destructive confirmation with a leading status icon, compose
84
113
  `dialog-confirmation` with the media object. The icon well accepts the shared
85
114
  intent and emphasis classes; `dialog-icon danger soft` creates the tinted red
86
115
  circle while keeping the glyph centered. The footer becomes a separate action
87
- band without changing the semantics of the form or its buttons.
116
+ band whose two actions sit at its edges, so the cancel-style control never sits
117
+ next to the committing one.
88
118
 
89
119
  ```html demo
90
120
  <button class="btn danger"
@@ -120,6 +150,49 @@ band without changing the semantics of the form or its buttons.
120
150
  </dialog>
121
151
  ```
122
152
 
153
+ For a trailing illustration, use a direct `<header>` as the message region and
154
+ compose it as a non-wrapping split cluster. `.grow` keeps the text column
155
+ flexible while `.dialog-icon` stays at the inline end. The confirmation variant
156
+ pads either this header or the leading-icon `.media` shape above.
157
+
158
+ Use the same anatomy in a `.card` instead when the surface belongs in page
159
+ content and does not need modal focus or a backdrop.
160
+
161
+ ```html demo
162
+ <button class="btn"
163
+ type="button"
164
+ commandfor="customize-dialog"
165
+ command="show-modal"
166
+ aria-haspopup="dialog"
167
+ aria-controls="customize-dialog">
168
+ Customize workspace
169
+ </button>
170
+
171
+ <dialog class="modal dialog-confirmation"
172
+ id="customize-dialog"
173
+ closedby="none"
174
+ aria-labelledby="customize-dialog-title">
175
+ <form method="dialog">
176
+ <header class="cluster"
177
+ style="--cluster-align: start; --cluster-justify: space-between; --cluster-wrap: nowrap">
178
+ <hgroup class="grow">
179
+ <h3 id="customize-dialog-title">Make this workspace your own</h3>
180
+ <p class="muted">Choose colors and defaults now, or continue with the current settings.</p>
181
+ </hgroup>
182
+
183
+ <span class="dialog-icon success soft" aria-hidden="true">
184
+ <i class="ti ti-check"></i>
185
+ </span>
186
+ </header>
187
+
188
+ <footer>
189
+ <button class="btn primary" value="customize">Customize</button>
190
+ <button class="btn ghost" value="later">Later</button>
191
+ </footer>
192
+ </form>
193
+ </dialog>
194
+ ```
195
+
123
196
  ## Information modal
124
197
 
125
198
  Use this shape for contextual information or lightweight secondary content. It
@@ -162,6 +235,67 @@ has no action button; the header close button dismisses the dialog.
162
235
  </dialog>
163
236
  ```
164
237
 
238
+ ## Action modal
239
+
240
+ Use this shape when contextual information leads to one non-destructive next
241
+ step, such as reviewing a setup before continuing. Unlike an alert dialog, it
242
+ can be dismissed without choosing the action; unlike an information modal, it
243
+ has a primary action in a footer.
244
+
245
+ Use `.dialog-icon` with the media object for a leading title icon. An `.alert`
246
+ belongs in the body only when the dialog contains a distinct status message;
247
+ the dialog title itself is not an alert.
248
+
249
+ ```html demo
250
+ <button class="btn primary"
251
+ type="button"
252
+ commandfor="setup-dialog"
253
+ command="show-modal"
254
+ aria-haspopup="dialog"
255
+ aria-controls="setup-dialog">
256
+ Continue setup
257
+ </button>
258
+
259
+ <dialog class="modal"
260
+ id="setup-dialog"
261
+ closedby="any"
262
+ data-dialog-dismissible
263
+ aria-labelledby="setup-dialog-title">
264
+ <header>
265
+ <div class="media items-center grow">
266
+ <span class="dialog-icon warning soft" aria-hidden="true">
267
+ <i class="ti ti-alert-triangle"></i>
268
+ </span>
269
+
270
+ <h2 id="setup-dialog-title">Finish workspace setup</h2>
271
+ </div>
272
+
273
+ <div class="cluster">
274
+ <button class="close"
275
+ type="button"
276
+ commandfor="setup-dialog"
277
+ command="request-close"
278
+ aria-controls="setup-dialog"
279
+ aria-label="Close dialog"></button>
280
+ </div>
281
+ </header>
282
+
283
+ <form method="dialog" class="stack">
284
+ <div class="card stack">
285
+ <p><strong>Acme workspace</strong></p>
286
+ <p>Team plan</p>
287
+ <p>Starts October 1</p>
288
+ </div>
289
+
290
+ <p>Confirm these settings to finish setting up the workspace.</p>
291
+
292
+ <footer>
293
+ <button class="btn primary inline-size-full" value="continue">Continue</button>
294
+ </footer>
295
+ </form>
296
+ </dialog>
297
+ ```
298
+
165
299
  ## Scrollable modal
166
300
 
167
301
  Use `modal scrollable` when the header and footer should stay visible while the
@@ -297,7 +431,9 @@ Available options:
297
431
  light dismiss and closes the dialog (or rewrites `closedby="any"` to
298
432
  `closedby="closerequest"` so the native dialog does not double-handle).
299
433
  It never affects Escape or explicit close requests.
300
- * `data-dialog-modal="false"` opens with `show()` instead of `showModal()`.
434
+ * `data-dialog-modal="false"` makes direct `openDialog()` calls use `show()`
435
+ instead of `showModal()`. The `show-modal` and `--show` commands override
436
+ this default.
301
437
  * `data-dialog-view-transition` enables a view transition that morphs the dialog to/from its trigger. Only active when the browser supports `document.startViewTransition` and the user allows motion.
302
438
  * `closedby` keeps its native meaning: `"any"` closes on backdrop click and
303
439
  Escape, `"closerequest"` closes on Escape only, `"none"` disables both. The
@@ -368,20 +504,16 @@ Prefer native dialog behavior whenever possible. The framework runtime should
368
504
  not replace the platform modal system; it should only make dialogs declarative,
369
505
  animation-friendly, and consistent across supported browsers.
370
506
 
371
- The close button is anchored to the dialog surface, not the content flow: it
372
- sits at the top `inline-end`, inside the panel. The button itself is out of
373
- flow, while the header reserves enough inline padding to keep its title clear.
374
- Keep the button a direct child of `dialog.modal` so the scrolling content
375
- (`> form` or `> .stack`) never competes with it.
376
-
377
507
  ## CSS hooks
378
508
 
379
509
  - `--modal-size` — maximum dialog width. Fallback-only, so a class on the
380
510
  dialog or an inherited value both reach it. A dialog is shrink-to-fit: a
381
511
  composition that must fill this width sets `inline-size` as well.
512
+ - `--modal-pad` — padding of the direct `<form>` or `.stack` content wrapper.
513
+ - `--modal-header-bg` — background of a direct header band.
382
514
  - `--dialog-viewport-gap` — distance kept between the dialog and the viewport edges.
383
515
  - `--dialog-icon-size` — diameter of the `.dialog-icon` circle.
384
516
  - `--dialog-icon-glyph-size` — size of the glyph centered inside `.dialog-icon`.
385
517
  - `--close-size` / `--close-icon-size` — size of the corner `.close` and of its
386
- X. The header reserves the close size plus `--space-30` on its inline end so
518
+ X. The header reserves the close size, its edge inset, and a content gap so
387
519
  the title never runs under it.
@@ -70,6 +70,8 @@ marker to that background rather than to the neutral intent.
70
70
 
71
71
  ### Hooks
72
72
 
73
+ - `--meter-track` — track color; raise it when the meter sits on a
74
+ `--surface-subtle` card, where the default track would disappear.
73
75
  - `--bar-height` — bar thickness, shared with `.progress`.
74
76
  - `--meter-value` — relative weight of a direct child in a segmented meter.
75
77
 
@@ -7,8 +7,8 @@
7
7
  - Use `aria-current="page"` for the current page.
8
8
  - Put `.sm` or `.lg` on `.pagination` to scale its controls.
9
9
  - Numeric links may use `aria-label="Page N"` for clearer screen reader output.
10
- - Prefer text labels for Previous and Next. Icon-only controls need an accessible name.
11
- - Page links can compose with `.btn` for button-like hit targets.
10
+ - Prefer text labels for Previous and Next; a chevron icon may accompany them. Icon-only controls need an accessible name.
11
+ - Compose page links with `.btn.ghost`; `aria-current="page"` fills the current one, so it is the only boxed item.
12
12
  - Arrows, page numbers, and truncation ellipses share one geometric system: every item gets the same minimum footprint, and wider content (100, 1000, text labels) grows past it naturally.
13
13
  - Truncated ranges are plain text with `.muted` and `aria-hidden="true"`, not `.btn` — they are decorative, not actionable.
14
14
 
@@ -24,12 +24,12 @@
24
24
  ```html demo
25
25
  <nav aria-label="Pagination">
26
26
  <ol class="pagination sm">
27
- <li><a href="?page=2" class="btn outline" rel="prev">Previous</a></li>
28
- <li><a href="?page=1" class="btn outline" aria-label="Page 1">1</a></li>
29
- <li><a href="?page=2" class="btn outline" aria-label="Page 2">2</a></li>
30
- <li><a href="?page=3" class="btn" aria-current="page" aria-label="Page 3">3</a></li>
31
- <li><a href="?page=4" class="btn outline" aria-label="Page 4">4</a></li>
32
- <li><a href="?page=5" class="btn outline" rel="next">Next</a></li>
27
+ <li><a href="?page=2" class="btn ghost" rel="prev"><i class="ti ti-chevron-left" aria-hidden="true"></i> Previous</a></li>
28
+ <li><a href="?page=1" class="btn ghost" aria-label="Page 1">1</a></li>
29
+ <li><a href="?page=2" class="btn ghost" aria-label="Page 2">2</a></li>
30
+ <li><a href="?page=3" class="btn ghost" aria-current="page" aria-label="Page 3">3</a></li>
31
+ <li><a href="?page=4" class="btn ghost" aria-label="Page 4">4</a></li>
32
+ <li><a href="?page=5" class="btn ghost" rel="next">Next <i class="ti ti-chevron-right" aria-hidden="true"></i></a></li>
33
33
  </ol>
34
34
  </nav>
35
35
  ```
@@ -42,15 +42,15 @@ buttons — they are decorative, not actionable.
42
42
  ```html demo
43
43
  <nav aria-label="Pagination">
44
44
  <ol class="pagination sm">
45
- <li><a href="?page=1" class="btn outline" rel="prev">Previous</a></li>
46
- <li><a href="?page=1" class="btn outline" aria-label="Page 1">1</a></li>
45
+ <li><a href="?page=7" class="btn ghost" rel="prev"><i class="ti ti-chevron-left" aria-hidden="true"></i> Previous</a></li>
46
+ <li><a href="?page=1" class="btn ghost" aria-label="Page 1">1</a></li>
47
47
  <li><span class="muted" aria-hidden="true">…</span></li>
48
- <li><a href="?page=7" class="btn outline" aria-label="Page 7">7</a></li>
49
- <li><a href="?page=8" class="btn" aria-current="page" aria-label="Page 8">8</a></li>
50
- <li><a href="?page=9" class="btn outline" aria-label="Page 9">9</a></li>
48
+ <li><a href="?page=7" class="btn ghost" aria-label="Page 7">7</a></li>
49
+ <li><a href="?page=8" class="btn ghost" aria-current="page" aria-label="Page 8">8</a></li>
50
+ <li><a href="?page=9" class="btn ghost" aria-label="Page 9">9</a></li>
51
51
  <li><span class="muted" aria-hidden="true">…</span></li>
52
- <li><a href="?page=20" class="btn outline" aria-label="Page 20">20</a></li>
53
- <li><a href="?page=9" class="btn outline" rel="next">Next</a></li>
52
+ <li><a href="?page=20" class="btn ghost" aria-label="Page 20">20</a></li>
53
+ <li><a href="?page=9" class="btn ghost" rel="next">Next <i class="ti ti-chevron-right" aria-hidden="true"></i></a></li>
54
54
  </ol>
55
55
  </nav>
56
56
  ```