@adia-ai/web-components 0.5.17 → 0.5.19

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.
@@ -15,7 +15,11 @@ composes:
15
15
  props: {}
16
16
  events:
17
17
  tree-select:
18
- description: "Fired when an item is selected. detail: { item, text, value }"
18
+ description: >
19
+ Fired when an item is selected.
20
+ detail: { item, text, value, ctrlKey, metaKey, shiftKey }.
21
+ Modifier flags mirror the originating MouseEvent / KeyboardEvent;
22
+ all default false for programmatic select() calls.
19
23
  detail:
20
24
  item:
21
25
  type: object
@@ -26,6 +30,15 @@ events:
26
30
  value:
27
31
  type: string
28
32
  description: Item value attribute.
33
+ ctrlKey:
34
+ type: boolean
35
+ description: Ctrl key held during activation (false for programmatic select()).
36
+ metaKey:
37
+ type: boolean
38
+ description: Meta (Cmd) key held during activation (false for programmatic select()).
39
+ shiftKey:
40
+ type: boolean
41
+ description: Shift key held during activation (false for programmatic select()).
29
42
  slots:
30
43
  default (tree-item-ui children):
31
44
  description: "Child content region for the `default (tree-item-ui children)` slot."
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
16
- import { html, stamp } from './template.js';
16
+ import { html, stamp, repeat } from './template.js';
17
17
 
18
18
  describe('html template — §250 (v0.5.11) ?attr=${bool} silent-failure trap', () => {
19
19
  let container;
@@ -179,3 +179,103 @@ describe('html template — FB-40 (v0.5.16) apostrophe in HTML comments does NOT
179
179
  expect(received).not.toBeNull();
180
180
  });
181
181
  });
182
+
183
+ describe('html template — FB-47 (v0.5.19) stamp() cache + repeat() keyed reuse', () => {
184
+ // FEEDBACK-47 claimed `stamp()` unconditionally calls `mount()` →
185
+ // `replaceChildren()`, defeating `repeat()` keyed reconciliation. Source
186
+ // inspection (template.js:94-102) shows `stamp()` has a per-container
187
+ // cache keyed on `result.strings`; `mount()` runs ONLY on cache miss
188
+ // (first stamp on this container OR new template strings). These tests
189
+ // verify the cache behavior empirically against the same-template
190
+ // re-stamp + repeat()-same-key paths.
191
+
192
+ let container;
193
+
194
+ beforeEach(() => {
195
+ container = document.createElement('div');
196
+ document.body.appendChild(container);
197
+ });
198
+
199
+ afterEach(() => {
200
+ container.remove();
201
+ });
202
+
203
+ it('stamp() with same template strings reuses the mounted instance (no replaceChildren)', () => {
204
+ // Build a template factory whose strings literal is reference-stable
205
+ // (the tagged-template caller — closure captures `strings` once).
206
+ const make = (value) => html`<div class="probe">${value}</div>`;
207
+
208
+ stamp(make('first'), container);
209
+ const firstDiv = container.querySelector('.probe');
210
+ expect(firstDiv).not.toBeNull();
211
+ expect(firstDiv.textContent).toBe('first');
212
+
213
+ // Mark the existing node so we can prove it survives the re-stamp.
214
+ firstDiv.dataset.marker = 'preserved';
215
+
216
+ stamp(make('second'), container);
217
+ const secondDiv = container.querySelector('.probe');
218
+ expect(secondDiv).toBe(firstDiv); // ✅ same node
219
+ expect(secondDiv.dataset.marker).toBe('preserved'); // ✅ no replaceChildren
220
+ expect(secondDiv.textContent).toBe('second'); // ✅ value updated in-place
221
+ });
222
+
223
+ it('repeat() with same key preserves wrapper children across re-renders', () => {
224
+ // FB-47's central reproduction: same-key repeat() should preserve the
225
+ // per-item wrapper-span AND the child element stamped into it on the
226
+ // prior render. Both the outer template AND the per-item template must
227
+ // share strings references across renders (single tagged-template-literal
228
+ // sites) — JS spec gives each source site its own strings array, so
229
+ // inline-duplicating the html`` between stamp calls would force a
230
+ // cache miss and defeat preservation. Real consumers naturally satisfy
231
+ // this by having one render function called repeatedly.
232
+ const items = [{ id: 'a', label: 'first' }];
233
+ const tplFn = (item) => html`<div class="probe" data-id=${item.id}>${item.label}</div>`;
234
+ const renderAll = (its) => html`${repeat(its, (x) => x.id, tplFn)}`;
235
+
236
+ stamp(renderAll(items), container);
237
+ const firstDiv = container.querySelector('.probe[data-id="a"]');
238
+ expect(firstDiv).not.toBeNull();
239
+
240
+ // Append a manually-added child that the framework should NOT destroy.
241
+ const probe = document.createElement('span');
242
+ probe.id = 'fb47-manual-child';
243
+ probe.textContent = 'sentinel';
244
+ firstDiv.appendChild(probe);
245
+
246
+ // Re-stamp with the same items array (same key 'a').
247
+ items[0] = { id: 'a', label: 'second' };
248
+ stamp(renderAll(items), container);
249
+
250
+ const secondDiv = container.querySelector('.probe[data-id="a"]');
251
+ expect(secondDiv).toBe(firstDiv); // ✅ wrapper reused
252
+ expect(secondDiv.querySelector('#fb47-manual-child')).not.toBeNull();
253
+ expect(secondDiv.firstChild.nodeValue).toBe('second'); // text updated in-place
254
+ });
255
+
256
+ it('Array.isArray() branch in applyValue does replace children on every re-render (the actual root cause)', () => {
257
+ // FB-47's empirical observation IS valid for `.map()` — the
258
+ // Array.isArray() branch in applyValue() (template.js:232-244) does
259
+ // unconditional `container.replaceChildren()` + allocates fresh wrapper
260
+ // spans per item. Switching consumers to repeat() avoids this. This
261
+ // test pins the current behavior so any future "make .map() smart"
262
+ // change is intentional, not accidental. Use a single render function
263
+ // (one tagged-template-literal site for the outer template) so the
264
+ // observed re-creation is isolated to the .map() Array branch, not
265
+ // a cache miss from inline-duplicated source sites.
266
+ const items = [{ id: 'a', label: 'first' }];
267
+ const tplFn = (item) => html`<div class="probe" data-id=${item.id}>${item.label}</div>`;
268
+ const renderAll = (its) => html`${its.map(tplFn)}`;
269
+
270
+ stamp(renderAll(items), container);
271
+ const firstDiv = container.querySelector('.probe[data-id="a"]');
272
+ expect(firstDiv).not.toBeNull();
273
+
274
+ items[0] = { id: 'a', label: 'second' };
275
+ stamp(renderAll(items), container);
276
+
277
+ const secondDiv = container.querySelector('.probe[data-id="a"]');
278
+ expect(secondDiv).not.toBe(firstDiv); // ✅ NEW node — .map() re-creates
279
+ expect(secondDiv.textContent).toBe('second');
280
+ });
281
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "AdiaUI web components — 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",