@adia-ai/web-components 0.8.21 → 0.8.23

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 (86) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/MIGRATION.md +9 -0
  3. package/components/agent-trace/agent-trace.js +4 -1
  4. package/components/alert/alert.a2ui.json +1 -1
  5. package/components/alert/alert.css +44 -5
  6. package/components/alert/alert.yaml +8 -4
  7. package/components/canvas/canvas.js +4 -1
  8. package/components/chat-thread/chat-input.js +4 -1
  9. package/components/code/code.css +29 -26
  10. package/components/combobox/combobox.a2ui.json +1 -1
  11. package/components/combobox/combobox.class.js +14 -4
  12. package/components/combobox/combobox.css +52 -4
  13. package/components/combobox/combobox.yaml +5 -1
  14. package/components/description-list/description-list.css +9 -0
  15. package/components/drawer/drawer.css +16 -0
  16. package/components/field/field.css +4 -1
  17. package/components/list/list.css +12 -1
  18. package/components/nav-group/nav-group.class.js +45 -2
  19. package/components/nav-group/nav-group.css +11 -2
  20. package/components/progress-row/progress-row.class.js +10 -2
  21. package/components/richtext/richtext.css +7 -0
  22. package/components/select/select.a2ui.json +5 -0
  23. package/components/select/select.class.js +39 -9
  24. package/components/select/select.yaml +13 -0
  25. package/components/stat/stat.js +4 -1
  26. package/components/stepper/stepper.css +25 -20
  27. package/components/table/table.a2ui.json +15 -1
  28. package/components/table/table.class.js +14 -1
  29. package/components/table/table.css +68 -3
  30. package/components/table/table.d.ts +5 -1
  31. package/components/table/table.yaml +34 -1
  32. package/components/tabs/tab.js +4 -1
  33. package/components/tag/tag.css +15 -9
  34. package/core/provider.js +5 -1
  35. package/dist/host.min.css +1 -1
  36. package/dist/host.sheet.js +1 -1
  37. package/dist/scale.min.css +1 -1
  38. package/dist/scale.sheet.js +1 -1
  39. package/dist/theme-provider.min.js +2 -2
  40. package/dist/web-components.min.css +1 -1
  41. package/dist/web-components.min.js +93 -93
  42. package/dist/web-components.sheet.js +1 -1
  43. package/package.json +1 -1
  44. package/patterns/access-requests/access-requests.examples.html +12 -3
  45. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +20 -5
  46. package/patterns/agent-cost/agent-cost.examples.html +12 -3
  47. package/patterns/agent-memory/agent-memory.examples.html +12 -3
  48. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +12 -3
  49. package/patterns/agent-tool-call/agent-tool-call.examples.html +16 -4
  50. package/patterns/alert-dialog/alert-dialog.examples.html +12 -3
  51. package/patterns/app-nav/app-nav.examples.html +36 -9
  52. package/patterns/approvals/approvals.examples.html +14 -10
  53. package/patterns/audit-log/audit-log.examples.html +5 -2
  54. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +210 -32
  55. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +83 -4
  56. package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +68 -0
  57. package/patterns/changelog-feed/changelog-feed.examples.html +4 -1
  58. package/patterns/chart-in-card/chart-in-card.examples.html +28 -7
  59. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +20 -5
  60. package/patterns/conversion-funnel/conversion-funnel.examples.html +24 -15
  61. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +15 -6
  62. package/patterns/diff-review/diff-review.examples.html +12 -3
  63. package/patterns/export-flow/export-flow.examples.html +12 -3
  64. package/patterns/filter-bar/filter-bar.examples.html +13 -4
  65. package/patterns/form-drawer/form-drawer.examples.html +15 -6
  66. package/patterns/form-system/form-system.examples.html +4 -1
  67. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +12 -3
  68. package/patterns/inline-dialog/inline-dialog.examples.html +12 -3
  69. package/patterns/kanban-board/kanban-board.examples.html +4 -1
  70. package/patterns/marketing-engagement/marketing-engagement.examples.html +24 -6
  71. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +12 -3
  72. package/patterns/permissions-matrix/permissions-matrix.examples.html +9 -3
  73. package/patterns/permissions-role-picker/permissions-role-picker.examples.html +8 -2
  74. package/patterns/permissions-sharing/permissions-sharing.examples.html +12 -3
  75. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +16 -4
  76. package/patterns/record-detail-drawer/record-detail-drawer.examples.html +10 -4
  77. package/patterns/retention-cohort/retention-cohort.examples.html +15 -6
  78. package/patterns/search-discovery/search-discovery.examples.html +16 -4
  79. package/patterns/system-banners/system-banners.examples.html +54 -48
  80. package/patterns/timezone-selector/timezone-selector.examples.html +8 -2
  81. package/patterns/timezone-selector/timezone-selector.examples.js +3 -1
  82. package/patterns/wizard-drawer/wizard-drawer.examples.html +25 -23
  83. package/patterns/wizard-drawer/wizard-drawer.examples.js +12 -1
  84. package/styles/foundation/radius.css +2 -2
  85. package/styles/scale.css +2 -2
  86. package/styles/type/scale.css +6 -6
@@ -2,60 +2,145 @@
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>
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>
6
6
  </div>
7
7
  </div>
8
- <p>When a table has row selection, swap the standard <code>&lt;table-toolbar-ui&gt;</code> for a contextual bulk-action bar at the same surface position. Selection count anchors the left, actions flow from the right, destructive action last.</p>
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>
9
9
  </header>
10
10
 
11
11
  <section data-section data-property="when-to-use">
12
12
  <h2 variant="section">When to use</h2>
13
13
  <p data-note>
14
14
  Any table where users act on multiple rows at once: archive, delete, export, move.
15
- Swapping the toolbar (rather than stacking a second bar) keeps the surface compact and
16
- signals "selection mode" with the strongest possible affordance — the chrome itself changes.
15
+ </p>
16
+ <p data-note>
17
+ Pick the <strong>toolbar swap</strong> for short or inline tables — everything fits in one
18
+ viewport, so the swapped bar at the top stays visible and "the chrome itself changes" is the
19
+ strongest possible affordance for selection mode.
20
+ </p>
21
+ <p data-note>
22
+ Pick the <strong>floating variant</strong> as the default once a table can run taller than
23
+ ~one viewport (long lists, paginated consoles at 50+ rows). Selections made mid-scroll get no
24
+ feedback from a toolbar that has already scrolled out of view — the floating bar stays
25
+ anchored to the bottom of the table's own scroll container so it's visible at the moment of
26
+ use, without pinning to the browser viewport and ignoring shell chrome.
17
27
  </p>
18
28
  </section>
19
29
 
20
- <section data-section data-property="toolbar-swap" data-chunk="bulk-action-toolbar-swap">
21
- <h2 variant="section">Toolbar swap on selection</h2>
22
- <p data-note>Tick the checkboxes below — the default <code>&lt;table-toolbar-ui&gt;</code> swaps for the bulk-action bar at the same height. Untick to return.</p>
30
+ <section data-section data-property="toolbar-swap" data-chunk="bulk-action-toolbar-swap" data-chunk-kind="block"
31
+ data-chunk-domain="data"
32
+ data-chunk-description="Bulk-action toolbar over a selectable table — the default table toolbar swaps at the same height for an 'N selected' bar with Archive, Export, Move, a divider, and Delete when rows are checked."
33
+ data-chunk-keywords="bulk actions toolbar selection selected rows table checkbox archive export move delete swap batch multi-select bar">
34
+ <h2 variant="section">Toolbar swap (top-anchored)</h2>
35
+ <p data-note>Tick the checkboxes below — the default <code>&lt;table-toolbar-ui&gt;</code> swaps for the bulk-action bar at the same height. Untick to return. Best for short/inline tables (see When to use).</p>
23
36
  <card-ui raw>
24
37
  <section bleed>
25
38
  <table-toolbar-ui id="pat-bulk-default" for="pat-bulk-table" text="Resources" count="6" no-filter no-sort no-columns></table-toolbar-ui>
26
39
 
27
- <toolbar-ui id="pat-bulk-actions" data-bulk-bar role="toolbar" aria-label="Bulk actions" hidden>
28
- <text-ui strong><span data-bulk-count>0</span> selected</text-ui>
29
- <span data-spacer style="flex:1 1 auto"></span>
30
- <button-ui text="Archive" icon="archive" variant="ghost" size="sm" data-bulk-action="archive"></button-ui>
31
- <button-ui text="Export" icon="download-simple" variant="ghost" size="sm" data-bulk-action="export"></button-ui>
32
- <button-ui text="Move" icon="folder-simple" variant="ghost" size="sm" data-bulk-action="move"></button-ui>
33
- <divider-ui vertical></divider-ui>
34
- <button-ui text="Delete" icon="trash" variant="ghost" size="sm" data-bulk-action="delete"></button-ui>
40
+ <!-- align="space-between" pushes the trailing group right — NOT a
41
+ flex-grow spacer span (gh#494 audit finding: toolbar-ui's overflow
42
+ reflow sums each top-level child's RENDERED width to decide what
43
+ fits; a flex-grow spacer has already expanded to fill the free
44
+ space by the time it's measured, so the sum always reads "full"
45
+ and spuriously spills the trailing action(s) into the "more" menu
46
+ Delete was silently unreachable here before this fix, even with
47
+ plenty of room). Grouping into two <toolbar-group-ui> atoms keeps
48
+ the reflow's width sum accurate; see the floating variant below
49
+ for the same fix applied at a smaller width. -->
50
+ <toolbar-ui id="pat-bulk-actions" data-bulk-bar role="toolbar" aria-label="Bulk actions" align="space-between" hidden>
51
+ <toolbar-group-ui>
52
+ <text-ui strong><span data-bulk-count>0</span> selected</text-ui>
53
+ </toolbar-group-ui>
54
+ <toolbar-group-ui>
55
+ <button-ui text="Archive" icon="archive" variant="ghost" size="sm" data-bulk-action="archive"></button-ui>
56
+ <button-ui text="Export" icon="download-simple" variant="ghost" size="sm" data-bulk-action="export"></button-ui>
57
+ <button-ui text="Move" icon="folder-simple" variant="ghost" size="sm" data-bulk-action="move"></button-ui>
58
+ <divider-ui vertical></divider-ui>
59
+ <button-ui text="Delete" icon="trash" variant="ghost" size="sm" data-bulk-action="delete"></button-ui>
60
+ </toolbar-group-ui>
35
61
  </toolbar-ui>
36
62
 
37
- <table-ui id="pat-bulk-table" selectable sortable striped></table-ui>
63
+ <table-ui frameless id="pat-bulk-table" selectable sortable striped></table-ui>
64
+ </section>
65
+ </card-ui>
66
+ </section>
67
+
68
+ <section data-section data-property="floating-variant" data-chunk="bulk-action-toolbar-floating" data-chunk-kind="block"
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."
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
+ <h2 variant="section">Floating (bottom-anchored)</h2>
73
+ <p data-note>
74
+ Tick the checkboxes below — a card-like elevated bar rises from the bottom of the table's own
75
+ scroll container (scroll the table, the bar tracks it, not the page). Press
76
+ <code>Escape</code> while the bar has focus, or use the explicit Clear button, to release
77
+ the selection.
78
+ </p>
79
+ <card-ui raw>
80
+ <section bleed>
81
+ <table-toolbar-ui id="pat-bulk-float-default" for="pat-bulk-float-table" text="Resources" count="12" no-filter no-sort no-columns></table-toolbar-ui>
82
+
83
+ <div data-table-scroll>
84
+ <table-ui frameless id="pat-bulk-float-table" selectable sortable striped></table-ui>
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>
118
+ </div>
38
119
  </section>
39
120
  </card-ui>
40
121
  </section>
41
122
 
42
123
  <section data-section data-property="anatomy">
43
- <h2 variant="section">Anatomy</h2>
124
+ <h2 variant="section">Anatomy — toolbar swap</h2>
44
125
  <p data-note>Both toolbars sit inside the same <code>&lt;section bleed&gt;</code> so they share one surface. Toggle visibility on the <code>select</code> event from the table.</p>
45
126
  <code-ui language="html">&lt;card-ui&gt;
46
127
  &lt;section bleed&gt;
47
128
  &lt;!-- Default state — modern foundation --&gt;
48
129
  &lt;table-toolbar-ui for="resources" text="Resources" count="6"&gt;&lt;/table-toolbar-ui&gt;
49
130
 
50
- &lt;!-- Bulk action bar — swaps in when rows are selected --&gt;
51
- &lt;toolbar-ui data-bulk-bar hidden role="toolbar" aria-label="Bulk actions"&gt;
52
- &lt;text-ui strong&gt;&lt;span data-bulk-count&gt;0&lt;/span&gt; selected&lt;/text-ui&gt;
53
- &lt;span data-spacer&gt;&lt;/span&gt;
54
- &lt;button-ui text="Archive" icon="archive" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
55
- &lt;button-ui text="Export" icon="download-simple" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
56
- &lt;button-ui text="Move" icon="folder-simple" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
57
- &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
58
- &lt;button-ui text="Delete" icon="trash" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
131
+ &lt;!-- Bulk action bar — swaps in when rows are selected. align="space-between"
132
+ (not a flex-grow spacer see Rules) pushes the action group right. --&gt;
133
+ &lt;toolbar-ui data-bulk-bar hidden role="toolbar" aria-label="Bulk actions" align="space-between"&gt;
134
+ &lt;toolbar-group-ui&gt;
135
+ &lt;text-ui strong&gt;&lt;span data-bulk-count&gt;0&lt;/span&gt; selected&lt;/text-ui&gt;
136
+ &lt;/toolbar-group-ui&gt;
137
+ &lt;toolbar-group-ui&gt;
138
+ &lt;button-ui text="Archive" icon="archive" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
139
+ &lt;button-ui text="Export" icon="download-simple" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
140
+ &lt;button-ui text="Move" icon="folder-simple" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
141
+ &lt;divider-ui vertical&gt;&lt;/divider-ui&gt;
142
+ &lt;button-ui text="Delete" icon="trash" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
143
+ &lt;/toolbar-group-ui&gt;
59
144
  &lt;/toolbar-ui&gt;
60
145
 
61
146
  &lt;table-ui id="resources" selectable sortable&gt;&lt;/table-ui&gt;
@@ -75,15 +160,108 @@ table.addEventListener('select', (e) =&gt; {
75
160
  });</code-ui>
76
161
  </section>
77
162
 
163
+ <section data-section data-property="anatomy-floating">
164
+ <h2 variant="section">Anatomy — floating (bottom-anchored)</h2>
165
+ <p data-note>
166
+ <code>[data-table-scroll]</code> wraps the table and IS its scroll container (bounded height +
167
+ <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
169
+ — 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>.
172
+ </p>
173
+ <code-ui language="html">&lt;section bleed&gt;
174
+ &lt;table-toolbar-ui for="resources" text="Resources" count="12"&gt;&lt;/table-toolbar-ui&gt;
175
+
176
+ &lt;div data-table-scroll&gt; &lt;!-- position: relative; overflow: auto; max-height: … --&gt;
177
+ &lt;table-ui id="resources" selectable sortable&gt;&lt;/table-ui&gt;
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;
212
+ &lt;/div&gt;
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
+ }
234
+
235
+ table.addEventListener('select', (e) =&gt; {
236
+ const n = e.detail?.selected?.length ?? 0;
237
+ countEl.textContent = String(n);
238
+ n &gt; 0 ? showBar(bar) : hideBar(bar);
239
+ });
240
+
241
+ // Escape clears selection ONLY when the bar owns focus (listener lives
242
+ // on the bar itself, so it only fires while focus is inside it).
243
+ bar.addEventListener('keydown', (e) =&gt; {
244
+ if (e.key === 'Escape') { e.stopPropagation(); table.clearSelection(); }
245
+ });
246
+
247
+ bar.addEventListener('press', (e) =&gt; {
248
+ if (e.target?.closest?.('[data-bulk-clear], [data-bulk-action]')) {
249
+ table.clearSelection();
250
+ }
251
+ });</code-ui>
252
+ </section>
253
+
78
254
  <section data-section data-property="rules">
79
255
  <h2 variant="section">Rules</h2>
80
256
  <ul>
81
- <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.</li>
82
- <li>Bulk bar uses <code>&lt;toolbar-ui&gt;</code> with <code>role="toolbar"</code> and <code>aria-label="Bulk actions"</code>. 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.</li>
83
- <li>Both toolbars sit inside the same <code>&lt;section bleed&gt;</code> as the table — one surface, flush alignment with the first table cell.</li>
84
- <li>Selection count leads the bulk bar; <code>&lt;span data-spacer&gt;</code> pushes actions right.</li>
85
- <li>Destructive actions go last and are visually separated with a <code>&lt;divider-ui vertical&gt;</code>.</li>
257
+ <li>Pick one variant per table — see When to use. Both stay valid; don't run both at once on the same table.</li>
258
+ <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
+ <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
+ <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>
262
+ <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
+ <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>
86
264
  <li>Keep bulk actions to 3–5; overflow belongs in a <code>&lt;popover-ui&gt;</code> "More" menu trailing the divider.</li>
87
- <li>Drive the swap from the table's <code>select</code> event (<code>detail.selected: number[]</code>) — toggle <code>hidden</code> on each toolbar; don't unmount.</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>
88
266
  </ul>
89
267
  </section>
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Bulk action toolbar — populates the demo table and wires the
3
- * selection-driven toolbar swap.
2
+ * Bulk action toolbar — populates the demo tables and wires both
3
+ * selection-driven variants: the top-anchored toolbar swap, and the
4
+ * bottom-anchored floating bar (gh#494).
4
5
  *
5
6
  * Default state shows <table-toolbar-ui> (Filter/Sort/Columns/Search,
6
- * wired via [for]). When rows are selected, swap to <toolbar-ui>
7
- * with the bulk actions; selection count anchors the left.
7
+ * wired via [for]). When rows are selected: the swap variant hides it
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.
8
11
  */
9
12
 
10
13
  const columns = [
@@ -26,7 +29,25 @@ const data = [
26
29
  { name: 'Hiring plan', owner: 'Sarah Miller', status: 'Draft', updated: '2 weeks ago' },
27
30
  ];
28
31
 
32
+ // Floating-variant demo needs more rows than fit in the bounded
33
+ // [data-table-scroll] height, so the scroll behavior (bar tracks the
34
+ // table's own scroll box, not the page) is actually exercisable.
35
+ const floatData = [
36
+ ...data,
37
+ { name: 'Vendor contracts', owner: 'James Park', status: 'Active', updated: '3 weeks ago' },
38
+ { name: 'Onboarding kit', owner: 'Maria Lopez', status: 'Draft', updated: '1 month ago' },
39
+ { name: 'Security review', owner: 'Alex Chen', status: 'Active', updated: '1 month ago' },
40
+ { name: 'Retro notes', owner: 'Sarah Miller', status: 'Archived', updated: '2 months ago' },
41
+ { name: 'Budget draft', owner: 'James Park', status: 'Draft', updated: '2 months ago' },
42
+ { name: 'Partner deck', owner: 'Maria Lopez', status: 'Active', updated: '3 months ago' },
43
+ ];
44
+
29
45
  export default function setup(router) {
46
+ setupSwap(router);
47
+ setupFloating(router);
48
+ }
49
+
50
+ function setupSwap(router) {
30
51
  const table = router.querySelector('#pat-bulk-table');
31
52
  const tbar = router.querySelector('#pat-bulk-default');
32
53
  const bulk = router.querySelector('#pat-bulk-actions');
@@ -67,3 +88,61 @@ export default function setup(router) {
67
88
  table.clearSelection();
68
89
  });
69
90
  }
91
+
92
+ function setupFloating(router) {
93
+ const table = router.querySelector('#pat-bulk-float-table');
94
+ const tbar = router.querySelector('#pat-bulk-float-default');
95
+ const bar = router.querySelector('#pat-bulk-float-bar');
96
+ const countEl = bar?.querySelector('[data-bulk-float-count]');
97
+ if (!table || !tbar || !bar) return;
98
+
99
+ table.columns = columns;
100
+ table.data = floatData;
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
+
124
+ table.addEventListener('select', (e) => {
125
+ const n = e.detail?.selected?.length ?? 0;
126
+ if (countEl) countEl.textContent = String(n);
127
+ if (n > 0) showBar(); else hideBar();
128
+ });
129
+
130
+ // Escape clears selection ONLY when the bar owns focus — the listener
131
+ // lives on the bar itself so it only fires while focus is inside it,
132
+ // never as a page-wide Escape trap.
133
+ bar.addEventListener('keydown', (e) => {
134
+ if (e.key === 'Escape') {
135
+ e.stopPropagation();
136
+ table.clearSelection();
137
+ }
138
+ });
139
+
140
+ // Demo-only: both bulk-action buttons and the explicit Clear button
141
+ // release the selection so the floating bar's exit transition is
142
+ // exercisable from either affordance.
143
+ bar.addEventListener('press', (e) => {
144
+ const target = e.target?.closest?.('[data-bulk-clear], [data-bulk-action]');
145
+ if (!target) return;
146
+ table.clearSelection();
147
+ });
148
+ }
@@ -10,10 +10,13 @@
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/badge/badge.css">
13
14
  <link rel="stylesheet" href="../../components/button/button.css">
14
15
  <link rel="stylesheet" href="../../components/card/card.css">
16
+ <link rel="stylesheet" href="../../components/check/check.css">
15
17
  <link rel="stylesheet" href="../../components/code/code.css">
16
18
  <link rel="stylesheet" href="../../components/divider/divider.css">
19
+ <link rel="stylesheet" href="../../components/icon/icon.css">
17
20
  <link rel="stylesheet" href="../../components/table/table.css">
18
21
  <link rel="stylesheet" href="../../components/table-toolbar/table-toolbar.css">
19
22
  <link rel="stylesheet" href="../../components/tag/tag.css">
@@ -21,19 +24,84 @@
21
24
  <link rel="stylesheet" href="../../components/toolbar/toolbar.css">
22
25
 
23
26
  <!-- Component JS -->
27
+ <script type="module" src="../../components/badge/badge.js"></script>
24
28
  <script type="module" src="../../components/button/button.js"></script>
25
29
  <script type="module" src="../../components/card/card.js"></script>
30
+ <script type="module" src="../../components/check/check.js"></script>
26
31
  <script type="module" src="../../components/code/code.js"></script>
27
32
  <script type="module" src="../../components/divider/divider.js"></script>
33
+ <script type="module" src="../../components/icon/icon.js"></script>
28
34
  <script type="module" src="../../components/table/table.js"></script>
29
35
  <script type="module" src="../../components/table-toolbar/table-toolbar.js"></script>
30
36
  <script type="module" src="../../components/tag/tag.js"></script>
31
37
  <script type="module" src="../../components/text/text.js"></script>
32
38
  <script type="module" src="../../components/toolbar/toolbar.js"></script>
33
39
 
40
+ <script type="module" src="../../core/icons-phosphor.js"></script>
41
+
34
42
  <style>
35
43
  :where(html, body) { margin: 0; min-height: 100vh; background: var(--md-sys-color-neutral-background); color: var(--md-sys-color-neutral-on-surface); font-family: var(--a-font-family); }
36
44
  main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
45
+
46
+ /* ── Floating bulk-action bar (gh#494) ──
47
+ [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. */
53
+ [data-table-scroll] {
54
+ position: relative;
55
+ overflow: auto;
56
+ max-height: 16rem;
57
+ }
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
+ }
37
105
  </style>
38
106
  </head>
39
107
  <body>
@@ -18,7 +18,10 @@
18
18
  <p data-note>Section-grouped layout (GitHub / Linear convention): each release carries a version + date header, then per-section eyebrows (Added / Changed / Fixed / Deprecated / Removed) followed by bulleted prose. Sections are <em>grouped</em>, not tagged per row — variable tag widths break left-edge alignment of prose; eyebrow + bullets keep one consistent reading column.</p>
19
19
  <div data-artifact-container>
20
20
  <div data-artifact-item data-artifact-label="changelog feed — 3 releases">
21
- <card-ui data-chunk="changelog-feed-basic" style="max-width: 640px;">
21
+ <card-ui data-chunk="changelog-feed-basic" data-chunk-kind="block"
22
+ data-chunk-domain="content"
23
+ data-chunk-description="Changelog feed card — 'What's new' releases in reverse order, each with version number, Latest tag, date, and categorized change entries (features, fixes) as text rows."
24
+ data-chunk-keywords="changelog whats-new releases feed version latest date features fixes updates notes product announcements" style="max-width: 640px;">
22
25
  <header>
23
26
  <span slot="heading" variant="section">What's new</span>
24
27
  <text-ui slot="description" color="subtle">3 releases in the last 30 days</text-ui>
@@ -22,7 +22,10 @@
22
22
  <p data-note>KPI + trend shape. Used in overview rows to show 4–6 metrics side-by-side.</p>
23
23
  <div data-artifact-container>
24
24
  <div data-artifact-item data-artifact-label="sparkline tile">
25
- <card-ui data-chunk="chart-in-card-sparkline-tile" style="max-width:320px;">
25
+ <card-ui data-chunk="chart-in-card-sparkline-tile" data-chunk-kind="block"
26
+ data-chunk-domain="dashboard"
27
+ data-chunk-description="Sparkline metric tile — compact card with metric name, big current value (Page Views 84,232), and an edge-to-edge sparkline chart bleeding below."
28
+ data-chunk-keywords="sparkline tile metric card page-views value chart compact dashboard kpi trend mini analytics" style="max-width:320px;">
26
29
  <header>
27
30
  <span slot="heading"><h2>Page Views</h2></span>
28
31
  <text-ui slot="description" variant="title">84,232</text-ui>
@@ -40,7 +43,10 @@
40
43
  <p data-note>Chart as primary content. Header names the metric; footer adds a supporting action.</p>
41
44
  <div data-artifact-container>
42
45
  <div data-artifact-item data-artifact-label="full chart">
43
- <card-ui data-chunk="chart-in-card-full-chart" style="max-width:520px;">
46
+ <card-ui data-chunk="chart-in-card-full-chart" data-chunk-kind="block"
47
+ data-chunk-domain="dashboard"
48
+ data-chunk-description="Full chart card — Revenue bar chart over the last 12 months with title and range description in the header and a stretch View-report footer button."
49
+ data-chunk-keywords="chart card bar revenue months report full analytics dashboard footer button range" style="max-width:520px;">
44
50
  <header>
45
51
  <span slot="heading"><h3>Revenue</h3></span>
46
52
  <text-ui slot="description" color="subtle">Last 12 months</text-ui>
@@ -61,7 +67,10 @@
61
67
  <p data-note>Stat in header pairs with the chart so the current value is legible before the chart renders.</p>
62
68
  <div data-artifact-container>
63
69
  <div data-artifact-item data-artifact-label="chart plus stat">
64
- <card-ui data-chunk="chart-in-card-chart-plus-stat" style="max-width:520px;">
70
+ <card-ui data-chunk="chart-in-card-chart-plus-stat" data-chunk-kind="block"
71
+ data-chunk-domain="dashboard"
72
+ data-chunk-description="Stat-headed chart card — an MRR stat with change delta and trend arrow as the card header, over a smooth line chart of the metric's history."
73
+ data-chunk-keywords="chart stat card mrr line smooth trend delta header metric history dashboard analytics revenue" style="max-width:520px;">
65
74
  <header>
66
75
  <span slot="heading">
67
76
  <stat-ui label="MRR" value="$48,290" change="+12.4%" trend="up"></stat-ui>
@@ -80,7 +89,10 @@
80
89
  <p data-note>Chart shows aggregate; breakdown beneath gives the composition. Most SaaS "Overview" tiles follow this shape.</p>
81
90
  <div data-artifact-container>
82
91
  <div data-artifact-item data-artifact-label="chart plus breakdown">
83
- <card-ui data-chunk="chart-in-card-chart-plus-breakdown" style="max-width:520px;">
92
+ <card-ui data-chunk="chart-in-card-chart-plus-breakdown" data-chunk-kind="block"
93
+ data-chunk-domain="dashboard"
94
+ data-chunk-description="Chart with breakdown card — Sessions bar chart for the week over a 3-up grid of stat tiles decomposing the total by segment (Desktop / Mobile / Tablet)."
95
+ data-chunk-keywords="chart breakdown card sessions bar stats grid desktop mobile tablet segments decomposition dashboard analytics" style="max-width:520px;">
84
96
  <header>
85
97
  <span slot="heading"><h3>Sessions</h3></span>
86
98
  <text-ui slot="description" color="subtle">This week</text-ui>
@@ -105,7 +117,10 @@
105
117
  <p data-note>Footer carries trend narrative + period caption using the new footer <code>heading</code> / <code>description</code> slot routing (symmetric with header). Shadcn-style revenue card translated to AdiaUI primitives.</p>
106
118
  <div data-artifact-container>
107
119
  <div data-artifact-item data-artifact-label="area chart + trend footer">
108
- <card-ui data-chunk="chart-in-card-trend-footer" style="max-width:520px;">
120
+ <card-ui data-chunk="chart-in-card-trend-footer" data-chunk-kind="block"
121
+ data-chunk-domain="dashboard"
122
+ data-chunk-description="Area chart card with trend footer — Revenue area chart for a date range, footer states the takeaway ('Trending up by 5.2% this month') with a data-scope caption."
123
+ data-chunk-keywords="chart area card trend footer takeaway revenue range summary insight dashboard analytics" style="max-width:520px;">
109
124
  <header>
110
125
  <span slot="heading"><h3>Revenue</h3></span>
111
126
  <text-ui slot="description" color="subtle">January – March 2024</text-ui>
@@ -127,7 +142,10 @@
127
142
  <p data-note>Stat-ui's <code>chart</code> slot positions a sparkline to the right of the hero number — KPI and its trajectory as one visual unit.</p>
128
143
  <div data-artifact-container>
129
144
  <div data-artifact-item data-artifact-label="stat with sparkline slot">
130
- <card-ui data-chunk="chart-in-card-stat-with-sparkline" style="max-width:320px;">
145
+ <card-ui data-chunk="chart-in-card-stat-with-sparkline" data-chunk-kind="block"
146
+ data-chunk-domain="dashboard"
147
+ data-chunk-description="Stat with inline sparkline — a single stat (Revenue $48.2k, +12.5% up) carrying a sparkline in its chart slot; the smallest chart-bearing KPI tile."
148
+ data-chunk-keywords="stat sparkline kpi tile revenue change trend inline chart slot compact dashboard metric" style="max-width:320px;">
131
149
  <section>
132
150
  <stat-ui label="Revenue" value="$48.2k" change="+12.5%" trend="up">
133
151
  <chart-ui id="pat-stat-spark" slot="chart" type="sparkline" x="month" y="value"></chart-ui>
@@ -143,7 +161,10 @@
143
161
  <p data-note>A <code>segmented-ui</code> in the header's <code>action</code> slot drives a time-range selection. Wire its <code>value</code> to refetch / slice the chart's <code>.data</code> via a controller.</p>
144
162
  <div data-artifact-container>
145
163
  <div data-artifact-item data-artifact-label="chart with 1w / 2w / 1m / 3m pills">
146
- <card-ui data-chunk="chart-in-card-chart-with-filter-pills" style="max-width:520px;">
164
+ <card-ui data-chunk="chart-in-card-chart-with-filter-pills" data-chunk-kind="block"
165
+ data-chunk-domain="dashboard"
166
+ data-chunk-description="Chart card with range picker — Sessions chart whose header action is a segmented control of time ranges (1w / 2w / 1m / 3m) that filters the plotted window."
167
+ data-chunk-keywords="chart card segmented range picker filter pills time window sessions week month dashboard analytics toggle" style="max-width:520px;">
147
168
  <header>
148
169
  <span slot="heading"><h3>Sessions</h3></span>
149
170
  <segmented-ui slot="action" value="1m">