actual-css 0.6.0 → 0.8.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 (143) hide show
  1. package/CHANGELOG.md +214 -1
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1005 -550
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +82 -25
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +99 -2
  18. package/docs/design-notes/popover-manual-poc.md +328 -0
  19. package/docs/design-notes/surface-reparenting.md +131 -0
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +9 -6
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +80 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +16 -6
  30. package/docs/pages/components/fab.md +4 -3
  31. package/docs/pages/components/list.md +42 -4
  32. package/docs/pages/components/navbar.md +17 -1
  33. package/docs/pages/components/pagination.md +2 -2
  34. package/docs/pages/components/steps.md +19 -0
  35. package/docs/pages/components/table.md +3 -2
  36. package/docs/pages/enhancements/flyout.md +144 -24
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +20 -0
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +40 -2
  41. package/docs/pages/examples/overview.md +38 -5
  42. package/docs/pages/forms/choice-cards.md +6 -2
  43. package/docs/pages/forms/combobox.md +1 -1
  44. package/docs/pages/forms/floating-field.md +4 -3
  45. package/docs/pages/forms/joined-controls.md +22 -0
  46. package/docs/pages/forms/otp.md +2 -1
  47. package/docs/pages/forms/overview.md +1 -1
  48. package/docs/pages/forms/states-and-sizes.md +16 -18
  49. package/docs/pages/forms/sticky-actions.md +2 -2
  50. package/docs/pages/forms/switch.md +1 -1
  51. package/docs/pages/foundations/philosophy.md +67 -22
  52. package/docs/pages/foundations/theming.md +10 -2
  53. package/docs/pages/foundations/tokens.md +118 -21
  54. package/docs/pages/foundations/typography.md +35 -14
  55. package/docs/pages/guides/introduction.md +11 -9
  56. package/docs/pages/guides/progressive-enhancement.md +136 -49
  57. package/docs/pages/guides/tailwind.md +3 -2
  58. package/docs/pages/layout/media.md +1 -1
  59. package/docs/pages/layout/overview.md +11 -1
  60. package/docs/pages/layout/scroll-snap.md +3 -3
  61. package/docs/pages/layout/stack.md +17 -9
  62. package/docs/pages/patterns/icon-input.md +4 -2
  63. package/docs/pages/patterns/icon-slot.md +2 -0
  64. package/docs/pages/patterns/intent-nesting.md +1 -1
  65. package/docs/pages/utilities/overview.md +23 -2
  66. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  67. package/docs/pages/utilities/spacing.md +10 -8
  68. package/llms.txt +2 -1
  69. package/package.json +19 -8
  70. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  71. package/src/css/components/accordion.css +42 -3
  72. package/src/css/components/alert.css +25 -13
  73. package/src/css/components/app-nav.css +18 -8
  74. package/src/css/components/avatar.css +13 -1
  75. package/src/css/components/badge.css +78 -12
  76. package/src/css/components/breadcrumb.css +2 -3
  77. package/src/css/components/button.css +9 -16
  78. package/src/css/components/card.css +9 -2
  79. package/src/css/components/chat.css +1 -0
  80. package/src/css/components/data-list.css +1 -1
  81. package/src/css/components/drawer.css +79 -25
  82. package/src/css/components/fab.css +0 -6
  83. package/src/css/components/flyout.css +46 -63
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/join.css +7 -0
  87. package/src/css/components/key.css +10 -0
  88. package/src/css/components/list.css +40 -4
  89. package/src/css/components/menu.css +18 -11
  90. package/src/css/components/modal.css +48 -33
  91. package/src/css/components/navbar.css +37 -4
  92. package/src/css/components/pagination.css +1 -0
  93. package/src/css/components/spinner.css +6 -6
  94. package/src/css/components/status-bar.css +0 -5
  95. package/src/css/components/steps.css +45 -59
  96. package/src/css/components/tab.css +29 -9
  97. package/src/css/components/table.css +5 -5
  98. package/src/css/components/tooltip.css +45 -13
  99. package/src/css/core/base.css +23 -1
  100. package/src/css/core/intents.css +17 -6
  101. package/src/css/core/theme.css +27 -0
  102. package/src/css/core/tokens.css +35 -15
  103. package/src/css/core/variants.css +64 -15
  104. package/src/css/forms/choice-card.css +4 -4
  105. package/src/css/forms/choice.css +6 -3
  106. package/src/css/forms/control.css +2 -1
  107. package/src/css/forms/floating-field.css +3 -3
  108. package/src/css/forms/form.css +4 -3
  109. package/src/css/forms/input-icon.css +20 -14
  110. package/src/css/forms/native.css +6 -1
  111. package/src/css/forms/otp.css +9 -1
  112. package/src/css/forms/range.css +2 -0
  113. package/src/css/forms/select.css +29 -9
  114. package/src/css/forms/switch.css +19 -5
  115. package/src/css/layout/app-layout.css +1 -1
  116. package/src/css/layout/grid.css +50 -76
  117. package/src/css/layout/scroll-snap.css +11 -7
  118. package/src/css/layout/scroller.css +3 -4
  119. package/src/css/layout/stack.css +2 -2
  120. package/src/css/typography/fluid.css +4 -3
  121. package/src/css/typography/prose.css +6 -5
  122. package/src/css/utilities/base.css +2 -1
  123. package/src/css/utilities/extra.css +20 -17
  124. package/src/js/context-menu.js +27 -26
  125. package/src/js/dialog.js +2 -1
  126. package/src/js/dismiss.js +5 -0
  127. package/src/js/enhance.js +34 -2
  128. package/src/js/enhancement-loader.js +36 -43
  129. package/src/js/events.js +0 -3
  130. package/src/js/filter.js +0 -1
  131. package/src/js/floating-space.js +42 -0
  132. package/src/js/flyout.js +9 -44
  133. package/src/js/focus-group.js +7 -0
  134. package/src/js/focus.js +6 -0
  135. package/src/js/menu.js +10 -1
  136. package/src/js/selectors.js +5 -9
  137. package/src/js/status.js +44 -7
  138. package/src/js/surface.js +106 -132
  139. package/src/js/tab.js +6 -1
  140. package/src/js/tooltip.js +152 -53
  141. package/src/js/validation.js +1 -1
  142. package/src/css/components/surface.css +0 -49
  143. package/src/js/floating.js +0 -432
@@ -29,9 +29,15 @@ list markers when order or bullets carry meaning.
29
29
  ```
30
30
 
31
31
  Leading and trailing content are unconstrained: avatars, native choices,
32
- badges, text, switches, and buttons retain their own component sizing. The
33
- middle region has no minimum content floor, so supporting text and long tokens
34
- wrap instead of widening the page.
32
+ badges, text, switches, and buttons retain their own component sizing.
33
+ `.list-item-content` has no minimum content floor, so supporting text and long
34
+ tokens wrap instead of widening the page.
35
+
36
+ Both outer regions are optional, and an absent one costs neither space nor a
37
+ gap: `.list-item-content` always takes the row's free space and
38
+ `.list-item-trailing` always sits on the end edge, whether the row has a
39
+ leading region or not. A settings row — a label, a description, and one
40
+ control on the end edge — is the two-region form.
35
41
 
36
42
  Regions align to the row's first line: the leading check or avatar, the title,
37
43
  and the trailing metadata or control sit on one horizontal line while
@@ -42,6 +48,30 @@ A pure traversal affordance such as a trailing caret can opt out of the
42
48
  first-line rule on its own item with `align-self: center`, keeping metadata
43
49
  rows unaffected.
44
50
 
51
+ ```html demo
52
+ <ul class="list">
53
+ <li class="list-item">
54
+ <label class="list-item-content" for="row-memory">
55
+ <span class="list-item-title">Memory</span>
56
+ <span class="list-item-text">Remember details across conversations.</span>
57
+ </label>
58
+ <span class="list-item-trailing">
59
+ <input class="switch" type="checkbox" role="switch" id="row-memory" checked>
60
+ </span>
61
+ </li>
62
+ <li class="list-item">
63
+ <label class="list-item-content list-item-title" for="row-theme">Theme</label>
64
+ <span class="list-item-trailing">
65
+ <select class="select sm fit" id="row-theme">
66
+ <option>System</option>
67
+ <option>Light</option>
68
+ <option>Dark</option>
69
+ </select>
70
+ </span>
71
+ </li>
72
+ </ul>
73
+ ```
74
+
45
75
  For a navigable row, keep the list semantics and put `.list-item` on the link:
46
76
 
47
77
  ```html demo
@@ -62,10 +92,18 @@ For a navigable row, keep the list semantics and put `.list-item` on the link:
62
92
  Do not attach click JavaScript to the entire row. Use a link for navigation and
63
93
  a button or native control for actions.
64
94
 
95
+ In a master/detail navigation, mark the current row with `aria-current` on its
96
+ link, using the value that matches the semantics (`page`, `location`, …).
97
+ Presence is the contract: set the attribute on the current row and remove it
98
+ elsewhere, never `aria-current="false"`. The [Workspaces example](../examples/overview.md)
99
+ shows a current row with trailing actions and a flyout. Navigable rows usually
100
+ want a non-zero `--list-item-pad-inline`: without that gutter, the current
101
+ indicator touches the leading region and the trailing touches the row edge.
102
+
65
103
  ### Hooks
66
104
 
67
105
  - `--list-item-min-size` controls the row's minimum touch height.
68
106
  - `--list-item-pad-block` and `--list-item-pad-inline` control row padding.
69
- - `--list-item-gap` controls spacing between the three regions.
107
+ - `--list-item-gap` controls spacing between the regions a row actually has.
70
108
  - `--list-divider` controls the leading and inter-row dividers. The final row
71
109
  deliberately has no trailing divider.
@@ -7,6 +7,9 @@
7
7
  - `.navbar-nav` is the horizontal link list; its items are `.nav-link`.
8
8
  - `.nav-list` is the shared vertical navigation list, reused by `.drawer` for stacked links.
9
9
  - Mark the current page with `aria-current="page"` on the active `.nav-link`.
10
+ - Presence is the contract: set `aria-current` on the current link and remove
11
+ it elsewhere, never `aria-current="false"`. Scrollspy follows the same rule
12
+ with `aria-current="location"`.
10
13
 
11
14
  For a public or normally scrolling page, `.navbar` belongs inside the semantic
12
15
  site header. Do not use `.topbar` or `.app-layout` unless the page actually has
@@ -16,6 +19,13 @@ the specialized persistent application-shell behavior those APIs describe.
16
19
  flex list, `.nav-list` a vertical grid. Putting both on one element resolves to
17
20
  `.nav-list` (vertical) — pick one per container.
18
21
 
22
+ `.navbar` stays the horizontal bar for a site or webapp header with a brand and
23
+ normal document scrolling. `.app-nav` is not merely a stacked-icon flavour of it:
24
+ reach for that component only when the destinations are part of a persistent
25
+ application shell — bottom navigation or labelled side navigation via
26
+ `.app-layout`. The choice follows the shell contract, not a preference for
27
+ icons. See [App Navigation](app-navigation.md).
28
+
19
29
  ## Class reference
20
30
 
21
31
  | Class | Kind | Description |
@@ -41,7 +51,9 @@ flex list, `.nav-list` a vertical grid. Putting both on one element resolves to
41
51
 
42
52
  For a vertical sidebar nav, use `.nav-list` inside the drawer or sidebar.
43
53
  Links inside `.nav-list` stretch to the available inline size, providing a
44
- full-row hit target for vertical navigation.
54
+ full-row hit target for vertical navigation. The [Workspaces example](../examples/overview.md)
55
+ shows the sectioned-sidebar composition: grouped `.nav-list` blocks with
56
+ section titles, a pinned account footer, and the native current state.
45
57
 
46
58
  ## Responsive: navbar + drawer
47
59
 
@@ -51,6 +63,10 @@ a `.drawer` with a vertical `.nav-list`, opened by a `command="show-modal"` /
51
63
  `commandfor` trigger. Keep that trigger outside the `<nav>` subtree you hide at
52
64
  narrow widths, or hiding the nav hides the control that opens it.
53
65
 
66
+ When the compact layout drops labels, keep that text in the DOM and hide it
67
+ visually instead of replacing it with `aria-label` strings: one source for the
68
+ accessible name.
69
+
54
70
  ```html demo
55
71
  <nav class="navbar" aria-label="Main">
56
72
  <a class="navbar-brand" href="/">Actual CSS</a>
@@ -5,7 +5,7 @@
5
5
  - Use a semantic `<nav>` landmark.
6
6
  - Put `.pagination` on the ordered list.
7
7
  - Use `aria-current="page"` for the current page.
8
- - Put `.sm` or `.lg` on `.pagination` to change the density of its controls.
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
10
  - Prefer text labels for Previous and Next. Icon-only controls need an accessible name.
11
11
  - Page links can compose with `.btn` for button-like hit targets.
@@ -17,7 +17,7 @@
17
17
  | Class | Kind | Description |
18
18
  | ------------- | --------- | ------------------------------ |
19
19
  | `.pagination` | Component | Ordered list of page controls. |
20
- | `.sm` / `.lg` | Size | Control density. |
20
+ | `.sm` / `.lg` | Size | Control geometry and type. |
21
21
 
22
22
  ## Basic usage
23
23
 
@@ -65,6 +65,25 @@ smaller than body text, and neutral in weight. Only the current step's label is
65
65
  emphasised — it is where you are now. A completed step needs no bold text; its
66
66
  filled marker already says as much.
67
67
 
68
+ ## Theming the states
69
+
70
+ The complete and current states consume the shared
71
+ `--state-selected` / `--state-selected-fg` tokens, which default to
72
+ `--primary` / `--primary-fg`. A `--primary` recolor therefore flows through
73
+ without any stepper-specific rule. To theme only this sequence with a local
74
+ accent, override the tokens on the component:
75
+
76
+ ```css
77
+ .checkout-steps {
78
+ --state-selected: var(--success);
79
+ --state-selected-fg: var(--success-fg);
80
+ }
81
+ ```
82
+
83
+ `--state-selected` drives the completed fill, connector and the current ring;
84
+ `--state-selected-fg` corrects the number inside a completed marker when the
85
+ accent needs a lighter ink.
86
+
68
87
  ## Horizontal steps
69
88
 
70
89
  `.steps-horizontal` is designed for **2 to 5 stages**.
@@ -100,8 +100,9 @@ table width before the wrapper scrolls horizontally.
100
100
 
101
101
  ## Compact tables
102
102
 
103
- `.compact` lowers the cell padding only — density never changes typography.
104
- It is the same vocabulary as `.card.compact`.
103
+ `.compact` lowers cell padding and establishes compact density for controls or
104
+ spacing helpers inside cells. Typography and icons stay unchanged. It is the
105
+ same vocabulary as `.card.compact`.
105
106
 
106
107
  ```html demo
107
108
  <div class="table-wrap">
@@ -19,8 +19,8 @@
19
19
 
20
20
  `.menu` has a strict `.menu > li > .menu-item` anatomy.
21
21
  | `.menu-label` | Variant | Muted section heading inside a menu; not interactive. |
22
- | `.menu-separator` | Variant | Divider between menu groups (`<hr role="separator">`). |
23
- | `.sm` / `.lg` | Variant | Compact or large density. |
22
+ | `.menu-separator` | Variant | Divider between menu groups (`<hr>`). |
23
+ | `.sm` / `.lg` | Size | Smaller or larger menu rows and type. |
24
24
 
25
25
  ## Usage
26
26
 
@@ -28,7 +28,12 @@ Flyout and context menu share one action-surface runtime:
28
28
 
29
29
  - Flyout = a visible trigger opens a surface.
30
30
  - Context menu = right click or a keyboard context action opens that same surface.
31
- - Sheet = mobile presentation mode of that same surface.
31
+ - Flyouts and context menus remain anchored, non-modal popovers at every viewport size.
32
+
33
+ One surface is open at a time. Opening any of them closes the others, so a
34
+ trigger placed inside an open panel replaces that panel instead of opening a
35
+ second level. Submenus and other nested panels are outside the current
36
+ contract.
32
37
 
33
38
  ### Trigger wiring
34
39
 
@@ -44,7 +49,8 @@ contract:
44
49
  context for the no-JS fallback: without it the panel lays out against the
45
50
  nearest positioned ancestor (or the page edge) instead of under the trigger.
46
51
  - Start the panel with `hidden` so it does not flash open before the runtime
47
- hides and positions it.
52
+ takes it over. The runtime then removes the attribute: a closed panel is
53
+ hidden by the popover transport, and `[hidden]` would outrank it.
48
54
 
49
55
  ```html
50
56
  <div class="flyout-trigger">
@@ -57,16 +63,9 @@ contract:
57
63
  </div>
58
64
  ```
59
65
 
60
- Use `data-flyout-mobile` on the flyout to control mobile behavior:
61
-
62
- - `auto` is the default. It keeps the surface anchored on desktop and switches to a bottom sheet on coarse pointers below the breakpoint.
63
- - `sheet` always uses the sheet presentation.
64
- - `anchored` always uses floating positioning.
65
- - `none` disables the mobile transformation.
66
-
67
- `data-flyout-breakpoint` only matters for `auto`. Prefer the built-in tokens
68
- (`sm`, `md`, `lg`) and use raw pixel values only as an escape hatch, for example
69
- `data-flyout-breakpoint="640"`.
66
+ For a modal bottom sheet or action sheet, use `dialog.drawer`; use
67
+ `dialog.modal` for a centered modal. Those components own modal focus,
68
+ dismissal, scroll locking, and backdrop behavior explicitly.
70
69
 
71
70
  Use `data-flyout-placement` for the preferred anchored placement. It accepts the
72
71
  placement strings supported by the floating runtime, such as `bottom-start`,
@@ -74,8 +73,101 @@ placement strings supported by the floating runtime, such as `bottom-start`,
74
73
 
75
74
  Use `data-flyout-distance` for the trigger gap in pixels. The default is `4`.
76
75
 
77
- Use `data-flyout-auto-close` when the default click dismissal behavior is not
78
- right. Escape still follows the shared surface lifecycle.
76
+ ### Native popover lifecycle
77
+
78
+ The `.flyout` presentation also composes with an element carrying `popover`.
79
+ Actual neutralizes the native popover geometry that conflicts with anchored
80
+ positioning and recognizes `:popover-open` as the visible state. The browser or
81
+ another library may therefore own opening, dismissal, and focus without
82
+ requiring `.is-open` or `[hidden]`.
83
+
84
+ Actual CSS does not position that independently managed popover. Its positioner
85
+ must couple fixed positioning with viewport coordinates or absolute positioning
86
+ with document coordinates (and may write `--available-height` and
87
+ `--surface-anchor-width`). The built-in `surface.js` runtime remains the full
88
+ fallback when Actual owns the lifecycle too. A bare `[popover]` without
89
+ `.flyout` or `.tooltip` keeps its platform appearance.
90
+
91
+ The Popover lifecycle is guaranteed across Minimal, so choosing it is a
92
+ behavior decision rather than a support one: native light dismiss closes on
93
+ outside clicks only, which cannot express `data-flyout-auto-close`. The two
94
+ lifecycles are alternatives, not layers.
95
+
96
+ `surface.js` itself uses `popover="manual"` as its transport — the panel is
97
+ promoted to the top layer where it stands, so a scoped theme, density, or
98
+ custom property still reaches it. That transport supplies no behavior, so the
99
+ runtime keeps owning dismissal, Escape ordering, focus and placement.
100
+
101
+ #### Choose one lifecycle owner
102
+
103
+ The Actual runtime path, where `surface.js` owns opening, dismissal, focus and
104
+ placement:
105
+
106
+ ```html
107
+ <div class="flyout-trigger">
108
+ <button data-enhance="flyout" aria-controls="actions" aria-expanded="false">
109
+ Actions
110
+ </button>
111
+
112
+ <ul id="actions" class="flyout menu" hidden>
113
+ <li><button class="menu-item" type="button">Rename</button></li>
114
+ </ul>
115
+ </div>
116
+ ```
117
+
118
+ The native path, where the browser owns the lifecycle and the top layer:
119
+
120
+ ```html
121
+ <button popovertarget="actions">Actions</button>
122
+
123
+ <ul id="actions" class="flyout menu" popover>
124
+ <li><button class="menu-item" type="button">Rename</button></li>
125
+ </ul>
126
+ ```
127
+
128
+ Do not combine them. `surface.js` writes `popover="manual"` on every panel it
129
+ manages and drives `showPopover()` / `hidePopover()` itself, so a
130
+ `popover="auto"` panel handed to the runtime has its mode overwritten — the
131
+ runtime keeps one lifecycle owner rather than letting native light dismiss
132
+ compete with `data-flyout-auto-close`. Keep `popovertarget` for panels the
133
+ runtime does not enhance.
134
+
135
+ **The native path still needs a positioner.** `popovertarget` gives the panel an
136
+ implicit anchor reference to its invoker, but it does not place it: Actual's
137
+ geometry reset clears the UA's centering, so an unpositioned native flyout is
138
+ laid out at its static position rather than under its trigger. Supply
139
+ coordinates the same way `surface.js` does:
140
+
141
+ ```js
142
+ import { autoUpdate, reposition } from "@lekoala/floating";
143
+
144
+ const trigger = document.querySelector("[popovertarget='actions']");
145
+ const panel = document.getElementById("actions");
146
+
147
+ // One tracker at a time. autoUpdate returns its own cleanup; releasing it
148
+ // before every state change keeps repeated opens from stacking trackers, and
149
+ // stops the scroll/resize work as soon as the panel closes.
150
+ let stop;
151
+
152
+ panel.addEventListener("toggle", (event) => {
153
+ stop?.();
154
+ stop = undefined;
155
+ if (event.newState !== "open") return;
156
+
157
+ stop = autoUpdate(trigger, panel, () => {
158
+ reposition(trigger, panel, { placement: "bottom-start", distance: 4 });
159
+ });
160
+ });
161
+ ```
162
+
163
+ CSS Anchor Positioning will eventually cover the simple anchored case from the
164
+ stylesheet alone, using that implicit anchor reference. It is still only
165
+ partially supported and is not the documented recipe yet.
166
+
167
+ **When `surface.js` owns the lifecycle**, use `data-flyout-auto-close` if the
168
+ default click dismissal behavior is not right. Escape still follows the shared
169
+ surface lifecycle. These options do not reach a natively managed
170
+ `popover="auto"`, whose dismissal belongs to the browser.
79
171
 
80
172
  - `true` is the default. Clicks inside or outside close the flyout.
81
173
  - `inside` closes on inside clicks only.
@@ -108,9 +200,9 @@ A list of *actions* the user can take: sign out, copy, delete.
108
200
  - Flyout triggers are opt-in: add `data-enhance="flyout"` to the trigger.
109
201
  - Wrap the trigger and flyout in `.flyout-trigger` when the flyout should have a local absolute-position fallback before JavaScript positions it. Add `.stretch` when the trigger must span its container, such as the last row of a full-width sidebar nav list.
110
202
  - Use `<menu class="flyout menu">` with strict anatomy: `.menu > li > .menu-item`. Items must carry the `.menu-item` class to participate in directional keyboard navigation. ArrowUp/Down and Home/End move focus without rewriting their normal tab stops.
111
- - Use `.menu-label` on a `li` for a muted section heading inside a menu (e.g. a group title before its items). It is non-interactive and does not participate in roving focus. Use `.menu-separator` (`<hr role="separator">`) between groups.
203
+ - Use `.menu-label` on a `li` for a muted section heading inside a menu (e.g. a group title before its items). It is non-interactive and does not participate in roving focus. Use `.menu-separator` (a plain `<hr>`) between groups.
112
204
  - Items are regular `<button>` or `<a>` elements.
113
- - Use `.sm` or `.lg` for density changes.
205
+ - Use `.sm` or `.lg` to scale one flyout; use a density context for surrounding UI.
114
206
  - Add `role="menu"` / `role="menuitem"` only when you intentionally need the ARIA menu pattern described below.
115
207
 
116
208
  ```html demo
@@ -131,7 +223,7 @@ A list of *actions* the user can take: sign out, copy, delete.
131
223
  hidden>
132
224
  <li><button class="menu-item" type="button">Profile</button></li>
133
225
  <li><button class="menu-item" type="button">Settings</button></li>
134
- <hr class="menu-separator" role="separator">
226
+ <hr class="menu-separator">
135
227
  <li><button class="menu-item danger" type="button">Sign out</button></li>
136
228
  </menu>
137
229
  </div>
@@ -149,6 +241,36 @@ tab stop instead.
149
241
  The runtime owns only focus movement and `tabindex`. The application still owns
150
242
  command state and updates attributes such as `aria-checked`.
151
243
 
244
+ ### Linting `role="menu"`
245
+
246
+ Biome's `a11y/noNoninteractiveElementToInteractiveRole` reports
247
+ `<menu role="menu">`, and the fix it offers — remove the role — dismantles the
248
+ pattern. It is a port of
249
+ `jsx-a11y/no-noninteractive-element-to-interactive-role` that lost the source
250
+ rule's default allowlist, and that allowlist exists to permit exactly this
251
+ markup: the source documents `<ul role="menu">` as *valid*, matching the
252
+ WAI-ARIA APG menu button pattern. Every ARIA composite built on a list is
253
+ affected, not only menus — `listbox`, `tablist`, `menubar`, `tree`, `grid`.
254
+
255
+ Until it is fixed upstream, turn the rule off:
256
+
257
+ ```json
258
+ {
259
+ "linter": {
260
+ "rules": {
261
+ "a11y": { "noNoninteractiveElementToInteractiveRole": "off" }
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ One line, and no `biome-ignore` comment above every menu in your markup. The
268
+ rest of the `a11y` group stays on and is worth keeping — with just this rule
269
+ off, the menu anatomy above lints clean.
270
+
271
+ [Accessibility decisions](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/accessibility-decisions.md)
272
+ records which patterns Biome rejects, and which of its findings were right.
273
+
152
274
  ### Rich items and selection state
153
275
 
154
276
  Wrap rich item content in the three optional slots when a menu needs aligned
@@ -214,7 +336,6 @@ can be multi-column with `<section>` / `<ul>` groups.
214
336
  <div class="flyout"
215
337
  id="products-panel"
216
338
  aria-label="Products"
217
- data-flyout-mobile="auto"
218
339
  hidden>
219
340
  <section aria-labelledby="products-design">
220
341
  <h3 id="products-design">Design</h3>
@@ -247,8 +368,8 @@ can be multi-column with `<section>` / `<ul>` groups.
247
368
  ## Mega menu
248
369
 
249
370
  Use `class="flyout grid-3"` when a nav panel needs multiple link groups. Keep
250
- links regular anchors and let the panel collapse to a one-column sheet on mobile
251
- with `data-flyout-mobile="auto"`.
371
+ links as regular anchors. The panel remains an anchored popover on mobile; use
372
+ a separate `dialog.drawer` when the content needs a modal mobile presentation.
252
373
 
253
374
  ```html demo
254
375
  <nav aria-label="Product navigation">
@@ -266,7 +387,6 @@ with `data-flyout-mobile="auto"`.
266
387
  <div class="flyout grid-3"
267
388
  id="product-mega-menu"
268
389
  aria-label="Platform"
269
- data-flyout-mobile="auto"
270
390
  style="--flyout-inline-size: 42rem; --flyout-max-inline-size: 42rem"
271
391
  hidden>
272
392
  <section aria-labelledby="mega-design">
@@ -367,7 +487,7 @@ delay; a number sets the delay in milliseconds.
367
487
  hidden>
368
488
  <li><button class="menu-item" type="button">Open</button></li>
369
489
  <li><button class="menu-item" type="button">Rename</button></li>
370
- <hr class="menu-separator" role="separator">
490
+ <hr class="menu-separator">
371
491
  <li><button class="menu-item danger" type="button">Delete</button></li>
372
492
  </menu>
373
493
  </div>
@@ -13,6 +13,31 @@ class API, shared tokens, and progressive enhancement.
13
13
  - Use shared button classes and variants for triggers.
14
14
  - Do not add toasts. Use alerts, status regions, dialogs, or inline validation instead.
15
15
 
16
+ ## Popover ownership
17
+
18
+ Actual drives the top layer itself. On every surface and tooltip its runtime
19
+ manages, `surface.js` and `tooltip.js` own the `popover` attribute and both
20
+ `showPopover()` and `hidePopover()`.
21
+
22
+ On a managed element, an application must not:
23
+
24
+ - set or change `popover`. The runtime writes `manual` over any other value, because `popover="auto"` hands the browser a dismissal policy of its own that competes with `data-flyout-auto-close`.
25
+ - call `showPopover()`, `hidePopover()`, or `togglePopover()`.
26
+ - cancel its `beforetoggle`. Preventing an open leaves Actual's state saying open while the element is not in the top layer, and nothing repairs that — Actual owns the lifecycle rather than mediating it.
27
+
28
+ To stop a surface from opening, cancel Actual's own `actual:surface-open`
29
+ instead. It is dispatched before anything is promoted, so cancelling it leaves
30
+ no state behind.
31
+
32
+ **What counts as managed is the runtime's decision, not the attribute's.** A
33
+ panel is managed when a `data-enhance="flyout"` trigger points at it through
34
+ `aria-controls`, or when a `data-context-menu` target names it; a tooltip is
35
+ managed when a `data-tooltip` trigger points at it. A `.flyout[popover]` or
36
+ `.tooltip[popover]` that no such trigger reaches is never touched — Actual
37
+ supplies its presentation and the application owns its lifecycle. That is the
38
+ supported way to drive either class with `popovertarget` or a third-party
39
+ lifecycle. Do not do both to the same element.
40
+
16
41
  ## Modules
17
42
 
18
43
  The full runtime is `actual-css/js/full`. Each enhancer is also importable on its own
@@ -15,6 +15,7 @@ runtime targets, empty by default.
15
15
  - Use it for transient status (`Saved.`, `Reconnected.`, `Copied.`). Critical, persistent, or actionable information belongs in `.alert`, inline messages, or dialogs.
16
16
  - Intents: `danger`, `success`, `warning`, `neutral`. The default (no intent) is a neutral dark pill.
17
17
  - On mobile, the runtime keeps the bar above the software keyboard by tracking `visualViewport` while a message is showing.
18
+ - A message originating inside an open modal dialog temporarily moves the singleton bar into that dialog. A modal inerts the rest of the document, and being painted above it would not undo that — inertness is computed on the DOM — so the bar has to be in the dialog's subtree to be announced and seen. The bar returns to its original DOM position after its exit.
18
19
 
19
20
  Clearing is deferred, not immediate: the bar closes first and only empties its
20
21
  text and intent once the exit transition has finished, so it leaves at the size
@@ -77,6 +78,25 @@ document.dispatchEvent(new CustomEvent("actual:status", {
77
78
  }));
78
79
  ```
79
80
 
81
+ Dispatch from the control or region that produced the feedback when it lives
82
+ inside a dialog; bubbling lets the runtime infer the active surface:
83
+
84
+ ```js
85
+ saveButton.dispatchEvent(new CustomEvent("actual:status", {
86
+ bubbles: true,
87
+ detail: { message: "Saved.", intent: "success" },
88
+ }));
89
+ ```
90
+
91
+ Code importing `status()` directly passes the same origin as `source`:
92
+
93
+ ```js
94
+ status("Saved.", { intent: "success", source: saveButton });
95
+ ```
96
+
97
+ Validation and declarative status commands propagate their source
98
+ automatically.
99
+
80
100
  ## CSS hooks
81
101
 
82
102
  - `--status-bg` — bar background.
@@ -14,51 +14,59 @@
14
14
 
15
15
  - Use real tab semantics when panels switch in place.
16
16
  - Use normal links and `aria-current="page"` for page navigation that only looks like tabs.
17
+ - On that link variant, `aria-current="page"` on the current link gets the same
18
+ color and underline treatment as `aria-selected="true"` on a widget tab;
19
+ `primary` tints it.
20
+ - Tab selection never changes text metrics: the active tab keeps the shared
21
+ weight and is marked by color and the indicator line.
17
22
  - JavaScript owns roving `tabindex`, `aria-selected`, `hidden`, and keyboard behavior.
18
23
  - Left/Right select tabs and wrap at the ends. Home/End jump to first/last. Down moves focus into the selected panel.
19
24
  - A tab list needs both `.tabs` and `role="tablist"`; `.tab` styles each trigger.
20
25
  - A `.tab` with an icon uses `--tab-gap` (default `0.375em`) for the space between icon and label.
26
+ - A tab that is `hidden`, `disabled`, `aria-disabled`, or has no panel is skipped
27
+ by the arrow keys. An application filtering a tab strip only has to set
28
+ `hidden`; deciding what to show when the selected tab is filtered out is the
29
+ application's call.
21
30
 
22
31
  ```html demo
23
32
  <div class="tabs" data-enhance="tabs" role="tablist" aria-label="Settings">
24
- <button class="tab primary"
25
- type="button"
26
- role="tab"
27
- aria-selected="true"
28
- aria-controls="panel-general"
29
- id="tab-general">
30
- General
31
- </button>
32
- <button class="tab"
33
- type="button"
34
- role="tab"
35
- aria-selected="false"
36
- aria-controls="panel-security"
37
- id="tab-security"
38
- tabindex="-1">
39
- Security
40
- </button>
41
- <button class="tab"
42
- type="button"
43
- role="tab"
44
- aria-selected="false"
45
- aria-controls="panel-billing"
46
- id="tab-billing"
47
- tabindex="-1">
48
- Billing
49
- </button>
50
- </div>
51
-
52
- <section role="tabpanel" id="panel-general" aria-labelledby="tab-general" tabindex="-1" class="py">
53
- General content
54
- </section>
55
- <section role="tabpanel" id="panel-security" aria-labelledby="tab-security" tabindex="-1" hidden class="py">
56
- Security content
57
- </section>
58
- <section role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabindex="-1" hidden class="py">
59
- Billing content
60
- </section>
33
+ <button class="tab primary"
34
+ type="button"
35
+ role="tab"
36
+ aria-selected="true"
37
+ aria-controls="panel-general"
38
+ id="tab-general">
39
+ General
40
+ </button>
41
+ <button class="tab"
42
+ type="button"
43
+ role="tab"
44
+ aria-selected="false"
45
+ aria-controls="panel-security"
46
+ id="tab-security"
47
+ tabindex="-1">
48
+ Security
49
+ </button>
50
+ <button class="tab"
51
+ type="button"
52
+ role="tab"
53
+ aria-selected="false"
54
+ aria-controls="panel-billing"
55
+ id="tab-billing"
56
+ tabindex="-1">
57
+ Billing
58
+ </button>
61
59
  </div>
60
+
61
+ <section role="tabpanel" id="panel-general" aria-labelledby="tab-general" tabindex="-1" class="py">
62
+ General content
63
+ </section>
64
+ <section role="tabpanel" id="panel-security" aria-labelledby="tab-security" tabindex="-1" hidden class="py">
65
+ Security content
66
+ </section>
67
+ <section role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabindex="-1" hidden class="py">
68
+ Billing content
69
+ </section>
62
70
  ```
63
71
 
64
72
  For navigation that only looks like tabs, keep it a plain link list — no
@@ -5,11 +5,14 @@
5
5
  Tooltips are supplemental: `data-tooltip` on the trigger wires a `.tooltip`
6
6
  element (`role="tooltip"`) that appears near the trigger. Position, placement,
7
7
  and arrow position are written by JavaScript; the element is shown and hidden
8
- with the `hidden` attribute.
8
+ by promoting it to the top layer with `popover="manual"`.
9
9
 
10
10
  - Use `data-tooltip` on the trigger. With text (`data-tooltip="Help"`), the tooltip element is generated. Empty (`data-tooltip`) marks an explicit tooltip connected via `aria-describedby`.
11
11
  - Tooltips are supplemental. Do not put required information or interactive controls inside them.
12
- - Show on hover and focus. Hide on Escape, blur, pointer leave, or scroll when appropriate.
12
+ - Show on hover and focus. Hide on Escape, blur, or pointer leave.
13
+ - A trigger scrolled out of view takes its tooltip down and brings it back when it returns; only a dismissal ends the tooltip.
14
+ - Escape outlasts the hover and focus that justified the tooltip (WCAG 2.1 SC 1.4.13); showing it again takes a fresh hover or focus, not a second click on a trigger that already has focus.
15
+ - Author an explicit tooltip with `hidden`. The runtime removes it when it takes the lifecycle over; until then it is what keeps the tooltip off screen.
13
16
  - JavaScript can generate tooltip elements from `data-tooltip`.
14
17
  - Add `data-tooltip-click` to toggle it on click instead of hover/focus.
15
18
  - Add `data-tooltip-visible` to show it immediately and keep it visible while its trigger is in view.
@@ -17,6 +20,32 @@ with the `hidden` attribute.
17
20
  - The arrow inherits the tooltip background — custom gradients carry through automatically.
18
21
  - Avoid relying on the native `title` attribute as the primary implementation.
19
22
 
23
+ A `.tooltip[popover]` may use a native or third-party-owned popover lifecycle.
24
+ Actual removes the native inset, margin, and overflow that conflict with
25
+ positioning and with the projecting arrow, while leaving bare `[popover]`
26
+ elements untouched. The lifecycle owner still writes `position`, the
27
+ coordinates, and the arrow position; Actual only supplies the presentation.
28
+
29
+ Do not combine them. `tooltip.js` writes `popover="manual"` on every tooltip it
30
+ manages and calls `showPopover()` / `hidePopover()` itself, so a tooltip handed
31
+ to the runtime has its mode overwritten. The runtime only ever reaches a
32
+ tooltip that a `data-tooltip` trigger points at, so a `.tooltip[popover]` with
33
+ no such trigger stays yours to drive. Use `data-tooltip` and the built-in
34
+ runtime when Actual should own showing and hiding.
35
+
36
+ **Where the element lives is part of the contract.** An explicit tooltip stays
37
+ exactly where you wrote it, so it keeps every scope that reaches it by
38
+ inheritance — a theme island, a density scope, `.inverted`, your own scoped
39
+ custom properties. It also means placement is yours to get right: a tooltip
40
+ used from inside a modal dialog must be authored inside that dialog, because
41
+ the top layer does not lift an element out of a modal's inertness.
42
+
43
+ A generated tooltip (`data-tooltip="Help"`) is Actual's to place, and it goes
44
+ at document level rather than beside the trigger, so it cannot disturb the
45
+ trigger's structural position — a generated sibling would take
46
+ `.join > :last-child` from a trigger that ends a group. It therefore makes no
47
+ inheritance promise. Use the explicit form when the local CSS context matters.
48
+
20
49
  ```html demo
21
50
  <button class="btn ghost"
22
51
  type="button"
@@ -86,6 +115,15 @@ viewport-aware maximum width.
86
115
  Set `data-tooltip-placement` to control where the tooltip appears relative to
87
116
  its trigger. The arrow follows the placement automatically.
88
117
 
118
+ The runtime also picks the coordinate space, and writes `position` to match. A
119
+ tooltip whose trigger scrolls with the page is placed in document coordinates
120
+ and `position: absolute`, so the browser scrolls it with the page instead of
121
+ the positioner correcting it a frame later — the lag visible while scrolling on
122
+ a touch device. A trigger that is viewport-anchored — inside a fixed or sticky
123
+ ancestor, an open popover, or a modal dialog — keeps `position: fixed`. Do not
124
+ override `position` on a tooltip the runtime manages. See
125
+ [Tooltip coordinate space](https://github.com/lekoala/actual-css/blob/master/docs/design-notes/tooltip-coordinate-space.md).
126
+
89
127
  ```html demo
90
128
  <p>
91
129
  <button class="btn outline" type="button" data-tooltip="Above the button" data-tooltip-placement="top">Top</button>