@adia-ai/web-components 0.8.24 → 0.8.26

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 (34) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/components/anchor-bar/anchor-bar.a2ui.json +139 -0
  3. package/components/anchor-bar/anchor-bar.class.js +124 -0
  4. package/components/anchor-bar/anchor-bar.css +140 -0
  5. package/components/anchor-bar/anchor-bar.d.ts +35 -0
  6. package/components/anchor-bar/anchor-bar.examples.md +32 -0
  7. package/components/anchor-bar/anchor-bar.js +17 -0
  8. package/components/anchor-bar/anchor-bar.yaml +167 -0
  9. package/components/chart/chart.a2ui.json +16 -0
  10. package/components/chart/chart.class.js +105 -27
  11. package/components/chart/chart.css +27 -0
  12. package/components/chart/chart.d.ts +4 -0
  13. package/components/chart/chart.yaml +28 -0
  14. package/components/index.js +1 -0
  15. package/components/richtext/richtext.css +7 -2
  16. package/components/stat/stat.a2ui.json +8 -0
  17. package/components/stat/stat.css +39 -0
  18. package/components/stat/stat.d.ts +2 -0
  19. package/components/stat/stat.js +5 -0
  20. package/components/stat/stat.yaml +11 -0
  21. package/components/tooltip/tooltip.class.js +17 -11
  22. package/dist/host.min.css +1 -1
  23. package/dist/host.sheet.js +1 -1
  24. package/dist/theme-provider.min.js +2 -2
  25. package/dist/web-components.min.css +1 -1
  26. package/dist/web-components.min.js +113 -113
  27. package/dist/web-components.sheet.js +1 -1
  28. package/package.json +1 -1
  29. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +81 -93
  30. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +12 -25
  31. package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +8 -52
  32. package/styles/api/parametric-scope.css +81 -0
  33. package/styles/components.css +1 -0
  34. package/styles/tokens.css +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.8.24",
3
+ "version": "0.8.26",
4
4
  "description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <h1>Bulk Action Toolbar</h1>
4
4
  <div data-actions>
5
- <tag-ui size="sm">table-toolbar-ui</tag-ui> <tag-ui size="sm">toolbar-ui</tag-ui> <tag-ui size="sm">table-ui</tag-ui> <tag-ui size="sm">card-ui</tag-ui>
5
+ <tag-ui size="sm">table-toolbar-ui</tag-ui> <tag-ui size="sm">toolbar-ui</tag-ui> <tag-ui size="sm">table-ui</tag-ui> <tag-ui size="sm">card-ui</tag-ui> <tag-ui size="sm">anchor-bar-ui</tag-ui>
6
6
  </div>
7
7
  </div>
8
8
  <p>When a table has row selection, surface a contextual bulk-action bar. Two variants: swap the standard <code>&lt;table-toolbar-ui&gt;</code> for a bar at the same surface position, or float a card-like elevated bar at the bottom of the table's own scroll container. Selection count anchors the left, actions flow from the right, destructive action last.</p>
@@ -67,7 +67,7 @@
67
67
 
68
68
  <section data-section data-property="floating-variant" data-chunk="bulk-action-toolbar-floating" data-chunk-kind="block"
69
69
  data-chunk-domain="data"
70
- data-chunk-description="Floating bottom-anchored bulk-action bar — a card-like elevated toolbar (elevation 3) that appears sticky to the bottom of a table's own scroll container (not the browser viewport) when rows are selected, with selection count, an optional per-flow controls slot, actions with destructive last, and an explicit Clear button. Composes table-ui's select event with card-ui + toolbar-ui; no focus steal on appearance, Escape clears selection when the bar owns focus, count is aria-live=polite."
70
+ data-chunk-description="Floating bottom-anchored bulk-action bar — an anchor-bar-ui elevated toolbar that appears sticky to the bottom of a table's own scroll container (not the browser viewport) when rows are selected, with selection count, an optional per-flow controls slot, actions with destructive last, and an explicit Clear button. Composes table-ui's select event with anchor-bar-ui + toolbar-ui; no focus steal on appearance, Escape clears selection when the bar owns focus, count is aria-live=polite."
71
71
  data-chunk-keywords="floating bulk action bar bottom anchored sticky selection toolbar card elevated table scroll container escape clear aria-live gmail linear notion">
72
72
  <h2 variant="section">Floating (bottom-anchored)</h2>
73
73
  <p data-note>
@@ -83,38 +83,36 @@
83
83
  <div data-table-scroll>
84
84
  <table-ui frameless id="pat-bulk-float-table" selectable sortable striped></table-ui>
85
85
 
86
- <card-ui id="pat-bulk-float-bar" elevation="3" data-bulk-bar-floating>
87
- <section>
88
- <!-- align="space-between" pushes the trailing group right
89
- NOT a flex-grow spacer span or an auto margin on a content
90
- item. toolbar-ui's overflow reflow sums each top-level
91
- child's RENDERED width to decide what fits; any child that
92
- absorbs free space (flex-grow, or an auto margin resolved
93
- to real pixels post-layout) has already expanded to fill
94
- that space by the time it's measured, so the sum always
95
- reads "full" and spuriously spills content. `justify-content`
96
- only repositions flex items it doesn't touch their
97
- individual widths (toolbar-ui's own CSS pins every direct
98
- child at `flex: 0 0 auto`) — so grouping into two
99
- `<toolbar-group-ui>` atoms and letting `align="space-between"`
100
- do the pushing keeps the reflow's width sum accurate. -->
101
- <toolbar-ui role="toolbar" aria-label="Bulk actions" align="space-between">
102
- <toolbar-group-ui>
103
- <text-ui strong aria-live="polite"><span data-bulk-float-count>0</span> selected</text-ui>
104
- <span data-bulk-controls></span>
105
- </toolbar-group-ui>
106
- <toolbar-group-ui>
107
- <button-ui icon="archive" icon-only aria-label="Archive" variant="ghost" size="sm" data-bulk-action="archive"></button-ui>
108
- <button-ui icon="download-simple" icon-only aria-label="Export" variant="ghost" size="sm" data-bulk-action="export"></button-ui>
109
- <button-ui icon="folder-simple" icon-only aria-label="Move" variant="ghost" size="sm" data-bulk-action="move"></button-ui>
110
- <divider-ui vertical></divider-ui>
111
- <button-ui icon="trash" icon-only aria-label="Delete" variant="ghost" size="sm" data-bulk-action="delete"></button-ui>
112
- <divider-ui vertical></divider-ui>
113
- <button-ui text="Clear" variant="ghost" size="sm" data-bulk-clear></button-ui>
114
- </toolbar-group-ui>
115
- </toolbar-ui>
116
- </section>
117
- </card-ui>
86
+ <anchor-bar-ui id="pat-bulk-float-bar" anchor="bottom">
87
+ <!-- align="space-between" pushes the trailing group right —
88
+ NOT a flex-grow spacer span or an auto margin on a content
89
+ item. toolbar-ui's overflow reflow sums each top-level
90
+ child's RENDERED width to decide what fits; any child that
91
+ absorbs free space (flex-grow, or an auto margin resolved
92
+ to real pixels post-layout) has already expanded to fill
93
+ that space by the time it's measured, so the sum always
94
+ reads "full" and spuriously spills content. `justify-content`
95
+ only repositions flex items it doesn't touch their
96
+ individual widths (toolbar-ui's own CSS pins every direct
97
+ child at `flex: 0 0 auto`) so grouping into two
98
+ `<toolbar-group-ui>` atoms and letting `align="space-between"`
99
+ do the pushing keeps the reflow's width sum accurate. -->
100
+ <toolbar-ui role="toolbar" aria-label="Bulk actions" align="space-between">
101
+ <toolbar-group-ui>
102
+ <text-ui strong aria-live="polite"><span data-bulk-float-count>0</span> selected</text-ui>
103
+ <span data-bulk-controls></span>
104
+ </toolbar-group-ui>
105
+ <toolbar-group-ui>
106
+ <button-ui icon="archive" icon-only aria-label="Archive" variant="ghost" size="sm" data-bulk-action="archive"></button-ui>
107
+ <button-ui icon="download-simple" icon-only aria-label="Export" variant="ghost" size="sm" data-bulk-action="export"></button-ui>
108
+ <button-ui icon="folder-simple" icon-only aria-label="Move" variant="ghost" size="sm" data-bulk-action="move"></button-ui>
109
+ <divider-ui vertical></divider-ui>
110
+ <button-ui icon="trash" icon-only aria-label="Delete" variant="ghost" size="sm" data-bulk-action="delete"></button-ui>
111
+ <divider-ui vertical></divider-ui>
112
+ <button-ui text="Clear" variant="ghost" size="sm" data-bulk-clear></button-ui>
113
+ </toolbar-group-ui>
114
+ </toolbar-ui>
115
+ </anchor-bar-ui>
118
116
  </div>
119
117
  </section>
120
118
  </card-ui>
@@ -165,10 +163,12 @@ table.addEventListener('select', (e) =&gt; {
165
163
  <p data-note>
166
164
  <code>[data-table-scroll]</code> wraps the table and IS its scroll container (bounded height +
167
165
  <code>overflow: auto</code>); the floating bar is a sibling of <code>&lt;table-ui&gt;</code>
168
- inside it, positioned with <code>position: sticky</code> so it pins to that box's bottom edge
166
+ inside it. The bar is an <code>&lt;anchor-bar-ui anchor="bottom"&gt;</code> (gh#495) its
167
+ default mode positions with <code>position: sticky</code> so it pins to that box's bottom edge
169
168
  — never the browser viewport, and never a child of <code>&lt;table-ui&gt;</code> (it owns its
170
- own light DOM). The bar is a <code>&lt;card-ui elevation="3"&gt;</code> for the card-like
171
- elevated container, holding a standard <code>&lt;toolbar-ui&gt;</code>.
169
+ own light DOM) holding a standard <code>&lt;toolbar-ui&gt;</code>. <code>anchor-bar-ui</code>
170
+ owns the elevated-surface chrome (bg/border/radius/shadow) directly; no wrapping
171
+ <code>&lt;card-ui&gt;</code> needed.
172
172
  </p>
173
173
  <code-ui language="html">&lt;section bleed&gt;
174
174
  &lt;table-toolbar-ui for="resources" text="Resources" count="12"&gt;&lt;/table-toolbar-ui&gt;
@@ -176,66 +176,54 @@ table.addEventListener('select', (e) =&gt; {
176
176
  &lt;div data-table-scroll&gt; &lt;!-- position: relative; overflow: auto; max-height: … --&gt;
177
177
  &lt;table-ui id="resources" selectable sortable&gt;&lt;/table-ui&gt;
178
178
 
179
- &lt;!-- Floating bar — position: sticky; bottom: in the pattern's CSS.
180
- No [hidden]/display:none toggle see the pattern's CSS comment
181
- on why (toolbar-ui's overflow measurement gets stuck at 0px). --&gt;
182
- &lt;card-ui elevation="3" data-bulk-bar-floating&gt;
183
- &lt;section&gt;
184
- &lt;!-- align="space-between" pushes the action group right — NOT a
185
- flex-grow spacer span or an auto margin on a content item.
186
- toolbar-ui's overflow reflow sums each top-level child's
187
- RENDERED width to decide what fits; anything that absorbs free
188
- space has already expanded to fill it by the time it's
189
- measured, so the sum always reads "full" and spuriously spills
190
- content. Two &lt;toolbar-group-ui&gt; atoms + align="space-between"
191
- keeps the width sum accurate (see Rules). --&gt;
192
- &lt;toolbar-ui role="toolbar" aria-label="Bulk actions" align="space-between"&gt;
193
- &lt;toolbar-group-ui&gt;
194
- &lt;text-ui strong aria-live="polite"&gt;&lt;span data-bulk-count&gt;0&lt;/span&gt; selected&lt;/text-ui&gt;
195
- &lt;span data-bulk-controls&gt;&lt;/span&gt; &lt;!-- optional per-flow controls, e.g. a shared-rationale input --&gt;
196
- &lt;/toolbar-group-ui&gt;
197
- &lt;toolbar-group-ui&gt;
198
- &lt;!-- Icon-only (icon-only + aria-label) — a compact floating
199
- pill has less width to spend than the full-width top-swap
200
- bar. --&gt;
201
- &lt;button-ui icon="archive" icon-only aria-label="Archive" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
202
- &lt;button-ui icon="download-simple" icon-only aria-label="Export" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
203
- &lt;button-ui icon="folder-simple" icon-only aria-label="Move" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
204
- &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
205
- &lt;button-ui icon="trash" icon-only aria-label="Delete" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
206
- &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
207
- &lt;button-ui text="Clear" variant="ghost" size="sm" data-bulk-clear&gt;&lt;/button-ui&gt;
208
- &lt;/toolbar-group-ui&gt;
209
- &lt;/toolbar-ui&gt;
210
- &lt;/section&gt;
211
- &lt;/card-ui&gt;
179
+ &lt;!-- Floating bar — anchor-bar-ui's default mode is position:sticky
180
+ within the nearest scroll container. [open] controls visibility
181
+ via opacity/visibility, never [hidden]/display:none see the
182
+ pattern's CSS comment on why (toolbar-ui's overflow measurement
183
+ gets stuck at 0px). --&gt;
184
+ &lt;anchor-bar-ui anchor="bottom"&gt;
185
+ &lt;!-- align="space-between" pushes the action group right NOT a
186
+ flex-grow spacer span or an auto margin on a content item.
187
+ toolbar-ui's overflow reflow sums each top-level child's
188
+ RENDERED width to decide what fits; anything that absorbs free
189
+ space has already expanded to fill it by the time it's
190
+ measured, so the sum always reads "full" and spuriously spills
191
+ content. Two &lt;toolbar-group-ui&gt; atoms + align="space-between"
192
+ keeps the width sum accurate (see Rules). --&gt;
193
+ &lt;toolbar-ui role="toolbar" aria-label="Bulk actions" align="space-between"&gt;
194
+ &lt;toolbar-group-ui&gt;
195
+ &lt;text-ui strong aria-live="polite"&gt;&lt;span data-bulk-count&gt;0&lt;/span&gt; selected&lt;/text-ui&gt;
196
+ &lt;span data-bulk-controls&gt;&lt;/span&gt; &lt;!-- optional per-flow controls, e.g. a shared-rationale input --&gt;
197
+ &lt;/toolbar-group-ui&gt;
198
+ &lt;toolbar-group-ui&gt;
199
+ &lt;!-- Icon-only (icon-only + aria-label) a compact floating
200
+ pill has less width to spend than the full-width top-swap
201
+ bar. --&gt;
202
+ &lt;button-ui icon="archive" icon-only aria-label="Archive" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
203
+ &lt;button-ui icon="download-simple" icon-only aria-label="Export" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
204
+ &lt;button-ui icon="folder-simple" icon-only aria-label="Move" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
205
+ &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
206
+ &lt;button-ui icon="trash" icon-only aria-label="Delete" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
207
+ &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
208
+ &lt;button-ui text="Clear" variant="ghost" size="sm" data-bulk-clear&gt;&lt;/button-ui&gt;
209
+ &lt;/toolbar-group-ui&gt;
210
+ &lt;/toolbar-ui&gt;
211
+ &lt;/anchor-bar-ui&gt;
212
212
  &lt;/div&gt;
213
213
  &lt;/section&gt;</code-ui>
214
- <code-ui language="javascript">// Enter/exit toggles [data-open]/[data-closing]; the CSS does the rest
215
- // including the delayed `visibility: hidden` on close (see the pattern's
216
- // own &lt;style&gt; block). No [hidden]/display:none toggle: toolbar-ui's
217
- // spillover system measures its own width via ResizeObserver on first
218
- // connect, and a display:none ancestor at that moment gives it a 0px
219
- // reading it does not reliably re-measure later items it moved to the
220
- // "more" overflow menu at that 0px reading stay stuck there even once the
221
- // bar has real width. `visibility` keeps the box laid out (real width,
222
- // always measurable) while still pulling it out of the a11y tree, tab
223
- // order, and click target when closed.
224
-
225
- function showBar(bar) {
226
- bar.removeAttribute('data-closing');
227
- bar.setAttribute('data-open', ''); // no .focus() — never steal focus on appearance
228
- }
229
-
230
- function hideBar(bar) {
231
- bar.setAttribute('data-closing', '');
232
- bar.removeAttribute('data-open');
233
- }
214
+ <code-ui language="javascript">// anchor-bar-ui's [open] IS the visibility state no [hidden]/display:none
215
+ // toggle: toolbar-ui's spillover system measures its own width via
216
+ // ResizeObserver on first connect, and a display:none ancestor at that
217
+ // moment gives it a 0px reading it does not reliably re-measure later —
218
+ // items it moved to the "more" overflow menu at that 0px reading stay
219
+ // stuck there even once the bar has real width. anchor-bar-ui's own CSS
220
+ // handles the enter/exit opacity/visibility transition; no .focus() call
221
+ // anywhere appearance never steals focus.
234
222
 
235
223
  table.addEventListener('select', (e) =&gt; {
236
224
  const n = e.detail?.selected?.length ?? 0;
237
225
  countEl.textContent = String(n);
238
- n &gt; 0 ? showBar(bar) : hideBar(bar);
226
+ bar.open = n &gt; 0;
239
227
  });
240
228
 
241
229
  // Escape clears selection ONLY when the bar owns focus (listener lives
@@ -258,10 +246,10 @@ bar.addEventListener('press', (e) =&gt; {
258
246
  <li>Default state uses <code>&lt;table-toolbar-ui&gt;</code> — the modern foundation. It already wires Filter / Sort / Columns / Search to the table via <code>[for]</code>; don't hand-roll those. The floating variant keeps it visible at all times (it doesn't swap).</li>
259
247
  <li>Every bulk bar uses <code>&lt;toolbar-ui&gt;</code> with <code>role="toolbar"</code> and <code>aria-label="Bulk actions"</code>.</li>
260
248
  <li><strong>Toolbar swap:</strong> match its <code>--toolbar-py</code> / <code>--toolbar-px</code> to <code>--table-toolbar-py</code> / <code>--table-toolbar-px</code> so the swap is seamless; both toolbars sit inside the same <code>&lt;section bleed&gt;</code> as the table, flush with the first table cell.</li>
261
- <li><strong>Floating:</strong> wrap the bar in <code>&lt;card-ui elevation="3"&gt;</code> for the card-like elevated surface (tokens only <code>--a-shadow-*</code> via <code>[elevation]</code>, never a raw color/shadow); position it with <code>position: sticky</code> inside a <code>[data-table-scroll]</code> wrapper that IS the table's own scroll container (bounded height + <code>overflow: auto</code>) — never <code>position: fixed</code> to the viewport, and never a child of <code>&lt;table-ui&gt;</code> itself (it owns its own light DOM).</li>
249
+ <li><strong>Floating:</strong> use <code>&lt;anchor-bar-ui anchor="bottom"&gt;</code> (gh#495) for the elevated-surface chromeits default mode is <code>position: sticky</code> inside a <code>[data-table-scroll]</code> wrapper that IS the table's own scroll container (bounded height + <code>overflow: auto</code>) — never a child of <code>&lt;table-ui&gt;</code> itself (it owns its own light DOM). Don't set <code>[viewport]</code> here — that opts into <code>position: fixed</code> to the browser viewport, which this pattern deliberately avoids.</li>
262
250
  <li>Selection count leads every bar. Group the count (+ optional controls) and the action buttons into two <code>&lt;toolbar-group-ui&gt;</code> atoms and set <code>&lt;toolbar-ui align="space-between"&gt;</code> to push the action group right — <strong>not</strong> a flex-grow <code>&lt;span&gt;</code> spacer or an auto margin on a content item. <code>&lt;toolbar-ui&gt;</code>'s overflow reflow sums each top-level child's rendered width to decide what fits; anything that absorbs free space (flex-grow, or an auto margin resolved to real pixels post-layout) has already expanded to fill that space by the time it's measured, so the sum always reads "full" and spuriously spills the trailing action(s) into an unreachable "more" menu — this is a real, previously-shipped bug in this pattern's own swap-variant demo (Delete was silently unreachable), fixed alongside the floating variant (gh#494 audit finding). <code>justify-content</code> only repositions flex items, never their widths, so it doesn't trip the same measurement. On the floating bar the count sits in an <code>aria-live="polite"</code> element so screen readers hear the running total; an optional <code>&lt;span data-bulk-controls&gt;</code> slot follows it for per-flow controls (e.g. a shared-rationale input) — empty by default.</li>
263
251
  <li>Destructive actions go last and are visually separated with a <code>&lt;divider-ui vertical&gt;</code>. The floating bar additionally ends with an explicit Clear button (its own divider before it) — don't make "untick every row" the only way to dismiss it.</li>
264
252
  <li>Keep bulk actions to 3–5; overflow belongs in a <code>&lt;popover-ui&gt;</code> "More" menu trailing the divider.</li>
265
- <li>Drive both variants from the table's <code>select</code> event (<code>detail.selected: number[]</code>) — don't unmount. The toolbar swap toggles <code>hidden</code>. The floating bar toggles <code>[data-open]</code>/<code>[data-closing]</code> instead — never <code>[hidden]</code>/<code>display:none</code>, which resets <code>&lt;toolbar-ui&gt;</code>'s overflow measurement to 0px and strands items in its "more" menu even once the bar has real width again. The floating bar additionally: never calls <code>.focus()</code> on appearance (no focus steal), and its <code>keydown</code> listener lives on the bar itself so Escape only clears selection while the bar owns focus.</li>
253
+ <li>Drive both variants from the table's <code>select</code> event (<code>detail.selected: number[]</code>) — don't unmount. The toolbar swap toggles <code>hidden</code>. The floating bar sets <code>anchor-bar-ui</code>'s <code>[open]</code> property instead — never <code>[hidden]</code>/<code>display:none</code>, which resets <code>&lt;toolbar-ui&gt;</code>'s overflow measurement to 0px and strands items in its "more" menu even once the bar has real width again. The floating bar additionally: never calls <code>.focus()</code> on appearance (no focus steal — <code>anchor-bar-ui</code> doesn't either), and its <code>keydown</code> listener lives on the bar itself so Escape only clears selection while the bar owns focus.</li>
266
254
  </ul>
267
255
  </section>
@@ -6,8 +6,8 @@
6
6
  * Default state shows <table-toolbar-ui> (Filter/Sort/Columns/Search,
7
7
  * wired via [for]). When rows are selected: the swap variant hides it
8
8
  * for <toolbar-ui data-bulk-bar>; the floating variant leaves it
9
- * visible and rises a <card-ui elevation="3"> bar from the bottom of
10
- * the table's own scroll container.
9
+ * visible and rises an <anchor-bar-ui anchor="bottom"> (gh#495) from the
10
+ * bottom of the table's own scroll container.
11
11
  */
12
12
 
13
13
  const columns = [
@@ -99,32 +99,19 @@ function setupFloating(router) {
99
99
  table.columns = columns;
100
100
  table.data = floatData;
101
101
 
102
- // Enter/exit toggles [data-open]/[data-closing]; the CSS does the rest
103
- // (packages/web-components/patterns/bulk-action-toolbar/bulk-action-toolbar.html),
104
- // including the delayed `visibility: hidden` on close. Deliberately NOT
105
- // [hidden]/display:none: toolbar-ui's spillover system measures its own
106
- // width via ResizeObserver on first connect, and a display:none ancestor
107
- // at that moment gives it a 0px reading it does not reliably re-measure
108
- // later items it moves to the "more" overflow menu at that 0px reading
109
- // stay stuck there even once the bar has real width (found composing
110
- // this pattern; not a toolbar-ui fix in scope here). `visibility` keeps
111
- // the box laid out (real, measurable width) while still pulling it out
112
- // of the a11y tree, tab order, and click target when closed. No
113
- // .focus() call anywhere — appearance never steals focus.
114
- function showBar() {
115
- bar.removeAttribute('data-closing');
116
- bar.setAttribute('data-open', '');
117
- }
118
-
119
- function hideBar() {
120
- bar.setAttribute('data-closing', '');
121
- bar.removeAttribute('data-open');
122
- }
123
-
102
+ // anchor-bar-ui's [open] property IS the visibility state (gh#495) — its
103
+ // own CSS handles the opacity/visibility enter/exit transition.
104
+ // Deliberately never [hidden]/display:none: toolbar-ui's spillover
105
+ // system measures its own width via ResizeObserver on first connect,
106
+ // and a display:none ancestor at that moment gives it a 0px reading it
107
+ // does not reliably re-measure later items it moves to the "more"
108
+ // overflow menu at that 0px reading stay stuck there even once the bar
109
+ // has real width (found composing this pattern; not a toolbar-ui fix in
110
+ // scope here). No .focus() call anywhere appearance never steals focus.
124
111
  table.addEventListener('select', (e) => {
125
112
  const n = e.detail?.selected?.length ?? 0;
126
113
  if (countEl) countEl.textContent = String(n);
127
- if (n > 0) showBar(); else hideBar();
114
+ bar.open = n > 0;
128
115
  });
129
116
 
130
117
  // Escape clears selection ONLY when the bar owns focus — the listener
@@ -10,6 +10,7 @@
10
10
  <link rel="stylesheet" href="../../styles/tokens.css">
11
11
 
12
12
  <!-- Component CSS — primitives this pattern composes -->
13
+ <link rel="stylesheet" href="../../components/anchor-bar/anchor-bar.css">
13
14
  <link rel="stylesheet" href="../../components/badge/badge.css">
14
15
  <link rel="stylesheet" href="../../components/button/button.css">
15
16
  <link rel="stylesheet" href="../../components/card/card.css">
@@ -24,6 +25,7 @@
24
25
  <link rel="stylesheet" href="../../components/toolbar/toolbar.css">
25
26
 
26
27
  <!-- Component JS -->
28
+ <script type="module" src="../../components/anchor-bar/anchor-bar.js"></script>
27
29
  <script type="module" src="../../components/badge/badge.js"></script>
28
30
  <script type="module" src="../../components/button/button.js"></script>
29
31
  <script type="module" src="../../components/card/card.js"></script>
@@ -45,63 +47,17 @@
45
47
 
46
48
  /* ── Floating bulk-action bar (gh#494) ──
47
49
  [data-table-scroll] IS the table's scroll container — bounded
48
- height + overflow: auto — so `position: sticky` on the floating
49
- bar pins it to THAT box's bottom edge, never the browser viewport.
50
- This is the compose-from-primitives shape gh#495's anchored-surface
51
- primitive would eventually own; card-ui + toolbar-ui + sticky
52
- positioning gets there without it. */
50
+ height + overflow: auto — so anchor-bar-ui's default (non-
51
+ [viewport]) `position: sticky` pins it to THAT box's bottom edge,
52
+ never the browser viewport. This used to be a hand-rolled
53
+ card-ui + position:sticky recipe; gh#495 promoted it to
54
+ <anchor-bar-ui>'s own default mode (see anchor-bar.css) — this
55
+ pattern is now that primitive's canonical consumer. */
53
56
  [data-table-scroll] {
54
57
  position: relative;
55
58
  overflow: auto;
56
59
  max-height: 16rem;
57
60
  }
58
- /* `width: fit-content` here would create a circular sizing trap:
59
- toolbar-ui's own overflow reflow measures ITS available width from
60
- this box, but this box's fit-content size would in turn be derived
61
- from the toolbar's (possibly already-collapsed) rendered content —
62
- a self-reinforcing collapse the first time reflow ever runs while
63
- hidden (0 available width) spills items into the overflow popover.
64
- A fixed `max-width` breaks the loop: the box's width is deterministic
65
- (bounded by the scroll container, capped at 40rem), independent of
66
- the toolbar's own spillover state. */
67
- [data-bulk-bar-floating] {
68
- position: sticky;
69
- bottom: var(--a-space-3);
70
- z-index: 1;
71
- margin-inline: auto;
72
- width: 100%;
73
- max-width: min(46rem, calc(100% - var(--a-space-6)));
74
- opacity: 0;
75
- transform: translateY(0.75rem);
76
- transition: opacity var(--a-duration) var(--a-easing-out),
77
- transform var(--a-duration) var(--a-easing-out),
78
- visibility 0s linear var(--a-duration);
79
- /* `visibility`, not `[hidden]`/`display:none` — closed state still
80
- has box + layout, kept out of the a11y tree + tab order + click
81
- target instead. toolbar-ui's spillover system measures its own
82
- width via ResizeObserver on first connect; a `display:none`
83
- ancestor at that moment gives it a 0px reading it never reliably
84
- re-measures after unhiding (observed empirically: items it moved
85
- to the "more" overflow menu at that 0px reading stayed stuck
86
- there even once the bar had 700px+ to work with). Never toggling
87
- display avoids the trap entirely — no toolbar-ui change needed. */
88
- visibility: hidden;
89
- }
90
- [data-bulk-bar-floating][data-open] {
91
- opacity: 1;
92
- transform: translateY(0);
93
- visibility: visible;
94
- transition: opacity var(--a-duration) var(--a-easing-out),
95
- transform var(--a-duration) var(--a-easing-out);
96
- }
97
- [data-bulk-bar-floating][data-closing] {
98
- opacity: 0;
99
- transform: translateY(0.75rem);
100
- }
101
-
102
- @media (prefers-reduced-motion: reduce) {
103
- [data-bulk-bar-floating] { transition: none; }
104
- }
105
61
  </style>
106
62
  </head>
107
63
  <body>
@@ -0,0 +1,81 @@
1
+ /* adia:attribute-api/parametric-scope — [data-parametric-scope] re-derivation.
2
+ gh#570: custom properties resolve their calc()/clamp() formula at the
3
+ DECLARING element's own cascade, then inherit the already-resolved value
4
+ down. The primitive layer (foundation/{radius,space,size}.css) declares
5
+ every --a-radius-, --a-space-, and --a-size- formula exactly once, on
6
+ :root — so a descendant overriding --a-density/--a-radius-k (e.g. a
7
+ theme-panel[parametric target="#some-scoped-element"]) never causes
8
+ those derived tokens to re-resolve for its own subtree; it only affects
9
+ what --a-density/--a-radius-k itself reads as, everywhere DOWNSTREAM
10
+ formulas are declared (which is nowhere but :root).
11
+
12
+ [data-parametric-scope] is set by theme-panel.js on a non-root [target]
13
+ when [parametric] is active (never authored by hand) and mirrors these
14
+ SAME formula bodies verbatim so that scoped subtree gets its own working
15
+ copy of the derivation chain. This is the established codebase pattern,
16
+ not a new one — [size="sm"] below in this same file already duplicates
17
+ --a-size-sm's calc() rather than only pointing at it, for the identical
18
+ reason (a scoped attribute needs its OWN resolved value, not :root's).
19
+
20
+ Layer position: imported into layer(utilities), same as the rest of this
21
+ file — wins over foundation/'s layer(tokens) by layer order, though for
22
+ a non-root element that never mattered (no competing :root rule exists
23
+ there to begin with). */
24
+
25
+ [data-parametric-scope] {
26
+ /* ── Radius (foundation/radius.css) ── */
27
+ --a-radius-xs: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-xs-k)), var(--a-radius-max));
28
+ --a-radius-sm: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-sm-k)), var(--a-radius-max));
29
+ --a-radius-md: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-md-k)), var(--a-radius-max));
30
+ --a-radius-lg: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-lg-k)), var(--a-radius-max));
31
+ --a-radius: var(--a-radius-md);
32
+
33
+ /* ── Spacing (foundation/space.css) ── */
34
+ --a-space-0-5: calc(var(--a-density) * 0.125rem);
35
+ --a-space-1: calc(var(--a-density) * 0.25rem);
36
+ --a-space-1-5: calc(var(--a-density) * 0.375rem);
37
+ --a-space-2: calc(var(--a-density) * 0.5rem);
38
+ --a-space-2-5: calc(var(--a-density) * 0.625rem);
39
+ --a-space-3: calc(var(--a-density) * 0.75rem);
40
+ --a-space-3-5: calc(var(--a-density) * 0.875rem);
41
+ --a-space-4: calc(var(--a-density) * 1rem);
42
+ --a-space-4-5: calc(var(--a-density) * 1.125rem);
43
+ --a-space-5: calc(var(--a-density) * 1.25rem);
44
+ --a-space-6: calc(var(--a-density) * 1.5rem);
45
+ --a-space-7: calc(var(--a-density) * 1.75rem);
46
+ --a-space-8: calc(var(--a-density) * 2rem);
47
+ --a-space-9: calc(var(--a-density) * 2.25rem);
48
+ --a-space-10: calc(var(--a-density) * 2.5rem);
49
+ --a-space-12: calc(var(--a-density) * 3rem);
50
+ --a-space-16: calc(var(--a-density) * 4rem);
51
+
52
+ --a-inset-sm: var(--a-space-3-5);
53
+ --a-inset-md: var(--a-space-4);
54
+ --a-inset-lg: var(--a-space-4-5);
55
+ --a-inset: var(--a-inset-md);
56
+
57
+ --a-gap-xs: calc(var(--a-space-2) * var(--a-gap-k));
58
+ --a-gap-sm: calc(var(--a-space-2-5) * var(--a-gap-k));
59
+ --a-gap-md: calc(var(--a-space-3) * var(--a-gap-k));
60
+ --a-gap-lg: calc(var(--a-space-3-5) * var(--a-gap-k));
61
+ --a-gap-xl: calc(var(--a-space-4) * var(--a-gap-k));
62
+ --a-gap: var(--a-gap-md);
63
+
64
+ --a-padding-xs: calc(var(--a-space-2) * var(--a-padding-k));
65
+ --a-padding-sm: calc(var(--a-space-4) * var(--a-padding-k));
66
+ --a-padding-md: calc(var(--a-space-6) * var(--a-padding-k));
67
+ --a-padding-lg: calc(var(--a-space-8) * var(--a-padding-k));
68
+ --a-padding-xl: calc(var(--a-space-10) * var(--a-padding-k));
69
+ --a-margin-xs: calc(var(--a-space-2) * var(--a-margin-k));
70
+ --a-margin-sm: calc(var(--a-space-4) * var(--a-margin-k));
71
+ --a-margin-md: calc(var(--a-space-6) * var(--a-margin-k));
72
+ --a-margin-lg: calc(var(--a-space-8) * var(--a-margin-k));
73
+ --a-margin-xl: calc(var(--a-space-10) * var(--a-margin-k));
74
+
75
+ /* ── Sizing (foundation/size.css) ── */
76
+ --a-size-sm: calc(var(--a-density) * 1.5rem);
77
+ --a-size-md: calc(var(--a-density) * 1.875rem);
78
+ --a-size-lg: calc(var(--a-density) * 2.25rem);
79
+ --a-size: var(--a-size-md);
80
+ --a-toggle-size: calc(var(--a-size-sm) - var(--a-space-1));
81
+ }
@@ -34,6 +34,7 @@
34
34
  @import "../components/modal/modal.css";
35
35
  @import "../components/feed/feed.css";
36
36
  @import "../components/toast/toast.css";
37
+ @import "../components/anchor-bar/anchor-bar.css";
37
38
  @import "../components/tabs/tabs.css";
38
39
  @import "../components/tooltip/tooltip.css";
39
40
  @import "../components/badge/badge.css";
package/styles/tokens.css CHANGED
@@ -19,3 +19,4 @@
19
19
  @import "./typography.css";
20
20
  @import "./foundation/index.css" layer(tokens);
21
21
  @import "./api/sizing.css" layer(utilities);
22
+ @import "./api/parametric-scope.css" layer(utilities);