@adia-ai/web-components 0.7.23 → 0.7.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 (46) hide show
  1. package/CHANGELOG.md +56 -25
  2. package/README.md +2 -2
  3. package/USAGE.md +60 -15
  4. package/components/accordion/accordion-item.a2ui.json +30 -1
  5. package/components/accordion/accordion-item.yaml +33 -0
  6. package/components/accordion/accordion.a2ui.json +9 -0
  7. package/components/accordion/accordion.class.js +53 -3
  8. package/components/accordion/accordion.css +61 -3
  9. package/components/accordion/accordion.d.ts +12 -0
  10. package/components/accordion/accordion.yaml +17 -0
  11. package/components/code/code.class.js +1 -1
  12. package/components/display-field/display-field.a2ui.json +185 -0
  13. package/components/display-field/display-field.css +91 -0
  14. package/components/display-field/display-field.d.ts +26 -0
  15. package/components/display-field/display-field.examples.md +44 -0
  16. package/components/display-field/display-field.js +141 -0
  17. package/components/display-field/display-field.test.js +115 -0
  18. package/components/display-field/display-field.yaml +191 -0
  19. package/components/drawer/drawer.class.js +2 -2
  20. package/components/feed/feed.a2ui.json +1 -1
  21. package/components/feed/feed.class.js +1 -1
  22. package/components/feed/feed.d.ts +1 -1
  23. package/components/feed/feed.yaml +2 -2
  24. package/components/field/field.class.js +1 -1
  25. package/components/icon/icon.class.js +34 -1
  26. package/components/index.js +1 -0
  27. package/components/loading-overlay/loading-overlay.class.js +1 -1
  28. package/components/loading-overlay/loading-overlay.yaml +1 -1
  29. package/components/modal/modal.class.js +1 -1
  30. package/components/nav/nav.class.js +1 -1
  31. package/components/nav/nav.examples.md +4 -4
  32. package/components/popover/popover.class.js +1 -1
  33. package/components/spinner/spinner.class.js +1 -1
  34. package/components/spinner/spinner.yaml +1 -1
  35. package/components/table/table.class.js +42 -11
  36. package/components/table/table.test.js +53 -0
  37. package/components/toast/toast.class.js +1 -1
  38. package/core/icons.js +14 -0
  39. package/custom-elements.json +5723 -2865
  40. package/dist/theme-provider.min.js +1 -1
  41. package/dist/web-components.min.css +1 -1
  42. package/dist/web-components.min.js +87 -79
  43. package/dist/web-components.sheet.js +1 -1
  44. package/package.json +3 -2
  45. package/styles/components.css +1 -0
  46. package/traits/CATEGORIES.md +1 -1
@@ -1,4 +1,4 @@
1
- # Hand-authored per SPEC-001 (docs/specs/implementation-ready/SPEC-001-spinner-loader.md).
1
+ # Hand-authored per SPEC-001 (.claude/docs/specs/implementation-ready/SPEC-001-spinner-loader.md).
2
2
  # Edit this file; run `npm run build:components` to regenerate spinner.a2ui.json.
3
3
  $schema: ../../../../scripts/schemas/component.yaml.schema.json
4
4
  name: UISpinner
@@ -240,6 +240,10 @@ export class UITable extends UIElement {
240
240
  // ── Public API: expansion ──
241
241
 
242
242
  toggleExpand(index) {
243
+ // A row gated out by `rowExpandable` cannot be toggled — but always allow
244
+ // collapsing one that is already open (e.g. the predicate flipped while a
245
+ // detail pane was showing), so it can't get stuck expanded.
246
+ if (!this.#isRowExpandable(index) && !this.#expanded.has(index)) return;
243
247
  if (this.#expanded.has(index)) {
244
248
  this.#expanded.delete(index);
245
249
  this.dispatchEvent(new CustomEvent('row-collapse', { detail: { index, row: this.#data[index] }, bubbles: true }));
@@ -255,6 +259,27 @@ export class UITable extends UIElement {
255
259
  /** @type {((row: object, index: number) => HTMLElement)|null} */
256
260
  expandRenderer = null;
257
261
 
262
+ /**
263
+ * Per-row expand gate. When `expandable` is on and this is set, a row shows
264
+ * the expand caret (and can open a detail pane) only when the predicate
265
+ * returns truthy for that row. Lets heterogeneous rows opt out of the
266
+ * uniform affordance — e.g. `rowExpandable: r => r.status === 'failed'` so a
267
+ * succeeded row carries no caret while a failed one does. The reserved
268
+ * expand column is kept either way, so column alignment never shifts.
269
+ * `null` (default) = every row is expandable (legacy behavior).
270
+ * @type {((row: object, index: number) => boolean)|null}
271
+ */
272
+ rowExpandable = null;
273
+
274
+ /** True when `expandable` is on AND this row passes the per-row gate. */
275
+ #isRowExpandable(dataIndex) {
276
+ if (!this.expandable) return false;
277
+ if (typeof this.rowExpandable === 'function') {
278
+ return !!this.rowExpandable(this.#data[dataIndex], dataIndex);
279
+ }
280
+ return true;
281
+ }
282
+
258
283
  // ── Public API: state persistence ──
259
284
 
260
285
  getState() {
@@ -601,16 +626,18 @@ export class UITable extends UIElement {
601
626
  if (existing) this.#updateRow(existing, idx, visCols);
602
627
 
603
628
  // Expand toggle in first cell
604
- if (this.expandable) {
629
+ if (this.#isRowExpandable(idx)) {
605
630
  const isExpanded = this.#expanded.has(idx);
606
631
  if (isExpanded) row.setAttribute('data-expanded', '');
607
632
  else row.removeAttribute('data-expanded');
633
+ } else {
634
+ row.removeAttribute('data-expanded');
608
635
  }
609
636
 
610
637
  bodyChildren.push(row);
611
638
 
612
639
  // Detail row (expansion)
613
- if (this.expandable && this.#expanded.has(idx)) {
640
+ if (this.#isRowExpandable(idx) && this.#expanded.has(idx)) {
614
641
  let detail = body.querySelector(`:scope > [data-detail-row][data-for="${idx}"]`);
615
642
  if (!detail) {
616
643
  detail = document.createElement('div');
@@ -779,19 +806,23 @@ export class UITable extends UIElement {
779
806
 
780
807
  const cells = [];
781
808
 
782
- // Expand toggle cell
809
+ // Expand toggle cell. The reserved column cell is always emitted (so the
810
+ // grid stays aligned), but the caret button is stamped only when this row
811
+ // passes the per-row expand gate — non-expandable rows show an empty cell.
783
812
  if (this.expandable) {
784
813
  const cell = document.createElement('div');
785
814
  cell.setAttribute('role', 'gridcell');
786
815
  cell.setAttribute('data-expand-col', '');
787
- const btn = document.createElement('button');
788
- btn.setAttribute('data-expand-toggle', '');
789
- btn.setAttribute('aria-label', 'Expand row');
790
- const icon = document.createElement('icon-ui');
791
- icon.setAttribute('name', 'caret-right');
792
- icon.setAttribute('size', 'xs');
793
- btn.appendChild(icon);
794
- cell.appendChild(btn);
816
+ if (this.#isRowExpandable(dataIndex)) {
817
+ const btn = document.createElement('button');
818
+ btn.setAttribute('data-expand-toggle', '');
819
+ btn.setAttribute('aria-label', 'Expand row');
820
+ const icon = document.createElement('icon-ui');
821
+ icon.setAttribute('name', 'caret-right');
822
+ icon.setAttribute('size', 'xs');
823
+ btn.appendChild(icon);
824
+ cell.appendChild(btn);
825
+ }
795
826
  cells.push(cell);
796
827
  }
797
828
 
@@ -336,3 +336,56 @@ describe('table-ui — raw mode passthrough (FB-53 §2)', () => {
336
336
  expect(el.querySelector(':scope > [data-body]')).not.toBeNull();
337
337
  });
338
338
  });
339
+
340
+ // ── F6 (upstream): per-row expand gate via `rowExpandable` ───────────────────
341
+ describe('table-ui — F6 per-row expand suppression (rowExpandable)', () => {
342
+ beforeEach(() => { document.body.innerHTML = ''; });
343
+
344
+ const bodyRows = (el) =>
345
+ el.querySelectorAll(':scope > [data-body] > [role="row"]:not([data-detail-row])');
346
+
347
+ it('default (no predicate): every row carries an expand caret', async () => {
348
+ const el = mount('<table-ui expandable></table-ui>');
349
+ el.columns = COLS;
350
+ el.data = ROWS;
351
+ await raf();
352
+ const rows = bodyRows(el);
353
+ expect(rows.length).toBe(2);
354
+ for (const row of rows) {
355
+ expect(row.querySelector('[data-expand-toggle]')).not.toBeNull();
356
+ }
357
+ });
358
+
359
+ it('predicate suppresses the caret on gated rows but keeps the reserved cell', async () => {
360
+ const el = mount('<table-ui expandable></table-ui>');
361
+ el.columns = COLS;
362
+ el.data = ROWS;
363
+ el.rowExpandable = (row) => row.id === 2; // only Bob expandable
364
+ await raf();
365
+ const rows = bodyRows(el);
366
+
367
+ // Alice (id 1) — reserved cell present, NO caret button.
368
+ expect(rows[0].querySelector(':scope > [data-expand-col]')).not.toBeNull();
369
+ expect(rows[0].querySelector('[data-expand-toggle]')).toBeNull();
370
+
371
+ // Bob (id 2) — caret button present.
372
+ expect(rows[1].querySelector('[data-expand-toggle]')).not.toBeNull();
373
+ });
374
+
375
+ it('toggleExpand is a no-op on a gated row (no detail row renders)', async () => {
376
+ const el = mount('<table-ui expandable></table-ui>');
377
+ el.columns = COLS;
378
+ el.data = ROWS;
379
+ el.rowExpandable = (row) => row.id === 2;
380
+ await raf();
381
+
382
+ el.toggleExpand(0); // Alice — gated out
383
+ await raf();
384
+ expect(el.querySelector('[data-detail-row][data-for="0"]')).toBeNull();
385
+ expect(el.expanded).not.toContain(0);
386
+
387
+ el.toggleExpand(1); // Bob — allowed
388
+ await raf();
389
+ expect(el.expanded).toContain(1);
390
+ });
391
+ });
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * <toast-ui> — Thin facade over `<feed-ui>` / `UIFeed.post()`.
16
16
  *
17
- * Phase 4 of `docs/specs/feed-channel.md` (SPEC-FEED-CHANNEL-001) —
17
+ * Phase 4 of `.claude/docs/specs/feed-channel.md` (SPEC-FEED-CHANNEL-001) —
18
18
  * toast-ui no longer owns its own per-position container. Both
19
19
  * declarative `<toast-ui>` and imperative `UIToast.show()` paths
20
20
  * route through `UIFeed`. The element exists for back-compat:
package/core/icons.js CHANGED
@@ -135,6 +135,20 @@ let registryReady = false;
135
135
  let resolveRegistryReady;
136
136
  export const whenIconRegistryReady = new Promise((r) => { resolveRegistryReady = r; });
137
137
 
138
+ /**
139
+ * True when NO icons are registered AND no loaders were ever installed — i.e.
140
+ * the icon subsystem was never wired. The usual cause: the zero-config
141
+ * `icons-phosphor.js` `import.meta.glob('/node_modules/...')` path is a
142
+ * build-time macro that does not transform from inside a published dependency
143
+ * under Vite 8 / rolldown, so `installIconLoaders` never ran and the registry
144
+ * stays empty. Distinct from a single bad icon name (registry non-empty, that
145
+ * one name absent). `<icon-ui>` uses this to fail loud once rather than render
146
+ * blank silently. See `USAGE.md` § Icons.
147
+ */
148
+ export function iconRegistryUnwired() {
149
+ return registry.size === 0 && !registryReady;
150
+ }
151
+
138
152
  /**
139
153
  * Install a per-weight loader map and mark the registry ready. The map
140
154
  * shape matches `import.meta.glob` output: `{ '/path/to/star.svg': loader }`.