@adia-ai/web-components 0.7.14 → 0.7.16

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.7.16] — 2026-06-08
4
+
5
+ ### Changed
6
+ - **Dist bundles rebuilt — the CDN / bundle path now matches source.** `dist/web-components.min.{css,js}` + the `.sheet.js` twin were regenerated to carry the 0.7.15 `badge-ui` / `tag-ui` padding (`--a-space-2` → `--a-space-1-5`) plus JS that the 0.7.15 cut shipped stale (it never rebundled). Consumers via `@adia-ai/web-components/css/bundled` + `/js/bundled` were getting the pre-0.7.15 output. `check:css-bundles-fresh` + `check:js-bundles-fresh` now gate this in CI.
7
+
8
+ ## [0.7.15] — 2026-06-08
9
+
10
+ ### Added
11
+ - **New `<menu-label-ui>` — labeled group headings inside `<menu-ui>`.** A co-located child of `<menu-ui>` (registered via `defineIfFree('menu-label-ui', UIMenuLabel)` in `menu.js`; `UIMenuLabel` declared in the parent `menu.d.ts` by dts-codegen, like `menu-item`/`menu-divider`) that renders a non-interactive section heading to group `<menu-item-ui>` rows under labeled headings. Files: `components/menu/{menu-label.yaml,menu-label.a2ui.json}`. (Powers the embedded-app HCC demo's grouped / by-flow menu.)
12
+
13
+ ### Changed
14
+ - **`badge-ui` + `tag-ui` horizontal padding tightened** — `--badge-px` / `--tag-px` `var(--a-space-2)` → `var(--a-space-1-5)` for a snugger chip that reads better in dense rows; vertical padding unchanged.
15
+
3
16
  ## [0.7.14] — 2026-06-08
4
17
 
5
18
  ### Fixed
@@ -4,7 +4,7 @@
4
4
  --badge-bg: var(--a-bg-muted);
5
5
  --badge-fg: var(--a-fg);
6
6
  --badge-radius: var(--a-radius-md);
7
- --badge-px: var(--a-space-2);
7
+ --badge-px: var(--a-space-1-5);
8
8
  --badge-py: 2px;
9
9
  --badge-gap: var(--a-space-1);
10
10
  --badge-font-size: var(--a-ui-sm);
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/MenuLabel.json",
4
+ "title": "MenuLabel",
5
+ "description": "Non-interactive group heading inside a `<menu-ui>` — a small uppercase kicker that names the group of `<menu-item-ui>` rows beneath it. Renders with role=\"presentation\" so the menu's roving focus skips it.",
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
+ "component": {
17
+ "const": "MenuLabel"
18
+ },
19
+ "text": {
20
+ "description": "The group label. Stamped into a `<span slot=\"text\">`; consumer-provided slot content wins.",
21
+ "type": "string",
22
+ "default": ""
23
+ }
24
+ },
25
+ "required": [
26
+ "component"
27
+ ],
28
+ "unevaluatedProperties": false,
29
+ "x-adiaui": {
30
+ "anti_patterns": [],
31
+ "category": "navigation",
32
+ "composes": [],
33
+ "events": {},
34
+ "examples": [],
35
+ "keywords": [
36
+ "menu-label",
37
+ "menu-group",
38
+ "section-header",
39
+ "menu-heading"
40
+ ],
41
+ "name": "UIMenuLabel",
42
+ "related": [
43
+ "Menu",
44
+ "MenuItem",
45
+ "MenuDivider"
46
+ ],
47
+ "slots": {
48
+ "text": {
49
+ "description": "Custom label content; falls through to the `[text]` prop if not slotted."
50
+ }
51
+ },
52
+ "states": [],
53
+ "status": "stable",
54
+ "synonyms": {
55
+ "menu-label": [
56
+ "menu-group-label",
57
+ "menu-section-header",
58
+ "menu-heading"
59
+ ]
60
+ },
61
+ "tag": "menu-label-ui",
62
+ "tokens": {},
63
+ "traits": [],
64
+ "version": 1
65
+ }
66
+ }
@@ -0,0 +1,55 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ #
3
+ # Authored alongside UIMenuDivider as the labeled-group counterpart: a divider
4
+ # SEPARATES groups, a label NAMES one. Ships as a sibling class in
5
+ # menu/menu.class.js + is registered alongside UIMenu + UIMenuItem.
6
+
7
+ # Child component of <menu-ui>. Surface only inside that parent.
8
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
9
+ name: UIMenuLabel
10
+ tag: menu-label-ui
11
+ status: stable
12
+ component: MenuLabel
13
+ category: navigation
14
+ version: 1
15
+ description: |-
16
+ Non-interactive group heading inside a `<menu-ui>` — a small uppercase kicker that names the group of `<menu-item-ui>` rows beneath it. Renders with role="presentation" so the menu's roving focus skips it.
17
+
18
+ props:
19
+ text:
20
+ description: The group label. Stamped into a `<span slot="text">`; consumer-provided slot content wins.
21
+ type: string
22
+ default: ""
23
+ slots:
24
+ text:
25
+ description: Custom label content; falls through to the `[text]` prop if not slotted.
26
+
27
+ a2ui:
28
+ rules:
29
+ - >-
30
+ <menu-label-ui> MUST be a direct child of <menu-ui>; like menu-item-ui
31
+ and menu-divider-ui it is hoisted into the top-layer popover via
32
+ <menu-ui>'s direct-descendant query — a label nested elsewhere renders
33
+ outside the popover.
34
+ - >-
35
+ Use a label to NAME a group and <menu-divider-ui> to SEPARATE groups.
36
+ The common pattern is one label at the top of each group with dividers
37
+ between groups; avoid a label with no items beneath it.
38
+ - >-
39
+ Non-interactive: role="presentation", no [value], fires no events, and
40
+ the menu's keyboard focus skips it. For a selectable row use
41
+ <menu-item-ui>; for an unavailable action use <menu-item-ui disabled>.
42
+
43
+ keywords:
44
+ - menu-label
45
+ - menu-group
46
+ - section-header
47
+ - menu-heading
48
+
49
+ related:
50
+ - Menu
51
+ - MenuItem
52
+ - MenuDivider
53
+
54
+ synonyms:
55
+ menu-label: [menu-group-label, menu-section-header, menu-heading]
@@ -93,7 +93,7 @@ export class UIMenu extends UIElement {
93
93
  // → an empty popover (FEEDBACK-92). Skip anything already relocated into
94
94
  // the popover so a re-entrant #show() (reactive re-render while open)
95
95
  // doesn't reorder items. Mirrors the descendant query #hide() already uses.
96
- const items = this.querySelectorAll('menu-item-ui, menu-divider-ui');
96
+ const items = this.querySelectorAll('menu-item-ui, menu-divider-ui, menu-label-ui');
97
97
  for (const item of items) {
98
98
  if (!pop.contains(item)) pop.appendChild(item);
99
99
  }
@@ -130,7 +130,7 @@ export class UIMenu extends UIElement {
130
130
  if (pop) {
131
131
  if (pop.matches(':popover-open')) pop.hidePopover?.();
132
132
  // Move items back to host (before the popover div) so next open can re-adopt them.
133
- const items = pop.querySelectorAll('menu-item-ui, menu-divider-ui');
133
+ const items = pop.querySelectorAll('menu-item-ui, menu-divider-ui, menu-label-ui');
134
134
  for (const item of items) this.insertBefore(item, pop);
135
135
  }
136
136
 
@@ -352,3 +352,38 @@ export class UIMenuDivider extends UIElement {
352
352
  this.setAttribute('role', 'separator');
353
353
  }
354
354
  }
355
+
356
+ export class UIMenuLabel extends UIElement {
357
+ static properties = {
358
+ text: { type: String, default: '', reflect: true },
359
+ };
360
+
361
+ static template = () => null;
362
+
363
+ connected() {
364
+ // Non-interactive group heading. role="presentation" so the menu's roving
365
+ // focus (which queries menu-item-ui only) skips it and a screen reader does
366
+ // not announce it as an actionable row — it visually labels the group below
367
+ // it (use a divider to separate groups; a label to name one).
368
+ this.setAttribute('role', 'presentation');
369
+ this.#stamp();
370
+ }
371
+
372
+ // Mirror menu-item-ui: stamp a <span slot="text"> from [text]; never touch a
373
+ // consumer-provided [slot="text"] (ADR-0010, slot content is source of truth).
374
+ #stamp() {
375
+ if (this.querySelector(':scope > [slot="text"]')) return;
376
+ if (this.text) {
377
+ const span = document.createElement('span');
378
+ span.setAttribute('slot', 'text');
379
+ span.dataset.menuLabelStamped = '1';
380
+ span.textContent = this.text;
381
+ this.appendChild(span);
382
+ }
383
+ }
384
+
385
+ render() {
386
+ const span = this.querySelector(':scope > [slot="text"]');
387
+ if (span?.dataset.menuLabelStamped === '1') span.textContent = this.text;
388
+ }
389
+ }
@@ -34,8 +34,10 @@
34
34
  are neither `:scope >` children nor under a `:scope > [role]` wrapper. */
35
35
  :scope > menu-item-ui,
36
36
  :scope > menu-divider-ui,
37
+ :scope > menu-label-ui,
37
38
  :scope > [role="presentation"] menu-item-ui,
38
- :scope > [role="presentation"] menu-divider-ui {
39
+ :scope > [role="presentation"] menu-divider-ui,
40
+ :scope > [role="presentation"] menu-label-ui {
39
41
  display: none;
40
42
  }
41
43
  }
@@ -200,3 +202,30 @@ menu-item-ui[variant="danger"]:hover [slot="icon"] {
200
202
  border: none;
201
203
  }
202
204
  }
205
+
206
+ @scope (menu-label-ui) {
207
+ :where(:scope) {
208
+ /* ── Layout ── */
209
+ --menu-label-px: var(--a-space-2);
210
+ --menu-label-pt: var(--a-space-2);
211
+ --menu-label-pb: var(--a-space-1);
212
+ }
213
+
214
+ :scope {
215
+ box-sizing: border-box;
216
+ display: block;
217
+ padding: var(--menu-label-pt) var(--menu-label-px) var(--menu-label-pb);
218
+ /* Section-header look — matches <text-ui variant="kicker"> (same tokens). */
219
+ font-family: var(--a-kicker-family);
220
+ font-weight: var(--a-kicker-weight);
221
+ font-size: var(--a-kicker-size);
222
+ line-height: var(--a-kicker-leading);
223
+ letter-spacing: var(--a-kicker-tracking);
224
+ text-transform: uppercase;
225
+ color: var(--a-kicker-color);
226
+ cursor: default;
227
+ user-select: none;
228
+ }
229
+
230
+ [slot="text"] { display: block; }
231
+ }
@@ -52,3 +52,8 @@ export class UIMenuItem extends UIElement {
52
52
  /** Semantic variant. */
53
53
  variant: 'default' | 'danger';
54
54
  }
55
+
56
+ export class UIMenuLabel extends UIElement {
57
+ /** The group label. Stamped into a `<span slot="text">`; consumer-provided slot content wins. */
58
+ text: string;
59
+ }
@@ -10,10 +10,11 @@
10
10
  */
11
11
 
12
12
  import { defineIfFree } from '../../core/register.js';
13
- import { UIMenu, UIMenuItem, UIMenuDivider } from './menu.class.js';
13
+ import { UIMenu, UIMenuItem, UIMenuDivider, UIMenuLabel } from './menu.class.js';
14
14
 
15
15
  defineIfFree('menu-ui', UIMenu);
16
16
  defineIfFree('menu-item-ui', UIMenuItem);
17
17
  defineIfFree('menu-divider-ui', UIMenuDivider);
18
+ defineIfFree('menu-label-ui', UIMenuLabel);
18
19
 
19
- export { UIMenu, UIMenuItem, UIMenuDivider };
20
+ export { UIMenu, UIMenuItem, UIMenuDivider, UIMenuLabel };
@@ -128,3 +128,49 @@ describe('<menu-ui> collects dynamically-rendered items (FEEDBACK-92)', () => {
128
128
  expect(order).toEqual(['1', '2']);
129
129
  });
130
130
  });
131
+
132
+ describe('<menu-label-ui> group heading', () => {
133
+ let host;
134
+ beforeEach(() => { host = document.createElement('div'); document.body.appendChild(host); });
135
+ afterEach(() => host.remove());
136
+
137
+ it('hoists labels into the popover, stamps [text], and marks them presentational', async () => {
138
+ const menu = document.createElement('menu-ui');
139
+ menu.innerHTML = `
140
+ <button-ui slot="trigger" text="Open"></button-ui>
141
+ <menu-label-ui text="Group A"></menu-label-ui>
142
+ <menu-item-ui text="One" value="1"></menu-item-ui>
143
+ <menu-divider-ui></menu-divider-ui>
144
+ <menu-label-ui text="Group B"></menu-label-ui>
145
+ <menu-item-ui text="Two" value="2"></menu-item-ui>`;
146
+ host.appendChild(menu);
147
+ await settle();
148
+ menu.open = true;
149
+ await settle();
150
+
151
+ const pop = menu.querySelector('[data-menu-popover]');
152
+ const labels = pop.querySelectorAll('menu-label-ui');
153
+ expect(labels.length).toBe(2);
154
+ expect(labels[0].getAttribute('role')).toBe('presentation');
155
+ expect(labels[0].textContent.trim()).toBe('Group A');
156
+ expect(labels[1].textContent.trim()).toBe('Group B');
157
+ });
158
+
159
+ it('is non-interactive — outside the menuitem set the roving focus queries', async () => {
160
+ const menu = document.createElement('menu-ui');
161
+ menu.innerHTML = `
162
+ <button-ui slot="trigger" text="Open"></button-ui>
163
+ <menu-label-ui text="Heading"></menu-label-ui>
164
+ <menu-item-ui text="One" value="1"></menu-item-ui>`;
165
+ host.appendChild(menu);
166
+ await settle();
167
+ menu.open = true;
168
+ await settle();
169
+
170
+ const pop = menu.querySelector('[data-menu-popover]');
171
+ // A label is neither a menu-item-ui nor role="menuitem", so #enabledItems()
172
+ // (a `menu-item-ui` query) never lands arrow-key focus on it.
173
+ expect(pop.querySelector('menu-label-ui[role="menuitem"]')).toBeNull();
174
+ expect(pop.querySelectorAll('menu-item-ui').length).toBe(1);
175
+ });
176
+ });
@@ -11,7 +11,7 @@ tag-ui[removable]:not([disabled]):hover {
11
11
  --tag-fg: var(--a-fg);
12
12
  --tag-border: transparent;
13
13
  --tag-radius: var(--a-radius-full);
14
- --tag-px: var(--a-space-2);
14
+ --tag-px: var(--a-space-1-5);
15
15
  --tag-py: 0.125rem;
16
16
  --tag-gap: var(--a-space-1);
17
17
  --tag-font: var(--a-ui-sm);