@adia-ai/web-components 0.6.22 → 0.6.24

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,29 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.24] — 2026-05-22
4
+
5
+ ### Fixed — `slider-ui` label and suffix template snapshot trap (FB-45)
6
+
7
+ - **`[slot="label"]` and `[slot="suffix"]` now re-sync in `render()`**, resolving the snapshot trap where `label=${expr}` via the template engine rendered the literal `{{p:N}}` placeholder instead of the resolved value. Both spans are updated on every reactive cycle; a missing label span is also created if absent at connection time. File: `components/slider/class.js`.
8
+
9
+ ### Fixed — `toast-ui` demo shell missing `feed.css` import
10
+
11
+ - **`toast.html` now imports `feed/feed.css` + `feed/feed.js`.** `<toast-ui>` renders via `<feed-ui>` — the demo shell was not loading feed's stylesheet, so feed items rendered without background, typography, or spacing. Files: `components/toast/toast.html`.
12
+
13
+ ## [0.6.23] — 2026-05-22
14
+
15
+ ### Added — `row-ui` `wrap-at="bp"` responsive wrap (`components/row/`)
16
+
17
+ - **`wrap-at="bp"`** enables `flex-wrap: wrap` from the named breakpoint upward (`xs/sm/md/lg/xl`). Complements the boolean `[wrap]` (always-wrap, unchanged) for the common "single row on desktop, wraps on mobile" toolbar pattern: `<row-ui gap="2 3@md" wrap-at="sm">`. Uses inline `flex-wrap` to avoid reactive re-entrancy from toggling the boolean attribute inside `render()`. File: `components/row/row.js` + `components/row/row.yaml`.
18
+
19
+ ### Added — `--a-ui-2xs: 11px;` UI typography scale slot (`styles/typography.css`)
20
+
21
+ - **`--a-ui-2xs`** (11px) slots cleanly into the existing UI scale between `--a-ui-tiny` (10px) and `--a-ui-xs` (12px). Currently unused — pre-staged for upcoming responsive UI-text variants that need a denser-than-`xs` step. File: `styles/typography.css`.
22
+
23
+ ### Documentation — `components/` examples third sweep (§428/§432)
24
+
25
+ - **22 `components/*/*.examples.html` files** updated with @bp annotations + missing prop / token tables across the docs sweep arc (badge, block, button, card, check, code, col, fields, grid, image, link, list, modal, nav family, pane, select, stat, stream, table, tabs, tag, text-ui, toggle-group, tooltip, tree). Picks up the v0.6.22 `@bp` substrate in living demos so consumers see responsive composition shapes inline. Source journal sections: §428 (third sweep + token tables) + §432 (collision fix).
26
+
3
27
  ## [0.6.22] — 2026-05-22
4
28
 
5
29
  ### Added — `@bp` responsive attribute system (`core/responsive.js`)
package/README.md CHANGED
@@ -215,7 +215,7 @@ class MyPanel extends UIElement {
215
215
  The `el` parameter is the element instance — every signal-backed property is reactively read.
216
216
 
217
217
  Full authoring contract: [`docs/specs/component-token-contract.md`](../../docs/specs/component-token-contract.md).
218
- The `adia-ui-author` skill encodes the 20 non-negotiable rules.
218
+ The `adia-ui-authoring` skill encodes the 20 non-negotiable rules.
219
219
 
220
220
  ## Card-n / drawer-ui composition parity
221
221
 
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { UIElement } from '../../core/element.js';
22
22
  import { draggable } from '../../traits/draggable/draggable.js';
23
- import { parseResponsive, breakpoint } from '../../core/responsive.js';
23
+ import { parseResponsive, breakpoint, BP_NAMES } from '../../core/responsive.js';
24
24
 
25
25
  export class UIRow extends UIElement {
26
26
  static properties = {
@@ -29,6 +29,9 @@ export class UIRow extends UIElement {
29
29
  gap: { type: String, default: 'md', reflect: true },
30
30
  grow: { type: Boolean, default: false, reflect: true },
31
31
  wrap: { type: Boolean, default: false, reflect: true },
32
+ // wrap-at="sm" = enable flex-wrap from sm upward; complements the
33
+ // boolean [wrap] (always-wrap) without modifying it.
34
+ wrapAt: { type: String, default: '', reflect: true, attribute: 'wrap-at' },
32
35
  draggable: { type: Boolean, default: false, reflect: true },
33
36
  };
34
37
  static template = () => null;
@@ -53,7 +56,8 @@ export class UIRow extends UIElement {
53
56
  const gap = this.gap;
54
57
  const align = this.align;
55
58
  const justify = this.justify;
56
- const anyR = gap?.includes('@') || align?.includes('@') || justify?.includes('@');
59
+ const wrapAt = this.wrapAt;
60
+ const anyR = gap?.includes('@') || align?.includes('@') || justify?.includes('@') || !!wrapAt;
57
61
  const bp = anyR ? breakpoint.value : '';
58
62
 
59
63
  if (gap?.includes('@')) {
@@ -73,6 +77,18 @@ export class UIRow extends UIElement {
73
77
  } else {
74
78
  this.style.removeProperty('--row-justify');
75
79
  }
80
+
81
+ // wrap-at="sm" enables flex-wrap from the named breakpoint upward.
82
+ // Uses inline style rather than toggling the [wrap] attribute to avoid
83
+ // reactive re-entrancy. Falls back to '' (CSS [wrap] handles the
84
+ // always-wrap case; this only fires when wrap-at is set).
85
+ if (wrapAt) {
86
+ const activeIdx = BP_NAMES.indexOf(bp);
87
+ const targetIdx = BP_NAMES.indexOf(wrapAt);
88
+ this.style.flexWrap = (targetIdx !== -1 && activeIdx >= targetIdx) ? 'wrap' : '';
89
+ } else {
90
+ this.style.flexWrap = '';
91
+ }
76
92
  }
77
93
  }
78
94
 
@@ -42,9 +42,14 @@
42
42
  "default": "start"
43
43
  },
44
44
  "wrap": {
45
- "description": "Enable flex wrap",
45
+ "description": "Enable flex-wrap unconditionally. For viewport-responsive wrapping use [wrap-at] instead.",
46
46
  "type": "boolean",
47
47
  "default": false
48
+ },
49
+ "wrapAt": {
50
+ "description": "Enable flex-wrap from the named breakpoint upward (xs/sm/md/lg/xl). `wrap-at=\"sm\"` = no-wrap on xs, wrap from sm up. Complements the boolean [wrap] (always-wrap) without conflicting with it.",
51
+ "type": "string",
52
+ "default": ""
48
53
  }
49
54
  },
50
55
  "required": [
@@ -30,6 +30,8 @@ export class UIRow extends UIElement {
30
30
  grow: boolean;
31
31
  /** Main-axis justify (start/center/end/between/space-between/space-around). Accepts `@bp` notation: justify="start between@md". */
32
32
  justify: string;
33
- /** Enable flex wrap */
33
+ /** Enable flex-wrap unconditionally. For viewport-responsive wrapping use [wrap-at] instead. */
34
34
  wrap: boolean;
35
+ /** Enable flex-wrap from the named breakpoint upward (xs/sm/md/lg/xl). `wrap-at="sm"` = no-wrap on xs, wrap from sm up. Complements the boolean [wrap] (always-wrap) without conflicting with it. */
36
+ wrapAt: string;
35
37
  }
@@ -44,9 +44,19 @@ props:
44
44
  type: string
45
45
  default: start
46
46
  wrap:
47
- description: Enable flex wrap
47
+ description: >-
48
+ Enable flex-wrap unconditionally. For viewport-responsive wrapping use
49
+ [wrap-at] instead.
48
50
  type: boolean
49
51
  default: false
52
+ wrapAt:
53
+ description: >-
54
+ Enable flex-wrap from the named breakpoint upward (xs/sm/md/lg/xl).
55
+ `wrap-at="sm"` = no-wrap on xs, wrap from sm up. Complements the
56
+ boolean [wrap] (always-wrap) without conflicting with it.
57
+ type: string
58
+ default: ""
59
+ attribute: wrap-at
50
60
  events: {}
51
61
  slots:
52
62
  default:
@@ -149,6 +149,21 @@ export class UISlider extends UIFormElement {
149
149
  const valueEl = this.querySelector('[slot="value"]');
150
150
  if (valueEl) valueEl.textContent = this.#format(this.value);
151
151
 
152
+ // §FB-45: label and suffix are read in connected() but may be set later
153
+ // by the template engine (which first writes {{p:N}} then resolves).
154
+ // Re-sync both slots on every render so reactive bindings work.
155
+ const labelEl = this.querySelector('[slot="label"]');
156
+ if (labelEl) {
157
+ labelEl.textContent = this.label;
158
+ } else if (this.label) {
159
+ const header = this.querySelector('[slot="header"]');
160
+ if (header) { const s = document.createElement('span'); s.slot = 'label'; s.textContent = this.label; header.prepend(s); }
161
+ }
162
+ if (this.label) this.setAttribute('aria-label', this.label);
163
+
164
+ const suffixEl = this.querySelector('[slot="suffix"]');
165
+ if (suffixEl) suffixEl.textContent = this.suffix;
166
+
152
167
  this.setAttribute('aria-valuenow', this.value);
153
168
  this.setAttribute('aria-valuetext', `${this.#format(this.value)}${this.suffix ? ' ' + this.suffix : ''}`);
154
169
  this.syncValue(String(this.value));
@@ -105,4 +105,43 @@ describe('slider-ui', () => {
105
105
  await tick();
106
106
  expect(s.getAttribute('aria-valuenow')).toBe('80');
107
107
  });
108
+
109
+ // §FB-45 (color-app, 2026-05-22) — template engine snapshot trap.
110
+ // connected() stamped label/suffix once; render() didn't re-sync them.
111
+ // Template engine writes {{p:N}} first, then resolves to the real value —
112
+ // simulated here by setAttribute('label', placeholder) then setting the
113
+ // real value. Both spans must reflect the final value, not the placeholder.
114
+ it('re-syncs [slot="label"] when label attribute changes after connection', async () => {
115
+ const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
116
+ await tick();
117
+ // No label initially — span absent
118
+ expect(s.querySelector('[slot="label"]')).toBeNull();
119
+
120
+ // Simulate template engine writing placeholder then real value
121
+ s.setAttribute('label', '{{p:7}}');
122
+ await tick();
123
+ s.setAttribute('label', 'Volume');
124
+ await tick();
125
+
126
+ const labelEl = s.querySelector('[slot="label"]');
127
+ expect(labelEl).not.toBeNull();
128
+ expect(labelEl.textContent).toBe('Volume');
129
+ expect(s.getAttribute('aria-label')).toBe('Volume');
130
+ });
131
+
132
+ it('re-syncs [slot="suffix"] when suffix attribute changes after connection', async () => {
133
+ const s = mount('<slider-ui value="50" min="0" max="100" label="Volume" suffix="%"></slider-ui>');
134
+ await tick();
135
+ const suffixEl = s.querySelector('[slot="suffix"]');
136
+ expect(suffixEl).not.toBeNull();
137
+ expect(suffixEl.textContent).toBe('%');
138
+
139
+ // Simulate placeholder → resolved suffix
140
+ s.setAttribute('suffix', '{{p:8}}');
141
+ await tick();
142
+ s.setAttribute('suffix', 'rem');
143
+ await tick();
144
+
145
+ expect(s.querySelector('[slot="suffix"]').textContent).toBe('rem');
146
+ });
108
147
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
4
4
  "description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -313,6 +313,7 @@
313
313
  --a-ui-family: var(--a-font-family-ui);
314
314
  --a-ui-weight: var(--a-weight-medium);
315
315
  --a-ui-tiny: 10px;
316
+ --a-ui-2xs: 11px;
316
317
  --a-ui-xs: 12px;
317
318
  --a-ui-sm: 13px;
318
319
  --a-ui-md: 14px;