@apliteni/apliteni-ui 0.113.0 → 0.114.0

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.
package/src/index.css CHANGED
@@ -36,6 +36,7 @@
36
36
  @import "./styles/filter-bar.css";
37
37
  @import "./styles/pagination.css";
38
38
  @import "./styles/stat.css";
39
+ @import "./styles/chart.css";
39
40
  @import "./styles/empty.css";
40
41
  @import "./styles/callout.css";
41
42
  @import "./styles/code.css";
package/src/index.d.ts CHANGED
@@ -86,6 +86,24 @@ export type NumericValueOptions = { value?: string | number | null; unit?: strin
86
86
  export type DeltaValueOptions = { value?: string | null; tone?: 'success' | 'danger' | 'neutral'; basisId?: string; missing?: string };
87
87
  export declare function formatNumericValue(options?: NumericValueOptions): { text: string; unit: string; missing: string | undefined };
88
88
  export declare function formatDeltaValue(options?: DeltaValueOptions): { text: string; className: string; basisId: string | undefined };
89
+
90
+ /** The smallest band a series is drawn in, as a share of its own reach. */
91
+ export declare const CHART_FLOOR: number;
92
+ export type ChartScaleOptions = { ticks?: number; zero?: boolean; floor?: number; nice?: boolean };
93
+ export type ChartScale = { min: number; max: number; step: number; ticks: number[] };
94
+ export declare function chartScale(values: Iterable<number>, options?: ChartScaleOptions): ChartScale;
95
+ export type BridgeStepInput = { label: string; value?: number; kind?: 'total' | 'change' };
96
+ export type BridgeBar<S> = S & { kind: 'total' | 'change'; value: number; from: number; to: number };
97
+ export declare function bridgeWalk<S extends BridgeStepInput>(steps: Iterable<S>): BridgeBar<S>[];
98
+ export type ChartBox = { left: number; right: number; top: number; bottom: number };
99
+ export type PointTargetLimits = {
100
+ floor: number; min?: number; box: ChartBox;
101
+ column: [number, number]; apart?: Iterable<number>;
102
+ };
103
+ export type ChartTarget = { x: number; y: number; w: number; h: number };
104
+ export declare function pointTarget(
105
+ point: { x: number; y: number }, limits: PointTargetLimits,
106
+ ): ChartTarget;
89
107
  export declare const SCORE: Record<'exact' | 'prefix' | 'wordStart' | 'contains' | 'keyword' | 'description' | 'subsequence', number>;
90
108
  export declare const iconNames: string[];
91
109
  export declare const iconCategories: { name: string; names: string[] }[];
package/src/index.js CHANGED
@@ -35,3 +35,4 @@ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from './logic/pagination.js';
35
35
  export { calloutIcons } from './logic/callout.js';
36
36
  export { formatNumericValue, formatDeltaValue } from './logic/table-values.js';
37
37
  export { accentSwatchStyle } from './logic/accents.js';
38
+ export { CHART_FLOOR, chartScale, bridgeWalk, pointTarget } from './logic/chart.js';
package/src/inline.js CHANGED
@@ -50,6 +50,7 @@ export const styles = {
50
50
  filterBar: read('styles/filter-bar.css'),
51
51
  pagination: read('styles/pagination.css'),
52
52
  stat: read('styles/stat.css'),
53
+ chart: read('styles/chart.css'),
53
54
  empty: read('styles/empty.css'),
54
55
  callout: read('styles/callout.css'),
55
56
  code: read('styles/code.css'),
@@ -86,6 +87,7 @@ export const cssText = [
86
87
  styles.filterBar,
87
88
  styles.pagination,
88
89
  styles.stat,
90
+ styles.chart,
89
91
  styles.empty,
90
92
  styles.callout,
91
93
  styles.code,
@@ -0,0 +1,143 @@
1
+ // Chart arithmetic, shared by the React Chart and by anything that draws the
2
+ // same shapes without it. No DOM, no pixels: a scale and a walk, both pure.
3
+ // why: docs/components.md#react-charts
4
+
5
+ /** The steps a whole-unit axis may take, per decade, smallest first. 2.5 is
6
+ * among them — 25000 is a whole step and the one that turns a four-tick axis
7
+ * into four ticks — but it is taken only where it lands on a whole number, so
8
+ * nothing below the units decade can put 2.5 on an axis. */
9
+ const STEPS = [1, 2, 2.5, 5, 10];
10
+
11
+ /** The smallest band a series is drawn in, as a share of its own reach. Below
12
+ * this, a series that barely moves would be stretched into a cliff. */
13
+ export const CHART_FLOOR = 0.25;
14
+
15
+ const round = (n) => Math.round(n * 1e6) / 1e6;
16
+
17
+ /** The smallest nice step at or above `raw` that is a whole unit. */
18
+ function niceStep(raw) {
19
+ if (!(raw > 0)) return 1;
20
+ const decade = 10 ** Math.floor(Math.log10(raw));
21
+ for (const s of STEPS) {
22
+ const step = round(s * decade);
23
+ if (step >= raw - 1e-9 && step >= 1 && Number.isInteger(step)) return step;
24
+ }
25
+ return Math.max(1, Math.ceil(raw));
26
+ }
27
+
28
+ /**
29
+ * The band a series is drawn in, and the ticks written beside it.
30
+ *
31
+ * @param {Iterable<number>} values every number that has to fit
32
+ * @param {object} [options]
33
+ * @param {number} [options.ticks] how many steps to aim for
34
+ * @param {boolean} [options.zero] keep the zero line inside the band — true for
35
+ * bars, which are read from zero, false for a line read against itself
36
+ * @param {number} [options.floor] the smallest band, as a share of the series'
37
+ * own reach; `CHART_FLOOR` for a sparkline
38
+ * @param {boolean} [options.nice] round the band out to whole steps and return
39
+ * them; false for a chart with no axis
40
+ * @returns {{min: number, max: number, step: number, ticks: number[]}}
41
+ */
42
+ export function chartScale(values, { ticks = 4, zero = true, floor = 0, nice = true } = {}) {
43
+ const finite = [...values].filter((v) => Number.isFinite(v));
44
+ if (finite.length === 0) return { min: 0, max: 1, step: 1, ticks: [0, 1] };
45
+
46
+ let min = Math.min(...finite);
47
+ let max = Math.max(...finite);
48
+ if (zero) { min = Math.min(min, 0); max = Math.max(max, 0); }
49
+
50
+ const reach = Math.max(Math.abs(min), Math.abs(max)) || 1;
51
+ const want = reach * floor;
52
+ if (max - min < want) {
53
+ const pad = (want - (max - min)) / 2;
54
+ min -= pad;
55
+ max += pad;
56
+ if (zero) { min = Math.min(min, 0); max = Math.max(max, 0); }
57
+ }
58
+ if (min === max) { min -= 0.5; max += 0.5; }
59
+
60
+ if (!nice) return { min: round(min), max: round(max), step: 0, ticks: [] };
61
+
62
+ const step = niceStep((max - min) / Math.max(ticks, 1));
63
+ min = Math.floor(round(min / step)) * step;
64
+ max = Math.ceil(round(max / step)) * step;
65
+ const out = [];
66
+ for (let t = min; t <= max + 1e-9; t += step) out.push(round(t));
67
+ return { min: round(min), max: round(max), step, ticks: out };
68
+ }
69
+
70
+ /**
71
+ * One period walked from a starting total through its components to a result.
72
+ *
73
+ * The first step is a total unless it says otherwise; every later step is a
74
+ * component added to the running total. A step marked `total` with no value of
75
+ * its own takes the running total, which is how the closing column is written
76
+ * without repeating a sum the caller already gave piece by piece.
77
+ *
78
+ * @param {Iterable<{label: string, value?: number, kind?: 'total'|'change'}>} steps
79
+ * @returns {Array<{label: string, kind: 'total'|'change', value: number, from: number, to: number}>}
80
+ * each column's own amount and the two ends of the bar that draws it
81
+ */
82
+ export function bridgeWalk(steps) {
83
+ let running = 0;
84
+ return [...steps].map((step, index) => {
85
+ const kind = step.kind || (index === 0 ? 'total' : 'change');
86
+ if (kind === 'total') {
87
+ const value = Number.isFinite(step.value) ? step.value : running;
88
+ running = value;
89
+ return { ...step, kind, value: round(value), from: 0, to: round(value) };
90
+ }
91
+ const value = Number.isFinite(step.value) ? step.value : 0;
92
+ const from = running;
93
+ running += value;
94
+ return { ...step, kind, value: round(value), from: round(from), to: round(running) };
95
+ });
96
+ }
97
+
98
+ const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
99
+
100
+ /**
101
+ * The pointer target over one drawn point, as a rectangle.
102
+ *
103
+ * A point is a mark a pointer has to reach rather than a control, so its target
104
+ * is a transparent box laid over it. The box grows to the floor this pointer
105
+ * asks for, stops at half the clear space to the nearest point on each side,
106
+ * and is then held inside the box the chart draws in — a target that reached
107
+ * past the chart's edge would be clipped there, and a nominal 44 that a
108
+ * drawing clips to 19 is not a target. `min` is the floor it keeps where the
109
+ * column has room. A narrower column owns the whole column instead, so no point
110
+ * answers for the period beside it.
111
+ * why: guidelines/accessibility-floor.md#tap-target-on-a-phone
112
+ *
113
+ * @param {{x: number, y: number}} point the point's centre
114
+ * @param {object} limits
115
+ * @param {number} limits.floor the target this pointer asks for, each way
116
+ * @param {number} [limits.min] the smallest target, where the column has room
117
+ * @param {{left: number, right: number, top: number, bottom: number}} limits.box
118
+ * the chart's own drawn box, which nothing may reach past
119
+ * @param {[number, number]} limits.column the point's own column, left and right
120
+ * @param {Iterable<number>} [limits.apart] the centre, down, of every other
121
+ * point in the same column
122
+ * @returns {{x: number, y: number, w: number, h: number}}
123
+ */
124
+ export function pointTarget({ x, y }, { floor, min = floor, box, column, apart = [] }) {
125
+ const fit = (centre, [outerLo, outerHi], [spaceLo, spaceHi], neighbours) => {
126
+ let lo = spaceLo;
127
+ let hi = spaceHi;
128
+ for (const n of neighbours) {
129
+ const mid = (centre + n) / 2;
130
+ if (n < centre) lo = Math.max(lo, mid);
131
+ if (n > centre) hi = Math.min(hi, mid);
132
+ }
133
+ const size = Math.min(floor, Math.max(0, hi - lo));
134
+ // Centred on the point, then held inside the space it may own, then inside
135
+ // the chart's box. The box is last so the point on the chart's own edge
136
+ // keeps a whole target instead of half of one.
137
+ const owned = clamp(centre - size / 2, lo, Math.max(lo, hi - size));
138
+ return { at: clamp(owned, outerLo, Math.max(outerLo, outerHi - size)), size };
139
+ };
140
+ const across = fit(x, [box.left, box.right], column, []);
141
+ const down = fit(y, [box.top, box.bottom], [box.top, box.bottom], apart);
142
+ return { x: across.at, y: down.at, w: across.size, h: down.size };
143
+ }
@@ -0,0 +1,282 @@
1
+ /* ============================================================================
2
+ * Chart — .ui-chart, the shapes a money dashboard keeps redrawing.
3
+ * .ui-chart__legend one key per series, plus the unfinished-period key
4
+ * .ui-chart__frame the tab stop; the value axis beside the scroller
5
+ * .ui-chart__scroll the scroller, masked on whichever side hides columns
6
+ * .ui-chart__plot the drawing and its column labels
7
+ * .ui-chart__data the same numbers as a table
8
+ * The component sets --ui-chart-h, the plot's height, which the axis matches.
9
+ * why: docs/components.md#react-charts
10
+ * ========================================================================== */
11
+
12
+ .ui-chart {
13
+ --ui-chart-col: var(--space-12);
14
+ /* The ground the marks are drawn on, so an unfinished period and the rim the
15
+ line is carried on read as cut out of it. A chart outside a card names its
16
+ own. */
17
+ --ui-chart-ground: var(--surface);
18
+ /* The tick's own line box. The axis spans the band the gridlines are drawn
19
+ in — the plot less the room a stroke needs at each edge — and is half a
20
+ line box taller at each end, so every tick's middle lands on its gridline
21
+ without taking a single label out of flow. */
22
+ --ui-chart-tick: calc(var(--text-xs) * 1.45);
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--space-3);
26
+ font-size: var(--text-xs);
27
+ color: var(--text);
28
+ }
29
+
30
+ .ui-chart--bridge { --ui-chart-col: calc(var(--space-16) + var(--space-6)); }
31
+ .ui-chart--spark {
32
+ --ui-chart-col: var(--space-6);
33
+ gap: 0;
34
+ min-width: 0;
35
+ }
36
+ /* No axis, so the scroller takes the whole frame rather than the auto column
37
+ the axis would have held. */
38
+ .ui-chart--spark .ui-chart__frame { grid-template-columns: minmax(0, 1fr); }
39
+ /* The box, not the drawing: a phone target does not fit in 32px. Written here
40
+ rather than left to the height attribute, which `.ui-stat__trend svg` beats.
41
+ why: docs/components.md#react-charts */
42
+ .ui-chart--spark .ui-chart__svg { height: var(--ui-chart-box); }
43
+
44
+ /* -- the legend ------------------------------------------------------------ */
45
+
46
+ .ui-chart__legend {
47
+ /* muted-ink: chrome — the key names the marks and is not the chart's words */
48
+ color: var(--muted);
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ align-items: center;
52
+ column-gap: var(--space-4);
53
+ row-gap: var(--space-1);
54
+ margin: 0;
55
+ padding: 0;
56
+ list-style: none;
57
+ }
58
+ .ui-chart__key {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--space-2);
62
+ white-space: nowrap;
63
+ }
64
+ .ui-chart__key-mark { flex: none; }
65
+ .ui-chart__key-swatch { fill: var(--ui-chart-tone); }
66
+ .ui-chart__key-line { fill: none; stroke: var(--ui-chart-tone); stroke-width: 2; stroke-linecap: round; }
67
+ .ui-chart__key-estimated {
68
+ fill: var(--ui-chart-tone);
69
+ stroke: var(--ui-chart-ground);
70
+ stroke-width: 1;
71
+ stroke-dasharray: 2 2;
72
+ }
73
+
74
+ /* -- the frame ------------------------------------------------------------- */
75
+
76
+ .ui-chart__frame {
77
+ display: grid;
78
+ grid-template-columns: auto minmax(0, 1fr);
79
+ align-items: start;
80
+ column-gap: var(--space-2);
81
+ padding-block: calc(var(--ring-width, 2px) + var(--ring-offset, 1px));
82
+ border-radius: var(--radius-xs);
83
+ }
84
+ .ui-chart__frame:focus-visible {
85
+ outline-offset: var(--ring-offset, 1px);
86
+ }
87
+
88
+ .ui-chart__axis {
89
+ /* muted-ink: chrome — a tick names the scale; exact values are reachable through the readout */
90
+ color: var(--muted);
91
+ display: flex;
92
+ flex-direction: column;
93
+ justify-content: space-between;
94
+ height: calc(var(--ui-chart-h) - var(--ui-chart-pad) * 2 + var(--ui-chart-tick));
95
+ margin-block: calc(var(--ui-chart-pad) - var(--ui-chart-tick) / 2);
96
+ text-align: right;
97
+ font-variant-numeric: tabular-nums;
98
+ }
99
+ .ui-chart__tick { line-height: 1.45; }
100
+
101
+ /* The mask fades whichever side still hides periods. It paints no ground of its
102
+ own, so a chart on the card and one on the page both fade correctly — and it
103
+ sits on the scroller rather than the tab stop, which would mask its ring. */
104
+ .ui-chart__scroll {
105
+ overflow-x: auto;
106
+ overflow-y: hidden;
107
+ overscroll-behavior-x: contain;
108
+ }
109
+ .ui-chart__scroll.is-more-before {
110
+ mask-image: linear-gradient(to right, transparent 0, #000 var(--space-6));
111
+ }
112
+ .ui-chart__scroll.is-more-after {
113
+ mask-image: linear-gradient(to right, #000 calc(100% - var(--space-6)), transparent 100%);
114
+ }
115
+ .ui-chart__scroll.is-more-before.is-more-after {
116
+ mask-image: linear-gradient(
117
+ to right, transparent 0, #000 var(--space-6),
118
+ #000 calc(100% - var(--space-6)), transparent 100%);
119
+ }
120
+
121
+ .ui-chart__plot { min-width: calc(var(--ui-chart-col) * var(--ui-chart-cols, 1)); }
122
+ .ui-chart__svg { display: block; }
123
+
124
+ .ui-chart__periods {
125
+ display: grid;
126
+ grid-auto-flow: column;
127
+ grid-auto-columns: 1fr;
128
+ /* The row stood 4px under the plot plus the 2px rule every label reserved for
129
+ the picked one. The rule is gone and the gap keeps that step on the scale
130
+ rather than between two of its steps. */
131
+ padding-top: var(--space-2);
132
+ }
133
+ .ui-chart__period {
134
+ /* muted-ink: chrome — a period label names a column; its values are reachable through the readout */
135
+ color: var(--muted);
136
+ padding-inline: var(--space-1);
137
+ text-align: center;
138
+ line-height: 1.45;
139
+ overflow-wrap: anywhere;
140
+ }
141
+ /* The picked column's label: the body ink the rest of the row gives up, at the
142
+ weight a reader who separates no colours still reads. Selection is otherwise
143
+ the band behind the column and the band alone — the outline the chart draws
144
+ belongs to focus. */
145
+ .ui-chart__period.is-selected {
146
+ color: var(--strong);
147
+ font-weight: 600;
148
+ }
149
+
150
+ /* -- the marks ------------------------------------------------------------- */
151
+
152
+ /* The rules. A hairline at each tick carries a bar's height across twelve columns
153
+ to the label that names it, which is the one job a grid has; it is the kit's
154
+ faintest edge, so it is found when a reader looks for it and not before. The
155
+ zero rule keeps --border-strong above it: the line a signed series is measured
156
+ from leads the ones it is measured against. Both sit under every mark. */
157
+ .ui-chart__grid { stroke: var(--border); stroke-width: 1; }
158
+ .ui-chart__zero { stroke: var(--border-strong); stroke-width: 1; }
159
+
160
+ /* The carry a bridge draws between two steps: the running total leaves one
161
+ column at the height it reached and meets the next where it starts from. It
162
+ takes --border-strong, the zero rule's weight rather than the grid's, because
163
+ it is read along rather than looked up. Solid, like every other rule here: a
164
+ dash would read as a forecast. */
165
+ .ui-chart__carry { stroke: var(--border-strong); stroke-width: 1; }
166
+
167
+ /* The picked column, behind every mark and every rule. A quiet fill rather than
168
+ the accent, which the chart spends on a series; it is wider than any bar, so
169
+ the column is found before the bar in it is read. */
170
+ .ui-chart__band { fill: var(--surface-3); }
171
+
172
+ .ui-chart__bar {
173
+ /* One flat tone, the whole bar. A ramp down the bar was the one thing in the
174
+ drawing that carried no number: it let the middle go while both edges held,
175
+ so a reader comparing two months compared two shaded cylinders. */
176
+ fill: var(--ui-chart-tone);
177
+ stroke: none;
178
+ }
179
+ /* Keep each series' fill weight. Ground-coloured dashes mark unfinished data
180
+ without joining the mirrored bars across the zero channel. */
181
+ .ui-chart__bar.is-estimated {
182
+ fill: var(--ui-chart-tone);
183
+ stroke: var(--ui-chart-ground);
184
+ stroke-width: 1;
185
+ stroke-dasharray: 3 2;
186
+ }
187
+
188
+ /* An isolated value has no segment to draw it. Connected points need no dot. */
189
+ .ui-chart__isolated { fill: var(--ui-chart-tone); stroke: var(--ui-chart-ground); stroke-width: 1; }
190
+
191
+ .ui-chart__line {
192
+ fill: none;
193
+ stroke: var(--ui-chart-tone);
194
+ stroke-width: 2;
195
+ stroke-linecap: round;
196
+ }
197
+ .ui-chart__line.is-estimated { stroke-dasharray: 5 3; }
198
+
199
+ /* The ground carried under the line. The line crosses bars of its own family,
200
+ where hue alone separates them and luminance does not: a stroke of the ground
201
+ each side gives the line the 3:1 it cannot get from the bar behind it. It is
202
+ not drawn where there is nothing to cross — a sparkline has no bars, and at
203
+ text size the casing would eat the line. The line carries no marker of its
204
+ own; a dot on every point drew nothing the line had not already drawn. */
205
+ .ui-chart__line-casing {
206
+ fill: none;
207
+ stroke: var(--ui-chart-ground);
208
+ stroke-width: 4;
209
+ stroke-linecap: round;
210
+ }
211
+ .ui-chart--spark .ui-chart__line-casing { display: none; }
212
+
213
+ .ui-chart--spark .ui-chart__line { stroke-width: 1.6; }
214
+
215
+ /* The edge the readout is placed against: the mark's top. Geometry only — it
216
+ paints nothing. The readout opens above that edge where the frame has the
217
+ room and flips below it where it has not, which near the top of the plot
218
+ means covering the mark and the first axis labels. The guideline asks for
219
+ that flip rather than for a readout outside the part.
220
+ why: guidelines/hover-readouts.md#prefer-above-the-mark */
221
+ .ui-chart__anchor { fill: none; pointer-events: none; }
222
+
223
+ /* The hit area a pointer lands on. It is the series' own band in the column, so
224
+ a pointer between two bars still has a bar to answer with. */
225
+ .ui-chart__hit { fill: transparent; }
226
+
227
+ /* A chart whose columns can be picked says so under the pointer. The key
228
+ compound names the element rather than a class, which is how the kit's
229
+ clickable-chrome gate reads `.ui-seg button` and `.ui-check input`: the mark
230
+ is this <g> and cannot be moved onto a <button>, so there is no button
231
+ chrome to measure. */
232
+ .ui-chart--pick [data-mark] { cursor: pointer; }
233
+
234
+ /* The tones. A series names one; the token behind it is the kit's, and the same
235
+ in both themes' terms. The neutral one is --muted, which is what
236
+ --signal-solid-neutral already is: a mid grey in both themes, where --dim
237
+ would paint a bridge's totals near-black in light and near-white in dark. */
238
+ .ui-chart__tone--accent { --ui-chart-tone: var(--accent); }
239
+ /* The accent's second weight: the same hue, let down towards the ground rather
240
+ than given an opacity, so a mark painted with it is opaque and a reader
241
+ measures one colour. The mix crosses 3:1 at 67% against the card AND against
242
+ the page, in both themes, under all four accents, so 70% ships with room over
243
+ the bar rather than sitting on it — the 40% the restyle's mock drew measures
244
+ 1.99:1 and 2.03:1 and does not ship. */
245
+ .ui-chart__tone--accent-soft {
246
+ --ui-chart-tone: color-mix(in srgb, var(--accent) 70%, var(--ui-chart-ground));
247
+ }
248
+ .ui-chart__tone--good { --ui-chart-tone: var(--green); }
249
+ .ui-chart__tone--bad { --ui-chart-tone: var(--pink); }
250
+ .ui-chart__tone--warn { --ui-chart-tone: var(--amber); }
251
+ .ui-chart__tone--info { --ui-chart-tone: var(--cyan); }
252
+ .ui-chart__tone--neutral { --ui-chart-tone: var(--muted); }
253
+
254
+ /* -- the table ------------------------------------------------------------- */
255
+
256
+ .ui-chart__data > summary {
257
+ width: max-content;
258
+ min-height: var(--tap-min, 44px);
259
+ display: inline-flex;
260
+ align-items: center;
261
+ box-sizing: border-box;
262
+ padding: var(--space-1) var(--space-2);
263
+ border-radius: var(--radius-xs);
264
+ cursor: pointer;
265
+ font-size: var(--text-sm);
266
+ }
267
+ .ui-chart__data[open] > summary { margin-bottom: var(--space-3); }
268
+ /* The table is the one place a phone reader gets exact values, and how wide it
269
+ is depends on the caller's series: it carries them sideways inside the kit's
270
+ own scroller, so the card and the page keep their width whatever it holds.
271
+ Sideways only — there are as many rows as the chart has columns, and capping
272
+ their height would hide the last period behind a second scrollbar. */
273
+ .ui-chart__data .ui-table-scroll { --ui-table-height: none; }
274
+ .ui-chart__data .ui-table { margin: 0; }
275
+ /* Why a period is not final is a sentence, not a label, so it wraps under the
276
+ period it qualifies instead of carrying the money columns another two hundred
277
+ pixels to the right of a 390px card. */
278
+ .ui-chart__data .ui-table tbody th { white-space: normal; }
279
+ .ui-chart__state {
280
+ /* muted-ink: state — the word that marks a period as not final */
281
+ color: var(--muted);
282
+ }