@adia-ai/web-components 0.8.24 → 0.8.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/components/anchor-bar/anchor-bar.a2ui.json +139 -0
  3. package/components/anchor-bar/anchor-bar.class.js +124 -0
  4. package/components/anchor-bar/anchor-bar.css +140 -0
  5. package/components/anchor-bar/anchor-bar.d.ts +35 -0
  6. package/components/anchor-bar/anchor-bar.examples.md +32 -0
  7. package/components/anchor-bar/anchor-bar.js +17 -0
  8. package/components/anchor-bar/anchor-bar.yaml +167 -0
  9. package/components/chart/chart.a2ui.json +16 -0
  10. package/components/chart/chart.class.js +105 -27
  11. package/components/chart/chart.css +27 -0
  12. package/components/chart/chart.d.ts +4 -0
  13. package/components/chart/chart.yaml +28 -0
  14. package/components/index.js +1 -0
  15. package/components/stat/stat.a2ui.json +8 -0
  16. package/components/stat/stat.css +39 -0
  17. package/components/stat/stat.d.ts +2 -0
  18. package/components/stat/stat.js +5 -0
  19. package/components/stat/stat.yaml +11 -0
  20. package/components/tooltip/tooltip.class.js +17 -11
  21. package/dist/host.min.css +1 -1
  22. package/dist/host.sheet.js +1 -1
  23. package/dist/theme-provider.min.js +2 -2
  24. package/dist/web-components.min.css +1 -1
  25. package/dist/web-components.min.js +115 -115
  26. package/dist/web-components.sheet.js +1 -1
  27. package/package.json +1 -1
  28. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +81 -93
  29. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +12 -25
  30. package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +8 -52
  31. package/styles/api/parametric-scope.css +81 -0
  32. package/styles/components.css +1 -0
  33. package/styles/tokens.css +1 -0
@@ -0,0 +1,167 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: UIAnchorBar
4
+ tag: anchor-bar-ui
5
+ status: beta
6
+ component: AnchorBar
7
+ category: container
8
+ version: 1
9
+ description: >-
10
+ Persistent, anchored, elevated surface for chrome that must stay visible
11
+ while a condition holds — floating bulk-action toolbars, undo bars,
12
+ sticky wizard/commit footers. Distinct from <toast-ui> (auto-dismissing,
13
+ notification semantics) and <modal-ui>/<drawer-ui> (blocking overlays
14
+ with backdrop + focus-trap semantics). Default mode anchors [anchor] to
15
+ the nearest scroll container via `position: sticky` — the shape gh#494's
16
+ bulk-action-toolbar pattern hand-rolled with card-ui + position:sticky.
17
+ Opt into [viewport] for a surface that anchors to the browser viewport
18
+ instead, promoted to the top layer via the Popover API (`popover="manual"`)
19
+ so it wins stacking over shell chrome without a z-index war. Visibility
20
+ is [open] (default false, no auto-dismiss timer) — arbitrary content
21
+ (typically <toolbar-ui> or <text-ui> + <button-ui>) rides the default slot.
22
+ props:
23
+ anchor:
24
+ description: Viewport/container edge the bar sticks to.
25
+ type: string
26
+ default: bottom
27
+ reflect: true
28
+ enum:
29
+ - bottom
30
+ - top
31
+ align:
32
+ description: Horizontal alignment along the anchored edge.
33
+ type: string
34
+ default: center
35
+ reflect: true
36
+ enum:
37
+ - start
38
+ - center
39
+ - end
40
+ viewport:
41
+ description: >-
42
+ false (default) anchors to the nearest scroll container via
43
+ `position: sticky` — safe for content with ResizeObserver-driven
44
+ children (e.g. <toolbar-ui>'s overflow measurement never sees a
45
+ display:none reset). true anchors to the browser viewport via
46
+ `position: fixed` + Popover API top-layer promotion — note the
47
+ native Popover UA stylesheet applies `display:none` while closed,
48
+ which DOES reset ResizeObserver-driven children on first open;
49
+ prefer the default for content wrapping <toolbar-ui>.
50
+ type: boolean
51
+ default: false
52
+ reflect: true
53
+ open:
54
+ description: Controls visibility. No auto-dismiss — caller toggles explicitly.
55
+ type: boolean
56
+ default: false
57
+ reflect: true
58
+ events:
59
+ open:
60
+ description: Fired when [open] becomes true.
61
+ close:
62
+ description: Fired when [open] becomes false.
63
+ slots:
64
+ default:
65
+ description: >-
66
+ Arbitrary bar content — typically <toolbar-ui> (bulk-action bars) or
67
+ <text-ui> + <button-ui> (undo bars, wizard footers). The bar owns
68
+ only the elevated surface chrome (bg/border/radius/shadow) and
69
+ positioning; content layout is the slotted children's own.
70
+ states:
71
+ - name: idle
72
+ description: Default, ready for interaction.
73
+ traits: []
74
+ tokens:
75
+ --anchor-bar-bg:
76
+ description: Override background color
77
+ --anchor-bar-fg:
78
+ description: Override text color
79
+ --anchor-bar-border:
80
+ description: Override border color
81
+ --anchor-bar-radius:
82
+ description: Override corner radius
83
+ --anchor-bar-shadow:
84
+ description: Override elevation shadow
85
+ --anchor-bar-offset:
86
+ description: Override distance from the anchored edge
87
+ a2ui:
88
+ rules:
89
+ - rule: >-
90
+ Persistent, non-blocking anchored surface. Use for floating
91
+ bulk-action toolbars, undo bars, and wizard/commit footers — never
92
+ for transient notifications (use <toast-ui>) or blocking dialogs
93
+ (use <modal-ui> / <drawer-ui>).
94
+ reason: 'Lifecycle boundary vs adjacent overlay surfaces.'
95
+ - rule: >-
96
+ Default [viewport=false] anchors within the nearest scroll
97
+ container via position:sticky — the correct choice when the bar
98
+ sits over a bounded scroll region (e.g. a table's own scroll box).
99
+ Set [viewport] only when the surface must anchor to the browser
100
+ viewport regardless of ancestor scroll containers (e.g. an undo
101
+ bar over an arbitrary page layout).
102
+ reason: 'Mode selection.'
103
+ - rule: >-
104
+ Does NOT join the modal mutual-exclusion convention — it is not a
105
+ modal surface and coexists with page interaction and open popovers.
106
+ reason: 'Overlay coexistence contract (gh#495).'
107
+ anti_patterns: []
108
+ examples:
109
+ - name: anchor-bar-demo
110
+ description: >-
111
+ Bottom-anchored bar containing a toolbar with a selection count and
112
+ action buttons, shown persistently while open.
113
+ a2ui: >-
114
+ [
115
+ {
116
+ "id": "root",
117
+ "component": "Card",
118
+ "children": [
119
+ "bar"
120
+ ]
121
+ },
122
+ {
123
+ "id": "bar",
124
+ "component": "AnchorBar",
125
+ "anchor": "bottom",
126
+ "open": true,
127
+ "children": [
128
+ "toolbar"
129
+ ]
130
+ },
131
+ {
132
+ "id": "toolbar",
133
+ "component": "Toolbar",
134
+ "children": [
135
+ "count"
136
+ ]
137
+ },
138
+ {
139
+ "id": "count",
140
+ "component": "Text",
141
+ "text": "3 selected"
142
+ }
143
+ ]
144
+ keywords:
145
+ - anchor
146
+ - bar
147
+ - floating
148
+ - toolbar
149
+ - undo
150
+ - persistent
151
+ - sticky
152
+ - footer
153
+ - selection
154
+ - bulk action
155
+ synonyms:
156
+ bar:
157
+ - toolbar
158
+ - anchor
159
+ - footer
160
+ floating:
161
+ - anchor
162
+ - sticky
163
+ - popover
164
+ undo:
165
+ - anchor
166
+ - toast
167
+ - alert
@@ -57,6 +57,17 @@
57
57
  "description": "JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x=\"month\" y=\"revenue\">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data=\"[…]\"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute.",
58
58
  "$ref": "common_types.json#/$defs/DynamicObjectList"
59
59
  },
60
+ "dots": {
61
+ "description": "Marker (dot) visibility for line and multi-line series. `all` draws a circle[data-dot] on every datum (default — matches pre-gh#561 behavior). `none` hides all dots so a dense series reads as a clean line. `hover` shows only the currently-hovered datum's dot. `last` shows only the terminal point per series (sparkline convention). Tooltip hit targets ([data-hit]) are a separate always-rendered layer — every mode keeps hover/click working along the whole line.",
62
+ "type": "string",
63
+ "enum": [
64
+ "all",
65
+ "none",
66
+ "hover",
67
+ "last"
68
+ ],
69
+ "default": "all"
70
+ },
60
71
  "format": {
61
72
  "description": "Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default \"$\"); `percent` multiplies × 100 and adds a % suffix.",
62
73
  "type": "string",
@@ -93,6 +104,11 @@
93
104
  "type": "number",
94
105
  "default": null
95
106
  },
107
+ "seriesEmphasis": {
108
+ "description": "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.",
109
+ "type": "string",
110
+ "default": ""
111
+ },
96
112
  "size": {
97
113
  "description": "Chart size",
98
114
  "type": "string",
@@ -26,6 +26,8 @@
26
26
  * hide-grid — hide gridlines
27
27
  * hide-values — hide value labels
28
28
  * radius — bar corner radius (default: 4)
29
+ * dots — line/multi-line marker visibility: all|none|hover|last (default: all)
30
+ * series-emphasis — multi-line: series key that keeps full area+line treatment; others render line-only, dimmed
29
31
  *
30
32
  * JS API:
31
33
  * .data = [{...}, ...] — array of data objects
@@ -222,6 +224,19 @@ export class UIChart extends UIElement {
222
224
  size: { type: String, default: '', reflect: true },
223
225
  format: { type: String, default: 'abbr', reflect: true },
224
226
  loading: { type: Boolean, default: false, reflect: true },
227
+ /* gh#561 — dot/marker visibility for line + multi-line. `all` is the
228
+ pre-561 default (every datum gets a circle[data-dot]); `none` /
229
+ `hover` / `last` exist because a 30-point small-integer series turns
230
+ into an unreadable bead string at `all`. Tooltip hit targets
231
+ ([data-hit]) are a separate always-rendered layer — every mode keeps
232
+ hover/click working along the whole line. */
233
+ dots: { type: String, default: 'all', reflect: true },
234
+ /* gh#561 — names one multi-line series (a `y` key) to render at full
235
+ strength (area + full-opacity line); every other series drops its
236
+ area and renders line-only at --chart-deemphasized-opacity. Empty
237
+ (default) applies full treatment to every series — unchanged from
238
+ pre-561 behavior. */
239
+ seriesEmphasis: { type: String, default: '', reflect: true, attribute: 'series-emphasis' },
225
240
  };
226
241
 
227
242
  static template = () => null;
@@ -235,6 +250,10 @@ export class UIChart extends UIElement {
235
250
  /* Set of series keys hidden by external chart-legend-ui[for=self] toggles.
236
251
  Kept at render time; repopulated lookups rebuild with the new set. */
237
252
  #hiddenSeriesKeys = new Set();
253
+ /* gh#561 — dots="hover": the currently-shown dot element (a sibling of the
254
+ hovered [data-hit] circle), tracked so it can be cleared on leave /
255
+ re-render without a full DOM query. */
256
+ #activeDotEl = null;
238
257
 
239
258
  /** Resolves the corner radius: the `radius` prop if explicitly set,
240
259
  * otherwise `--a-radius` from the host's computed style. Falls back
@@ -463,7 +482,11 @@ export class UIChart extends UIElement {
463
482
  }
464
483
  this.setAttribute('data-has-data', '');
465
484
 
466
- this.#injectSeriesColors();
485
+ /* gh#561 — the DOM this pointed at is about to be wiped; drop the
486
+ reference so a stale node isn't held onto (its data-dot-active
487
+ attribute doesn't matter once detached, but nulling avoids leaking
488
+ the reference between renders). */
489
+ this.#activeDotEl = null;
467
490
 
468
491
  const svgWrap = document.createElement('div');
469
492
  if (this.type === 'sparkline') svgWrap.setAttribute('data-sparkline', '');
@@ -522,25 +545,26 @@ export class UIChart extends UIElement {
522
545
  tooltip-ui[follows=pointer] lands. */
523
546
  }
524
547
 
525
- /* ── Per-series --color-{key} injection ──
526
- For each declared series key (`y="revenue,users"`), emit an inline CSS
527
- custom property on the host mapping the key to the categorical palette
528
- slot it occupies. Consumers override `--color-revenue` at any ancestor
529
- to recolor that series across chart + legend + (Phase 2) tooltip. */
530
- #injectSeriesColors() {
531
- const keys = this.#yKeys();
532
- for (let i = 0; i < keys.length; i++) {
533
- const slot = i % 10;
534
- this.style.setProperty(`--color-${keys[i]}`, `var(--chart-${slot})`);
535
- }
536
- }
548
+ /* ── Per-series --color-{key} hook ──
549
+ gh#561 previously this method wrote `--color-{key}: var(--chart-N)`
550
+ as an inline style on the HOST. Inline styles win the cascade over
551
+ everything except `!important`, so a consumer setting `--color-MAU` on
552
+ any ancestor lost to the chart's own inline default the documented
553
+ "override --color-{key} to recolor a series" hook was unusable from
554
+ outside. The fix is to not set the host property at all: each
555
+ series-keyed element already carries its own inline
556
+ `var(--color-{key}, var(--chart-{slot}))` fallback chain (see
557
+ #seriesFill / #seriesStroke below), so an unset --color-{key} still
558
+ resolves to the right palette slot, and a consumer-set one now
559
+ actually wins. */
537
560
 
538
561
  /* Render-time helpers — emit data-slice + series-key + inline style that
539
- references `--color-{key}` with fallback to `--chart-{slot}`. Inline
540
- style beats the stylesheet's data-slice rule, so consumer overrides of
541
- --color-{key} at any ancestor recolor that series. Non-series-keyed
542
- elements (pie/donut/segments categorical slots) keep data-slice only
543
- and are coloured by the stylesheet. */
562
+ references `--color-{key}` with fallback to `--chart-{slot}`. Because
563
+ nothing sets `--color-{key}` on the host anymore, an ancestor-set
564
+ value flows through the normal cascade into this var() and wins. Only
565
+ an unset `--color-{key}` falls through to `--chart-{slot}`.
566
+ Non-series-keyed elements (pie/donut/segments categorical slots) keep
567
+ data-slice only and are coloured by the stylesheet. */
544
568
  #seriesFill(slotIdx, seriesKey) {
545
569
  if (!seriesKey) return ` data-slice="${slotIdx}"`;
546
570
  return ` data-slice="${slotIdx}" data-series-key="${esc(seriesKey)}" style="fill: var(--color-${seriesKey}, var(--chart-${slotIdx}))"`;
@@ -813,6 +837,7 @@ export class UIChart extends UIElement {
813
837
 
814
838
  #emitHover(target, event) {
815
839
  this.#hoveredTarget = target;
840
+ if (this.dots === 'hover') this.#setActiveDot(target);
816
841
  this.dispatchEvent(new CustomEvent('chart-hover', {
817
842
  bubbles: true,
818
843
  detail: this.#tipPayload(target, event),
@@ -821,9 +846,32 @@ export class UIChart extends UIElement {
821
846
 
822
847
  #emitLeave() {
823
848
  this.#hoveredTarget = null;
849
+ if (this.dots === 'hover') this.#clearActiveDot();
824
850
  this.dispatchEvent(new CustomEvent('chart-leave', { bubbles: true }));
825
851
  }
826
852
 
853
+ /* gh#561 — dots="hover". The dot circle for a point is always the DOM
854
+ sibling immediately before its [data-hit] circle (both renderers push
855
+ them back-to-back: `<circle data-dot>` then `<circle data-hit>`), so
856
+ no separate index bookkeeping is needed to pair them. */
857
+ #setActiveDot(target) {
858
+ const dot = target?.previousElementSibling;
859
+ if (this.#activeDotEl && this.#activeDotEl !== dot) {
860
+ this.#activeDotEl.removeAttribute('data-dot-active');
861
+ }
862
+ if (dot && dot.hasAttribute('data-dot')) {
863
+ dot.setAttribute('data-dot-active', '');
864
+ this.#activeDotEl = dot;
865
+ } else {
866
+ this.#activeDotEl = null;
867
+ }
868
+ }
869
+
870
+ #clearActiveDot() {
871
+ if (this.#activeDotEl) this.#activeDotEl.removeAttribute('data-dot-active');
872
+ this.#activeDotEl = null;
873
+ }
874
+
827
875
  /* Build the standard event payload from a hovered/clicked datum element.
828
876
  OD-CHART-05 — per-X-column granularity. For multi-series renderers
829
877
  (stacked-bar, grouped-bar, multi-line, composed), the pointer is
@@ -959,6 +1007,20 @@ export class UIChart extends UIElement {
959
1007
  return this.y ? this.y.split(',').map(k => k.trim()).filter(Boolean) : [];
960
1008
  }
961
1009
 
1010
+ /* gh#561 — dots mode. `none`/`last` decide at render time (no dot markup
1011
+ emitted for suppressed points); `hover` renders every dot but CSS
1012
+ hides them by default (`[dots="hover"] [data-dot]`), and the pointer
1013
+ handlers above toggle `data-dot-active` on the one under the cursor. */
1014
+ #shouldRenderDot(i, total) {
1015
+ switch (this.dots) {
1016
+ case 'none': return false;
1017
+ case 'last': return i === total - 1;
1018
+ case 'hover':
1019
+ case 'all':
1020
+ default: return true;
1021
+ }
1022
+ }
1023
+
962
1024
  /* ── Grid + axes helper ───────────────────────────────────────── */
963
1025
 
964
1026
  #gridAndAxes(width, height, ticks, labels, pad, dims) {
@@ -1096,13 +1158,15 @@ export class UIChart extends UIElement {
1096
1158
  const showValues = !this.hideValues && !isSm;
1097
1159
  const showAverage = !this.hideAverage && vals.length > 1 && !isSm && !this.hideGrid;
1098
1160
 
1099
- for (const p of points) {
1100
- svg += `<circle data-dot cx="${p.x}" cy="${p.y}" r="${dotR}"/>`;
1161
+ points.forEach((p, i) => {
1162
+ if (this.#shouldRenderDot(i, points.length)) {
1163
+ svg += `<circle data-dot cx="${p.x}" cy="${p.y}" r="${dotR}"/>`;
1164
+ }
1101
1165
  svg += `<circle data-hit${tip({ label: p.label, value: p.v })} cx="${p.x}" cy="${p.y}" r="${hitR}" fill="transparent"/>`;
1102
1166
  if (showValues) {
1103
1167
  svg += `<text data-value x="${p.x}" y="${p.y - 8}" text-anchor="middle" font-size="${dims.valueSize}">${this.#fmtValue(p.v)}</text>`;
1104
1168
  }
1105
- }
1169
+ });
1106
1170
 
1107
1171
  if (showAverage) {
1108
1172
  const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
@@ -2082,18 +2146,32 @@ export class UIChart extends UIElement {
2082
2146
  const baseline = pad.top + plotH;
2083
2147
  const t = Math.max(0, Math.min(1, this.smooth));
2084
2148
 
2085
- /* Area fill */
2086
- svg += `<path data-area${this.#seriesFill(k % 10, keys[k])} d="${smoothAreaPath(points, baseline, t)}"/>`;
2149
+ /* gh#561 per-series emphasis. When `series-emphasis` names a key,
2150
+ only that series keeps its area fill + full-strength line; every
2151
+ other series drops the area and renders line-only at
2152
+ --chart-deemphasized-opacity (data-deemphasized, CSS-driven — no
2153
+ raw colors touched). Unset (default) emphasizes every series,
2154
+ matching pre-561 behavior. */
2155
+ const emphasisKey = this.seriesEmphasis;
2156
+ const isEmphasized = !emphasisKey || keys[k] === emphasisKey;
2157
+ const deemphAttr = isEmphasized ? '' : ' data-deemphasized';
2158
+
2159
+ /* Area fill — omitted entirely for deemphasized series. */
2160
+ if (isEmphasized) {
2161
+ svg += `<path data-area${this.#seriesFill(k % 10, keys[k])} d="${smoothAreaPath(points, baseline, t)}"/>`;
2162
+ }
2087
2163
 
2088
2164
  /* Line */
2089
- svg += `<path data-line${this.#seriesStroke(k % 10, keys[k])} d="${smoothPath(points, t)}"/>`;
2165
+ svg += `<path data-line${this.#seriesStroke(k % 10, keys[k])}${deemphAttr} d="${smoothPath(points, t)}"/>`;
2090
2166
 
2091
2167
  /* Dots + hit targets. Hit circles deliberately omit data-slice so
2092
2168
  they aren't caught by the circle[data-slice] fill rule in CSS. */
2093
- for (const p of points) {
2094
- svg += `<circle data-dot${this.#seriesFill(k % 10, keys[k])} cx="${p.x}" cy="${p.y}" r="3"/>`;
2169
+ points.forEach((p, i) => {
2170
+ if (this.#shouldRenderDot(i, points.length)) {
2171
+ svg += `<circle data-dot${this.#seriesFill(k % 10, keys[k])}${deemphAttr} cx="${p.x}" cy="${p.y}" r="3"/>`;
2172
+ }
2095
2173
  svg += `<circle data-hit${tip({ label: p.label, value: p.v, series: keys[k] })} cx="${p.x}" cy="${p.y}" r="10" fill="transparent"/>`;
2096
- }
2174
+ });
2097
2175
  }
2098
2176
 
2099
2177
  this.#legendData = keys.map((k, i) => ({ label: k, key: k, slot: i % 10 }));
@@ -25,6 +25,9 @@
25
25
  --chart-area-opacity: 0.15;
26
26
  --chart-line-width: 2;
27
27
  --chart-radius: 4;
28
+ /* gh#561 — opacity applied to a multi-line series' line when
29
+ series-emphasis names a different series (data-deemphasized). */
30
+ --chart-deemphasized-opacity: 0.4;
28
31
 
29
32
  /* Layout */
30
33
  --chart-border: var(--md-sys-color-neutral-outline-variant);
@@ -227,6 +230,30 @@
227
230
  r: 5;
228
231
  }
229
232
 
233
+ /* ── gh#561 — dots mode ──
234
+ `all` (default) leaves [data-dot] at full opacity, matching pre-561
235
+ behavior. `none`/`last` decide what to render in JS (no markup
236
+ emitted for suppressed points) so no CSS is needed for them. `hover`
237
+ renders every dot but hides them by default; the pointer handlers in
238
+ chart.js toggle [data-dot-active] on the one under the cursor. */
239
+ :scope[dots="hover"] [data-dot] {
240
+ opacity: 0;
241
+ transition: opacity var(--chart-duration) var(--chart-easing), r var(--chart-duration) var(--chart-easing);
242
+ }
243
+
244
+ :scope[dots="hover"] [data-dot][data-dot-active] {
245
+ opacity: 1;
246
+ }
247
+
248
+ /* ── gh#561 — multi-line per-series emphasis ──
249
+ `series-emphasis` names one series to keep full strength; every other
250
+ series' [data-line] (and its dots, if rendered) carry data-deemphasized
251
+ and dim to this token instead. Its [data-area] is never emitted at
252
+ all (see chart.js), so there's no fill to dim. */
253
+ [data-deemphasized] {
254
+ opacity: var(--chart-deemphasized-opacity);
255
+ }
256
+
230
257
  /* ── Average line ── */
231
258
 
232
259
  [data-avg] {
@@ -24,6 +24,8 @@ export class UIChart extends UIElement {
24
24
  color: 'accent' | 'success' | 'warning' | 'danger' | 'info';
25
25
  /** JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x="month" y="revenue">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data="[…]"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute. */
26
26
  data: string;
27
+ /** Marker (dot) visibility for line and multi-line series. `all` draws a circle[data-dot] on every datum (default — matches pre-gh#561 behavior). `none` hides all dots so a dense series reads as a clean line. `hover` shows only the currently-hovered datum's dot. `last` shows only the terminal point per series (sparkline convention). Tooltip hit targets ([data-hit]) are a separate always-rendered layer — every mode keeps hover/click working along the whole line. */
28
+ dots: 'all' | 'none' | 'hover' | 'last';
27
29
  /** Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default "$"); `percent` multiplies × 100 and adds a % suffix. */
28
30
  format: 'abbr' | 'decimal' | 'currency' | 'percent';
29
31
  /** When true, suppress the overlaid average line */
@@ -36,6 +38,8 @@ export class UIChart extends UIElement {
36
38
  loading: boolean;
37
39
  /** Bar corner radius (null = let CSS tokens decide) */
38
40
  radius: number;
41
+ /** 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
+ seriesEmphasis: string;
39
43
  /** Chart size */
40
44
  size: 'sm' | 'md' | 'lg';
41
45
  /** Line smoothing factor */
@@ -92,6 +92,34 @@ props:
92
92
  description: Line smoothing factor
93
93
  type: number
94
94
  default: 0.4
95
+ dots:
96
+ description: >-
97
+ Marker (dot) visibility for line and multi-line series. `all` draws a
98
+ circle[data-dot] on every datum (default — matches pre-gh#561
99
+ behavior). `none` hides all dots so a dense series reads as a clean
100
+ line. `hover` shows only the currently-hovered datum's dot. `last`
101
+ shows only the terminal point per series (sparkline convention).
102
+ Tooltip hit targets ([data-hit]) are a separate always-rendered
103
+ layer — every mode keeps hover/click working along the whole line.
104
+ type: string
105
+ default: all
106
+ enum:
107
+ - all
108
+ - none
109
+ - hover
110
+ - last
111
+ reflect: true
112
+ seriesEmphasis:
113
+ description: >-
114
+ Multi-line only. Names one series key (matching a `y` key) to render
115
+ at full strength — area fill + full-opacity line. Every other series
116
+ drops its area fill and renders line-only at
117
+ --chart-deemphasized-opacity. Empty (default) applies full treatment
118
+ to every series, unchanged from pre-gh#561 behavior.
119
+ type: string
120
+ default: ""
121
+ attribute: series-emphasis
122
+ reflect: true
95
123
  loading:
96
124
  description: >-
97
125
  Show a skeleton placeholder instead of the chart body. Set to true
@@ -41,6 +41,7 @@ export { UIDrawer } from './drawer/drawer.js';
41
41
  export { UIModal } from './modal/modal.js';
42
42
  export { UIToast } from './toast/toast.js';
43
43
  export { UIFeedContainer, UIFeedItem, UIFeed } from './feed/feed.js';
44
+ export { UIAnchorBar } from './anchor-bar/anchor-bar.js';
44
45
  export { UITabs } from './tabs/tabs.js';
45
46
  export { UITab } from './tabs/tab.js';
46
47
  export { UITooltip } from './tooltip/tooltip.js';
@@ -13,6 +13,11 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "band": {
17
+ "description": "Chart-band KPI layout. With a `slot=\"chart\"` child, label / value / change stack full width top to bottom and the chart forms a short full-width band between the value and the change row (the Gmail/Vercel/shadcn stat-card shape). Composes with `icon` exactly as the base layout does. No effect without a chart slot.",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
16
21
  "bleed": {
17
22
  "description": "Horizontal-bleed KPI layout. With a `slot=\"chart\"` child, the value / label / change stack on the left while the chart fills the right column at full height and bleeds to the card's top / right / bottom edges (the horizontal counterpart to a chart in a card `<section bleed>`). No effect without a chart slot.",
18
23
  "type": "boolean",
@@ -233,6 +238,9 @@
233
238
  "--stat-change-size": {
234
239
  "description": "Font size for the change badge"
235
240
  },
241
+ "--stat-chart-band-height": {
242
+ "description": "Fixed height of the full-width chart band in `band` layout"
243
+ },
236
244
  "--stat-column-gap": {
237
245
  "description": "Horizontal gap between grid columns"
238
246
  },
@@ -16,6 +16,9 @@
16
16
  --stat-column-gap: var(--a-gap-self, var(--a-gap));
17
17
  --stat-row-gap: var(--a-gap-sm);
18
18
  text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
19
+
20
+ /* ── Chart-band mode ── */
21
+ --stat-chart-band-height: 3rem;
19
22
  }
20
23
 
21
24
  :scope {
@@ -127,6 +130,42 @@
127
130
  aspect-ratio: auto;
128
131
  }
129
132
 
133
+ /* ── Chart-band KPI tile ──
134
+ label / value / chart-band / change stack full width, top to bottom —
135
+ the Gmail/Vercel/shadcn stat-card shape: a short full-width trend band
136
+ sits between the hero number and the delta footer, instead of the
137
+ inline chart resting beside the value (compact) or the bleed panel
138
+ replacing the text column (wide). Text rows stay baseline-aligned;
139
+ the band row stretches to its own fixed height. Compose inside a card
140
+ section:
141
+ <stat-ui band value=… label=… change=… trend=…>
142
+ <chart-ui slot="chart" type="sparkline" …></chart-ui>
143
+ </stat-ui> */
144
+ :scope[band]:has([slot="chart"]) {
145
+ grid-template-columns: minmax(0, 1fr) auto;
146
+ grid-template-areas:
147
+ "label icon"
148
+ "value value"
149
+ "chart chart"
150
+ "change change";
151
+ align-items: baseline;
152
+ }
153
+ /* Restore full-width value span — the inline-chart reflow above confines
154
+ value to column 1 to make room for the beside-value chart; band has no
155
+ such column, so the value spans both again. */
156
+ :scope[band] [slot="value"] {
157
+ grid-column: 1 / -1;
158
+ }
159
+ :scope[band] [slot="chart"] {
160
+ grid-area: chart;
161
+ /* Band row stretches to its own fixed short height rather than the
162
+ inline-chart's baseline-anchored 4:3 box — aspect-ratio released. */
163
+ align-self: stretch;
164
+ width: 100%;
165
+ height: var(--stat-chart-band-height);
166
+ aspect-ratio: auto;
167
+ }
168
+
130
169
  /* ── Label (eyebrow) ── */
131
170
  [slot="label"] {
132
171
  grid-area: label;
@@ -13,6 +13,8 @@
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export class UIStat extends UIElement {
16
+ /** Chart-band KPI layout. With a `slot="chart"` child, label / value / change stack full width top to bottom and the chart forms a short full-width band between the value and the change row (the Gmail/Vercel/shadcn stat-card shape). Composes with `icon` exactly as the base layout does. No effect without a chart slot. */
17
+ band: boolean;
16
18
  /** Horizontal-bleed KPI layout. With a `slot="chart"` child, the value / label / change stack on the left while the chart fills the right column at full height and bleeds to the card's top / right / bottom edges (the horizontal counterpart to a chart in a card `<section bleed>`). No effect without a chart slot. */
17
19
  bleed: boolean;
18
20
  /** Change indicator text (e.g. '+12%', '-3%') */
@@ -23,6 +23,11 @@ class UIStat extends UIElement {
23
23
  // slot="chart" child fills the right column and bleeds to the card edges.
24
24
  // Reflected so the [bleed] CSS selector gates the layout.
25
25
  bleed: { type: Boolean, default: false, reflect: true },
26
+ // Chart-band layout: label/value/chart/change stack full width, top to
27
+ // bottom, with the slot="chart" child forming a short full-width band
28
+ // between the value and the change row. Reflected so the [band] CSS
29
+ // selector gates the layout.
30
+ band: { type: Boolean, default: false, reflect: true },
26
31
  };
27
32
 
28
33
  static template = () => null;
@@ -58,6 +58,15 @@ props:
58
58
  without a chart slot.
59
59
  type: boolean
60
60
  default: false
61
+ band:
62
+ description: >-
63
+ Chart-band KPI layout. With a `slot="chart"` child, label / value /
64
+ change stack full width top to bottom and the chart forms a short
65
+ full-width band between the value and the change row (the
66
+ Gmail/Vercel/shadcn stat-card shape). Composes with `icon` exactly as
67
+ the base layout does. No effect without a chart slot.
68
+ type: boolean
69
+ default: false
61
70
  events: {}
62
71
  slots:
63
72
  change:
@@ -81,6 +90,8 @@ traits: []
81
90
  tokens:
82
91
  --stat-change-size:
83
92
  description: Font size for the change badge
93
+ --stat-chart-band-height:
94
+ description: Fixed height of the full-width chart band in `band` layout
84
95
  --stat-column-gap:
85
96
  description: Horizontal gap between grid columns
86
97
  --stat-down-fg: