@adia-ai/web-components 0.8.23 → 0.8.25

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 (69) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/components/agent-questions/agent-questions.yaml +0 -2
  3. package/components/alert/alert.class.js +5 -0
  4. package/components/alert/alert.yaml +2 -0
  5. package/components/anchor-bar/anchor-bar.a2ui.json +139 -0
  6. package/components/anchor-bar/anchor-bar.class.js +124 -0
  7. package/components/anchor-bar/anchor-bar.css +140 -0
  8. package/components/anchor-bar/anchor-bar.d.ts +35 -0
  9. package/components/anchor-bar/anchor-bar.examples.md +32 -0
  10. package/components/anchor-bar/anchor-bar.js +17 -0
  11. package/components/anchor-bar/anchor-bar.yaml +167 -0
  12. package/components/calendar-grid/calendar-grid.yaml +3 -0
  13. package/components/card/card.css +12 -0
  14. package/components/chart/chart.a2ui.json +16 -0
  15. package/components/chart/chart.class.js +105 -27
  16. package/components/chart/chart.css +27 -0
  17. package/components/chart/chart.d.ts +4 -0
  18. package/components/chart/chart.yaml +28 -0
  19. package/components/combobox/combobox.class.js +1 -1
  20. package/components/combobox/combobox.yaml +0 -1
  21. package/components/date-range-picker/date-range-picker.class.js +1 -1
  22. package/components/date-range-picker/date-range-picker.yaml +0 -1
  23. package/components/datetime-picker/datetime-picker.class.js +1 -1
  24. package/components/datetime-picker/datetime-picker.yaml +0 -1
  25. package/components/drawer/drawer.class.js +5 -0
  26. package/components/drawer/drawer.yaml +2 -0
  27. package/components/feed/feed.class.js +5 -0
  28. package/components/feed/feed.yaml +2 -0
  29. package/components/index.js +1 -0
  30. package/components/input/input.class.js +5 -0
  31. package/components/input/input.yaml +3 -0
  32. package/components/list/list.css +13 -4
  33. package/components/modal/modal.class.js +5 -0
  34. package/components/modal/modal.yaml +2 -0
  35. package/components/nav-group/nav-group.class.js +5 -0
  36. package/components/nav-group/nav-group.yaml +2 -0
  37. package/components/pagination/pagination.yaml +3 -0
  38. package/components/select/select.class.js +2 -2
  39. package/components/select/select.yaml +0 -1
  40. package/components/stat/stat.a2ui.json +8 -0
  41. package/components/stat/stat.css +39 -0
  42. package/components/stat/stat.d.ts +2 -0
  43. package/components/stat/stat.js +5 -0
  44. package/components/stat/stat.yaml +11 -0
  45. package/components/swiper/swiper.class.js +5 -0
  46. package/components/swiper/swiper.yaml +3 -0
  47. package/components/toolbar/toolbar.class.js +20 -0
  48. package/components/toolbar/toolbar.yaml +2 -0
  49. package/components/tooltip/tooltip.class.js +17 -11
  50. package/dist/host.min.css +1 -1
  51. package/dist/host.sheet.js +1 -1
  52. package/dist/theme-provider.min.js +2 -2
  53. package/dist/web-components.min.css +1 -1
  54. package/dist/web-components.min.js +117 -117
  55. package/dist/web-components.sheet.js +1 -1
  56. package/package.json +1 -1
  57. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +3 -3
  58. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +81 -93
  59. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +12 -25
  60. package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +8 -52
  61. package/patterns/changelog-feed/changelog-feed.examples.html +3 -3
  62. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +4 -4
  63. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  64. package/patterns/marketing-engagement/marketing-engagement.examples.html +8 -8
  65. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  66. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +2 -2
  67. package/styles/api/parametric-scope.css +81 -0
  68. package/styles/components.css +1 -0
  69. package/styles/tokens.css +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.8.25] — 2026-07-31
4
+
5
+ ### Fixed
6
+ - **`theme-panel[parametric]` works with a non-root `[target]`** (gh#570). Custom-property formulas resolve at their DECLARING element's own cascade, then inherit the already-resolved value down — the `--a-radius-*`/`--a-space-*`/`--a-size-*` derived tokens are declared once, on `:root`, so a scoped target overriding `--a-density`/`--a-radius-k` never triggered their re-derivation; only root-target parametric theming ever actually worked. `theme-panel.js` now marks a non-root `[parametric]` target with `data-parametric-scope`; a new `styles/api/parametric-scope.css` (attribute-API layer, mirroring the codebase's own established pattern of duplicating `calc()` formulas across layers — see `[size="sm"]` in `api/sizing.css`) re-declares the same formula bodies scoped to that marker, so the target's own subtree gets a working copy of the full derivation chain (radius, space, inset, gap, padding, margin, size). `[theme]`/`color-scheme` scoping was never affected (both resolve per-element natively). Browser-verified: a scoped preview pane's radius and padding both react to the panel's sliders while a sibling under `:root` stays untouched.
7
+ - **`chart-ui`'s `--color-<key>` per-series color hook is actually overridable from an ancestor** (gh#561). `#injectSeriesColors()` wrote `--color-{key}: var(--chart-N)` as an inline style on the chart HOST — inline styles beat everything but `!important`, so a consumer setting `--color-MAU` on any ancestor silently lost to the chart's own default. The method (and its call) are gone; each series element already carries its own `var(--color-{key}, var(--chart-{slot}))` fallback, which resolves the same default when unset and now lets an ancestor override win. `tooltip-ui`'s top-layer popover — which bridges `--color-{key}` values by copying them at show time, since a popover appended to `document.body` doesn't inherit custom properties from the chart's ancestor chain — read only the (now-removed) inline style; it now resolves each declared series key via `getComputedStyle` off `chart-ui`'s public `legendData`, so it correctly picks up an ancestor-set value too.
8
+
9
+ ### Added
10
+ - **`anchor-bar-ui`** (gh#495, `status: beta`) — persistent, anchored, elevated surface for chrome that must stay visible while a condition holds: floating bulk-action toolbars, undo bars, sticky wizard/commit footers. Pre-build audit (§0) found the gap real: `toast-ui` is auto-dismissing (wrong lifecycle), `modal-ui`/`drawer-ui` are blocking overlays (wrong semantics), and the bulk-action-toolbar pattern's floating variant (gh#494/PR#509) hand-rolled a `card-ui` + `position: sticky` recipe scoped to one consumer that by its own documented rule "never `position: fixed` to the viewport" — structurally can't reach the viewport-global case (undo bars, arbitrary layouts). Default mode (`[viewport]` unset) generalizes that exact recipe as the primitive's own default: `position: sticky` within the nearest scroll container, `[anchor="bottom"\|"top"]` + `[align="start"\|"center"\|"end"]`. `[viewport]` opts into `position: fixed` to the browser viewport, promoted to the top layer via the Popover API (`popover="manual"`, mirroring `popover-ui`'s convention) so it wins stacking over shell chrome without a z-index war — manual popovers never light-dismiss or close on Escape, matching the "no auto-dismiss" persistence contract. `[open]` (default `false`) controls visibility via `opacity`/`visibility` transitions, never `display`/`[hidden]` — a `display:none` toggle resets `toolbar-ui`'s `ResizeObserver`-driven overflow measurement at 0px, the same trap the bulk-action-toolbar pattern's own code comments document. Bulk-action-toolbar pattern's own comment already named this primitive as its eventual home; its floating-variant demo now composes `anchor-bar-ui` directly instead of the hand-rolled `card-ui` recipe.
11
+ - **`stat-ui` gains a `band` chart layout** (gh#557, adia-v2 ADIA2-7059). Neither existing chart placement produces the Gmail/Vercel/shadcn stat-card shape — inline squeezes the value column at 4-up widths (titles truncating to "Acti…"), `[bleed]` halves the text column. `band` (alongside `bleed`) stacks label/icon, value, chart, and change as four full-width rows via `grid-template-areas: "label icon" "value value" "chart chart" "change change"`; the chart band gets its own fixed-height token (`--stat-chart-band-height`, default `3rem`) with `aspect-ratio` released, text rows stay baseline-aligned, and it composes with `icon` exactly as the base layout does. No effect without a `slot="chart"` child, same guard as `bleed`. Replaces the interim `stat-ui[data-chart-band]` app-side override shipped downstream.
12
+ - **`chart-ui` line/multi-line dot control + per-series emphasis** (gh#561, adia-v2 dashboard pass). Three related line-chart gaps: (1) `dots="all|none|hover|last"` (default `all`, back-compat) — `none` renders no `[data-dot]`s on dense series, `hover` shows only the hovered datum's dot (paired via DOM adjacency to its `[data-hit]` sibling, toggled through `chart-hover`/`chart-leave`), `last` shows only the terminal point per series; `[data-hit]` tooltip targets are untouched in every mode. (2) `series-emphasis="<seriesKey>"` for multi-line — the named series keeps area fill + full-strength line, every other series drops its area and renders line-only at the new `--chart-deemphasized-opacity` token (default 0.4). `y-max`/domain-padding for small-integer domains (also filed on the issue) is deferred — out of scope for this pass, noted on the issue.
13
+
14
+ ### Maintenance
15
+ - **`components/` touched in this release window** (47 file(s), e.g. `adia-mark/adia-mark.examples.html`) — carried by the entries above.
16
+ - **`dist/` bundles rebuilt** in this cut's window (6 file(s)) — regenerated from the source changes described above, not independent edits.
17
+ - **`patterns/` touched in this release window** (3 file(s), e.g. `bulk-action-toolbar/bulk-action-toolbar.examples.html`) — carried by the entries above.
18
+
19
+ ## [0.8.24] — 2026-07-30
20
+
21
+ ### Fixed
22
+ - **`list-item-ui` two-line items center the leading glyph on the whole item** (gh#558, operator screenshot). The icon was pinned to the title row, hanging 9–18px above the item center on title+description rows — Material's two-line convention centers it. Explicit `grid-row: 1 / span 2` scoped by `:has([slot="description"])` (with no `grid-template-rows` the explicit grid has zero rows, so the old `1 / -1` collapsed to one row — the reason the earlier anatomy comment called them "identical"); single-line items untouched (probe: two-line offset 0/0/0, single-line 24px rows unchanged).
23
+ - **`card-ui` pins a direct `<footer>` to the card bottom** (gh#559, operator screenshot). In an equal-height grid row, a shorter card's CTA floated at content end (Free tier 42px above the edge vs 17px on its siblings) — the buttons were already in footers; the card just had no pinning rule. `:has(> footer)` promotes the card to a flex column with `margin-top: auto` on the footer; footerless cards keep block flow. Probe: 17/17/17.
24
+ - **`requiredIcons` drift cleared across 16 components** (gh#550). Eight primitives that auto-stamp icons (alert/drawer/feed/modal `x`, input steppers `caret-up`/`caret-down`, nav-group `caret-right`, swiper `caret-left`/`caret-right`, toolbar overflow `dots-three`) now declare `static requiredIcons`, mirrored in their yaml SoTs (calendar-grid and pagination gained the missing yaml blocks); four stale declarations pruned (combobox `check`, date-range-picker `arrow-right`, datetime-picker `clock`, select `magnifying-glass` — none ever stamped) and agent-questions' leftover yaml block removed (options render via option-card-ui since gh#276). Checker itself hardened: `//` line comments no longer read as stamp sites (button `plus` false positive) and yaml list entries may carry trailing `#` comments (select's three phantom mismatches). 125/125 components clean.
25
+ - **`toolbar-ui` overflow measurement traps closed** (gh#528). (1) Flex-grow children (spacers, stretched groups) measured at their EXPANDED width, so the row always read "full" and the last action spilled unrecoverably — grow is zeroed for the read and restored. (2) A `display:none` mount committed a spill from a 0-width reading and stranded the bar collapsed — a zero-width guard now leaves everything visible until the ResizeObserver delivers a real size. Browser-verified both ways.
26
+ - **Corpus/demo enum drift swept** (gh#529): 41 uses of never-implemented values (Tag `muted`/`primary`, Text `primary`/`muted`/`xxl`, Card `soft`/`primary`, Badge `subtle`, Chart `muted`, Button `danger`) mapped to their nearest real values across 15 harvest sources — they silently rendered as defaults. `verify:corpus` warnings 138 → 97.
27
+ - **10 standalone example routes actually render** (gh#530): the sitemap pointed iframe-served routes at headless `.contents.html` fragments; thin standalone wrappers added (single-source fetch-inject), annotations repointed, `frame`/`theme-provider` demos wired for phosphor. 108/108 clean. The `check-required-icons` checker also now reads `<comp>.class.js` and all stamp shapes — 20 false positives gone, 19 real findings exposed → gh#550.
28
+
29
+ ### Maintenance
30
+ - **`dist/` bundles rebuilt** in this cut's window (4 file(s)) — regenerated from the source changes described above, not independent edits.
31
+ - **`patterns/` touched in this release window** (7 file(s), e.g. `admin-keys-and-export/admin-keys-and-export.examples.html`) — carried by the entries above.
32
+
3
33
  ## [0.8.23] — 2026-07-30
4
34
 
5
35
  ### Removed
@@ -56,8 +56,6 @@ states:
56
56
  description: Default, ready for interaction.
57
57
  traits: []
58
58
  tokens: {}
59
- requiredIcons:
60
- - check
61
59
  a2ui:
62
60
  rules:
63
61
  - rule: 'Multi-choice clarifying-question card emitted by an agent when it needs disambiguation before proceeding.'
@@ -42,6 +42,11 @@ function _warnOnce(key, message) {
42
42
  }
43
43
 
44
44
  export class UIAlert extends UIElement {
45
+ // Phosphor icons this primitive auto-stamps (without consumer markup).
46
+ // Aggregated by installIconLoadersForRegistered() across all defined
47
+ // elements. Audited by check-required-icons.mjs.
48
+ static requiredIcons = ['x'];
49
+
45
50
  static properties = {
46
51
  text: { type: String, default: '', reflect: true },
47
52
  title: { type: String, default: '', reflect: true },
@@ -173,6 +173,8 @@ states:
173
173
  description: Default, ready for interaction.
174
174
  traits: []
175
175
  tokens: {}
176
+ requiredIcons:
177
+ - x
176
178
  a2ui:
177
179
  rules:
178
180
  - rule: 'Inline alert/banner for status messages within a content region. Severity via variant (info, success, warn, error).'
@@ -0,0 +1,139 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/AnchorBar.json",
4
+ "title": "AnchorBar",
5
+ "description": "Persistent, anchored, elevated surface for chrome that must stay visible while a condition holds — floating bulk-action toolbars, undo bars, sticky wizard/commit footers. Distinct from <toast-ui> (auto-dismissing, notification semantics) and <modal-ui>/<drawer-ui> (blocking overlays with backdrop + focus-trap semantics). Default mode anchors [anchor] to the nearest scroll container via `position: sticky` — the shape gh#494's bulk-action-toolbar pattern hand-rolled with card-ui + position:sticky. Opt into [viewport] for a surface that anchors to the browser viewport instead, promoted to the top layer via the Popover API (`popover=\"manual\"`) so it wins stacking over shell chrome without a z-index war. Visibility is [open] (default false, no auto-dismiss timer) — arbitrary content (typically <toolbar-ui> or <text-ui> + <button-ui>) rides the default slot.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "align": {
17
+ "description": "Horizontal alignment along the anchored edge.",
18
+ "type": "string",
19
+ "enum": [
20
+ "start",
21
+ "center",
22
+ "end"
23
+ ],
24
+ "default": "center"
25
+ },
26
+ "anchor": {
27
+ "description": "Viewport/container edge the bar sticks to.",
28
+ "type": "string",
29
+ "enum": [
30
+ "bottom",
31
+ "top"
32
+ ],
33
+ "default": "bottom"
34
+ },
35
+ "component": {
36
+ "const": "AnchorBar"
37
+ },
38
+ "open": {
39
+ "description": "Controls visibility. No auto-dismiss — caller toggles explicitly.",
40
+ "type": "boolean",
41
+ "default": false
42
+ },
43
+ "viewport": {
44
+ "description": "false (default) anchors to the nearest scroll container via `position: sticky` — safe for content with ResizeObserver-driven children (e.g. <toolbar-ui>'s overflow measurement never sees a display:none reset). true anchors to the browser viewport via `position: fixed` + Popover API top-layer promotion — note the native Popover UA stylesheet applies `display:none` while closed, which DOES reset ResizeObserver-driven children on first open; prefer the default for content wrapping <toolbar-ui>.",
45
+ "type": "boolean",
46
+ "default": false
47
+ }
48
+ },
49
+ "required": [
50
+ "component"
51
+ ],
52
+ "unevaluatedProperties": false,
53
+ "x-adiaui": {
54
+ "anti_patterns": [],
55
+ "category": "container",
56
+ "composes": [],
57
+ "events": {
58
+ "close": {
59
+ "description": "Fired when [open] becomes false."
60
+ },
61
+ "open": {
62
+ "description": "Fired when [open] becomes true."
63
+ }
64
+ },
65
+ "examples": [
66
+ {
67
+ "description": "Bottom-anchored bar containing a toolbar with a selection count and action buttons, shown persistently while open.",
68
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Card\",\n \"children\": [\n \"bar\"\n ]\n },\n {\n \"id\": \"bar\",\n \"component\": \"AnchorBar\",\n \"anchor\": \"bottom\",\n \"open\": true,\n \"children\": [\n \"toolbar\"\n ]\n },\n {\n \"id\": \"toolbar\",\n \"component\": \"Toolbar\",\n \"children\": [\n \"count\"\n ]\n },\n {\n \"id\": \"count\",\n \"component\": \"Text\",\n \"text\": \"3 selected\"\n }\n]",
69
+ "name": "anchor-bar-demo"
70
+ }
71
+ ],
72
+ "keywords": [
73
+ "anchor",
74
+ "bar",
75
+ "floating",
76
+ "toolbar",
77
+ "undo",
78
+ "persistent",
79
+ "sticky",
80
+ "footer",
81
+ "selection",
82
+ "bulk action"
83
+ ],
84
+ "name": "UIAnchorBar",
85
+ "related": [],
86
+ "slots": {
87
+ "default": {
88
+ "description": "Arbitrary bar content — typically <toolbar-ui> (bulk-action bars) or <text-ui> + <button-ui> (undo bars, wizard footers). The bar owns only the elevated surface chrome (bg/border/radius/shadow) and positioning; content layout is the slotted children's own."
89
+ }
90
+ },
91
+ "states": [
92
+ {
93
+ "description": "Default, ready for interaction.",
94
+ "name": "idle"
95
+ }
96
+ ],
97
+ "status": "beta",
98
+ "synonyms": {
99
+ "bar": [
100
+ "toolbar",
101
+ "anchor",
102
+ "footer"
103
+ ],
104
+ "floating": [
105
+ "anchor",
106
+ "sticky",
107
+ "popover"
108
+ ],
109
+ "undo": [
110
+ "anchor",
111
+ "toast",
112
+ "alert"
113
+ ]
114
+ },
115
+ "tag": "anchor-bar-ui",
116
+ "tokens": {
117
+ "--anchor-bar-bg": {
118
+ "description": "Override background color"
119
+ },
120
+ "--anchor-bar-border": {
121
+ "description": "Override border color"
122
+ },
123
+ "--anchor-bar-fg": {
124
+ "description": "Override text color"
125
+ },
126
+ "--anchor-bar-offset": {
127
+ "description": "Override distance from the anchored edge"
128
+ },
129
+ "--anchor-bar-radius": {
130
+ "description": "Override corner radius"
131
+ },
132
+ "--anchor-bar-shadow": {
133
+ "description": "Override elevation shadow"
134
+ }
135
+ },
136
+ "traits": [],
137
+ "version": 1
138
+ }
139
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Non-side-effect class export for `<anchor-bar-ui>`.
3
+ *
4
+ * Importing this file gives you the class(es) without auto-registering the tag.
5
+ * Useful for test isolation, subclassing with tag-name override, or selective
6
+ * composition.
7
+ *
8
+ * The auto-register path stays at `@adia-ai/web-components/components/anchor-bar`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * <anchor-bar-ui> — Persistent, anchored, elevated surface (gh#495).
16
+ *
17
+ * Fills the gap between <toast-ui> (auto-dismissing) and <modal-ui>/
18
+ * <drawer-ui> (blocking overlays): chrome that must stay visible while a
19
+ * condition holds — floating bulk-action toolbars, undo bars, sticky
20
+ * wizard/commit footers.
21
+ *
22
+ * <!-- Default — sticky within the nearest scroll container -->
23
+ * <anchor-bar-ui anchor="bottom" open>
24
+ * <toolbar-ui role="toolbar" aria-label="Bulk actions">…</toolbar-ui>
25
+ * </anchor-bar-ui>
26
+ *
27
+ * <!-- Viewport-anchored, top-layer (undo bars, arbitrary layouts) -->
28
+ * <anchor-bar-ui anchor="bottom" viewport open>
29
+ * <text-ui>3 items archived</text-ui>
30
+ * <button-ui text="Undo" variant="ghost"></button-ui>
31
+ * </anchor-bar-ui>
32
+ *
33
+ * [viewport] promotes the element itself to the top layer via the Popover
34
+ * API (`popover="manual"` + `showPopover()`/`hidePopover()`), mirroring
35
+ * `popover-ui`'s established convention — but `manual` popovers, unlike
36
+ * `auto`, never light-dismiss or close on Escape: visibility is entirely
37
+ * caller-driven via [open], matching the "no auto-dismiss" persistence
38
+ * contract. No listeners are added when [viewport] is false — a `[toggle]`
39
+ * listener exists only while the element carries `popover="manual"`, to
40
+ * reflect programmatic/browser-driven `showPopover()`/`hidePopover()` calls
41
+ * back into the [open] property.
42
+ */
43
+
44
+ import { UIElement } from '../../core/element.js';
45
+
46
+ export class UIAnchorBar extends UIElement {
47
+ static properties = {
48
+ anchor: { type: String, default: 'bottom', reflect: true },
49
+ align: { type: String, default: 'center', reflect: true },
50
+ viewport: { type: Boolean, default: false, reflect: true },
51
+ open: { type: Boolean, default: false, reflect: true },
52
+ };
53
+
54
+ static template = () => null;
55
+
56
+ #popoverBound = false;
57
+
58
+ connected() {
59
+ if (this.viewport) this.#bindPopover();
60
+ }
61
+
62
+ updated(changed) {
63
+ if (changed.has('viewport')) {
64
+ if (this.viewport) this.#bindPopover();
65
+ else this.#unbindPopover();
66
+ }
67
+ if (changed.has('open')) {
68
+ this.#sync();
69
+ this.dispatchEvent(new Event(this.open ? 'open' : 'close', { bubbles: true }));
70
+ }
71
+ }
72
+
73
+ render() {
74
+ // Re-assert popover state on every reactive pass — cheap no-op when
75
+ // already in sync (guards match(':popover-open') before calling).
76
+ if (this.viewport) this.#sync();
77
+ }
78
+
79
+ disconnected() {
80
+ if (this.viewport && this.matches(':popover-open')) {
81
+ try { this.hidePopover(); } catch { /* popover API unavailable */ }
82
+ }
83
+ if (this.#popoverBound) {
84
+ this.#popoverBound = false;
85
+ this.removeEventListener('toggle', this.#onToggle);
86
+ }
87
+ }
88
+
89
+ // ── Internal ──
90
+
91
+ #bindPopover() {
92
+ if (!this.hasAttribute('popover')) this.setAttribute('popover', 'manual');
93
+ if (!this.#popoverBound) {
94
+ this.#popoverBound = true;
95
+ this.addEventListener('toggle', this.#onToggle);
96
+ }
97
+ this.#sync();
98
+ }
99
+
100
+ #unbindPopover() {
101
+ if (this.matches(':popover-open')) {
102
+ try { this.hidePopover(); } catch { /* popover API unavailable */ }
103
+ }
104
+ if (this.#popoverBound) {
105
+ this.#popoverBound = false;
106
+ this.removeEventListener('toggle', this.#onToggle);
107
+ }
108
+ this.removeAttribute('popover');
109
+ }
110
+
111
+ #sync() {
112
+ if (!this.viewport) return;
113
+ if (this.open && !this.matches(':popover-open')) {
114
+ try { this.showPopover(); } catch { /* popover API unavailable */ }
115
+ } else if (!this.open && this.matches(':popover-open')) {
116
+ try { this.hidePopover(); } catch { /* popover API unavailable */ }
117
+ }
118
+ }
119
+
120
+ #onToggle = (e) => {
121
+ const nowOpen = e.newState === 'open';
122
+ if (nowOpen !== this.open) this.open = nowOpen;
123
+ };
124
+ }
@@ -0,0 +1,140 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ ANCHOR-BAR-N — Persistent anchored surface (floating toolbars,
3
+ undo bars, wizard footers). Issue gh-495.
4
+
5
+ [open] is a dynamic runtime-toggled attribute and this component's
6
+ base rule anchors to `:popover-open` in [viewport] mode — both
7
+ selector shapes are hoisted OUT of @scope per BROWSER-COMPAT.md §3a
8
+ Flavor B (Safari 17.x fails to restyle `:scope[dyn-attr]` on
9
+ attribute removal) and the top-layer convention in
10
+ component-token-contract.md (popover-promoted content's non-token
11
+ rules stay outside @scope). [anchor] / [align] / [viewport] are
12
+ set once at markup (mode selection, not toggled per-frame) and stay
13
+ inside @scope alongside the token block, mirroring toast-ui's
14
+ [position="…"] mode-attribute rules.
15
+ ═══════════════════════════════════════════════════════════════ */
16
+
17
+ @scope (anchor-bar-ui) {
18
+ :where(:scope) {
19
+ --anchor-bar-bg: var(--md-sys-color-neutral-surface-bright);
20
+ --anchor-bar-fg: var(--md-sys-color-neutral-on-surface);
21
+ --anchor-bar-border: var(--md-sys-color-neutral-outline-variant);
22
+ --anchor-bar-radius: var(--a-radius-lg);
23
+ --anchor-bar-shadow: var(--a-shadow-lg);
24
+ --anchor-bar-px: var(--a-space-4);
25
+ --anchor-bar-py: var(--a-space-3);
26
+ --anchor-bar-gap: var(--a-space-3);
27
+ --anchor-bar-offset: var(--a-space-4);
28
+ --anchor-bar-duration: var(--a-duration);
29
+ --anchor-bar-easing: var(--a-easing-out);
30
+ --anchor-bar-font: var(--a-font-family);
31
+ --anchor-bar-size: var(--a-ui-md);
32
+ /* width:100% + a capped max-width, NOT width:max-content/fit-content —
33
+ the bulk-action-toolbar pattern (gh-494) hit this exact trap first:
34
+ a fit-content box derives its size from slotted <toolbar-ui>'s
35
+ rendered content, but toolbar-ui's own overflow reflow measures ITS
36
+ available width FROM this box — a self-reinforcing collapse the
37
+ first time reflow runs while still narrow (0 available width spills
38
+ every item into the "more" overflow popover, and toolbar-ui does not
39
+ reliably re-measure afterward). A fixed max-width breaks the loop:
40
+ deterministic, independent of the toolbar's own spillover state.
41
+ Both tokens are consumer-overridable, matching toast-ui's own
42
+ `--toast-max-width` precedent. */
43
+ --anchor-bar-width: 100%;
44
+ --anchor-bar-max-width: min(46rem, calc(100% - var(--a-space-6)));
45
+ }
46
+
47
+ :scope {
48
+ box-sizing: border-box;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--anchor-bar-gap);
52
+ width: var(--anchor-bar-width);
53
+ max-width: var(--anchor-bar-max-width);
54
+ padding: var(--anchor-bar-py) var(--anchor-bar-px);
55
+ background: var(--anchor-bar-bg);
56
+ color: var(--anchor-bar-fg);
57
+ font-family: var(--anchor-bar-font);
58
+ font-size: var(--anchor-bar-size);
59
+ border: 1px solid var(--anchor-bar-border);
60
+ border-radius: var(--anchor-bar-radius);
61
+ box-shadow: var(--anchor-bar-shadow);
62
+ }
63
+
64
+ /* Horizontal alignment along the anchored edge — static mode attr. */
65
+ :where(:scope),
66
+ :where(:scope[align="center"]) { margin-inline: auto; }
67
+ :where(:scope[align="start"]) { margin-inline: 0; }
68
+ :where(:scope[align="end"]) { margin-inline: auto 0; }
69
+
70
+ /* A slotted <toolbar-ui> (the bulk-action-bar shape) must fill the bar's
71
+ real width, not shrink to its own content — mirrors toast-ui's
72
+ `[slot="message"] { flex: 1; min-width: 0; }`. Without this, toolbar-ui
73
+ is a default `flex: 0 1 auto` item that hugs its own content, and its
74
+ overflow reflow then measures ITS OWN shrunk size as "available width"
75
+ — the same circular-collapse trap the width token comment above
76
+ documents, one level down. Other default-slot content (text-ui +
77
+ button-ui for an undo bar) keeps natural content sizing. */
78
+ :scope > toolbar-ui {
79
+ flex: 1;
80
+ min-width: 0;
81
+ }
82
+ }
83
+
84
+ /* ── Positioning + visibility — outside @scope, see file-header note ──
85
+ Default mode: sticky within the nearest scroll container (never
86
+ `position: fixed` to the viewport) — the same recipe the
87
+ bulk-action-toolbar pattern (issue gh-494) hand-rolled with card-ui, now
88
+ the primitive's own default. Visibility toggles opacity/visibility,
89
+ never display/[hidden] — a display:none reset stalls toolbar-ui's
90
+ ResizeObserver-driven overflow measurement at 0px (documented gotcha,
91
+ packages/web-components/patterns/bulk-action-toolbar). */
92
+ anchor-bar-ui {
93
+ position: sticky;
94
+ z-index: 9999; /* fallback stacking; [viewport] wins via top-layer */
95
+ opacity: 0;
96
+ visibility: hidden;
97
+ pointer-events: none;
98
+ transform: translateY(var(--anchor-bar-offset));
99
+ transition: opacity var(--anchor-bar-duration) var(--anchor-bar-easing),
100
+ transform var(--anchor-bar-duration) var(--anchor-bar-easing),
101
+ visibility 0s linear var(--anchor-bar-duration);
102
+ }
103
+ anchor-bar-ui[anchor="top"] {
104
+ transform: translateY(calc(-1 * var(--anchor-bar-offset)));
105
+ }
106
+ anchor-bar-ui[open] {
107
+ opacity: 1;
108
+ visibility: visible;
109
+ pointer-events: auto;
110
+ transform: translateY(0);
111
+ transition: opacity var(--anchor-bar-duration) var(--anchor-bar-easing),
112
+ transform var(--anchor-bar-duration) var(--anchor-bar-easing);
113
+ }
114
+
115
+ anchor-bar-ui,
116
+ anchor-bar-ui[anchor="bottom"] { bottom: var(--anchor-bar-offset); top: auto; }
117
+ anchor-bar-ui[anchor="top"] { top: var(--anchor-bar-offset); bottom: auto; }
118
+
119
+ /* [viewport] — position:fixed to the browser viewport; [popover="manual"]
120
+ (set in anchor-bar.class.js) promotes to the top layer once shown, so
121
+ this wins stacking over shell chrome without a z-index war. `env()`
122
+ safe-area insets are a CSS environment function, not a raw px literal
123
+ — no token exists for device safe-area, this is the platform-standard
124
+ way to read it. */
125
+ anchor-bar-ui[viewport] {
126
+ position: fixed;
127
+ left: 0;
128
+ right: 0;
129
+ }
130
+ anchor-bar-ui[viewport][anchor="bottom"] {
131
+ bottom: calc(var(--anchor-bar-offset) + env(safe-area-inset-bottom, 0px));
132
+ }
133
+ anchor-bar-ui[viewport][anchor="top"] {
134
+ top: calc(var(--anchor-bar-offset) + env(safe-area-inset-top, 0px));
135
+ }
136
+
137
+ @media (prefers-reduced-motion: reduce) {
138
+ anchor-bar-ui,
139
+ anchor-bar-ui[open] { transition: none; }
140
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `<anchor-bar-ui>` — Persistent, anchored, elevated surface for chrome that must stay visible while a condition holds — floating bulk-action toolbars, undo bars, sticky wizard/commit footers. Distinct from <toast-ui> (auto-dismissing, notification semantics) and <modal-ui>/<drawer-ui> (blocking overlays with backdrop + focus-trap semantics). Default mode anchors [anchor] to the nearest scroll container via `position: sticky` — the shape gh#494's bulk-action-toolbar pattern hand-rolled with card-ui + position:sticky. Opt into [viewport] for a surface that anchors to the browser viewport instead, promoted to the top layer via the Popover API (`popover="manual"`) so it wins stacking over shell chrome without a z-index war. Visibility is [open] (default false, no auto-dismiss timer) — arbitrary content (typically <toolbar-ui> or <text-ui> + <button-ui>) rides the default slot.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/anchor-bar
5
+ *
6
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
7
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
8
+ * run `npm run build:components`, then `npm run codegen:dts` to
9
+ * regenerate; or hand-author this file fully if rich event types are
10
+ * needed beyond what the yaml `events:` block can express.
11
+ */
12
+
13
+ import { UIElement } from '../../core/element.js';
14
+
15
+ export type AnchorBarCloseEvent = CustomEvent<unknown>;
16
+ export type AnchorBarOpenEvent = CustomEvent<unknown>;
17
+
18
+ export class UIAnchorBar extends UIElement {
19
+ /** Horizontal alignment along the anchored edge. */
20
+ align: 'start' | 'center' | 'end';
21
+ /** Viewport/container edge the bar sticks to. */
22
+ anchor: 'bottom' | 'top';
23
+ /** Controls visibility. No auto-dismiss — caller toggles explicitly. */
24
+ open: boolean;
25
+ /** false (default) anchors to the nearest scroll container via `position: sticky` — safe for content with ResizeObserver-driven children (e.g. <toolbar-ui>'s overflow measurement never sees a display:none reset). true anchors to the browser viewport via `position: fixed` + Popover API top-layer promotion — note the native Popover UA stylesheet applies `display:none` while closed, which DOES reset ResizeObserver-driven children on first open; prefer the default for content wrapping <toolbar-ui>. */
26
+ viewport: boolean;
27
+
28
+ addEventListener<K extends keyof HTMLElementEventMap>(
29
+ type: K,
30
+ listener: (this: UIAnchorBar, ev: HTMLElementEventMap[K]) => unknown,
31
+ options?: boolean | AddEventListenerOptions,
32
+ ): void;
33
+ addEventListener(type: 'close', listener: (ev: AnchorBarCloseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
34
+ addEventListener(type: 'open', listener: (ev: AnchorBarOpenEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
35
+ }
@@ -0,0 +1,32 @@
1
+ # anchor-bar — Examples
2
+
3
+ ## anchor
4
+
5
+ ```html
6
+ <div style="position: relative; height: 6rem; overflow: auto; border: 1px solid var(--md-sys-color-neutral-outline-variant); border-radius: var(--a-radius-md);">
7
+ <div style="height: 10rem;"></div>
8
+ <anchor-bar-ui anchor="bottom" open>
9
+ <text-ui>Bottom-anchored</text-ui>
10
+ </anchor-bar-ui>
11
+ </div>
12
+ ```
13
+
14
+ ## align
15
+
16
+ ```html
17
+ <div style="position: relative; height: 4rem; overflow: auto; border: 1px solid var(--md-sys-color-neutral-outline-variant); border-radius: var(--a-radius-md);">
18
+ <anchor-bar-ui anchor="bottom" align="start" open>
19
+ <text-ui>Start</text-ui>
20
+ </anchor-bar-ui>
21
+ </div>
22
+ ```
23
+
24
+ ## viewport
25
+
26
+ ```html
27
+ <button-ui text="Archive item" variant="outline" size="sm" onclick="customElements.whenDefined('anchor-bar-ui').then(()=>{const b=document.getElementById('viewport-undo-bar'); if(b) b.open = true;})"></button-ui>
28
+ <anchor-bar-ui id="viewport-undo-bar" anchor="bottom" viewport>
29
+ <text-ui>1 item archived</text-ui>
30
+ <button-ui text="Undo" variant="ghost" size="sm" onclick="const b=this.closest('anchor-bar-ui'); if(b) b.open=false;"></button-ui>
31
+ </anchor-bar-ui>
32
+ ```
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<anchor-bar-ui>` — auto-registers the tag on import.
3
+ *
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
6
+ *
7
+ * import { UIAnchorBar } from '@adia-ai/web-components/components/anchor-bar/class';
8
+ *
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
10
+ */
11
+
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UIAnchorBar } from './anchor-bar.class.js';
14
+
15
+ defineIfFree('anchor-bar-ui', UIAnchorBar);
16
+
17
+ export { UIAnchorBar };