actual-css 0.10.0 → 0.11.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 (71) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +2 -2
  3. package/components.json +2 -1
  4. package/dist/actual.full.css +314 -106
  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 +2 -0
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/button.md +1 -1
  15. package/docs/pages/components/chat.md +5 -0
  16. package/docs/pages/components/dialog.md +5 -2
  17. package/docs/pages/components/pagination.md +15 -15
  18. package/docs/pages/components/shimmer.md +57 -0
  19. package/docs/pages/components/skeleton.md +2 -1
  20. package/docs/pages/components/spinner.md +11 -6
  21. package/docs/pages/components/table.md +13 -0
  22. package/docs/pages/enhancements/flyout.md +2 -0
  23. package/docs/pages/enhancements/tabs.md +4 -0
  24. package/docs/pages/forms/choice-cards.md +77 -4
  25. package/docs/pages/forms/combobox.md +16 -1
  26. package/docs/pages/forms/overview.md +4 -1
  27. package/docs/pages/forms/range.md +110 -5
  28. package/docs/pages/forms/rich-text.md +110 -0
  29. package/docs/pages/forms/validation.md +5 -1
  30. package/docs/pages/foundations/theming.md +49 -3
  31. package/docs/pages/foundations/tokens.md +15 -5
  32. package/docs/pages/guides/bootstrap.md +2 -1
  33. package/docs/pages/guides/introduction.md +1 -1
  34. package/docs/pages/guides/modular-import.md +1 -1
  35. package/docs/pages/guides/progressive-enhancement.md +13 -14
  36. package/docs/pages/guides/tailwind.md +4 -2
  37. package/llms.txt +4 -3
  38. package/package.json +5 -1
  39. package/reserved-classes.json +1 -0
  40. package/src/css/components/avatar.css +1 -1
  41. package/src/css/components/flyout.css +4 -1
  42. package/src/css/components/meter.css +1 -1
  43. package/src/css/components/pagination.css +3 -2
  44. package/src/css/components/rating.css +1 -1
  45. package/src/css/components/status-bar.css +2 -2
  46. package/src/css/components/steps.css +3 -3
  47. package/src/css/components/tab.css +3 -3
  48. package/src/css/components/table.css +6 -0
  49. package/src/css/components/tooltip.css +1 -1
  50. package/src/css/core/focus.css +14 -4
  51. package/src/css/core/theme.css +8 -0
  52. package/src/css/core/tokens.css +8 -4
  53. package/src/css/core/variants.css +2 -1
  54. package/src/css/effects/aura.css +51 -33
  55. package/src/css/effects/index.css +1 -0
  56. package/src/css/effects/shimmer.css +57 -0
  57. package/src/css/forms/choice-card.css +71 -23
  58. package/src/css/forms/control.css +2 -2
  59. package/src/css/forms/otp.css +9 -1
  60. package/src/css/forms/range.css +64 -21
  61. package/src/js/command.js +61 -32
  62. package/src/js/dialog.js +8 -23
  63. package/src/js/dismiss.js +1 -2
  64. package/src/js/enhance.js +84 -123
  65. package/src/js/full.js +1 -0
  66. package/src/js/password.js +1 -8
  67. package/src/js/range.js +151 -0
  68. package/src/js/scrollspy.js +4 -5
  69. package/src/js/status.js +2 -10
  70. package/src/js/tooltip.js +11 -1
  71. package/src/js/validation.js +4 -10
@@ -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
@@ -67,6 +67,7 @@
67
67
  "pagination",
68
68
  "progress",
69
69
  "rating",
70
+ "shimmer",
70
71
  "skeleton",
71
72
  "spinner",
72
73
  "steps",
@@ -80,6 +81,7 @@
80
81
  "overview",
81
82
  "controls",
82
83
  "combobox",
84
+ "rich-text",
83
85
  "labels",
84
86
  "floating-field",
85
87
  "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.
@@ -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 |
@@ -22,7 +22,8 @@ layout.
22
22
 
23
23
  Modals use the platform-native `<dialog class="modal">` element with `commandfor` and `command` buttons.
24
24
 
25
- Use `command="show-modal"` to open a modal dialog.
25
+ Use `command="show-modal"` to open a modal dialog, or `command="--show"` to open
26
+ it non-modally.
26
27
 
27
28
  Add `aria-haspopup="dialog"` and `aria-controls="<id>"` to opening buttons so
28
29
  their semantics are present before either native or framework JavaScript runs.
@@ -297,7 +298,9 @@ Available options:
297
298
  light dismiss and closes the dialog (or rewrites `closedby="any"` to
298
299
  `closedby="closerequest"` so the native dialog does not double-handle).
299
300
  It never affects Escape or explicit close requests.
300
- * `data-dialog-modal="false"` opens with `show()` instead of `showModal()`.
301
+ * `data-dialog-modal="false"` makes direct `openDialog()` calls use `show()`
302
+ instead of `showModal()`. The `show-modal` and `--show` commands override
303
+ this default.
301
304
  * `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
305
  * `closedby` keeps its native meaning: `"any"` closes on backdrop click and
303
306
  Escape, `"closerequest"` closes on Escape only, `"none"` disables both. The
@@ -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
  ```
@@ -0,0 +1,57 @@
1
+ # Shimmer
2
+
3
+ > **Module** — import `actual-css/css/effects/shimmer` or `actual-css/css/effects`.
4
+
5
+ Shimmer sweeps a highlight across a text label to show an activity in
6
+ progress: an agent working, a chatbot composing, a job processing.
7
+
8
+ ```html demo
9
+ <div class="stack">
10
+ <span class="shimmer" role="status">Working…</span>
11
+ <p class="cluster">
12
+ <span class="spinner" aria-hidden="true"></span>
13
+ <span class="shimmer" role="status">Generating the report…</span>
14
+ </p>
15
+ </div>
16
+ ```
17
+
18
+ **Related terms:** shimmering text, loading text, thinking indicator, pending
19
+ label, typing indicator.
20
+
21
+ Shimmer is presentation only and selects no ARIA attribute. Put
22
+ `role="status"` on a message that should be announced, and `aria-busy="true"`
23
+ on the region whose content is being updated — not on the message because it
24
+ looks busy. Assistive technology may hold back changes inside a busy region
25
+ until it is released.
26
+
27
+ The label rests on `--shimmer-color` and the band passes in
28
+ `--shimmer-highlight`; one pass takes `--duration-shimmer`, the pace shared
29
+ with `.skeleton` and indeterminate `.progress`. With reduced motion, or in a
30
+ browser without `background-clip: text`, the label is plain
31
+ `--shimmer-color` text. Recolor a label through these hooks: its own `color`
32
+ shows only where the sweep does not run.
33
+
34
+ ## Choosing a waiting signal
35
+
36
+ | Primitive | Signals |
37
+ | -------------------------- | -------------------------------------------- |
38
+ | [`.spinner`](spinner.md) | An operation takes time |
39
+ | [`.skeleton`](skeleton.md) | Content is not available yet |
40
+ | `.shimmer` | This label describes an activity in progress |
41
+ | [`.aura`](aura.md) | This object is emphasized |
42
+
43
+ They compose rather than imply one another: `aria-busy` never adds an aura or
44
+ a shimmer. A card being processed can carry several, each for its own reason:
45
+
46
+ ```html demo
47
+ <span class="aura primary">
48
+ <article class="card stack" style="inline-size: 18rem">
49
+ <p><strong>Quarterly import</strong></p>
50
+ <p class="shimmer" role="status">Processing 1,204 rows…</p>
51
+ <div class="stack" aria-busy="true">
52
+ <div class="skeleton" data-shape="text" aria-hidden="true"></div>
53
+ <div class="skeleton" data-shape="text" aria-hidden="true"></div>
54
+ </div>
55
+ </article>
56
+ </span>
57
+ ```
@@ -8,7 +8,8 @@
8
8
  - Use layout utilities for placeholder arrangement.
9
9
  - Only relevant for simple placeholders - use more structural solutions like phantom-ui for complex cases.
10
10
 
11
- **Related terms:** skeleton screen, shimmer, loading placeholder.
11
+ **Related terms:** skeleton screen, shimmer, loading placeholder. For a text
12
+ label showing an activity in progress, see [Shimmer](shimmer.md).
12
13
 
13
14
  ## Class reference
14
15
 
@@ -12,11 +12,15 @@
12
12
 
13
13
  ## Class reference
14
14
 
15
- | Class | Kind | Description |
16
- | -------------- | --------- | ------------------------------------------------------------------ |
17
- | `.spinner` | Component | Circular loading indicator using `currentColor`. |
18
- | `.sm` / `.lg` | Size | Smaller (`0.75em`) or larger (`2rem`) spinner. |
19
- | Shared intents | Intent | `.primary`, `.secondary`, `.success`, `.warning`, `.danger` color. |
15
+ | Class | Kind | Description |
16
+ | -------------------- | --------- | ------------------------------------------------ |
17
+ | `.spinner` | Component | Circular loading indicator using `currentColor`. |
18
+ | `[aria-busy="true"]` | State | Loading region; see Busy state below. |
19
+ | `.sm` / `.lg` | Size | Smaller (`0.75em`) or larger (`2rem`) spinner. |
20
+ | Shared intents | Intent | `.primary`, `.secondary`, `.success`, … |
21
+
22
+ The shared intents are `.primary`, `.secondary`, `.success`, `.warning` and
23
+ `.danger`.
20
24
 
21
25
  ## Basic usage
22
26
 
@@ -68,7 +72,8 @@ Size follows the font (`1em`); use `.sm` / `.lg` rather than sizing it directly.
68
72
 
69
73
  ## Busy state
70
74
 
71
- A container-wide loading state that keeps the underlying content in place. It is
75
+ The spinner is the indicator; `aria-busy` is the state. A container-wide
76
+ loading state keeps the underlying content in place. It is
72
77
  driven by `aria-busy="true"` plus a direct last-child `.spinner` — there is no
73
78
  separate `.busy` class. The spinner is shown as a centered overlay over a faded
74
79
  surface; buttons keep their spinners inline instead (see Button).
@@ -124,6 +124,19 @@ same vocabulary as `.card.compact`.
124
124
  </div>
125
125
  ```
126
126
 
127
+ ## When a table stops reading as a table
128
+
129
+ Responsive strategy is horizontal overflow: the wrap scrolls, the table keeps
130
+ its columns and its column scanability. No `data-label` restructuring, no
131
+ second representation of the data.
132
+
133
+ For datasets that no longer read well as a table on narrow screens, compose a
134
+ different view instead of restructuring the table: a `.list` / `.media` row
135
+ with badges and actions for a CRUD collection, [`.data-list`](data-list.md)
136
+ for one object's properties, or a dedicated data grid for interactive tables
137
+ (sorting, filtering, selection). Two views for two contexts is a feature, not
138
+ a duplication.
139
+
127
140
  ## CSS hooks
128
141
 
129
142
  - `--table-cell-pad` — cell padding; `.compact` re-declares it tighter.
@@ -522,6 +522,8 @@ delay; a number sets the delay in milliseconds.
522
522
 
523
523
  The repetition is the point: the two hooks are different contracts, and only
524
524
  the cap may enter the runtime's `min()`.
525
+ - `--flyout-pad` — panel padding. Defaults to `var(--space-10)`, tight for
526
+ action menus; rich panels opt into dialog air per instance.
525
527
  - `--menu-item-size` — minimum row height of `.menu-item`.
526
528
  - `--menu-item-icon-size` — shared leading-column width for icons and checked-state indicators.
527
529
 
@@ -22,6 +22,10 @@
22
22
  - JavaScript owns roving `tabindex`, `aria-selected`, `hidden`, and keyboard behavior.
23
23
  - Left/Right select tabs and wrap at the ends. Home/End jump to first/last. Down moves focus into the selected panel.
24
24
  - A tab list needs both `.tabs` and `role="tablist"`; `.tab` styles each trigger.
25
+ - Naming follows the container/item convention (like `menu` / `menu-item`):
26
+ `.tabs` is the strip, `.tab` is one trigger. The JS module
27
+ (`actual-css/js/tab`) is named for the file; the behavior token
28
+ (`data-enhance="tabs"`) is named for what it wires.
25
29
  - A `.tab` with an icon uses `--tab-gap` (default `0.375em`) for the space between icon and label.
26
30
  - A tab that is `hidden`, `disabled`, `aria-disabled`, or has no panel is skipped
27
31
  by the arrow keys. An application filtering a tab strip only has to set
@@ -11,9 +11,31 @@
11
11
  | `.primary` / `.secondary` | Intent | Accent for the selected state. |
12
12
 
13
13
  - Use `.choice-card` on a `<label>` wrapping a native `<input type="radio">` or `<input type="checkbox">`. The input is visually hidden; in browsers without `:has()` it stays visible so selection state never disappears.
14
- - Selected items show a primary border, subtle background tint, and a checkmark badge at the top-right corner — round for radio, rounded square for checkbox.
15
- - Pair with grid or flex utilities for group layout. The badge overhangs the top-right corner, so avoid `overflow: hidden` on the direct container.
16
- - Supports disabled, checked, focus-visible, and invalid states.
14
+ - The control shows before any choice: a ring (radio) or a rounded box (checkbox) on the leading edge, level with the first line. Checked, it fills with a check, and the card takes a primary border and a subtle tint.
15
+ - Each child element of the label sits on its own row beside the indicator — a title and a `.muted` detail need no wrapper. Wrap loose text in an element.
16
+ - Stack cards for a list of options (a person, an address, a delivery method), or pair them with grid utilities for options compared side by side.
17
+ - Supports disabled, checked, focus-visible, and invalid states; the resting ring turns danger with the border.
18
+
19
+ ```html demo
20
+ <fieldset class="field-group stack">
21
+ <legend class="field-label">Deliver to</legend>
22
+ <label class="choice-card">
23
+ <input type="radio" name="address" value="home" checked />
24
+ <strong>Home</strong>
25
+ <span class="muted">12 Rue des Tanneurs, 1000 Brussels</span>
26
+ </label>
27
+ <label class="choice-card">
28
+ <input type="radio" name="address" value="work" />
29
+ <strong>Work</strong>
30
+ <span class="muted">Avenue Louise 54, 1050 Ixelles</span>
31
+ </label>
32
+ <label class="choice-card">
33
+ <input type="radio" name="address" value="pickup" />
34
+ <strong>Pickup point</strong>
35
+ <span class="muted">Collect within 7 days</span>
36
+ </label>
37
+ </fieldset>
38
+ ```
17
39
 
18
40
  ```html demo
19
41
  <div class="grid-3">
@@ -94,6 +116,52 @@ follows through its `em` sizing. Padding is density, so use a `.compact` or
94
116
  `.spacious` context to change it. Intent classes (`.primary`, `.secondary`) are
95
117
  supported on the `.choice-card` element.
96
118
 
119
+ ## Corner badge
120
+
121
+ A tile whose content is the choice itself — a product image, a centred plan
122
+ name — may want no indicator column, and a check badge over its corner once
123
+ selected (radio/checkbox tile, selected tick, check in the corner). That is a
124
+ recipe on the card's own `::before` (disc) and `::after` (check) layers,
125
+ written in application CSS after the framework:
126
+
127
+ ```css
128
+ .choice-tile {
129
+ --choice-card-check-size: 1.5em;
130
+
131
+ display: block;
132
+ }
133
+
134
+ .choice-tile::before,
135
+ .choice-tile::after {
136
+ position: absolute;
137
+ inset-inline-end: -0.375em;
138
+ inset-block-start: -0.375em;
139
+ margin: 0;
140
+ }
141
+
142
+ .choice-tile::before {
143
+ border: 0;
144
+ box-shadow: 0 0 0 2px var(--surface-opaque);
145
+ transform: scale(0);
146
+ }
147
+
148
+ .choice-tile:has(:checked)::before {
149
+ transform: scale(1);
150
+ }
151
+ ```
152
+
153
+ ```html
154
+ <label class="choice-card choice-tile">
155
+ <input type="radio" name="cpu" value="1" checked />
156
+ <strong>8-core CPU</strong><br />
157
+ <span class="muted">32 GB RAM</span>
158
+ </label>
159
+ ```
160
+
161
+ The badge overhangs the corner, so no ancestor up to the grid may clip with
162
+ `overflow: hidden`. The [Neon Ramen item page](../../demo/sites/neon-ramen/item.html)
163
+ uses it for its sizes and toppings.
164
+
97
165
  ## Chips
98
166
 
99
167
  Add `.chip` for the compact pill form: filter chips, selectable tags, toggle
@@ -101,6 +169,9 @@ pills, multi-select facets. A radio group gives single-choice chips. A leading
101
169
  dot turns into the check in a fixed slot, so checking never moves the label.
102
170
  Use `.cluster` for a wrapping row. A chip is sized like a control: `.btn` and
103
171
  `.input` height, `--control-pad-x` inline padding, `.sm` / `.lg` scale.
172
+ A chip is a `<label>` around its checkbox/radio — never nest a `.close` or any
173
+ other button inside it. A removable value (combobox tag) belongs to the widget
174
+ that produced it, or to a sibling `.badge` + `.close`, not to the chip label.
104
175
 
105
176
  ```html demo
106
177
  <fieldset class="field-group">
@@ -129,8 +200,10 @@ by widening the clip, as the `material` theme does.
129
200
  ## CSS hooks
130
201
 
131
202
  - `--choice-card-pad` — inner padding.
203
+ - `--choice-card-gap` — space between the indicator column and the label.
132
204
  - `--choice-card-radius` — corner radius.
133
- - `--choice-card-check-size` — size of the check indicator.
205
+ - `--choice-card-check-size` — size of the indicator, and the width of its
206
+ column.
134
207
  - `--choice-card-border` / `--choice-card-bg` — resting border and background;
135
208
  the checked state and hover rebind them, so a checked override needs
136
209
  `:has(:checked)`.
@@ -10,7 +10,7 @@ real product needs a JavaScript combobox (searchable select, removable tags,
10
10
  autocomplete suggestions), load a dedicated component and bridge its custom
11
11
  properties onto Actual's tokens.
12
12
 
13
- **Related terms:** combo box, combobox, autocomplete, datalist, searchable select, enhanced select, multiple select, tags, chips, tag input, pill input.
13
+ **Related terms:** combo box, combobox, autocomplete, datalist, select, custom select, native select, searchable select, enhanced select, multiple select, dropdown, form controls, tags, chips, tag input, pill input.
14
14
 
15
15
  ## Theme bridge
16
16
 
@@ -36,6 +36,21 @@ mix tokens.
36
36
 
37
37
  The card in the template documents the full token map of this recipe.
38
38
 
39
+ ## Bridge contract
40
+
41
+ Actual owns the visual language; the widget owns the behavior. Actual provides
42
+ surface, border, radius, inset focus outline, danger state, `.join` corners,
43
+ and the contrast contracts those tokens carry. The widget provides search,
44
+ keyboard navigation, multi-value state, removable tags, and option rendering.
45
+ The bridge CSS stays in docs/demo — it is a recipe, not package API, so no
46
+ `actual-combobox.css` ships with the framework and no widget version is pinned
47
+ by it.
48
+
49
+ Multi-value and i18n (for example a "n selected" summary string) are widget
50
+ configuration: pass locale strings and tag rendering through the widget's own
51
+ options, and keep skinning on the `--cb-*` tokens so light/dark mode and preset
52
+ themes keep applying for free.
53
+
39
54
  ## Lightweight pass-through
40
55
 
41
56
  Most of the skin is three one-line token bridges. The visible vocabulary —