@adia-ai/web-components 0.8.43 → 0.8.44

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 (93) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/components/avatar/avatar.class.js +12 -1
  3. package/components/button/button.d.ts +4 -0
  4. package/components/chart/chart.a2ui.json +26 -0
  5. package/components/chart/chart.class.js +1304 -222
  6. package/components/chart/chart.css +151 -1
  7. package/components/chart/chart.d.ts +6 -0
  8. package/components/chart/chart.yaml +57 -0
  9. package/components/chart-legend/chart-legend.a2ui.json +17 -2
  10. package/components/chart-legend/chart-legend.class.js +69 -4
  11. package/components/chart-legend/chart-legend.css +65 -1
  12. package/components/chart-legend/chart-legend.d.ts +4 -2
  13. package/components/chart-legend/chart-legend.examples.md +4 -4
  14. package/components/chart-legend/chart-legend.yaml +43 -3
  15. package/components/combobox/combobox.a2ui.json +3 -0
  16. package/components/combobox/combobox.class.js +9 -8
  17. package/components/combobox/combobox.css +25 -4
  18. package/components/combobox/combobox.yaml +6 -0
  19. package/components/command/command.css +17 -1
  20. package/components/context-menu/context-menu.d.ts +2 -0
  21. package/components/heatmap/heatmap.a2ui.json +5 -0
  22. package/components/heatmap/heatmap.d.ts +2 -0
  23. package/components/heatmap/heatmap.yaml +8 -0
  24. package/components/input/input.a2ui.json +2 -2
  25. package/components/input/input.class.js +12 -7
  26. package/components/input/input.yaml +6 -5
  27. package/components/integration-card/integration-card.d.ts +2 -0
  28. package/components/noodles/noodles.a2ui.json +0 -15
  29. package/components/noodles/noodles.yaml +0 -12
  30. package/components/number-format/number-format.class.js +10 -0
  31. package/components/pagination/pagination.class.js +88 -4
  32. package/components/popover/popover.d.ts +2 -0
  33. package/components/progress/progress.class.js +11 -6
  34. package/components/range/range.class.js +9 -1
  35. package/components/select/select.a2ui.json +5 -1
  36. package/components/select/select.class.js +14 -14
  37. package/components/select/select.css +14 -1
  38. package/components/select/select.yaml +7 -1
  39. package/components/stream/stream.d.ts +2 -0
  40. package/components/table/table.a2ui.json +6 -1
  41. package/components/table/table.class.js +18 -1
  42. package/components/table/table.css +31 -6
  43. package/components/table/table.d.ts +3 -1
  44. package/components/table/table.examples.md +2 -2
  45. package/components/table/table.yaml +9 -1
  46. package/components/table-toolbar/table-toolbar.a2ui.json +28 -4
  47. package/components/table-toolbar/table-toolbar.class.js +346 -11
  48. package/components/table-toolbar/table-toolbar.css +265 -0
  49. package/components/table-toolbar/table-toolbar.d.ts +5 -1
  50. package/components/table-toolbar/table-toolbar.yaml +130 -15
  51. package/components/tag/tag.class.js +13 -1
  52. package/components/text/text.a2ui.json +3 -3
  53. package/components/text/text.d.ts +5 -3
  54. package/components/text/text.yaml +3 -3
  55. package/components/textarea/textarea.class.js +9 -1
  56. package/components/tour/tour.d.ts +10 -0
  57. package/core/a11y.d.ts +22 -0
  58. package/core/a11y.js +64 -0
  59. package/core/anchor.js +9 -5
  60. package/core/element.js +1 -1
  61. package/custom-elements.json +73 -29
  62. package/dist/host.min.css +1 -1
  63. package/dist/host.sheet.js +1 -1
  64. package/dist/theme-provider.min.js +1 -1
  65. package/dist/web-components.min.css +1 -1
  66. package/dist/web-components.min.js +119 -119
  67. package/dist/web-components.sheet.js +1 -1
  68. package/package.json +1 -1
  69. package/patterns/access-requests/access-requests.examples.html +1 -1
  70. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
  71. package/patterns/agent-cost/agent-cost.examples.html +6 -6
  72. package/patterns/agent-memory/agent-memory.examples.html +3 -3
  73. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
  74. package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
  75. package/patterns/approvals/approvals.examples.html +6 -6
  76. package/patterns/audit-log/audit-log.examples.html +1 -1
  77. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
  78. package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
  79. package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
  80. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
  81. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  82. package/patterns/diff-review/diff-review.examples.html +6 -6
  83. package/patterns/export-flow/export-flow.examples.html +2 -2
  84. package/patterns/filter-bar/filter-bar.examples.html +2 -2
  85. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
  86. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  87. package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
  88. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  89. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  90. package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
  91. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
  92. package/patterns/search-discovery/search-discovery.examples.html +2 -2
  93. package/styles/type/roles.css +29 -9
@@ -130,7 +130,25 @@
130
130
  NOTE: size="sm|md|lg" used to cap max-width (25/50/100%) which was a bug:
131
131
  a chart in a 160px grid cell would be clamped to 40px wide. Size now
132
132
  only tunes typography/padding/density inside #dims(); layout is the
133
- responsibility of the consumer. */
133
+ responsibility of the consumer.
134
+
135
+ ADR-0074 (Chart 2.0 ratio grammar, gh#1624 plan step 1) — [ratio]
136
+ PINS one of three named buckets (3:2|1:1|2:3), overriding whatever
137
+ height the box would otherwise get; unset auto-snaps by classifying
138
+ the chart's own live rendered aspect against two midpoint boundaries.
139
+ Because this primitive derives height in JS from a measured
140
+ clientWidth/clientHeight rather than from CSS layout (see the note
141
+ above — the SAME reason `size` stopped clamping max-width), the
142
+ ratified "CSS container query" mechanism is realized inside
143
+ chart.class.js's #dims() (the same measurement pass the pre-existing
144
+ ResizeObserver already drives), not as a literal `@container` rule
145
+ here: `container-type: size` (required to query aspect-ratio, not
146
+ just inline-size like the legend query below) would force block-size
147
+ containment on :scope, collapsing any chart with no explicit height
148
+ set today — the common case across this file's own demo gallery.
149
+ Same ratified grammar/snap-boundary/override contract either way;
150
+ `#dims()` reflects the resolved bucket onto the host as
151
+ `data-ratio-resolved` for later waves' per-type CSS to key off. */
134
152
 
135
153
  /* Debug resize handle */
136
154
  :scope[resize] {
@@ -158,6 +176,16 @@
158
176
  fill: none;
159
177
  }
160
178
 
179
+ /* REQ-R-007 (Chart 2.0 color-role plan) — "denser 1:1/2:3 layouts need a
180
+ quieter gridline than spacious 3:2": both buckets already thin the
181
+ gridline COUNT (#gridAndAxes' thinGrid, chart.class.js); this dims the
182
+ remaining lines' CONTRAST tier a step further rather than minting a
183
+ second gridline token. */
184
+ :scope[data-ratio-resolved="1:1"] [data-chart-grid],
185
+ :scope[data-ratio-resolved="2:3"] [data-chart-grid] {
186
+ opacity: 0.72;
187
+ }
188
+
161
189
  /* ── Y-axis labels ── */
162
190
 
163
191
  [data-y-label] {
@@ -387,6 +415,51 @@
387
415
  stroke: none;
388
416
  }
389
417
 
418
+ /* REQ-R-003 (pie, 1:1) — "legend as a ring of labels around the pie":
419
+ one label per slice, positioned OUTSIDE the ring (chart.class.js sizes
420
+ the pie itself smaller at 1:1 to leave this room). Sits on the plain
421
+ chart background, not on a colored slice fill, so it uses the ordinary
422
+ on-surface label token rather than treemap/donut's on-color token. */
423
+ [data-ring-label] {
424
+ fill: var(--chart-label);
425
+ font-weight: var(--a-weight-medium);
426
+ pointer-events: none;
427
+ }
428
+
429
+ /* REQ-R-003 (pie/donut, 1:1) — the ring of labels (pie) / maximized ring
430
+ + center-stat (donut) already function as the legend at 1:1, so the
431
+ always-visible inline `[data-legend]` block below the chart would
432
+ duplicate that information and compete for the same square footprint;
433
+ suppress it at 1:1 only. Tooltip-on-hover and (donut) the center-stat
434
+ text both stay unaffected either way.
435
+
436
+ REQ-R-003 (radial-bar, 1:1 "legend as ring") — same rationale, one
437
+ ring's worth of `[data-ring-label]` text already IS the legend at 1:1
438
+ (chart.class.js).
439
+
440
+ REQ-R-003 (multi-line, 3:2 "all series labeled inline at line end (no
441
+ legend needed)") — same rationale, inverted axis: multi-line's own
442
+ inline end-labels ([data-series-end-label]) only render at 3:2, so
443
+ THAT'S the ratio whose internal legend is redundant and suppressed;
444
+ 1:1/2:3 drop the end-labels and rely on the ordinary legend instead
445
+ (unsuppressed there), matching the matrix's own progressive
446
+ de-emphasis for this type. */
447
+ :scope[type="pie"][data-ratio-resolved="1:1"] [data-legend],
448
+ :scope[type="donut"][data-ratio-resolved="1:1"] [data-legend],
449
+ :scope[type="radial-bar"][data-ratio-resolved="1:1"] [data-legend],
450
+ :scope[type="multi-line"][data-ratio-resolved="3:2"] [data-legend] {
451
+ display: none;
452
+ }
453
+
454
+ /* REQ-R-003 (multi-line, 3:2) — the inline end-of-line series label the
455
+ suppression rule above pairs with. Colored per-series via the same
456
+ `--color-{key}` fallback-chain hook every other series-keyed element
457
+ in this file uses (chart.class.js's `#seriesFill`). */
458
+ [data-series-end-label] {
459
+ font-weight: var(--a-weight-medium);
460
+ pointer-events: none;
461
+ }
462
+
390
463
  /* ── Donut center text ── */
391
464
 
392
465
  [data-donut-total] {
@@ -553,6 +626,15 @@
553
626
  fill: var(--chart-label);
554
627
  }
555
628
 
629
+ /* REQ-R-003 (funnel, 1:1) — "stage labels inline": placed inside the
630
+ colored trapezoid body instead of the plain chart background, so it
631
+ needs the on-color label token (same convention treemap's own
632
+ in-tile label already uses) rather than the ordinary chart-fg. */
633
+ [data-funnel-label-inline] {
634
+ fill: var(--a-canvas-0);
635
+ font-weight: var(--a-weight-semibold);
636
+ }
637
+
556
638
  /* ── Treemap ──
557
639
  Flat tiles — each rect filled by the data palette. Labels +
558
640
  values appear only on tiles large enough to fit them (size
@@ -625,6 +707,22 @@
625
707
  stroke-width: 2;
626
708
  }
627
709
 
710
+ /* REQ-R-003 (composed, 2:3) — the sparkline-inset backing + line. Quiet
711
+ container-low surface (REQ-R-007's own "quiet surfaces" role) so the
712
+ inset reads as a distinct panel without competing with the primary
713
+ bar series for visual weight. */
714
+ [data-composed-inset-bg] {
715
+ fill: var(--md-sys-color-neutral-container-low);
716
+ }
717
+ [data-composed-inset-line] {
718
+ fill: none;
719
+ stroke-width: calc(var(--chart-line-width) - 0.5);
720
+ }
721
+ [data-composed-inset-label] {
722
+ fill: var(--chart-label);
723
+ font-family: var(--chart-font);
724
+ }
725
+
628
726
  /* ── Sparkline container ──
629
727
  `height: 100%` fills an explicit parent height (e.g. when the
630
728
  containing section is a flex-grow item inside card-ui). Falls
@@ -640,6 +738,33 @@
640
738
  :scope[type="segments"] svg {
641
739
  height: 1.5rem;
642
740
  }
741
+
742
+ /* REQ-R-003 (segments, 2:3) — "tall vertical stacked bars": the
743
+ horizontal default's fixed-HEIGHT/fluid-WIDTH strip inverts to a
744
+ fixed-WIDTH/fluid-HEIGHT column — same technique every other type's
745
+ own base `svg { width: 100%; height: auto }` rule already uses
746
+ (a definite dimension on one axis, `auto` on the other so the SVG's
747
+ own intrinsic viewBox aspect ratio solves the rest), just with the
748
+ definite axis swapped. `height: 100%` was tried first and produces a
749
+ wrong render: chart-ui has no definite height of its own in ordinary
750
+ use for `height: 100%` to resolve against, and the actual resolved
751
+ value in testing came from an unrelated ancestor percentage chain —
752
+ visually correct-looking proportions by coincidence, but the wrong
753
+ mechanism (caught by a real browser probe, not the Vitest DOM
754
+ assertions above, which never render CSS at all). */
755
+ :scope[type="segments"][data-ratio-resolved="2:3"] svg {
756
+ width: 2rem;
757
+ height: auto;
758
+ margin-inline: auto;
759
+ }
760
+
761
+ /* REQ-R-003 (segments, 2:3) — "legend stacked": one item per line
762
+ instead of the default wrapped row, matching the vertical strip's own
763
+ tall/narrow footprint. */
764
+ :scope[type="segments"][data-ratio-resolved="2:3"] [data-legend] {
765
+ flex-direction: column;
766
+ align-items: flex-start;
767
+ }
643
768
  [data-sparkline] {
644
769
  display: block;
645
770
  width: 100%;
@@ -694,6 +819,31 @@
694
819
  [data-hit] { cursor: default; }
695
820
  }
696
821
 
822
+ /* ─────────────────────────────────────────────────────────────────
823
+ REQ-R-001/R-007 (Chart 2.0 full-bleed anatomy + color-role plan) —
824
+ full-bleed background depth.
825
+
826
+ Deliberately OUTSIDE `@scope (chart-ui)`: the rule needs to match an
827
+ ANCESTOR (`card-ui[padding="none"]`) of the scoping root, which a
828
+ selector inside the scope block can't express against :scope itself.
829
+ card-ui's own `padding="none"` already zeroes its inset (`--card-inset:
830
+ 0`, card.css) — a chart filling that slot at 100%×100% was already
831
+ edge-to-edge (REQ-R-001's two pre-existing mechanisms), but with a fully
832
+ transparent background it read as "no background" rather than a
833
+ deliberate flush surface. This tints it with a quiet container-low role
834
+ (ADR-0064/0073's ladder) — REQ-P-002's own opt-in requirement is what
835
+ the `card-ui[padding="none"]` ancestor gate enforces: a chart in a
836
+ normal-padding card never matches this selector, so its rendering is
837
+ unchanged (additive, not a new default). --md-sys-color-neutral-
838
+ container-low was chosen over -surface-dim (the SPEC's other named
839
+ option) for a closer value-step to the ambient canvas so the categorical
840
+ series data stays the visual foreground.
841
+ ───────────────────────────────────────────────────────────────── */
842
+ card-ui[padding="none"] chart-ui {
843
+ background: var(--md-sys-color-neutral-container-low);
844
+ border-radius: inherit;
845
+ }
846
+
697
847
  /* ─────────────────────────────────────────────────────────────────
698
848
  Chart tooltip (top-layer, outside @scope)
699
849
 
@@ -38,6 +38,8 @@ export class UIChart extends UIElement {
38
38
  noGrid: boolean;
39
39
  /** Hide value labels. Canonical spelling (ADR-0063, gh#1563) — the deprecated `hideValues` dual-read shim was cut in 0.8.43 (gh#1617). */
40
40
  noValues: boolean;
41
+ /** Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — this attribute lands the grammar + snap plumbing only; per-type studied renderings per bucket are later waves (gh#1624 plan steps 2-4). */
42
+ ratio: '3:2' | '1:1' | '2:3';
41
43
  /** Multi-line only. Names one series key (matching a `y` key) to render at full strength — area fill + full-opacity line. Every other series drops its area fill and renders line-only at --chart-deemphasized-opacity. Empty (default) applies full treatment to every series, unchanged from pre-gh#561 behavior. */
42
44
  seriesEmphasis: string;
43
45
  /** Chart size */
@@ -48,6 +50,10 @@ export class UIChart extends UIElement {
48
50
  x: string;
49
51
  /** Y-axis key(s), comma-separated for multi-series */
50
52
  y: string;
53
+ /** Pins the value-axis domain ceiling for cartesian types (bar, line, area, scatter, multi-line, stacked-bar, grouped-bar, composed). Unset (default, null) keeps today's auto-scale ceiling (the data max). Pairs with `yMin` to pin the full domain; either may be set alone. N/A for radial/part-to-whole types — see `yMin` (gh#1690). A datum outside the pinned domain is CLAMPED to the nearest edge for rendering (the value label/tooltip still shows the real value) rather than escaping the plot box. */
54
+ yMax: number | null;
55
+ /** Pins the value-axis domain floor for cartesian types (bar, line, area, scatter, multi-line, stacked-bar, grouped-bar, composed). Unset (default, null) already floors a positive series at 0 (the pre-1690 behavior, unchanged) — a migrating d3 consumer's own "pin the floor to 0" habit is the built-in default, not something to configure. Pin `y-min` explicitly for three OTHER cases: (1) zooming a narrow window — e.g. `y-min="0.9" y-max="1"` magnifies a percent series clustered near 92-95%, which reads flatter against the full auto-scaled [0,1] domain; (2) a mixed-sign series, where the unpinned floor never goes below 0 and negative values clip at the axis; (3) aligning two charts to one shared scale for a fair comparison. N/A for radial/part-to-whole types (pie, donut, radar, radial-bar, gauge, funnel, treemap, sankey, segments, sparkline) — those never read a shared linear value axis, so this prop is a no-op there (gh#1690). A datum outside the pinned domain is CLAMPED to the nearest edge for rendering (the value label/tooltip still shows the real value) rather than escaping the plot box. */
56
+ yMin: number | null;
51
57
 
52
58
  addEventListener(type: 'chart-hover', listener: (ev: ChartHoverEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
53
59
  addEventListener(type: 'chart-leave', listener: (ev: ChartLeaveEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
@@ -96,6 +96,25 @@ props:
96
96
  - sm
97
97
  - md
98
98
  - lg
99
+ ratio:
100
+ description: >-
101
+ Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps
102
+ to the nearest of the three studied ratios by comparing the chart's
103
+ own live box aspect against two midpoint boundaries (5:4 between
104
+ 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved
105
+ bucket reflects onto the host as `data-ratio-resolved`. Setting
106
+ `ratio` explicitly pins that bucket regardless of the box's actual
107
+ aspect, overriding the auto-snap. Orthogonal to `size` (overall
108
+ scale) and `type`/`color` (ADR-0064's family axis) — this attribute
109
+ lands the grammar + snap plumbing only; per-type studied renderings
110
+ per bucket are later waves (gh#1624 plan steps 2-4).
111
+ type: string
112
+ default: ""
113
+ enum:
114
+ - "3:2"
115
+ - "1:1"
116
+ - "2:3"
117
+ reflect: true
99
118
  smooth:
100
119
  description: Line smoothing factor
101
120
  type: number
@@ -138,6 +157,44 @@ props:
138
157
  type: boolean
139
158
  default: false
140
159
  reflect: true
160
+ yMin:
161
+ description: >-
162
+ Pins the value-axis domain floor for cartesian types (bar, line,
163
+ area, scatter, multi-line, stacked-bar, grouped-bar, composed).
164
+ Unset (default, null) already floors a positive series at 0 (the
165
+ pre-1690 behavior, unchanged) — a migrating d3 consumer's own
166
+ "pin the floor to 0" habit is the built-in default, not something
167
+ to configure. Pin `y-min` explicitly for three OTHER cases: (1)
168
+ zooming a narrow window — e.g. `y-min="0.9" y-max="1"` magnifies a
169
+ percent series clustered near 92-95%, which reads flatter against
170
+ the full auto-scaled [0,1] domain; (2) a mixed-sign series, where
171
+ the unpinned floor never goes below 0 and negative values clip at
172
+ the axis; (3) aligning two charts to one shared scale for a fair
173
+ comparison. N/A for radial/part-to-whole types (pie, donut, radar,
174
+ radial-bar, gauge, funnel, treemap, sankey, segments, sparkline) —
175
+ those never read a shared linear value axis, so this prop is a
176
+ no-op there (gh#1690).
177
+ A datum outside the pinned domain is CLAMPED to the nearest edge
178
+ for rendering (the value label/tooltip still shows the real value)
179
+ rather than escaping the plot box.
180
+ type: number
181
+ default: null
182
+ attribute: y-min
183
+ reflect: true
184
+ yMax:
185
+ description: >-
186
+ Pins the value-axis domain ceiling for cartesian types (bar, line,
187
+ area, scatter, multi-line, stacked-bar, grouped-bar, composed).
188
+ Unset (default, null) keeps today's auto-scale ceiling (the data
189
+ max). Pairs with `yMin` to pin the full domain; either may be set
190
+ alone. N/A for radial/part-to-whole types — see `yMin` (gh#1690).
191
+ A datum outside the pinned domain is CLAMPED to the nearest edge
192
+ for rendering (the value label/tooltip still shows the real value)
193
+ rather than escaping the plot box.
194
+ type: number
195
+ default: null
196
+ attribute: y-max
197
+ reflect: true
141
198
  x:
142
199
  description: Data key for x-axis (category) values
143
200
  type: string
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "properties": {
16
16
  "items": {
17
- "description": "JSON array of {key, label, slot?, pct?} legend items. Takes precedence over [for] when both are provided.",
17
+ "description": "JSON array of {key, label, slot?, pct?, deemphasized?} legend items. Takes precedence over [for] when both are provided. `deemphasized` (REQ-R-007) mutes the row's swatch/label to the same muted-neutral tier chart-ui's own internal legend uses for a de-emphasized series (e.g. `composed@1:1`'s secondary line) — mirrored automatically from a `[for]`-bound chart's own `.legendData`, or settable explicitly here.",
18
18
  "type": "string",
19
19
  "default": ""
20
20
  },
@@ -41,7 +41,7 @@
41
41
  "default": "hide"
42
42
  },
43
43
  "position": {
44
- "description": "Layout hint — drives flex-direction. Actual placement follows DOM order.",
44
+ "description": "Layout hint — drives flex-direction. Actual placement follows DOM order. Superseded by a resolved `ratio` bucket (below) whenever one is in effect; this is the pre-ADR-0074 fallback for a legend with neither an explicit `ratio` nor a `[for]`-bound chart.",
45
45
  "type": "string",
46
46
  "enum": [
47
47
  "top",
@@ -51,6 +51,16 @@
51
51
  ],
52
52
  "default": "bottom"
53
53
  },
54
+ "ratio": {
55
+ "description": "Chart 2.0 legend layout bucket (ADR-0074, gh#1624 plan step 5, REQ-R-005): 3:2 renders a horizontal row, 1:1 a wrapped 2-column grid, 2:3 a vertical stacked list. An explicit value PINS the bucket regardless of any bound chart. Unset (default) auto-mirrors the `[for]`-bound chart's own resolved `data-ratio-resolved` bucket — the legend never runs its own container-query classification, since its own box is typically a thin strip with no studied aspect of its own. With neither a pin nor a bound chart, no bucket resolves and `position`'s flex-direction rules apply unchanged. The resolved bucket reflects read-only onto the host as `data-ratio-resolved`, mirroring chart-ui's own convention.",
56
+ "type": "string",
57
+ "enum": [
58
+ "3:2",
59
+ "1:1",
60
+ "2:3"
61
+ ],
62
+ "default": ""
63
+ },
54
64
  "shape": {
55
65
  "description": "Swatch shape. Maps to badge-ui's icon for dot/square variants.",
56
66
  "type": "string",
@@ -99,6 +109,11 @@
99
109
  "a2ui": "[\n {\n \"id\": \"legend\", \"component\": \"ChartLegend\",\n \"shape\": \"dot\",\n \"items\": \"[{\\\"key\\\":\\\"revenue\\\",\\\"label\\\":\\\"Revenue\\\",\\\"slot\\\":0},{\\\"key\\\":\\\"users\\\",\\\"label\\\":\\\"Users\\\",\\\"slot\\\":1}]\"\n }\n]",
100
110
  "name": "standalone-with-items"
101
111
  },
112
+ {
113
+ "description": "Standalone legend pinned to the 1:1 (2-col grid) layout bucket.",
114
+ "a2ui": "[\n {\n \"id\": \"legend\", \"component\": \"ChartLegend\",\n \"ratio\": \"1:1\",\n \"items\": \"[{\\\"key\\\":\\\"revenue\\\",\\\"label\\\":\\\"Revenue\\\",\\\"slot\\\":0},{\\\"key\\\":\\\"users\\\",\\\"label\\\":\\\"Users\\\",\\\"slot\\\":1},{\\\"key\\\":\\\"churn\\\",\\\"label\\\":\\\"Churn\\\",\\\"slot\\\":2},{\\\"key\\\":\\\"nps\\\",\\\"label\\\":\\\"NPS\\\",\\\"slot\\\":3}]\"\n }\n]",
115
+ "name": "ratio-pinned-grid"
116
+ },
102
117
  {
103
118
  "description": "Legend mirrors a named chart's series data.",
104
119
  "a2ui": "[\n {\n \"id\": \"root\", \"component\": \"Column\",\n \"children\": [\"chart\", \"legend\"]\n },\n {\n \"id\": \"chart\", \"component\": \"Chart\",\n \"type\": \"multi-line\", \"x\": \"month\", \"y\": \"revenue,users\"\n },\n {\n \"id\": \"legend\", \"component\": \"ChartLegend\",\n \"for\": \"chart\",\n \"shape\": \"line\",\n \"position\": \"bottom\"\n }\n]",
@@ -34,7 +34,27 @@
34
34
  * aren't representable as icon glyphs.
35
35
  * position — top | bottom | left | right. Layout hint; actual placement
36
36
  * is where the element is placed in the DOM. Drives
37
- * flex-direction.
37
+ * flex-direction. Superseded by a resolved `ratio` bucket
38
+ * (below) whenever one is in effect — `position` is the
39
+ * pre-ADR-0074 fallback for a legend with neither an
40
+ * explicit `ratio` nor a `[for]`-bound chart.
41
+ * ratio — '' | 3:2 | 1:1 | 2:3 (ADR-0074, gh#1624 plan step 5).
42
+ * Explicit value PINS the legend's own layout bucket
43
+ * (row / 2-col grid / vertical list — REQ-R-005) regardless
44
+ * of any bound chart. Unset (default) auto-mirrors the
45
+ * `[for]`-bound chart's own resolved `data-ratio-resolved`
46
+ * bucket — the legend does not run its own container-query
47
+ * classification (REQ-R-005: "follows the per-type legend
48
+ * position column rather than introducing a separate
49
+ * legend-only ratio contract"); a legend's own box is
50
+ * typically a thin strip whose aspect carries no studied
51
+ * meaning of its own. With neither a pin nor a bound chart,
52
+ * no bucket resolves and `position`'s pre-existing
53
+ * flex-direction rules apply unchanged (zero regression for
54
+ * standalone legends that predate this attribute). The
55
+ * resolved bucket is reflected read-only as
56
+ * `data-ratio-resolved`, mirroring chart-ui's own
57
+ * convention.
38
58
  * interactive — canonical opt-in spelling (ADR-0063, gh#1563). Rows
39
59
  * default interactive (`role="button"` badges that toggle
40
60
  * on click + Enter/Space) until the 0.9.0 default-inversion
@@ -49,6 +69,12 @@
49
69
  * interactive. `active` is the new state (true=visible).
50
70
  */
51
71
 
72
+ // ADR-0074's three ratified ratio buckets — same enum chart-ui pins/resolves
73
+ // to (CHART_RATIOS in chart.class.js). Kept as a Set here rather than
74
+ // re-importing chart.class.js: the legend must resolve standalone (no
75
+ // chart-ui ancestor required, per its own "renders standalone" contract).
76
+ const RATIO_BUCKETS = new Set(['3:2', '1:1', '2:3']);
77
+
52
78
  import { UIElement } from '../../core/element.js';
53
79
 
54
80
  export class UIChartLegend extends UIElement {
@@ -60,6 +86,9 @@ export class UIChartLegend extends UIElement {
60
86
  items: { type: String, default: '', reflect: false },
61
87
  shape: { type: String, default: 'dot', reflect: true },
62
88
  position: { type: String, default: 'bottom', reflect: true },
89
+ // ADR-0074 (gh#1624 plan step 5) — explicit pin; see the class-doc
90
+ // comment above for the full pin > mirror > position-fallback chain.
91
+ ratio: { type: String, default: '', reflect: true },
63
92
  // ADR-0063 (gh#1563) — `interactive` (opt-in) is the canonical
64
93
  // positively-named boolean; the deprecated `static` opt-out alias was
65
94
  // cut in 0.8.43 (gh#1617). Deliberately keeps its `true` default:
@@ -122,6 +151,25 @@ export class UIChartLegend extends UIElement {
122
151
  this.#targetListener = null;
123
152
  }
124
153
 
154
+ /* ── Ratio resolution (ADR-0074, gh#1624 plan step 5) ─────────────── */
155
+
156
+ /**
157
+ * Pin > mirror > none. A pinned `[ratio]` always wins; absent that, a
158
+ * `[for]`-bound chart's own `data-ratio-resolved` is mirrored (set by
159
+ * the SAME `#renderChart()` pass that fires the `legend-update` event
160
+ * this class already listens for — the mirror is always reading a
161
+ * value already fresh by the time `#paint()` runs). Neither present →
162
+ * '' (no bucket resolves; `position`'s pre-existing CSS applies).
163
+ */
164
+ #resolveRatio() {
165
+ if (RATIO_BUCKETS.has(this.ratio)) return this.ratio;
166
+ if (this.#target) {
167
+ const mirrored = this.#target.getAttribute('data-ratio-resolved');
168
+ if (RATIO_BUCKETS.has(mirrored)) return mirrored;
169
+ }
170
+ return '';
171
+ }
172
+
125
173
  /* ── Item source ────────────────────────────────────────────────── */
126
174
 
127
175
  #resolveItems() {
@@ -142,6 +190,15 @@ export class UIChartLegend extends UIElement {
142
190
  /* ── Render ─────────────────────────────────────────────────────── */
143
191
 
144
192
  #paint() {
193
+ const resolved = this.#resolveRatio();
194
+ if (resolved) {
195
+ if (this.getAttribute('data-ratio-resolved') !== resolved) {
196
+ this.setAttribute('data-ratio-resolved', resolved);
197
+ }
198
+ } else if (this.hasAttribute('data-ratio-resolved')) {
199
+ this.removeAttribute('data-ratio-resolved');
200
+ }
201
+
145
202
  const items = this.#resolveItems();
146
203
  this.innerHTML = '';
147
204
  if (!items.length) return;
@@ -165,6 +222,12 @@ export class UIChartLegend extends UIElement {
165
222
  row.setAttribute('text', label);
166
223
  if (key) row.setAttribute('data-key', key);
167
224
  if (active) row.setAttribute('data-active', '');
225
+ // REQ-R-007 (Chart 2.0 ratio-and-surface SPEC) — a de-emphasized
226
+ // series (e.g. composed@1:1's secondary line, Wave C) mutes the
227
+ // mirrored legend row the same way chart-ui's own internal legend
228
+ // already mutes its dot (chart.class.js #buildLegend): the muted
229
+ // neutral token, not the series' categorical color slot.
230
+ if (item.deemphasized) row.setAttribute('data-deemphasized', '');
168
231
  if (this.interactive) {
169
232
  row.setAttribute('tabindex', '0');
170
233
  row.setAttribute('aria-pressed', active ? 'true' : 'false');
@@ -184,9 +247,11 @@ export class UIChartLegend extends UIElement {
184
247
  const swatch = document.createElement('swatch-ui');
185
248
  swatch.setAttribute('shape', swatchShape);
186
249
  swatch.setAttribute('size', 'sm');
187
- const swatchColor = key
188
- ? `var(--color-${key}, var(--a-data-${slot}))`
189
- : `var(--a-data-${slot})`;
250
+ const swatchColor = item.deemphasized
251
+ ? 'var(--md-sys-color-neutral-on-surface-variant)'
252
+ : key
253
+ ? `var(--color-${key}, var(--a-data-${slot}))`
254
+ : `var(--a-data-${slot})`;
190
255
  swatch.style.setProperty('--swatch-color', swatchColor);
191
256
  row.appendChild(swatch);
192
257
 
@@ -35,7 +35,11 @@
35
35
  }
36
36
 
37
37
  /* Position variants — drive flex-direction. DOM placement still wins for
38
- actual layout; these tune the internal row direction. */
38
+ actual layout; these tune the internal row direction. Pre-ADR-0074
39
+ fallback: superseded below by a resolved [data-ratio-resolved] bucket
40
+ whenever one is in effect (an explicit [ratio] pin, or a [for]-bound
41
+ chart's own mirrored bucket) — a legend with neither keeps this exact
42
+ behavior, unchanged. */
39
43
  :scope[position="top"],
40
44
  :scope[position="bottom"] {
41
45
  flex-direction: row;
@@ -48,6 +52,32 @@
48
52
  align-items: flex-start;
49
53
  }
50
54
 
55
+ /* Ratio-resolved layout buckets (ADR-0074, gh#1624 plan step 5, REQ-R-005).
56
+ Same selector specificity as the [position] rules above — placed AFTER
57
+ them in source order so a resolved bucket always wins the cascade over
58
+ a stale/default [position] on the same element. */
59
+
60
+ /* 3:2 — horizontal row (beside/below the chart). */
61
+ :scope[data-ratio-resolved="3:2"] {
62
+ display: flex;
63
+ flex-direction: row;
64
+ justify-content: flex-start;
65
+ }
66
+
67
+ /* 1:1 — wrapped 2-column grid. */
68
+ :scope[data-ratio-resolved="1:1"] {
69
+ display: grid;
70
+ grid-template-columns: repeat(2, minmax(0, 1fr));
71
+ align-items: start;
72
+ }
73
+
74
+ /* 2:3 — vertical stacked list. */
75
+ :scope[data-ratio-resolved="2:3"] {
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: flex-start;
79
+ }
80
+
51
81
  /* Rows are <badge-ui> chips. We override badge-ui's default tokens
52
82
  to give them a quieter, click-to-toggle appearance — transparent
53
83
  bg by default (so they read as inline text not chips), highlighted
@@ -93,6 +123,16 @@
93
123
  opacity: 0.4;
94
124
  }
95
125
 
126
+ /* De-emphasized (REQ-R-007) rows — a mirrored chart's own muted series
127
+ (e.g. composed@1:1's secondary line). Distinct from :not([data-active])
128
+ above: this is the chart's OWN designed treatment for a still-visible
129
+ series, not a user-toggled-off one; the swatch color itself is set
130
+ inline per-row (matches the neutral token, not opacity) so only the
131
+ label's --badge-fg needs the muted tier here. */
132
+ badge-ui[data-row][data-deemphasized] {
133
+ --badge-fg: var(--chart-legend-fg-inactive);
134
+ }
135
+
96
136
  /* Swatch is composed via <swatch-ui shape="…">; per-shape styling
97
137
  lives in swatch.css. The legend just hands its [shape] attr through
98
138
  and writes --swatch-color inline. Pre-2026-05-01 this file owned a
@@ -101,3 +141,27 @@
101
141
  rule (cascade leak). Composing the primitive moves the styling to
102
142
  one place. */
103
143
  }
144
+
145
+ /* ─────────────────────────────────────────────────────────────────
146
+ gh#1624 plan step 7 — full-bleed × legend horizontal breathing room.
147
+
148
+ Deliberately OUTSIDE `@scope (chart-legend-ui)`, same reason as
149
+ chart.css's own `card-ui[padding="none"] chart-ui` rule: this needs to
150
+ match an ANCESTOR of the scoping root. `--chart-legend-px` defaults to
151
+ 0 because a legend normally sits inside a card section that already
152
+ supplies horizontal inset via `--card-inset` (card.css's own
153
+ margin-based section model); `card-ui[padding="none"]` zeroes that
154
+ inset CARD-WIDE (every section, not just the chart's), so a legend
155
+ paired with a full-bleed chart lost its own horizontal clearance too —
156
+ confirmed via browser probe: badge rows sat ~1px from the card's
157
+ rounded edge, at real risk of visual crowding against the corner-radius
158
+ `overflow: hidden` (card.css :scope). The chart itself doesn't need this
159
+ — its own internal plot margins (REQ-R-004) already supply clearance
160
+ regardless of the card's inset — but the legend's chip rows have no
161
+ equivalent internal margin of their own. Restoring `--chart-legend-px`
162
+ only (not `-py`, which the legend already carries unconditionally) is
163
+ the minimal fix: vertical spacing between the chart and legend sections
164
+ is unaffected, only the legend's own left/right clearance is restored. */
165
+ card-ui[padding="none"] chart-legend-ui {
166
+ --chart-legend-px: var(--a-space-4);
167
+ }
@@ -24,7 +24,7 @@ export interface ChartLegendToggleEventDetail {
24
24
  export type ChartLegendToggleEvent = CustomEvent<ChartLegendToggleEventDetail>;
25
25
 
26
26
  export class UIChartLegend extends UIElement {
27
- /** JSON array of {key, label, slot?, pct?} legend items. Takes precedence over [for] when both are provided. */
27
+ /** JSON array of {key, label, slot?, pct?, deemphasized?} legend items. Takes precedence over [for] when both are provided. `deemphasized` (REQ-R-007) mutes the row's swatch/label to the same muted-neutral tier chart-ui's own internal legend uses for a de-emphasized series (e.g. `composed@1:1`'s secondary line) — mirrored automatically from a `[for]`-bound chart's own `.legendData`, or settable explicitly here. */
28
28
  items: string;
29
29
  /** id-ref of a chart-ui / heatmap-ui to mirror series from. */
30
30
  for: string;
@@ -32,8 +32,10 @@ export class UIChartLegend extends UIElement {
32
32
  interactive: boolean;
33
33
  /** Series-toggle mode emitted via the `toggle` event. `hide` removes the series from the render; `opacity` fades it. Wired via [for] on chart-ui. */
34
34
  onToggle: 'hide' | 'opacity';
35
- /** Layout hint — drives flex-direction. Actual placement follows DOM order. */
35
+ /** Layout hint — drives flex-direction. Actual placement follows DOM order. Superseded by a resolved `ratio` bucket (below) whenever one is in effect; this is the pre-ADR-0074 fallback for a legend with neither an explicit `ratio` nor a `[for]`-bound chart. */
36
36
  position: 'top' | 'bottom' | 'left' | 'right';
37
+ /** Chart 2.0 legend layout bucket (ADR-0074, gh#1624 plan step 5, REQ-R-005): 3:2 renders a horizontal row, 1:1 a wrapped 2-column grid, 2:3 a vertical stacked list. An explicit value PINS the bucket regardless of any bound chart. Unset (default) auto-mirrors the `[for]`-bound chart's own resolved `data-ratio-resolved` bucket — the legend never runs its own container-query classification, since its own box is typically a thin strip with no studied aspect of its own. With neither a pin nor a bound chart, no bucket resolves and `position`'s flex-direction rules apply unchanged. The resolved bucket reflects read-only onto the host as `data-ratio-resolved`, mirroring chart-ui's own convention. */
38
+ ratio: '3:2' | '1:1' | '2:3';
37
39
  /** Swatch shape. Maps to badge-ui's icon for dot/square variants. */
38
40
  shape: 'dot' | 'square' | 'line' | 'dashed';
39
41
 
@@ -12,11 +12,11 @@
12
12
  <chart-legend-ui id="legend-bottom" shape="dot" position="bottom" items='[{"key":"a","label":"Series A","slot":0},{"key":"b","label":"Series B","slot":1},{"key":"c","label":"Series C","slot":2}]'></chart-legend-ui>
13
13
  ```
14
14
 
15
- ## formirror a chart's legendData
15
+ ## ratiomirrors a bound chart's resolved ratio (ADR-0074)
16
16
 
17
17
  ```html
18
- <div style="display:flex; flex-direction:column; gap:var(--a-space-2);">
19
- <chart-ui id="demo-chart" type="multi-line" x="month" y="revenue,users,churn"></chart-ui>
20
- <chart-legend-ui id="legend-for" for="demo-chart" shape="line"></chart-legend-ui>
18
+ <div style="display:flex; flex-direction:column; gap:var(--a-space-2); width:360px;">
19
+ <chart-ui id="ratio-chart-32" type="multi-line" ratio="3:2" x="month" y="revenue,users,churn"></chart-ui>
20
+ <chart-legend-ui id="ratio-legend-32" for="ratio-chart-32" shape="line"></chart-legend-ui>
21
21
  </div>
22
22
  ```