@adia-ai/web-modules 0.8.40 → 0.8.41

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 (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -6
  3. package/agent-admin/admin-roster/admin-roster.a2ui.json +1 -0
  4. package/agent-admin/admin-roster/admin-roster.css +10 -2
  5. package/agent-admin/admin-roster/admin-roster.js +70 -22
  6. package/agent-admin/admin-settings/admin-settings.a2ui.json +1 -0
  7. package/agent-admin/agent-admin/agent-admin.a2ui.json +1 -0
  8. package/billing/billing-overview/billing-overview.a2ui.json +10 -1
  9. package/billing/billing-overview/billing-overview.class.js +142 -73
  10. package/billing/billing-overview/billing-overview.css +70 -57
  11. package/billing/billing-overview/billing-overview.yaml +5 -0
  12. package/billing/invoice-detail/invoice-detail.a2ui.json +9 -4
  13. package/billing/invoice-detail/invoice-detail.class.js +6 -5
  14. package/billing/invoice-detail/invoice-detail.yaml +9 -4
  15. package/billing/invoice-history/invoice-history.a2ui.json +7 -4
  16. package/billing/invoice-history/invoice-history.class.js +29 -18
  17. package/billing/invoice-history/invoice-history.css +18 -15
  18. package/billing/invoice-history/invoice-history.yaml +10 -4
  19. package/billing/payment-method-form/payment-method-form.a2ui.json +1 -0
  20. package/billing/payment-method-form/payment-method-form.class.js +3 -3
  21. package/billing/payment-method-form/payment-method-form.css +4 -4
  22. package/billing/payment-method-list/payment-method-list.a2ui.json +4 -0
  23. package/billing/payment-method-list/payment-method-list.class.js +31 -11
  24. package/billing/payment-method-list/payment-method-list.css +13 -10
  25. package/billing/payment-method-list/payment-method-list.yaml +3 -0
  26. package/billing/plan-picker/plan-picker.a2ui.json +6 -0
  27. package/billing/plan-picker/plan-picker.class.js +70 -22
  28. package/billing/plan-picker/plan-picker.css +21 -38
  29. package/billing/plan-picker/plan-picker.yaml +5 -0
  30. package/chat/chat-composer/chat-composer.a2ui.json +1 -0
  31. package/chat/chat-empty/chat-empty.a2ui.json +4 -0
  32. package/chat/chat-empty/chat-empty.yaml +2 -0
  33. package/chat/chat-header/chat-header.a2ui.json +5 -0
  34. package/chat/chat-header/chat-header.yaml +3 -0
  35. package/chat/chat-shell/chat-shell.a2ui.json +4 -0
  36. package/chat/chat-shell/chat-shell.yaml +2 -0
  37. package/chat/chat-sidebar/chat-sidebar.a2ui.json +4 -0
  38. package/chat/chat-sidebar/chat-sidebar.yaml +2 -0
  39. package/chat/chat-status/chat-status.a2ui.json +4 -0
  40. package/chat/chat-status/chat-status.yaml +2 -0
  41. package/chat/chat-thread/chat-thread.a2ui.json +4 -0
  42. package/chat/chat-thread/chat-thread.yaml +2 -0
  43. package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +8 -8
  44. package/dashboard/dashboard-layout/dashboard-layout.css +4 -4
  45. package/dashboard/dashboard-layout/dashboard-layout.js +10 -9
  46. package/dashboard/dashboard-layout/dashboard-layout.yaml +19 -7
  47. package/dashboard/date-range-selector/date-range-selector.a2ui.json +4 -3
  48. package/dashboard/date-range-selector/date-range-selector.yaml +6 -4
  49. package/dist/chat/chat-shell.min.js +23 -23
  50. package/dist/editor/editor-shell.min.js +3 -3
  51. package/dist/everything.min.js +119 -132
  52. package/dist/shell/admin-shell.min.css +1 -1
  53. package/dist/shell/admin-shell.min.js +6 -6
  54. package/dist/simple/simple-shell.min.js +5 -5
  55. package/dist/web-modules.min.css +1 -1
  56. package/dist/web-modules.sheet.js +1 -1
  57. package/editor/editor-canvas/editor-canvas.a2ui.json +4 -0
  58. package/editor/editor-canvas/editor-canvas.yaml +2 -0
  59. package/editor/editor-canvas-empty/editor-canvas-empty.a2ui.json +4 -0
  60. package/editor/editor-canvas-empty/editor-canvas-empty.yaml +2 -0
  61. package/editor/editor-canvas-toolbar/editor-canvas-toolbar.a2ui.json +4 -0
  62. package/editor/editor-canvas-toolbar/editor-canvas-toolbar.yaml +2 -0
  63. package/editor/editor-shell/editor-shell.a2ui.json +5 -0
  64. package/editor/editor-shell/editor-shell.yaml +3 -0
  65. package/editor/editor-sidebar/editor-sidebar.a2ui.json +4 -0
  66. package/editor/editor-sidebar/editor-sidebar.yaml +2 -0
  67. package/editor/editor-statusbar/editor-statusbar.a2ui.json +4 -0
  68. package/editor/editor-statusbar/editor-statusbar.yaml +2 -0
  69. package/editor/editor-toolbar/editor-toolbar.a2ui.json +4 -0
  70. package/editor/editor-toolbar/editor-toolbar.yaml +2 -0
  71. package/feedback/confirm-dialog/confirm-dialog.a2ui.json +1 -0
  72. package/form/form-popover/form-popover.a2ui.json +1 -0
  73. package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +2 -1
  74. package/onboarding/onboarding-checklist/onboarding-checklist.js +2 -2
  75. package/onboarding/onboarding-checklist/onboarding-checklist.yaml +1 -1
  76. package/package.json +6 -6
  77. package/runtime/a2ui-root/a2ui-root.a2ui.json +6 -5
  78. package/runtime/a2ui-root/a2ui-root.css +75 -0
  79. package/runtime/a2ui-root/a2ui-root.js +118 -10
  80. package/runtime/a2ui-root/a2ui-root.yaml +7 -2
  81. package/runtime/gen-root/gen-root.a2ui.json +6 -5
  82. package/runtime/gen-root/gen-root.yaml +10 -2
  83. package/settings/integrations-page/integrations-page.a2ui.json +3 -2
  84. package/settings/integrations-page/integrations-page.js +8 -8
  85. package/settings/integrations-page/integrations-page.yaml +2 -2
  86. package/settings/notification-preferences/notification-preferences.a2ui.json +1 -0
  87. package/shell/admin-command/admin-command.a2ui.json +5 -1
  88. package/shell/admin-command/admin-command.yaml +3 -1
  89. package/shell/admin-content/admin-content.a2ui.json +4 -0
  90. package/shell/admin-content/admin-content.yaml +2 -0
  91. package/shell/admin-entity-item/admin-entity-item.a2ui.json +1 -0
  92. package/shell/admin-page/admin-page.a2ui.json +5 -0
  93. package/shell/admin-page/admin-page.yaml +3 -0
  94. package/shell/admin-page-body/admin-page-body.a2ui.json +4 -0
  95. package/shell/admin-page-body/admin-page-body.yaml +2 -0
  96. package/shell/admin-page-footer/admin-page-footer.a2ui.json +4 -0
  97. package/shell/admin-page-footer/admin-page-footer.yaml +2 -0
  98. package/shell/admin-page-header/admin-page-header.a2ui.json +4 -0
  99. package/shell/admin-page-header/admin-page-header.yaml +2 -0
  100. package/shell/admin-scroll/admin-scroll.a2ui.json +4 -0
  101. package/shell/admin-scroll/admin-scroll.yaml +2 -0
  102. package/shell/admin-shell/admin-shell.a2ui.json +7 -3
  103. package/shell/admin-shell/admin-shell.yaml +3 -1
  104. package/shell/admin-shell/css/admin-shell.sidebar.css +2 -2
  105. package/shell/admin-sidebar/admin-sidebar.a2ui.json +4 -0
  106. package/shell/admin-sidebar/admin-sidebar.yaml +2 -0
  107. package/shell/admin-statusbar/admin-statusbar.a2ui.json +6 -0
  108. package/shell/admin-statusbar/admin-statusbar.yaml +4 -0
  109. package/shell/admin-topbar/admin-topbar.a2ui.json +6 -0
  110. package/shell/admin-topbar/admin-topbar.yaml +4 -0
  111. package/shell/embed-shell/embed-shell.a2ui.json +1 -0
  112. package/simple/simple-content/simple-content.a2ui.json +4 -0
  113. package/simple/simple-content/simple-content.yaml +2 -0
  114. package/simple/simple-hero/simple-hero.a2ui.json +4 -0
  115. package/simple/simple-hero/simple-hero.yaml +2 -0
  116. package/simple/simple-shell/simple-shell.a2ui.json +4 -0
  117. package/simple/simple-shell/simple-shell.yaml +2 -0
  118. package/theme/theme-panel/theme-panel.a2ui.json +1 -0
@@ -27,14 +27,14 @@
27
27
  "default": ""
28
28
  },
29
29
  "density": {
30
- "description": "Row density, forwarded to <table-ui>. `compact` for dense lists,\n`standard` for default rows, `comfortable` for spacious rows.\n",
30
+ "description": "Row density, forwarded to <table-ui>. `compact` for dense lists,\n`spacious` for airier rows, unset (default) for standard rows.\nConverged onto the global `[density]` Enum·preset vocabulary\n(gh#1335) — `standard` retired (never a real state) and\n`comfortable` renamed to `spacious` (ADR-0054's own migration\nmapping).\n",
31
31
  "type": "string",
32
32
  "enum": [
33
+ "",
33
34
  "compact",
34
- "standard",
35
- "comfortable"
35
+ "spacious"
36
36
  ],
37
- "default": "standard"
37
+ "default": ""
38
38
  },
39
39
  "exportable": {
40
40
  "description": "When set, the default toolbar carries an \"Export CSV\" button.\nConsumers overriding the toolbar via slot=\"toolbar\" can omit\nthis and stamp their own export affordance.\n",
@@ -96,6 +96,8 @@
96
96
  "composes": [
97
97
  "table-ui",
98
98
  "table-toolbar-ui",
99
+ "card-ui",
100
+ "row-ui",
99
101
  "badge-ui",
100
102
  "button-ui",
101
103
  "empty-state-ui",
@@ -210,6 +212,7 @@
210
212
  "export-invoices"
211
213
  ],
212
214
  "name": "InvoiceHistory",
215
+ "parts": {},
213
216
  "related": [
214
217
  "Table",
215
218
  "TableToolbar",
@@ -12,9 +12,12 @@
12
12
  *
13
13
  * Architecture (light-DOM, ADR-0033):
14
14
  * This composite stamps a three-region skeleton once on first connect:
15
- * [data-toolbar] (slot toolbar / default toolbar)
16
- * [data-table] (the <table-ui>)
17
- * [data-empty] (default <empty-state-ui> branch)
15
+ * [data-toolbar] (slot toolbar / default toolbar; filter chips
16
+ * live in a <row-ui data-invoice-filter>)
17
+ * [data-table-host] (a <card-ui> wrapping <section bleed> wrapping
18
+ * the <table-ui> — the canonical invoices-card
19
+ * grammar from billing.contents.html, gh#1519)
20
+ * [data-empty] (default <empty-state-ui> branch)
18
21
  * Then mutates the table's `.data` / `.columns` on every render. The
19
22
  * table primitive owns row stamping; this composite only wires its
20
23
  * inputs and converts cell renderers.
@@ -121,15 +124,14 @@ function csvEscape(val) {
121
124
  }
122
125
 
123
126
  /**
124
- * Translate invoice-history-ui's own `density` vocabulary (compact/standard/
125
- * comfortable, unchanged public API) onto table-ui's global [density]
126
- * grammar (ADR-0054): compact stays compact, standard is the absent
127
- * attribute, comfortable becomes spacious. Returns null for "no attribute".
127
+ * Translate invoice-history-ui's own `density` onto table-ui's global
128
+ * [density] grammar (ADR-0054). gh#1335 the vocabulary now IS the
129
+ * global one (compact/spacious/unset): identity passthrough for a real
130
+ * value, anything else is the absent attribute. Returns null for "no
131
+ * attribute".
128
132
  */
129
133
  function mapDensity(density) {
130
- if (density === 'compact') return 'compact';
131
- if (density === 'comfortable') return 'spacious';
132
- return null;
134
+ return density === 'compact' || density === 'spacious' ? density : null;
133
135
  }
134
136
 
135
137
  function forwardDensity(tableEl, density) {
@@ -142,7 +144,7 @@ export class InvoiceHistory extends UIElement {
142
144
  static properties = {
143
145
  invoices: { type: Array, default: [] },
144
146
  currency: { type: String, default: 'USD', reflect: true },
145
- density: { type: String, default: 'standard', reflect: true },
147
+ density: { type: String, default: '', reflect: true },
146
148
  loading: { type: Boolean, default: false, reflect: true },
147
149
  selectable: { type: Boolean, default: false, reflect: true },
148
150
  hrefPattern: { type: String, default: '/invoices/{number}', reflect: true, attribute: 'href-pattern' },
@@ -163,10 +165,10 @@ export class InvoiceHistory extends UIElement {
163
165
  #cleanups = [];
164
166
  #toolbarHost = null; // <div data-toolbar>
165
167
  #defaultToolbar = null; // <table-toolbar-ui> (when no slot=toolbar provided)
166
- #filterChipsHost = null; // <div data-invoice-filter>
168
+ #filterChipsHost = null; // <row-ui data-invoice-filter>
167
169
  #exportBtn = null; // <button-ui data-export>
168
- #tableHost = null; // <div data-table-host>
169
- #table = null; // <table-ui>
170
+ #tableHost = null; // <card-ui data-table-host>
171
+ #table = null; // <table-ui> (inside <section bleed>)
170
172
  #emptyHost = null; // <empty-state-ui data-empty>
171
173
  #internalUpdate = false;
172
174
  #loadCount = 0; // monotonic guard against stale fetches
@@ -377,9 +379,12 @@ export class InvoiceHistory extends UIElement {
377
379
  tt.setAttribute('data-default-toolbar', '');
378
380
  tt.setAttribute('text', 'Invoices');
379
381
  // Status-filter chips live in the [slot="actions"] region so they
380
- // sit alongside the toolbar's title + search affordances.
381
- const chips = document.createElement('div');
382
+ // sit alongside the toolbar's title + search affordances. The
383
+ // cluster is a <row-ui> (framework layout primitive, gh#1519).
384
+ const chips = document.createElement('row-ui');
382
385
  chips.setAttribute('data-invoice-filter', '');
386
+ chips.setAttribute('gap', '2');
387
+ chips.setAttribute('wrap', '');
383
388
  chips.setAttribute('role', 'group');
384
389
  chips.setAttribute('aria-label', 'Filter invoices by status');
385
390
  chips.setAttribute('slot', 'actions');
@@ -401,16 +406,22 @@ export class InvoiceHistory extends UIElement {
401
406
  }
402
407
 
403
408
  // ── Table region ───────────────────────────────────────────────
404
- const tableHost = document.createElement('div');
409
+ // Canonical invoices-card grammar (billing.contents.html):
410
+ // <card-ui><section bleed><table-ui sortable></table-ui></section></card-ui>
411
+ const tableHost = document.createElement('card-ui');
405
412
  tableHost.setAttribute('data-table-host', '');
406
413
  this.appendChild(tableHost);
407
414
  this.#tableHost = tableHost;
408
415
 
416
+ const tableSection = document.createElement('section');
417
+ tableSection.setAttribute('bleed', '');
418
+ tableHost.appendChild(tableSection);
419
+
409
420
  const table = document.createElement('table-ui');
410
421
  table.id = this.id ? `${this.id}__table` : `invoice-history-${Math.random().toString(36).slice(2, 8)}`;
411
422
  table.setAttribute('data-invoice-table', '');
412
423
  table.setAttribute('sortable', '');
413
- tableHost.appendChild(table);
424
+ tableSection.appendChild(table);
414
425
  this.#table = table;
415
426
 
416
427
  // Wire the default toolbar to this table by id.
@@ -1,10 +1,12 @@
1
1
  /* ═══════════════════════════════════════════════════════════════
2
2
  <invoice-history-ui> — Canonical billing-list composite.
3
3
 
4
- Composite over <table-ui> + <table-toolbar-ui> + <badge-ui> +
5
- <button-ui> + <empty-state-ui>. The host stacks three regions in
6
- DOM order: toolbar · table · empty-state. Each row in the table
7
- is owned by <table-ui>; this composite only styles its own
4
+ Composite over <table-ui> + <table-toolbar-ui> + <card-ui> +
5
+ <row-ui> + <badge-ui> + <button-ui> + <empty-state-ui>. The host
6
+ stacks three regions in DOM order: toolbar · table card · empty-
7
+ state. The table card is the canonical invoices grammar —
8
+ <card-ui><section bleed><table-ui> (gh#1519). Each row in the
9
+ table is owned by <table-ui>; this composite only styles its own
8
10
  region scaffolding + cell-internal billing chrome (status badge,
9
11
  amount alignment, action cluster).
10
12
 
@@ -40,9 +42,10 @@ invoice-history-ui {
40
42
  background: var(--invoice-history-bg);
41
43
  }
42
44
 
43
- /* ── Region stacking (toolbar · table · empty) ── */
45
+ /* ── Region stacking (toolbar · table card · empty) ──
46
+ [data-table-host] is a <card-ui> since gh#1519 — card-ui's own
47
+ @scope owns its display/chrome; don't override it here. */
44
48
  :scope > [data-toolbar],
45
- :scope > [data-table-host],
46
49
  :scope > [data-empty] {
47
50
  display: block;
48
51
  }
@@ -51,12 +54,11 @@ invoice-history-ui {
51
54
  margin-block-end: var(--invoice-history-gap);
52
55
  }
53
56
 
54
- /* Status-filter chip cluster lives in the toolbar's actions slot. */
57
+ /* Status-filter chip cluster a <row-ui wrap> in the toolbar's
58
+ actions slot; forward this composite's gap token into row-ui's
59
+ own contract. */
55
60
  :scope [data-invoice-filter] {
56
- display: inline-flex;
57
- flex-wrap: wrap;
58
- gap: var(--invoice-history-filter-gap);
59
- align-items: center;
61
+ --row-gap: var(--invoice-history-filter-gap);
60
62
  }
61
63
 
62
64
  /* ── Underlying table cascade ── */
@@ -64,7 +66,7 @@ invoice-history-ui {
64
66
  @scope assigns `display: grid` + sets `grid-template-columns` via
65
67
  JS; overriding the display collapses the grid into a single
66
68
  column. (Caught in v0.6.34 dogfood-sweep visual probe.) */
67
- :scope > [data-table-host] > table-ui {
69
+ :scope > [data-table-host] table-ui {
68
70
  --table-row-h: var(--invoice-history-row-min-height);
69
71
  width: 100%;
70
72
  }
@@ -120,11 +122,12 @@ invoice-history-ui {
120
122
  --empty-state-icon-fg: var(--a-danger-strong, var(--md-sys-color-danger));
121
123
  }
122
124
 
123
- /* ── Compact density forwards to the table primitive ── */
125
+ /* ── Density forwards to the table primitive (gh#1335 — `comfortable`
126
+ renamed to `spacious`, converged onto the global [density] grammar) ── */
124
127
  :scope[density="compact"] {
125
128
  --invoice-history-row-min-height: var(--table-row-h-compact, 2.25rem);
126
129
  }
127
- :scope[density="comfortable"] {
128
- --invoice-history-row-min-height: var(--table-row-h-comfortable, 3.5rem);
130
+ :scope[density="spacious"] {
131
+ --invoice-history-row-min-height: var(--table-row-h-spacious, 3.5rem);
129
132
  }
130
133
  }
@@ -31,6 +31,8 @@ description: |
31
31
  composes:
32
32
  - table-ui
33
33
  - table-toolbar-ui
34
+ - card-ui
35
+ - row-ui
34
36
  - badge-ui
35
37
  - button-ui
36
38
  - empty-state-ui
@@ -59,13 +61,17 @@ props:
59
61
  density:
60
62
  description: |
61
63
  Row density, forwarded to <table-ui>. `compact` for dense lists,
62
- `standard` for default rows, `comfortable` for spacious rows.
64
+ `spacious` for airier rows, unset (default) for standard rows.
65
+ Converged onto the global `[density]` Enum·preset vocabulary
66
+ (gh#1335) — `standard` retired (never a real state) and
67
+ `comfortable` renamed to `spacious` (ADR-0054's own migration
68
+ mapping).
63
69
  type: string
64
- default: standard
70
+ default: ""
65
71
  enum:
72
+ - ""
66
73
  - compact
67
- - standard
68
- - comfortable
74
+ - spacious
69
75
  reflect: true
70
76
 
71
77
  loading:
@@ -195,6 +195,7 @@
195
195
  "add-card"
196
196
  ],
197
197
  "name": "UIPaymentMethodForm",
198
+ "parts": {},
198
199
  "related": [
199
200
  "Form",
200
201
  "Field",
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Architecture (light-DOM, ADR-0033):
24
24
  * Stamps a fixed skeleton once on first connect:
25
- * [data-alert] · [data-summary] · [data-grid > field rows] · [data-legal]
25
+ * [data-alert] · [data-summary] · [data-field-grid > field rows] · [data-legal]
26
26
  * Mutates per-field state via reflected attributes on each sub-control.
27
27
  * No template rebuilds across renders — field nodes keep identity so
28
28
  * focus + IME composition state survive.
@@ -207,7 +207,7 @@ export class UIPaymentMethodForm extends UIFormElement {
207
207
  #alertHost = null; // <alert-ui data-alert>
208
208
  #summaryHost = null; // <div data-summary>
209
209
  #editButton = null; // <button-ui> inside summary
210
- #gridHost = null; // <div data-grid>
210
+ #gridHost = null; // <div data-field-grid>
211
211
 
212
212
  // Per-row <field-ui> hosts (keyed by field name).
213
213
  #fields = {
@@ -434,7 +434,7 @@ export class UIPaymentMethodForm extends UIFormElement {
434
434
 
435
435
  // Grid host.
436
436
  const grid = document.createElement('div');
437
- grid.setAttribute('data-grid', '');
437
+ grid.setAttribute('data-field-grid', '');
438
438
  this.appendChild(grid);
439
439
  this.#gridHost = grid;
440
440
 
@@ -82,23 +82,23 @@ payment-method-form-ui {
82
82
  }
83
83
 
84
84
  /* ── Field grid ── */
85
- :scope > [data-grid] {
85
+ :scope > [data-field-grid] {
86
86
  display: grid;
87
87
  grid-template-columns: 1fr 1fr;
88
88
  grid-template-areas: var(--payment-method-form-grid-template);
89
89
  gap: var(--payment-method-form-gap);
90
90
  }
91
- :scope > [data-grid][hidden] {
91
+ :scope > [data-field-grid][hidden] {
92
92
  display: none;
93
93
  }
94
94
 
95
- :scope[require-billing-address] > [data-grid] {
95
+ :scope[require-billing-address] > [data-field-grid] {
96
96
  grid-template-areas: var(--payment-method-form-grid-template-billing);
97
97
  }
98
98
 
99
99
  /* Hidden rows (address lines when require-billing-address is unset)
100
100
  collapse out of the grid without affecting sibling cell flow. */
101
- :scope > [data-grid] > field-ui[hidden] {
101
+ :scope > [data-field-grid] > field-ui[hidden] {
102
102
  display: none;
103
103
  }
104
104
 
@@ -80,6 +80,9 @@
80
80
  "category": "input",
81
81
  "composes": [
82
82
  "card-ui",
83
+ "col-ui",
84
+ "row-ui",
85
+ "divider-ui",
83
86
  "button-ui",
84
87
  "icon-ui",
85
88
  "tag-ui",
@@ -161,6 +164,7 @@
161
164
  "method-list"
162
165
  ],
163
166
  "name": "PaymentMethodList",
167
+ "parts": {},
164
168
  "related": [
165
169
  "PlanPicker",
166
170
  "Card",
@@ -12,9 +12,12 @@
12
12
  * Sibling: <plan-picker-ui> (same id-keyed-diff pattern)
13
13
  *
14
14
  * Architecture (light-DOM, ADR-0033):
15
- * This composite stamps a three-region skeleton once on first
16
- * connect:
17
- * [data-rows] · [data-empty] · [data-add]
15
+ * This composite stamps a region skeleton once on first connect:
16
+ * [data-rows] (a <col-ui> stack) · [data-empty] ·
17
+ * [data-add-divider] · [data-add] (a <row-ui>)
18
+ * The rows host and add row are framework layout primitives
19
+ * (col-ui / row-ui / divider-ui — gh#1519), mirroring the canonical
20
+ * billing page's layout-primitive grammar instead of raw divs.
18
21
  * Then mutates per-row DOM by id-keyed in-place diff on every
19
22
  * `methods` / `value` write. The diff preserves identity across
20
23
  * re-renders (id-keyed map) so focus survives unrelated row updates.
@@ -162,9 +165,10 @@ export class PaymentMethodList extends UIFormElement {
162
165
  // ── Field refs ────────────────────────────────────────────────────
163
166
  #stamped = false;
164
167
  #cleanups = [];
165
- #rowsHost = null; // <div data-rows role=presentation>
168
+ #rowsHost = null; // <col-ui data-rows role=presentation>
166
169
  #emptyHost = null; // <empty-state-ui data-empty>
167
- #addHost = null; // <div data-add>
170
+ #addHost = null; // <row-ui data-add>
171
+ #addDivider = null; // <divider-ui data-add-divider>
168
172
  #addButton = null; // <button-ui> inside #addHost
169
173
  #rowsById = new Map(); // id → <card-ui>
170
174
  #internalUpdate = false;
@@ -288,10 +292,14 @@ export class PaymentMethodList extends UIFormElement {
288
292
  this.removeAttribute('empty');
289
293
  }
290
294
 
291
- // Add-CTA visibility.
295
+ // Add-CTA visibility (the divider tracks the add row, and also
296
+ // hides when the rows list is empty — nothing to divide).
292
297
  if (this.#addHost) {
293
298
  this.#addHost.hidden = !!this['no-add'];
294
299
  }
300
+ if (this.#addDivider) {
301
+ this.#addDivider.hidden = !!this['no-add'] || methods.length === 0;
302
+ }
295
303
  if (this.#addButton) {
296
304
  this.#addButton.setAttribute('text', this['add-label'] || 'Add payment method');
297
305
  const icon = this['add-icon'] || 'plus';
@@ -332,10 +340,12 @@ export class PaymentMethodList extends UIFormElement {
332
340
  }
333
341
 
334
342
  #stamp() {
335
- // Rows host — the radiogroup container for the method cards.
336
- // role="presentation" because the outer host carries radiogroup.
337
- const rows = document.createElement('div');
343
+ // Rows host — a <col-ui> stack (framework layout primitive) that
344
+ // the method cards live in as DIRECT children. role="presentation"
345
+ // because the outer host carries radiogroup.
346
+ const rows = document.createElement('col-ui');
338
347
  rows.setAttribute('data-rows', '');
348
+ rows.setAttribute('gap', '2');
339
349
  rows.setAttribute('role', 'presentation');
340
350
  this.appendChild(rows);
341
351
  this.#rowsHost = rows;
@@ -351,9 +361,19 @@ export class PaymentMethodList extends UIFormElement {
351
361
  this.#emptyHost = empty;
352
362
  }
353
363
 
354
- // Add CTA host overridable via slot="add".
355
- const addHost = document.createElement('div');
364
+ // Divider between the rows list and the add CTA — the canonical
365
+ // grammar divides a list from its trailing action group
366
+ // (billing.contents.html drawer footers). Hidden alongside the
367
+ // add row via CSS ([empty] / [no-add]).
368
+ const addDivider = document.createElement('divider-ui');
369
+ addDivider.setAttribute('data-add-divider', '');
370
+ this.appendChild(addDivider);
371
+ this.#addDivider = addDivider;
372
+
373
+ // Add CTA host — a <row-ui>; overridable via slot="add".
374
+ const addHost = document.createElement('row-ui');
356
375
  addHost.setAttribute('data-add', '');
376
+ addHost.setAttribute('gap', '2');
357
377
  this.appendChild(addHost);
358
378
  this.#addHost = addHost;
359
379
 
@@ -2,8 +2,9 @@
2
2
  <payment-method-list-ui> — Saved payment methods (cards / bank /
3
3
  wallet) rendered as a 1-of-N radiogroup list.
4
4
 
5
- Composite over <card-ui> + <icon-ui> + <tag-ui> + <button-ui> +
6
- <empty-state-ui>. Each row is a <card-ui data-row> with a single
5
+ Composite over <card-ui> + <col-ui> + <row-ui> + <divider-ui> +
6
+ <icon-ui> + <tag-ui> + <button-ui> + <empty-state-ui>. Each row is
7
+ a <card-ui data-row> with a single
7
8
  <header> child carrying the canonical card-ui slot composition:
8
9
  slot=icon brand mark
9
10
  slot=heading "Visa ···· 4242" + inline <tag-ui> when default
@@ -43,11 +44,10 @@ payment-method-list-ui {
43
44
  background: var(--payment-method-list-bg);
44
45
  }
45
46
 
46
- /* ── Rows host — column stack of <card-ui> rows ── */
47
+ /* ── Rows host — a <col-ui> stack of <card-ui> rows; forward this
48
+ composite's gap token into col-ui's own contract. ── */
47
49
  :scope > [data-rows] {
48
- display: flex;
49
- flex-direction: column;
50
- gap: var(--payment-method-list-gap);
50
+ --col-gap: var(--payment-method-list-gap);
51
51
  }
52
52
 
53
53
  /* ── Per-row card — chrome only; card-ui owns layout via <header>. ── */
@@ -87,12 +87,15 @@ payment-method-list-ui {
87
87
  display: none;
88
88
  }
89
89
 
90
- /* ── Add CTA row ── */
90
+ /* ── Add CTA row — a <row-ui>; forward the justify token. ── */
91
91
  :scope > [data-add] {
92
- display: flex;
93
- justify-content: var(--payment-method-list-add-justify);
92
+ --row-justify: var(--payment-method-list-add-justify);
93
+ }
94
+ :scope > [data-add][hidden],
95
+ :scope > [data-add-divider][hidden] {
96
+ display: none;
94
97
  }
95
- :scope > [data-add][hidden] {
98
+ :scope[empty] > [data-add-divider] {
96
99
  display: none;
97
100
  }
98
101
 
@@ -27,6 +27,9 @@ description: |
27
27
  # .html demo shell) must explicitly side-effect-import each one.
28
28
  composes:
29
29
  - card-ui
30
+ - col-ui
31
+ - row-ui
32
+ - divider-ui
30
33
  - button-ui
31
34
  - icon-ui
32
35
  - tag-ui
@@ -100,6 +100,11 @@
100
100
  "option-card-ui",
101
101
  "segmented-ui",
102
102
  "segment-ui",
103
+ "field-ui",
104
+ "grid-ui",
105
+ "col-ui",
106
+ "row-ui",
107
+ "divider-ui",
103
108
  "button-ui",
104
109
  "icon-ui",
105
110
  "text-ui",
@@ -201,6 +206,7 @@
201
206
  "plan-selection"
202
207
  ],
203
208
  "name": "PlanPicker",
209
+ "parts": {},
204
210
  "related": [
205
211
  "OptionCard",
206
212
  "Segmented",
@@ -5,15 +5,25 @@
5
5
  * marketing pricing page (SPEC-012). Each plan card is composed from
6
6
  * <option-card-ui> (radio semantics) decorated with a price block,
7
7
  * feature list, and CTA button derived from the plan record. The
8
- * optional cycle toggle is a <segmented-ui> that swaps monthly /
9
- * annual prices in place across every card.
8
+ * optional cycle toggle is a <field-ui label="Billing interval">
9
+ * wrapping a <segmented-ui> the canonical billing-interval chain
10
+ * from apps/saas/app/billing/billing.contents.html — that swaps
11
+ * monthly / annual prices in place across every card.
12
+ *
13
+ * Internal layout composes the framework layout primitives (gh#1519):
14
+ * the plans host is a <grid-ui columns="auto-fit">, each card body is
15
+ * a <col-ui> of <row-ui data-price-row> + <divider-ui> + a
16
+ * <col-ui data-features> of <row-ui> feature lines — mirroring the
17
+ * canonical's col-ui/row-ui feature-list grammar instead of raw
18
+ * div/ul/li.
10
19
  *
11
20
  * SPEC: .claude/docs/specs/implementation-ready/SPEC-011-plan-picker-in-app-upgrade.md
12
21
  * WAI-APG: https://www.w3.org/WAI/ARIA/apg/patterns/radio/
13
22
  *
14
23
  * Architecture (light-DOM, ADR-0033):
15
- * This composite stamps a four-region skeleton once on first connect:
16
- * [data-toggle] · [data-plans] · [data-skeleton] · [data-empty]
24
+ * This composite stamps a region skeleton once on first connect:
25
+ * [data-toggle] · [data-toggle-divider] · [data-plans] ·
26
+ * [data-skeleton] · [data-empty]
17
27
  * Then mutates per-card DOM by id-keyed in-place diff on every
18
28
  * `plans` / `value` / `current` / `cycle` change. The diff preserves
19
29
  * focus-within during re-render (replacing children would break the
@@ -116,8 +126,10 @@ export class PlanPicker extends UIFormElement {
116
126
  #stamped = false;
117
127
  #cleanups = [];
118
128
  #toggleHost = null; // <div data-toggle> wrapper
119
- #toggleSegmented = null; // <segmented-ui> inside toggle host
120
- #plansHost = null; // <div data-plans role=radiogroup>
129
+ #toggleField = null; // <field-ui> inside toggle host (canonical chain)
130
+ #toggleSegmented = null; // <segmented-ui> inside the field
131
+ #toggleDivider = null; // <divider-ui data-toggle-divider> after the toggle
132
+ #plansHost = null; // <grid-ui data-plans role=presentation>
121
133
  #skeletonHost = null; // <div data-skeleton>
122
134
  #emptyHost = null; // <empty-state-ui data-empty>
123
135
  #cardsById = new Map(); // id → <option-card-ui>
@@ -285,10 +297,11 @@ export class PlanPicker extends UIFormElement {
285
297
  this.removeAttribute('empty');
286
298
  }
287
299
 
288
- // Toggle visibility / contents.
300
+ // Toggle visibility / contents (divider tracks the toggle).
289
301
  if (this.#toggleHost) {
290
302
  const show = this.cycleToggle && plans.length > 0;
291
303
  this.#toggleHost.hidden = !show;
304
+ if (this.#toggleDivider) this.#toggleDivider.hidden = !show;
292
305
  if (show) this.#renderToggle();
293
306
  }
294
307
 
@@ -321,12 +334,24 @@ export class PlanPicker extends UIFormElement {
321
334
  this.appendChild(toggle);
322
335
  this.#toggleHost = toggle;
323
336
 
324
- // Plans host the radiogroup container the option-card-ui kids
325
- // live in. We give it role="presentation" because the OUTER host
326
- // already carries role="radiogroup" and nesting two radiogroups
327
- // would confuse AT.
328
- const plansHost = document.createElement('div');
337
+ // Divider between the interval toggle and the plan list — the
338
+ // canonical change-plan grammar separates the two groups with a
339
+ // <divider-ui> (billing.contents.html drawer). Visible only when
340
+ // the toggle is.
341
+ const toggleDivider = document.createElement('divider-ui');
342
+ toggleDivider.setAttribute('data-toggle-divider', '');
343
+ toggleDivider.hidden = true;
344
+ this.appendChild(toggleDivider);
345
+ this.#toggleDivider = toggleDivider;
346
+
347
+ // Plans host — a <grid-ui> (the framework grid primitive; the
348
+ // [layout] mode overrides its columns via plan-picker.css) that
349
+ // the option-card-ui kids live in as DIRECT children. We give it
350
+ // role="presentation" because the OUTER host already carries
351
+ // role="radiogroup" and nesting two radiogroups would confuse AT.
352
+ const plansHost = document.createElement('grid-ui');
329
353
  plansHost.setAttribute('data-plans', '');
354
+ plansHost.setAttribute('columns', 'auto-fit');
330
355
  plansHost.setAttribute('role', 'presentation');
331
356
  this.appendChild(plansHost);
332
357
  this.#plansHost = plansHost;
@@ -354,9 +379,16 @@ export class PlanPicker extends UIFormElement {
354
379
  if (!this.#toggleHost) return;
355
380
  let seg = this.#toggleSegmented;
356
381
  if (!seg) {
382
+ // Canonical billing-interval chain: <field-ui label="Billing
383
+ // interval"> wrapping the <segmented-ui> (billing.contents.html
384
+ // "Current plan" region).
385
+ const field = document.createElement('field-ui');
386
+ field.setAttribute('label', 'Billing interval');
357
387
  seg = document.createElement('segmented-ui');
358
388
  seg.setAttribute('aria-label', 'Billing cycle');
359
- this.#toggleHost.appendChild(seg);
389
+ field.appendChild(seg);
390
+ this.#toggleHost.appendChild(field);
391
+ this.#toggleField = field;
360
392
  this.#toggleSegmented = seg;
361
393
  // Stamp segments — known cycles in display order.
362
394
  for (const cycleKey of ['monthly', 'annual']) {
@@ -443,8 +475,9 @@ export class PlanPicker extends UIFormElement {
443
475
  // bodies show always.
444
476
  let body = card.querySelector(':scope > [data-plan-body]');
445
477
  if (!body) {
446
- body = document.createElement('div');
478
+ body = document.createElement('col-ui');
447
479
  body.setAttribute('data-plan-body', '');
480
+ body.setAttribute('gap', '3');
448
481
  card.appendChild(body);
449
482
  }
450
483
  this.#decorateBody(body, plan);
@@ -478,9 +511,11 @@ export class PlanPicker extends UIFormElement {
478
511
  // Wipe + restamp body (small DOM; cycle / feature-list / cta).
479
512
  body.replaceChildren();
480
513
 
481
- // Price row.
482
- const priceRow = document.createElement('div');
514
+ // Price row — <row-ui> per the canonical inline-cluster grammar.
515
+ const priceRow = document.createElement('row-ui');
483
516
  priceRow.setAttribute('data-price-row', '');
517
+ priceRow.setAttribute('gap', '1');
518
+ priceRow.setAttribute('align', 'baseline');
484
519
  const cycle = KNOWN_CYCLES.has(this.cycle) ? this.cycle : 'monthly';
485
520
  const prices = plan.prices || {};
486
521
  const priceRec = prices[cycle] || prices.monthly || null;
@@ -513,20 +548,33 @@ export class PlanPicker extends UIFormElement {
513
548
  body.appendChild(note);
514
549
  }
515
550
 
516
- // Feature list.
551
+ // Feature list — the canonical feature-line grammar:
552
+ // <col-ui gap="1"> of <row-ui gap="2"><icon-ui/><span/></row-ui>
553
+ // (billing.contents.html trial-banner / what's-included blocks),
554
+ // separated from the price block by a <divider-ui>.
517
555
  if (Array.isArray(plan.features) && plan.features.length) {
518
- const list = document.createElement('ul');
556
+ const divider = document.createElement('divider-ui');
557
+ divider.setAttribute('data-body-divider', '');
558
+ body.appendChild(divider);
559
+
560
+ const list = document.createElement('col-ui');
519
561
  list.setAttribute('data-features', '');
562
+ list.setAttribute('gap', '1');
563
+ list.setAttribute('role', 'list');
520
564
  for (const feature of plan.features) {
521
- const li = document.createElement('li');
565
+ const line = document.createElement('row-ui');
566
+ line.setAttribute('data-feature', '');
567
+ line.setAttribute('gap', '2');
568
+ line.setAttribute('align', 'start');
569
+ line.setAttribute('role', 'listitem');
522
570
  const icon = document.createElement('icon-ui');
523
571
  icon.setAttribute('name', 'check');
524
572
  icon.setAttribute('aria-hidden', 'true');
525
- li.appendChild(icon);
573
+ line.appendChild(icon);
526
574
  const label = document.createElement('span');
527
575
  label.textContent = String(feature);
528
- li.appendChild(label);
529
- list.appendChild(li);
576
+ line.appendChild(label);
577
+ list.appendChild(line);
530
578
  }
531
579
  body.appendChild(list);
532
580
  }