@adia-ai/web-components 0.8.44 → 0.8.46

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 (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/components/card/card.css +12 -23
  3. package/components/card/card.yaml +11 -0
  4. package/components/chart/chart.a2ui.json +16 -1
  5. package/components/chart/chart.class.js +611 -41
  6. package/components/chart/chart.css +174 -0
  7. package/components/chart/chart.d.ts +5 -1
  8. package/components/chart/chart.yaml +45 -1
  9. package/components/context-menu/context-menu.a2ui.json +8 -3
  10. package/components/context-menu/context-menu.class.js +46 -5
  11. package/components/context-menu/context-menu.d.ts +6 -3
  12. package/components/context-menu/context-menu.examples.md +2 -2
  13. package/components/context-menu/context-menu.yaml +22 -5
  14. package/components/field/field.css +24 -2
  15. package/components/index.js +1 -0
  16. package/components/input/input.css +7 -0
  17. package/components/nav/nav.a2ui.json +2 -2
  18. package/components/nav/nav.css +1 -1
  19. package/components/nav/nav.d.ts +1 -1
  20. package/components/nav/nav.yaml +14 -3
  21. package/components/nav-group/nav-group.css +37 -3
  22. package/components/pagination/pagination.class.js +52 -22
  23. package/components/search/search.class.js +39 -5
  24. package/components/select/select.a2ui.json +5 -0
  25. package/components/select/select.class.js +20 -0
  26. package/components/select/select.css +25 -0
  27. package/components/select/select.d.ts +2 -0
  28. package/components/select/select.yaml +12 -0
  29. package/components/table/cell-types.js +9 -0
  30. package/components/table/table.a2ui.json +19 -4
  31. package/components/table/table.class.js +410 -45
  32. package/components/table/table.css +7 -4
  33. package/components/table/table.d.ts +9 -3
  34. package/components/table/table.yaml +115 -9
  35. package/components/table-footer/table-footer.a2ui.json +150 -0
  36. package/components/table-footer/table-footer.class.js +391 -0
  37. package/components/table-footer/table-footer.css +64 -0
  38. package/components/table-footer/table-footer.d.ts +39 -0
  39. package/components/table-footer/table-footer.examples.md +46 -0
  40. package/components/table-footer/table-footer.js +17 -0
  41. package/components/table-footer/table-footer.yaml +219 -0
  42. package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
  43. package/components/table-toolbar/table-toolbar.class.js +61 -17
  44. package/components/table-toolbar/table-toolbar.css +34 -0
  45. package/components/table-toolbar/table-toolbar.d.ts +10 -0
  46. package/components/table-toolbar/table-toolbar.yaml +58 -15
  47. package/core/data-stream.js +37 -2
  48. package/core/index.d.ts +1 -0
  49. package/core/index.js +1 -0
  50. package/core/provider.d.ts +9 -13
  51. package/core/provider.js +9 -113
  52. package/core/store.d.ts +46 -0
  53. package/core/store.js +89 -0
  54. package/custom-elements.json +192 -8
  55. package/dist/host.min.css +1 -1
  56. package/dist/host.sheet.js +1 -1
  57. package/dist/theme-provider.min.js +1 -1
  58. package/dist/web-components.min.css +1 -1
  59. package/dist/web-components.min.js +96 -96
  60. package/dist/web-components.sheet.js +1 -1
  61. package/package.json +1 -1
  62. package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
  63. package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
  64. package/patterns/new-enrollments/new-enrollments.html +54 -0
  65. package/patterns/table-in-card/table-in-card.examples.html +168 -0
  66. package/patterns/table-in-card/table-in-card.examples.js +139 -0
  67. package/patterns/table-in-card/table-in-card.html +86 -0
  68. package/styles/api/sizing.css +46 -0
  69. package/styles/components.css +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.8.44",
3
+ "version": "0.8.46",
4
4
  "description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui.",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -185,13 +185,23 @@
185
185
  <section data-section data-property="full-bleed-with-legend">
186
186
  <h2 variant="section">Full-bleed chart + legend band</h2>
187
187
  <p data-note>
188
- <code>bleed</code> only earns true edge-to-edge treatment for bare plotted
189
- marks a sparkline, or another chart type with both <code>no-grid</code>
190
- and <code>no-values</code> set (gh#1095). A multi-series chart still
191
- needs a legend, so keep <code>&lt;chart-legend-ui&gt;</code> OUT of the
192
- bled section — wire it to the chart via <code>[for]</code> in a normal
193
- (inset) section below instead. It reads as its own band beneath the
194
- plot, and never collides with the card's rounded corners.
188
+ <code>bleed</code> is unconditionally author-controlled (gh#1801) card.css
189
+ never restores the section's inset for you, so it's on the author to keep
190
+ any guide/value text or legend clear of the card's rounded corners. Bare
191
+ plotted marks (a sparkline, or another chart type with both
192
+ <code>no-grid</code> and <code>no-values</code> set) are naturally safe to
193
+ bleed since they draw no such text. A multi-series chart still needs a
194
+ legend, so keep <code>&lt;chart-legend-ui&gt;</code> OUT of the bled
195
+ section — wire it to the chart via <code>[for]</code> in a normal (inset)
196
+ section below instead. It reads as its own band beneath the plot, and
197
+ never collides with the card's rounded corners. As of the Charts 2.0
198
+ foundations (ADR-0081), <code>no-grid</code>/<code>no-values</code> bare-marks
199
+ isn't the only corner-safe option anymore — <code>labels="chip"</code>
200
+ (REQ-F-004/006) keeps real gridlines and axis chips inset within the plot
201
+ box with normative clamp/clearance rules, so a bled chart can show its own
202
+ axis without hand-suppressing it. Bare-marks stays the right call for a
203
+ sparkline-style tile with no chrome at all; chip mode is the right call
204
+ for a bled chart that still needs to show its axis.
195
205
  </p>
196
206
  <div data-artifact-container>
197
207
  <div data-artifact-item data-artifact-label="bled multi-line + legend band">
@@ -211,6 +221,23 @@
211
221
  </section>
212
222
  </card-ui>
213
223
  </div>
224
+ <div data-artifact-item data-artifact-label="bled bar + chip labels + legend band (ADR-0081)">
225
+ <card-ui data-chunk="chart-in-card-full-bleed-chip-legend" data-chunk-kind="block"
226
+ data-chunk-domain="dashboard"
227
+ data-chunk-description="Full-bleed bar chart with chip-label axis and legend band — Revenue/Target grouped-bar chart bleeding edge-to-edge inside the card with inset axis-value chips, paired with a chart-legend-ui mirroring its series in a normal-inset section below."
228
+ data-chunk-keywords="chart card bar chip labels legend bleed edge-to-edge revenue target dashboard analytics adr-0081" style="max-width:520px;">
229
+ <header>
230
+ <span slot="heading"><h3>Revenue vs target</h3></span>
231
+ <text-ui slot="description" color="subtle">Last 6 months</text-ui>
232
+ </header>
233
+ <section bleed>
234
+ <chart-ui id="pat-bleed-chip-legend-chart" type="grouped-bar" labels="chip" x="month" y="revenue,target"></chart-ui>
235
+ </section>
236
+ <section>
237
+ <chart-legend-ui for="pat-bleed-chip-legend-chart" shape="line"></chart-legend-ui>
238
+ </section>
239
+ </card-ui>
240
+ </div>
214
241
  </div>
215
242
  </section>
216
243
 
@@ -353,8 +380,8 @@
353
380
  <h2 variant="section">Rules</h2>
354
381
  <ul>
355
382
  <li>For sparklines, set <code>bleed</code> on the card's <code>&lt;section&gt;</code> so the line touches the edges.</li>
356
- <li><code>bleed</code> is a bare-marks affordance, not a general "edge-to-edge chart" switch (gh#1095). It's safe for a sparkline, or for any other chart <code>type</code> that also sets both <code>no-grid</code> and <code>no-values</code> — i.e. nothing that renders axis ticks, gridlines, or value text. A chart that still shows guides or values needs the card's normal inset so that text doesn't collide with (and clip under) the card's rounded corners; card.css restores the inset automatically if it detects either case, so a forgotten <code>no-grid</code>/<code>no-values</code> fails safe rather than clipping silently.</li>
357
- <li>Never put <code>&lt;chart-legend-ui&gt;</code> inside a <code>bleed</code> section — its badge rows need their own breathing room too. Place it in a plain <code>&lt;section&gt;</code> immediately after the chart's section, wired via <code>chart-legend-ui[for="&lt;chart id&gt;"]</code> (mirrors <code>.legendData</code>; see the "Full-bleed chart + legend band" example above and <a href="/site/components/chart-legend">chart-legend-ui</a>'s own <code>for</code> docs). card.css also restores the section's inset if a legend ends up inside a bled section anyway, so the fallback stays readable rather than edge-clipped.</li>
383
+ <li><code>bleed</code> is unconditionally author-controlled card.css never re-inserts the card's inset for you (gh#1801; supersedes gh#1095's removed auto-restore guard). A bare-marks chart (a sparkline, or any other <code>type</code> that also sets both <code>no-grid</code> and <code>no-values</code> — i.e. nothing that renders axis ticks, gridlines, or value text) is naturally safe to bleed since it draws no such text. A chart that needs to keep its axis visible while bleeding sets <code>labels="chip"</code> instead (Charts 2.0 foundations, ADR-0081/REQ-F-004–006): axis-value and category chips render inset within the plot box with normative clamp/corner-clearance rules, so nothing renders close enough to the card's rounded corners to clip. A chart that shows guides or values inside a bled section with NEITHER <code>no-grid</code>/<code>no-values</code> NOR <code>labels="chip"</code> WILL collide with (and clip under) the card's rounded corners — that combination now clips silently instead of failing safe, so treat it as a real authoring hazard, not a style preference.</li>
384
+ <li>Never put <code>&lt;chart-legend-ui&gt;</code> inside a <code>bleed</code> section — its badge rows need their own breathing room too, and nothing restores that room automatically anymore. Place it in a plain <code>&lt;section&gt;</code> immediately after the chart's section, wired via <code>chart-legend-ui[for="&lt;chart id&gt;"]</code> (mirrors <code>.legendData</code>; see the "Full-bleed chart + legend band" example above and <a href="/site/components/chart-legend">chart-legend-ui</a>'s own <code>for</code> docs). A legend left inside a bled section now clips under the card's rounded corner instead of falling back to a readable inset.</li>
358
385
  <li>Put <code>no-values</code> on bar/line charts inside cards — the stat in the header already shows the current number.</li>
359
386
  <li>Use <code>&lt;stat-ui&gt;</code> in the heading slot only when the card's primary metric is a single number; otherwise use <code>&lt;h3&gt;</code> + description.</li>
360
387
  <li>Keep breakdowns shallow — 3–4 stats max. More belongs in a full section, not inside a tile.</li>
@@ -0,0 +1,140 @@
1
+ <header>
2
+ <div>
3
+ <h1>New Enrollments</h1>
4
+ </div>
5
+ <div data-tags>
6
+ <code>card-ui</code> <code>chart-ui</code> <code>chart-legend-ui</code> <code>select-ui</code> <code>button-ui</code> <code>row-ui</code>
7
+ </div>
8
+ <p>Recreation of Figma "Claude Code Gateway" node 129-2881 — a "New enrollments" dashboard card rendered four ways (stacked-bar, area/spline, sparkline + tooltip, radial-bar), one shared header grammar (title + date-range select + kebab menu), each chart body full-bleed per the Charts 2.0 foundations model.</p>
9
+ </header>
10
+
11
+ <section data-section data-property="new-enrollments-family">
12
+ <h2 variant="section">The card, four ways</h2>
13
+ <!-- Pattern source: docs/ops/spec/spec-charts-2-0-foundations.md (Design source: Figma node 129-2881) -->
14
+ <p data-note>
15
+ Built on <a href="/site/docs/adr/adr-0081">ADR-0081</a>'s Charts 2.0 foundations: <code>labels="chip"</code>
16
+ keeps a real axis inset within a bled plot box, <code>today</code> marks the boundary between complete
17
+ and in-progress periods, a per-datum <code>.provisional</code> flag renders the current/incomplete period
18
+ as a soft tonal fill with a dashed outline (bar family) or dashed-stroke-with-faded-fill (area/line), and the legend band
19
+ (<code>&lt;chart-legend-ui for&gt;</code> in a plain section below the bled plot) is the only sanctioned
20
+ legend pairing per OPEN-6.
21
+ </p>
22
+ <p data-note>
23
+ The Figma source itself carries five defects, corrected here rather than reproduced — see
24
+ "Fixes applied against the Figma source" below.
25
+ </p>
26
+ <div data-artifact-container>
27
+ <div data-artifact-item data-artifact-label="stacked-bar — provisional period + legend band">
28
+ <card-ui data-chunk="new-enrollments-stacked-bar" data-chunk-kind="block"
29
+ data-chunk-domain="dashboard"
30
+ data-chunk-description="New enrollments stacked-bar card — New vs Returning enrollments by month, the current month rendered with a soft tonal fill and dashed outline as a provisional/incomplete period, paired with a legend band naming both segments."
31
+ data-chunk-keywords="new enrollments dashboard card stacked-bar provisional tonal dashed legend today-marker new returning analytics chart"
32
+ style="max-width:360px;">
33
+ <header>
34
+ <span slot="heading"><h3>New enrollments</h3></span>
35
+ <row-ui slot="action" gap="2" align="center">
36
+ <select-ui size="sm" variant="ghost" value="6m">
37
+ <option value="6m">Last 6 months</option>
38
+ <option value="3m">Last 3 months</option>
39
+ <option value="1m">Last month</option>
40
+ </select-ui>
41
+ <button-ui icon="dots-three-vertical" variant="ghost" size="sm" aria-label="Card options"></button-ui>
42
+ </row-ui>
43
+ </header>
44
+ <section bleed>
45
+ <chart-ui id="ne-stacked-bar" type="stacked-bar" labels="chip" today="Aug" x="month" y="New,Returning"
46
+ data='[{"month":"Mar","New":5,"Returning":3},{"month":"Apr","New":6,"Returning":4},{"month":"May","New":7,"Returning":4},{"month":"Jun","New":6,"Returning":5},{"month":"Jul","New":10,"Returning":6},{"month":"Aug","New":12,"Returning":8,"provisional":true}]'>
47
+ </chart-ui>
48
+ </section>
49
+ <section>
50
+ <chart-legend-ui for="ne-stacked-bar"></chart-legend-ui>
51
+ </section>
52
+ </card-ui>
53
+ </div>
54
+ <div data-artifact-item data-artifact-label="area/spline — provisional period (fix #3)">
55
+ <card-ui data-chunk="new-enrollments-area" data-chunk-kind="block"
56
+ data-chunk-domain="dashboard"
57
+ data-chunk-description="New enrollments area/spline card — total monthly enrollments trend, the current month rendered with a dashed stroke and faded fill as a provisional/incomplete period, matching the stacked-bar sibling's treatment for the same underlying state."
58
+ data-chunk-keywords="new enrollments dashboard card area spline trend provisional dashed faded today-marker analytics chart"
59
+ style="max-width:360px;">
60
+ <header>
61
+ <span slot="heading"><h3>New enrollments</h3></span>
62
+ <row-ui slot="action" gap="2" align="center">
63
+ <select-ui size="sm" variant="ghost" value="6m">
64
+ <option value="6m">Last 6 months</option>
65
+ <option value="3m">Last 3 months</option>
66
+ <option value="1m">Last month</option>
67
+ </select-ui>
68
+ <button-ui icon="dots-three-vertical" variant="ghost" size="sm" aria-label="Card options"></button-ui>
69
+ </row-ui>
70
+ </header>
71
+ <section bleed>
72
+ <chart-ui id="ne-area" type="area" labels="chip" today="Aug" smooth="0.4" no-values no-average x="month" y="value"
73
+ data='[{"month":"Mar","value":8},{"month":"Apr","value":10},{"month":"May","value":11},{"month":"Jun","value":11},{"month":"Jul","value":16},{"month":"Aug","value":20,"provisional":true}]'>
74
+ </chart-ui>
75
+ </section>
76
+ </card-ui>
77
+ </div>
78
+ <div data-artifact-item data-artifact-label="sparkline + tooltip">
79
+ <card-ui data-chunk="new-enrollments-sparkline" data-chunk-kind="block"
80
+ data-chunk-domain="dashboard"
81
+ data-chunk-description="New enrollments sparkline card — 30-day daily enrollment trend as a bare-marks sparkline, hovering any point surfaces the exact count and day via the built-in tooltip."
82
+ data-chunk-keywords="new enrollments dashboard card sparkline tooltip trend daily compact bare-marks analytics chart"
83
+ style="max-width:360px;">
84
+ <header>
85
+ <span slot="heading"><h3>New enrollments</h3></span>
86
+ <row-ui slot="action" gap="2" align="center">
87
+ <select-ui size="sm" variant="ghost" value="30d">
88
+ <option value="30d">Last 30 days</option>
89
+ <option value="7d">Last 7 days</option>
90
+ </select-ui>
91
+ <button-ui icon="dots-three-vertical" variant="ghost" size="sm" aria-label="Card options"></button-ui>
92
+ </row-ui>
93
+ </header>
94
+ <section bleed>
95
+ <chart-ui id="ne-sparkline" type="sparkline" x="day" y="value"
96
+ data='[{"day":"Jul 25","value":1580},{"day":"Jul 26","value":1620},{"day":"Jul 27","value":1550},{"day":"Jul 28","value":1670},{"day":"Jul 29","value":1710},{"day":"Jul 30","value":1640},{"day":"Jul 31","value":1690},{"day":"Aug 1","value":1730},{"day":"Aug 2","value":1660},{"day":"Aug 3","value":1780},{"day":"Aug 4","value":1810},{"day":"Aug 5","value":1750},{"day":"Aug 6","value":1700},{"day":"Aug 7","value":1840},{"day":"Aug 8","value":1790},{"day":"Aug 9","value":1880},{"day":"Aug 10","value":1820},{"day":"Aug 11","value":1760},{"day":"Aug 12","value":1910},{"day":"Aug 13","value":1850},{"day":"Aug 14","value":1800},{"day":"Aug 15","value":1940},{"day":"Aug 16","value":1890},{"day":"Aug 17","value":1830},{"day":"Aug 18","value":1970},{"day":"Aug 19","value":1920},{"day":"Aug 20","value":1870},{"day":"Aug 21","value":2032},{"day":"Aug 22","value":1950},{"day":"Aug 23","value":1990}]'>
97
+ </chart-ui>
98
+ </section>
99
+ </card-ui>
100
+ </div>
101
+ <div data-artifact-item data-artifact-label="radial-bar — real content + legend band (fixes #1, #2, #5)">
102
+ <card-ui data-chunk="new-enrollments-radial-bar" data-chunk-kind="block"
103
+ data-chunk-domain="dashboard"
104
+ data-chunk-description="New enrollments radial-bar card — enrollment channel mix (Online, Referral, Walk-in) each as percent of its monthly target reached, paired with a legend band naming all three channels."
105
+ data-chunk-keywords="new enrollments dashboard card radial-bar channel mix online referral walk-in target legend analytics chart"
106
+ style="max-width:360px;">
107
+ <header>
108
+ <span slot="heading"><h3>New enrollments</h3></span>
109
+ <row-ui slot="action" gap="2" align="center">
110
+ <select-ui size="sm" variant="ghost" value="30d">
111
+ <option value="30d">Last 30 days</option>
112
+ <option value="7d">Last 7 days</option>
113
+ </select-ui>
114
+ <button-ui icon="dots-three-vertical" variant="ghost" size="sm" aria-label="Card options"></button-ui>
115
+ </row-ui>
116
+ </header>
117
+ <section bleed>
118
+ <chart-ui id="ne-radial-bar" type="radial-bar" x="channel" y="v"
119
+ data='[{"channel":"Online","v":82},{"channel":"Referral","v":61},{"channel":"Walk-in","v":45}]'>
120
+ </chart-ui>
121
+ </section>
122
+ <section>
123
+ <chart-legend-ui for="ne-radial-bar"></chart-legend-ui>
124
+ </section>
125
+ </card-ui>
126
+ </div>
127
+ </div>
128
+ </section>
129
+
130
+ <section data-section data-property="figma-defect-fixes">
131
+ <h2 variant="section">Fixes applied against the Figma source</h2>
132
+ <p data-note>Figma node 129-2881 itself carries five defects. Each is corrected in this recreation rather than reproduced:</p>
133
+ <ul>
134
+ <li><strong>Content-truth mismatch (radial-bar).</strong> The Figma frame's title/subtitle read "New enrollments" / "Last 30 days" over unrelated CPU/Memory/Disk content. Replaced with real enrollment-channel data (Online/Referral/Walk-in against monthly target) so title and body agree.</li>
135
+ <li><strong>"DISC" legend typo (radial-bar).</strong> Moot once the CPU/Memory/Disk placeholder content is replaced — that legend row, and its typo, no longer ship.</li>
136
+ <li><strong>Missing provisional state (area/spline).</strong> The Figma area frame showed no distinct treatment for its own incomplete current period, unlike its stacked-bar sibling. Applied the same <code>.provisional</code> datum flag here (dashed stroke, faded fill — REQ-F-015/OPEN-3), so siblings give consistent feedback for the same state.</li>
137
+ <li><strong>No legend for stacked segments (stacked-bar).</strong> The Figma stacked-bar frame had two unlabeled segments with no legend, while its radial-bar sibling had one. Added a legend band below the plot naming both segments (New, Returning).</li>
138
+ <li><strong>Sub-region count diverged by chart type (radial-bar).</strong> The Figma radial-bar frame paired its rings with an inline legend column beside them — a second sub-region no other frame had, and the exact chrome-layer legend shape OPEN-6 rules OUT. Moved to the same legend-band section every multi-series variant here uses. Sub-region count now tracks series count, not chart type: the two single-series cards (area, sparkline) render only the plot; the two multi-series cards (stacked-bar, radial-bar) pair the plot with one legend band.</li>
139
+ </ul>
140
+ </section>
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" theme="auto">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>New Enrollments (pattern) — AdiaUI</title>
7
+
8
+ <!-- Token base -->
9
+ <link rel="stylesheet" href="../../styles/resets.css">
10
+ <link rel="stylesheet" href="../../styles/tokens.css">
11
+
12
+ <!-- Component CSS — primitives this pattern composes -->
13
+ <link rel="stylesheet" href="../../components/button/button.css">
14
+ <link rel="stylesheet" href="../../components/card/card.css">
15
+ <link rel="stylesheet" href="../../components/chart/chart.css">
16
+ <link rel="stylesheet" href="../../components/chart-legend/chart-legend.css">
17
+ <link rel="stylesheet" href="../../components/icon/icon.css">
18
+ <link rel="stylesheet" href="../../components/row/row.css">
19
+ <link rel="stylesheet" href="../../components/select/select.css">
20
+
21
+ <!-- Component JS -->
22
+ <script type="module" src="../../components/button/button.js"></script>
23
+ <script type="module" src="../../components/card/card.js"></script>
24
+ <script type="module" src="../../components/chart/chart.js"></script>
25
+ <script type="module" src="../../components/chart-legend/chart-legend.js"></script>
26
+ <script type="module" src="../../components/icon/icon.js"></script>
27
+ <script type="module" src="../../components/row/row.js"></script>
28
+ <script type="module" src="../../components/select/select.js"></script>
29
+
30
+ <style>
31
+ :where(html, body) { margin: 0; min-height: 100vh; background: var(--md-sys-color-neutral-background); color: var(--md-sys-color-neutral-on-surface); font-family: var(--a-font-family); }
32
+ main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
33
+ </style>
34
+ </head>
35
+ <body>
36
+
37
+ <main id="demo-root">
38
+ <p>Loading examples…</p>
39
+ </main>
40
+
41
+ <script type="module">
42
+ const root = document.getElementById('demo-root');
43
+ try {
44
+ const res = await fetch('./new-enrollments.examples.html');
45
+ if (!res.ok) throw new Error(`fetch failed (${res.status})`);
46
+ root.innerHTML = await res.text();
47
+ } catch (err) {
48
+ root.innerHTML = `<p style="color:var(--a-danger-strong);">Failed to load new-enrollments.examples.html — ${err.message}</p>`;
49
+ console.error('[new-enrollments.html]', err);
50
+ }
51
+ </script>
52
+
53
+ </body>
54
+ </html>
@@ -0,0 +1,168 @@
1
+ <header>
2
+ <div>
3
+ <h1>Table in Card</h1>
4
+ </div>
5
+ <div data-tags>
6
+ <code>card-ui</code> <code>table-toolbar-ui</code> <code>table-ui</code> <code>table-footer-ui</code>
7
+ </div>
8
+ <p>Composition pattern: the full table structural system (gh#1807, spec-table-structural-system) — a card header (title + primary action), a title-less toolbar row (count + search + filter/sort/columns), a bleed table, and a footer (range label + pager). Figma reference: "Metabolic Tests" (node 134-4348).</p>
9
+ </header>
10
+
11
+ <section data-section data-property="when-to-use">
12
+ <h2 variant="section">When to use</h2>
13
+ <p data-note>
14
+ The canonical shape for any card-framed data grid that needs a pager — a
15
+ standalone <code>table-ui</code> (no card, no toolbar, no footer) is
16
+ still correct for a small, unpaginated list; reach for this pattern the
17
+ moment the table earns a toolbar (search/filter/sort/columns) and/or a
18
+ footer (a page range). The title lives in the card header, never
19
+ duplicated on the toolbar — see table-toolbar.yaml's title-collision rule.
20
+ </p>
21
+ </section>
22
+
23
+ <section data-section data-property="anatomy">
24
+ <h2 variant="section">Anatomy</h2>
25
+ <code-ui language="html">&lt;card-ui&gt;
26
+ &lt;header&gt;
27
+ &lt;span slot="heading"&gt;Metabolic Tests&lt;/span&gt;
28
+ &lt;button-ui slot="action" variant="primary" size="sm"&gt;New test&lt;/button-ui&gt;
29
+ &lt;/header&gt;
30
+ &lt;section&gt;
31
+ &lt;!-- title-less: the card header already owns the title --&gt;
32
+ &lt;table-toolbar-ui for="tests" count="128"&gt;&lt;/table-toolbar-ui&gt;
33
+ &lt;/section&gt;
34
+ &lt;section bleed&gt;
35
+ &lt;table-ui id="tests" sortable&gt;&lt;/table-ui&gt;
36
+ &lt;/section&gt;
37
+ &lt;footer divider&gt;
38
+ &lt;table-footer-ui for="tests" page="1" page-size="10" range-total="128"&gt;&lt;/table-footer-ui&gt;
39
+ &lt;/footer&gt;
40
+ &lt;/card-ui&gt;</code-ui>
41
+ </section>
42
+
43
+ <section data-section data-property="server-mode">
44
+ <h2 variant="section">Server-mode (Shape 1)</h2>
45
+ <p data-note>The server returns the page's rows plus the total row count (gh#1754 Shape 1). <code>table-ui[paginate="0"]</code> holds only the current page's rows; <code>table-footer-ui[range-total]</code> carries the server's total. The reference anatomy: leading status-dot column, sortable headers, tag chips, a status badge, and a kebab row-actions column — all via existing table-ui cell types and <code>render</code> cells.</p>
46
+ <div data-artifact-container>
47
+ <div data-artifact-item data-artifact-label="Metabolic Tests — server-mode, canonical composition"
48
+ data-chunk="table-in-card-server-mode"
49
+ data-chunk-kind="block"
50
+ data-chunk-domain="data"
51
+ data-chunk-description="Metabolic Tests card — card header with title and New test action, title-less toolbar with count/search/filter/sort/columns, bleed table with status-dot/sortable-name/patient/date/status-badge/tag-chips/kebab-actions columns, and a footer with range label and pager (server-mode, gh#1754 Shape 1)."
52
+ data-chunk-keywords="table card toolbar footer pagination metabolic tests status dot badge tags kebab actions server mode data grid healthcare">
53
+ <card-ui style="max-width:720px;">
54
+ <header>
55
+ <span slot="heading">Metabolic Tests</span>
56
+ <button-ui slot="action" variant="primary" size="sm" icon="plus" text="New test"></button-ui>
57
+ </header>
58
+ <section>
59
+ <table-toolbar-ui for="pat-tests" count="128" placeholder="Search tests…"></table-toolbar-ui>
60
+ </section>
61
+ <section bleed>
62
+ <table-ui id="pat-tests" sortable></table-ui>
63
+ </section>
64
+ <footer divider>
65
+ <table-footer-ui for="pat-tests" page="1" page-size="10" range-total="128"></table-footer-ui>
66
+ </footer>
67
+ </card-ui>
68
+ </div>
69
+ </div>
70
+ </section>
71
+
72
+ <section data-section data-property="client-mode">
73
+ <h2 variant="section">Client-mode (paginate + no-pager)</h2>
74
+ <p data-note><code>table-ui[paginate]</code> slices the FULL dataset client-side; <code>[no-pager]</code> hides the table's own internal pagination bar so the bound footer's composed pager is the only one visible (ADR-0080's anti-doubled-pager rule — explicit, never automatic).</p>
75
+ <div data-artifact-container>
76
+ <div data-artifact-item data-artifact-label="Metabolic Tests — client-mode, paginate + no-pager"
77
+ data-chunk="table-in-card-client-mode"
78
+ data-chunk-kind="block"
79
+ data-chunk-domain="data"
80
+ data-chunk-description="Metabolic Tests card in client-mode — table-ui paginate=6 no-pager slices a full in-browser dataset, with table-footer-ui as the sole visible pager."
81
+ data-chunk-keywords="table card toolbar footer pagination client mode paginate no-pager metabolic tests data grid healthcare">
82
+ <card-ui style="max-width:720px;">
83
+ <header>
84
+ <span slot="heading">Metabolic Tests</span>
85
+ </header>
86
+ <section>
87
+ <table-toolbar-ui for="pat-tests-client" count="18"></table-toolbar-ui>
88
+ </section>
89
+ <section bleed>
90
+ <table-ui id="pat-tests-client" paginate="6" no-pager sortable></table-ui>
91
+ </section>
92
+ <footer divider>
93
+ <table-footer-ui for="pat-tests-client"></table-footer-ui>
94
+ </footer>
95
+ </card-ui>
96
+ </div>
97
+ </div>
98
+ </section>
99
+
100
+ <section data-section data-property="empty">
101
+ <h2 variant="section">Empty</h2>
102
+ <p data-note>A confirmed-empty result set (explicit <code>range-total="0"</code>) — the footer's <code>empty</code> slot renders in the range label's own position; the pager stays hidden.</p>
103
+ <div data-artifact-container>
104
+ <div data-artifact-item data-artifact-label="Metabolic Tests — empty result set"
105
+ data-chunk="table-in-card-empty"
106
+ data-chunk-kind="block"
107
+ data-chunk-domain="data"
108
+ data-chunk-description="Metabolic Tests card with zero matching rows — table-ui empty overlay in the bleed section, table-footer-ui range-total=0 with a slotted 'no tests match your filters' message."
109
+ data-chunk-keywords="table card toolbar footer pagination empty no results filters metabolic tests data grid healthcare">
110
+ <card-ui style="max-width:720px;">
111
+ <header>
112
+ <span slot="heading">Metabolic Tests</span>
113
+ </header>
114
+ <section>
115
+ <table-toolbar-ui for="pat-tests-empty" count="0"></table-toolbar-ui>
116
+ </section>
117
+ <section bleed>
118
+ <table-ui id="pat-tests-empty"></table-ui>
119
+ </section>
120
+ <footer divider>
121
+ <table-footer-ui for="pat-tests-empty" range-total="0">
122
+ <span slot="empty">No tests match your filters</span>
123
+ </table-footer-ui>
124
+ </footer>
125
+ </card-ui>
126
+ </div>
127
+ </div>
128
+ </section>
129
+
130
+ <section data-section data-property="loading">
131
+ <h2 variant="section">Loading</h2>
132
+ <p data-note>While data fetches, <code>table-ui[loading]</code> renders its own skeleton rows and the footer's <code>range-total</code> stays absent — the footer contributes silence, not a spinner (REQ-S-001).</p>
133
+ <div data-artifact-container>
134
+ <div data-artifact-item data-artifact-label="Metabolic Tests — loading"
135
+ data-chunk="table-in-card-loading"
136
+ data-chunk-kind="block"
137
+ data-chunk-domain="data"
138
+ data-chunk-description="Metabolic Tests card mid-fetch — table-ui loading skeleton rows, table-footer-ui with no range-total set (both the range label and pager stay hidden)."
139
+ data-chunk-keywords="table card toolbar footer pagination loading skeleton fetch metabolic tests data grid healthcare">
140
+ <card-ui style="max-width:720px;">
141
+ <header>
142
+ <span slot="heading">Metabolic Tests</span>
143
+ </header>
144
+ <section>
145
+ <table-toolbar-ui for="pat-tests-loading"></table-toolbar-ui>
146
+ </section>
147
+ <section bleed>
148
+ <table-ui id="pat-tests-loading" loading></table-ui>
149
+ </section>
150
+ <footer divider>
151
+ <table-footer-ui for="pat-tests-loading"></table-footer-ui>
152
+ </footer>
153
+ </card-ui>
154
+ </div>
155
+ </div>
156
+ </section>
157
+
158
+ <section data-section data-property="rules">
159
+ <h2 variant="section">Rules</h2>
160
+ <ul>
161
+ <li>The title lives in the card header (<code>[slot="heading"]</code>) — the toolbar carries no <code>[text]</code>. A toolbar with <code>[text]</code> sharing a card with a heading-slotted header doubles the title row (table-toolbar.yaml's title-collision rule, gh#1807 REQ-C-004); a title-less toolbar under a card header is the canonical shape, not doubled chrome.</li>
162
+ <li>The toolbar's <code>[count]</code> badge and the footer's range label coexist — they answer different questions (collection summary vs. page-window position) and neither replaces the other.</li>
163
+ <li>In a composition that includes a <code>table-footer-ui</code>, leave the toolbar's <code>[range-start]</code>/<code>[range-end]</code>/<code>[range-total]</code> unset — the range summary's home is the footer.</li>
164
+ <li>Client-mode (<code>[paginate]</code>) needs <code>[no-pager]</code> on the table so the internal bar and the footer's composed pager don't both render.</li>
165
+ <li>The <code>&lt;footer divider&gt;</code> — card-ui's own footer grammar supplies inset, bottom-pin, and the divider line; the bleed section's zero trailing margin puts the divider flush under the last row.</li>
166
+ <li>Status-dot, tag chips, and the kebab row-actions column are all <code>render</code>-function columns over existing primitives (<code>swatch-ui</code>, <code>tag-ui</code>, <code>menu-ui</code>/<code>menu-item-ui</code>) — no new table-ui cell type.</li>
167
+ </ul>
168
+ </section>
@@ -0,0 +1,139 @@
1
+ /** Table in Card pattern — render-cell columns + sample data (gh#1807) */
2
+
3
+ // A semantic-token dot (swatch-ui) per status — no new cell type, no raw
4
+ // colors: --swatch-color always resolves to a real --a-* family token.
5
+ const STATUS_DOT_TOKEN = {
6
+ Complete: 'var(--a-success)',
7
+ Pending: 'var(--a-warning)',
8
+ Failed: 'var(--a-danger)',
9
+ };
10
+
11
+ const STATUS_BADGE_VARIANT = { Complete: 'success', Pending: 'warning', Failed: 'danger' };
12
+
13
+ function renderStatusDot(value) {
14
+ const dot = document.createElement('swatch-ui');
15
+ dot.setAttribute('shape', 'dot');
16
+ dot.setAttribute('size', 'sm');
17
+ dot.setAttribute('aria-hidden', 'true');
18
+ dot.style.setProperty('--swatch-color', STATUS_DOT_TOKEN[value] || 'var(--a-fg-muted)');
19
+ return dot;
20
+ }
21
+
22
+ function renderTags(value) {
23
+ const wrap = document.createElement('row-ui');
24
+ wrap.setAttribute('gap', '1');
25
+ wrap.setAttribute('wrap', '');
26
+ for (const t of value || []) {
27
+ const tag = document.createElement('tag-ui');
28
+ tag.setAttribute('text', t);
29
+ tag.setAttribute('size', 'sm');
30
+ wrap.appendChild(tag);
31
+ }
32
+ return wrap;
33
+ }
34
+
35
+ function renderKebabActions(_value, row) {
36
+ const menu = document.createElement('menu-ui');
37
+ const trigger = document.createElement('button-ui');
38
+ trigger.setAttribute('slot', 'trigger');
39
+ trigger.setAttribute('icon', 'dots-three-vertical');
40
+ trigger.setAttribute('variant', 'ghost');
41
+ trigger.setAttribute('size', 'sm');
42
+ trigger.setAttribute('aria-label', `Actions for ${row.name}`);
43
+ menu.appendChild(trigger);
44
+ for (const [text, icon] of [['View', 'eye'], ['Edit', 'pencil-simple'], ['Archive', 'archive']]) {
45
+ const item = document.createElement('menu-item-ui');
46
+ item.setAttribute('text', text);
47
+ item.setAttribute('icon', icon);
48
+ menu.appendChild(item);
49
+ }
50
+ return menu;
51
+ }
52
+
53
+ const COLUMNS = [
54
+ { key: 'status', label: '', render: (v) => renderStatusDot(v), width: 32, sortable: false },
55
+ { key: 'name', label: 'Test', sortable: true },
56
+ { key: 'patient', label: 'Patient', sortable: true },
57
+ { key: 'date', label: 'Date', type: 'date', sortable: true },
58
+ { key: 'status', label: 'Status', type: 'badge', meta: { variants: STATUS_BADGE_VARIANT } },
59
+ { key: 'tags', label: 'Tags', render: (v) => renderTags(v) },
60
+ // Render-only column, no backing data field — the idiom table-toolbar-ui's
61
+ // own docs establish for columns like this (`_cancel`/`_download`/`_replay`).
62
+ { key: '_actions', label: '', render: renderKebabActions, width: 48, sortable: false },
63
+ ];
64
+
65
+ const NAMES = ['CMP', 'BMP', 'Lipid Panel', 'A1C', 'TSH', 'CBC', 'Vitamin D', 'Ferritin', 'PT/INR', 'Liver Panel', 'Renal Panel', 'Electrolyte Panel'];
66
+ const PATIENTS = ['A. Chen', 'J. Park', 'L. Kim', 'M. Rivera', 'S. Torres', 'P. Nair', 'O. Haddad', 'T. Baker'];
67
+ const STATUSES = ['Complete', 'Pending', 'Failed'];
68
+ const TAG_SETS = [['Routine'], ['STAT'], ['Fasting', 'Routine'], ['Repeat'], []];
69
+
70
+ function makeRows(count) {
71
+ return Array.from({ length: count }, (_, i) => ({
72
+ name: NAMES[i % NAMES.length],
73
+ patient: PATIENTS[i % PATIENTS.length],
74
+ date: `2026-08-${String((i % 27) + 1).padStart(2, '0')}`,
75
+ status: STATUSES[i % STATUSES.length],
76
+ tags: TAG_SETS[i % TAG_SETS.length],
77
+ }));
78
+ }
79
+
80
+ export default function setup(router) {
81
+ // Server-mode (Shape 1) — the table only ever holds the CURRENT page's
82
+ // rows; the total (128) lives on the footer's own range-total.
83
+ const server = router.querySelector('#pat-tests');
84
+ const serverFooter = router.querySelector('table-footer-ui[for="pat-tests"]');
85
+ if (server) {
86
+ server.columns = COLUMNS;
87
+ server.data = makeRows(10);
88
+
89
+ // REQ-D-002's fetch loop (CodeRabbit review, PR #1811): the footer
90
+ // never optimistically advances its own [page] in server-mode — this
91
+ // handler's write-back after the "fetch" resolves is the state
92
+ // authority. `makeRows`'s own generator is offset-seeded per page so
93
+ // each page's fixture rows visibly differ (deterministic, not a real
94
+ // fetch, but the same swap-then-confirm shape a real one would have).
95
+ if (serverFooter) {
96
+ serverFooter.addEventListener('page-change', (e) => {
97
+ const page = e.detail?.page;
98
+ server.data = makeRows(10).map((row, i) => ({
99
+ ...row,
100
+ name: NAMES[(i + page * 3) % NAMES.length],
101
+ patient: PATIENTS[(i + page * 5) % PATIENTS.length],
102
+ }));
103
+ serverFooter.setAttribute('page', String(page));
104
+ });
105
+ }
106
+ }
107
+
108
+ // Client-mode — the FULL in-browser dataset; table-ui[paginate] slices it.
109
+ const client = router.querySelector('#pat-tests-client');
110
+ if (client) {
111
+ client.columns = COLUMNS;
112
+ client.data = makeRows(18);
113
+
114
+ // REQ-W-006's named staleness edge (see table-footer.examples.js for
115
+ // the full rationale) — the static markup's zero-attribute footer
116
+ // connects before this setup() runs; recreate it now that real data
117
+ // is in place so its bind-time derivation reads the populated table.
118
+ const oldFooter = router.querySelector('table-footer-ui[for="pat-tests-client"]');
119
+ if (oldFooter) {
120
+ const freshFooter = document.createElement('table-footer-ui');
121
+ freshFooter.setAttribute('for', 'pat-tests-client');
122
+ oldFooter.replaceWith(freshFooter);
123
+ }
124
+ }
125
+
126
+ // Empty — columns set, zero rows; table-ui renders its own empty overlay.
127
+ const empty = router.querySelector('#pat-tests-empty');
128
+ if (empty) {
129
+ empty.columns = COLUMNS;
130
+ empty.data = [];
131
+ }
132
+
133
+ // Loading — columns set so the header renders; loading=true shows
134
+ // skeleton rows regardless of .data.
135
+ const loading = router.querySelector('#pat-tests-loading');
136
+ if (loading) {
137
+ loading.columns = COLUMNS;
138
+ }
139
+ }