@ultimat3/ui 23.0.0 → 25.0.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/CATALOG.md +119 -15
- package/CLAUDE.md +17 -8
- package/README.md +119 -33
- package/package.json +6 -6
- package/src/a11y.ts +27 -13
- package/src/catalog/build-catalog.ts +2 -2
- package/src/components/Accordion.tsx +8 -2
- package/src/components/AppShell.module.scss +105 -4
- package/src/components/AppShell.tsx +48 -10
- package/src/components/BarChart.module.scss +47 -9
- package/src/components/BarChart.tsx +152 -72
- package/src/components/Card.tsx +52 -16
- package/src/components/ChartFrame.module.scss +188 -0
- package/src/components/ChartFrame.tsx +155 -0
- package/src/components/CommandPalette.module.scss +8 -5
- package/src/components/Container.tsx +46 -9
- package/src/components/DataTable.module.scss +190 -0
- package/src/components/DataTable.tsx +161 -51
- package/src/components/Divider.tsx +0 -2
- package/src/components/DonutChart.module.scss +73 -0
- package/src/components/DonutChart.tsx +154 -0
- package/src/components/Dropzone.module.scss +3 -1
- package/src/components/Dropzone.tsx +17 -8
- package/src/components/ErrorState.tsx +39 -12
- package/src/components/FileInput.module.scss +3 -1
- package/src/components/FileInput.tsx +6 -2
- package/src/components/Form.tsx +17 -4
- package/src/components/Gauge.module.scss +48 -0
- package/src/components/Gauge.tsx +67 -0
- package/src/components/Grid.tsx +34 -13
- package/src/components/Image.tsx +2 -2
- package/src/components/InlineBar.module.scss +23 -0
- package/src/components/InlineBar.tsx +31 -0
- package/src/components/LineChart.module.scss +147 -0
- package/src/components/LineChart.tsx +169 -0
- package/src/components/Menu.tsx +9 -5
- package/src/components/Meter.tsx +3 -3
- package/src/components/PageHeader.tsx +5 -2
- package/src/components/Popover.module.scss +17 -20
- package/src/components/Popover.tsx +14 -7
- package/src/components/Section.tsx +45 -26
- package/src/components/Stack.tsx +59 -18
- package/src/components/Table.module.scss +30 -0
- package/src/components/Table.tsx +14 -3
- package/src/components/Tabs.module.scss +6 -0
- package/src/components/Tabs.tsx +2 -2
- package/src/components/Text.tsx +41 -15
- package/src/components/Textarea.tsx +7 -4
- package/src/components/Toaster.tsx +1 -1
- package/src/components/Toolbar.tsx +7 -6
- package/src/components/Tooltip.module.scss +8 -3
- package/src/components/app-shell-view.ts +6 -4
- package/src/components/bar-chart-view.ts +11 -6
- package/src/components/chart-frame-view.ts +219 -0
- package/src/components/chart-ticks-view.ts +173 -0
- package/src/components/data-table-view.ts +68 -0
- package/src/components/donut-chart-view.ts +120 -0
- package/src/components/file-input-view.ts +32 -13
- package/src/components/gauge-view.ts +43 -0
- package/src/components/heading-node.tsx +62 -0
- package/src/components/image-source.ts +3 -3
- package/src/components/line-chart-view.ts +153 -0
- package/src/components/link-target.ts +21 -1
- package/src/components/meter-view.ts +13 -0
- package/src/components/money-view.ts +2 -7
- package/src/components/qr-matrix.ts +16 -23
- package/src/components/relative-time-view.ts +11 -4
- package/src/form/form-binding.ts +32 -14
- package/src/form/use-form.ts +3 -3
- package/src/global.scss +9 -0
- package/src/icons/build-icons.ts +109 -24
- package/src/index.ts +47 -15
- package/src/jsx-probe.ts +5 -4
- package/src/theme/brand-declarations.ts +113 -0
- package/src/theme/brand.ts +104 -96
- package/src/theme/context.ts +1 -1
- package/src/theme/preset-scifi.ts +99 -0
- package/src/theme/preset-shape.ts +18 -0
- package/src/theme/presets.ts +15 -0
- package/src/theme/shadow-value.ts +39 -0
- package/src/theme/theme.ts +36 -11
- package/src/toast/toast-state.ts +0 -0
- package/src/toast/toast-store.ts +1 -1
- package/src/toast/use-toasts.ts +1 -1
- package/src/tokens/_colors.scss +20 -0
- package/src/tokens/_effects.scss +59 -0
- package/src/tokens/_index.scss +2 -0
- package/src/tokens/_mixins.scss +6 -4
- package/src/tokens/_shadow.scss +16 -0
- package/src/tokens/_touch.scss +39 -0
- package/src/tokens/_typography.scss +14 -0
- package/src/tokens/color-tokens.ts +133 -0
- package/src/tokens/colour-vision.ts +106 -0
- package/src/tokens/contrast-pairs.ts +6 -1
- package/src/tokens/contrast.ts +1 -1
- package/src/tokens/theme.scss +2 -0
- package/src/tokens/tokens.ts +41 -87
- package/src/fake-dom.ts +0 -206
- package/src/sass-probe.ts +0 -55
- package/src/theme/inline-script.ts +0 -49
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
@use '../tokens' as t;
|
|
2
2
|
|
|
3
3
|
.chart {
|
|
4
|
+
display: grid;
|
|
5
|
+
gap: t.space(1);
|
|
6
|
+
inline-size: 100%;
|
|
7
|
+
// The svg does not mirror in a right-to-left page; the keyboard layer over it must not either.
|
|
8
|
+
direction: ltr;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.box {
|
|
12
|
+
position: relative;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.plot {
|
|
4
16
|
display: block;
|
|
5
17
|
inline-size: 100%;
|
|
6
18
|
block-size: auto;
|
|
@@ -26,10 +38,15 @@
|
|
|
26
38
|
}
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
// The second series — what went wrong, stacked on what went right.
|
|
41
|
+
// The second series — what went wrong, stacked on what went right. Its dashed outline is the cue
|
|
42
|
+
// that does not depend on telling red from blue; the legend's swatch carries the same edge.
|
|
30
43
|
.secondary {
|
|
31
44
|
fill: t.role('danger');
|
|
32
45
|
opacity: 0.6;
|
|
46
|
+
stroke: t.role('fg-strong');
|
|
47
|
+
stroke-width: 1;
|
|
48
|
+
stroke-dasharray: 3 2;
|
|
49
|
+
vector-effect: non-scaling-stroke;
|
|
33
50
|
transition: opacity t.duration(fast) t.easing(out);
|
|
34
51
|
|
|
35
52
|
&:hover {
|
|
@@ -44,13 +61,34 @@
|
|
|
44
61
|
.axis {
|
|
45
62
|
@include t.data-text;
|
|
46
63
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
// HTML text, outside the viewBox: it keeps this step at every chart width (issue 494).
|
|
65
|
+
font-size: t.text(xs);
|
|
66
|
+
line-height: t.leading(tight);
|
|
67
|
+
color: t.role('fg-muted');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The busiest value, over the top grid line's end.
|
|
71
|
+
.max {
|
|
72
|
+
justify-self: end;
|
|
73
|
+
}
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
// The first and last keys, under the first and last bars.
|
|
76
|
+
.keys {
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: space-between;
|
|
79
|
+
gap: t.space(2);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Legend keys: the bar's own fill, and the second series' fill with its dashed edge.
|
|
83
|
+
.swatch {
|
|
84
|
+
display: block;
|
|
85
|
+
inline-size: t.space(3);
|
|
86
|
+
block-size: t.space(3);
|
|
87
|
+
border-radius: t.radius(sm);
|
|
88
|
+
background: t.role('accent');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.swatchSecondary {
|
|
92
|
+
border: t.stroke(hairline) dashed t.role('fg-strong');
|
|
93
|
+
background: t.role('danger', 0.6);
|
|
56
94
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// A bar chart of one series — clicks per day, signups per week — or two stacked (done, and failed
|
|
2
|
-
// on top),
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// on top): SVG bars, HTML axis labels. No charting library: the framework's own docs use a
|
|
3
|
+
// sparkline pulling one in as the cautionary example, and a route's JS budget counts raw minified
|
|
4
|
+
// bytes. `<rect>` bars cost nothing to hydrate, so the server-rendered shell IS the chart. The
|
|
5
|
+
// labels sit outside the svg because text inside a viewBox scales with it — a speck at phone width.
|
|
6
|
+
// It stands in a `ChartFrame`: a legend for two series, and a hidden table of the same numbers.
|
|
6
7
|
|
|
7
8
|
import type { JSX } from 'solid-js';
|
|
8
9
|
import { cx } from '../cx';
|
|
10
|
+
import { useUi } from '../theme/context';
|
|
9
11
|
import styles from './BarChart.module.scss';
|
|
10
12
|
import type { ChartPoint } from './bar-chart-view';
|
|
11
13
|
import { BAR_CHART, barRects, GRID_STEPS, gridY, maxOf, secondaryRects } from './bar-chart-view';
|
|
14
|
+
import type { ChartHit, ChartLegendItem } from './ChartFrame';
|
|
15
|
+
import { ChartFrame, chartHits } from './ChartFrame';
|
|
16
|
+
import type { ChartSeries } from './chart-frame-view';
|
|
17
|
+
import { chartNumberFormat, chartTable, cssPercent, hitEdge, pointLabel } from './chart-frame-view';
|
|
12
18
|
|
|
13
19
|
export interface BarChartProps {
|
|
14
20
|
/** The accessible name for the whole chart, already translated. */
|
|
@@ -18,87 +24,161 @@ export interface BarChartProps {
|
|
|
18
24
|
/** Draw the last bar at full strength — the eye lands where the live number is. Default on. */
|
|
19
25
|
highlightLast?: boolean | undefined;
|
|
20
26
|
/**
|
|
21
|
-
* Names of the two series, already translated, read by each rect's `<title
|
|
22
|
-
* point carries `secondary`: a stacked bar with no name for its
|
|
27
|
+
* Names of the two series, already translated, read by each rect's `<title>`, the legend and
|
|
28
|
+
* the data table. Give them when a point carries `secondary`: a stacked bar with no name for its
|
|
29
|
+
* top is two numbers and no key.
|
|
23
30
|
*/
|
|
24
31
|
seriesLabels?: { readonly primary: string; readonly secondary: string } | undefined;
|
|
32
|
+
/** The key column's head in the data table, already translated — "Day". */
|
|
33
|
+
keyLabel?: string | undefined;
|
|
34
|
+
/** Formats every value. Default: `Intl.NumberFormat` in the page's locale. */
|
|
35
|
+
format?: ((value: number) => string) | undefined;
|
|
36
|
+
/** Give every bar a tab stop and a hover/focus readout. Default off: no tab stops. */
|
|
37
|
+
focusable?: boolean | undefined;
|
|
38
|
+
/** Show the caption above the plot. Default off — it names the figure either way. */
|
|
39
|
+
showCaption?: boolean | undefined;
|
|
40
|
+
/** Lands on the root `<figure>`, which holds the svg and its HTML axis labels. */
|
|
25
41
|
class?: string | undefined;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export function BarChart(props: BarChartProps): JSX.Element {
|
|
45
|
+
const ui = useUi();
|
|
46
|
+
const format = (value: number): string => (props.format ?? chartNumberFormat(ui.locale))(value);
|
|
29
47
|
const rects = (): readonly ReturnType<typeof barRects>[number][] => barRects(props.points);
|
|
30
48
|
const tops = (): ReturnType<typeof secondaryRects> => secondaryRects(props.points);
|
|
31
49
|
const named = (label: string | undefined, value: number): string =>
|
|
32
|
-
label === undefined ?
|
|
50
|
+
label === undefined ? format(value) : `${label} ${format(value)}`;
|
|
33
51
|
const lastIndex = (): number => props.points.length - 1;
|
|
34
|
-
const first = (): ChartPoint | undefined => props.points[0];
|
|
35
|
-
const last = (): ChartPoint | undefined => props.points.at(-1);
|
|
36
52
|
const highlight = (): boolean => props.highlightLast !== false;
|
|
53
|
+
const stacked = (): boolean => props.points.some((point) => (point.secondary ?? 0) > 0);
|
|
54
|
+
|
|
55
|
+
const series = (): readonly ChartSeries[] => [
|
|
56
|
+
{ label: props.seriesLabels?.primary ?? props.label, values: props.points.map((p) => p.value) },
|
|
57
|
+
...(props.seriesLabels !== undefined || stacked()
|
|
58
|
+
? [
|
|
59
|
+
{
|
|
60
|
+
label: props.seriesLabels?.secondary ?? '',
|
|
61
|
+
values: props.points.map((p) => p.secondary ?? 0),
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: []),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const legend = (): readonly ChartLegendItem[] | undefined =>
|
|
68
|
+
props.seriesLabels === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: [
|
|
71
|
+
{ label: props.seriesLabels.primary, swatch: <span class={styles['swatch']} /> },
|
|
72
|
+
{
|
|
73
|
+
label: props.seriesLabels.secondary,
|
|
74
|
+
swatch: <span class={cx(styles['swatch'], styles['swatchSecondary'])} />,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const hits = (): readonly ChartHit[] =>
|
|
79
|
+
props.points.flatMap((point, index) => {
|
|
80
|
+
const rect = rects()[index];
|
|
81
|
+
if (rect === undefined) return [];
|
|
82
|
+
const top = tops()[index] ?? null;
|
|
83
|
+
const x = rect.x + rect.width / 2;
|
|
84
|
+
const own = (y: number, label: string | undefined, value: number): ChartHit => ({
|
|
85
|
+
x: cssPercent(x, BAR_CHART.width),
|
|
86
|
+
y: cssPercent(y, BAR_CHART.height),
|
|
87
|
+
label: pointLabel(point.key, label, format(value)),
|
|
88
|
+
edge: hitEdge(x, BAR_CHART.width),
|
|
89
|
+
});
|
|
90
|
+
const first = own(rect.y, props.seriesLabels?.primary, point.value);
|
|
91
|
+
return top === null
|
|
92
|
+
? [first]
|
|
93
|
+
: [first, own(top.y, props.seriesLabels?.secondary, point.secondary ?? 0)];
|
|
94
|
+
});
|
|
95
|
+
|
|
37
96
|
return (
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
<ChartFrame
|
|
98
|
+
label={props.label}
|
|
99
|
+
showCaption={props.showCaption}
|
|
100
|
+
class={props.class}
|
|
101
|
+
legend={legend()}
|
|
102
|
+
table={chartTable({
|
|
103
|
+
caption: props.label,
|
|
104
|
+
keyLabel: props.keyLabel,
|
|
105
|
+
keys: props.points.map((point) => point.key),
|
|
106
|
+
series: series(),
|
|
107
|
+
format,
|
|
108
|
+
})}
|
|
43
109
|
>
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
<div class={styles['chart']}>
|
|
111
|
+
{/* aria-hidden: the svg is the image and names itself; a bare "9" read aloud is noise. */}
|
|
112
|
+
<span class={cx(styles['axis'], styles['max'])} data-axis="max" aria-hidden="true">
|
|
113
|
+
{format(maxOf(props.points))}
|
|
114
|
+
</span>
|
|
115
|
+
<div class={styles['box']}>
|
|
116
|
+
<svg
|
|
117
|
+
role="img"
|
|
118
|
+
aria-label={props.label}
|
|
119
|
+
viewBox={`0 0 ${BAR_CHART.width} ${BAR_CHART.height}`}
|
|
120
|
+
class={styles['plot']}
|
|
121
|
+
>
|
|
122
|
+
{GRID_STEPS.map((step) => (
|
|
123
|
+
<line
|
|
124
|
+
class={styles['grid']}
|
|
125
|
+
x1={0}
|
|
126
|
+
x2={BAR_CHART.width}
|
|
127
|
+
y1={gridY(step)}
|
|
128
|
+
y2={gridY(step)}
|
|
129
|
+
/>
|
|
130
|
+
))}
|
|
131
|
+
{props.points.map((point, index) => {
|
|
132
|
+
const rect = rects()[index];
|
|
133
|
+
if (rect === undefined) return null;
|
|
134
|
+
const top = tops()[index] ?? null;
|
|
135
|
+
const last = highlight() && index === lastIndex();
|
|
136
|
+
return (
|
|
137
|
+
<g>
|
|
138
|
+
<rect
|
|
139
|
+
class={cx(styles['bar'], last && styles['last'])}
|
|
140
|
+
x={rect.x}
|
|
141
|
+
y={rect.y}
|
|
142
|
+
width={rect.width}
|
|
143
|
+
height={rect.height}
|
|
144
|
+
rx={2}
|
|
145
|
+
data-bar="true"
|
|
146
|
+
>
|
|
147
|
+
<title>{`${point.key}: ${named(props.seriesLabels?.primary, point.value)}`}</title>
|
|
148
|
+
</rect>
|
|
149
|
+
{top === null ? null : (
|
|
150
|
+
<rect
|
|
151
|
+
class={cx(styles['secondary'], last && styles['last'])}
|
|
152
|
+
x={top.x}
|
|
153
|
+
y={top.y}
|
|
154
|
+
width={top.width}
|
|
155
|
+
height={top.height}
|
|
156
|
+
rx={2}
|
|
157
|
+
data-series="secondary"
|
|
158
|
+
>
|
|
159
|
+
<title>{`${point.key}: ${named(props.seriesLabels?.secondary, point.secondary ?? 0)}`}</title>
|
|
160
|
+
</rect>
|
|
161
|
+
)}
|
|
162
|
+
</g>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</svg>
|
|
166
|
+
{props.focusable === true ? chartHits(hits()) : null}
|
|
167
|
+
</div>
|
|
168
|
+
{props.points.length === 0 ? null : (
|
|
169
|
+
<div class={styles['keys']} aria-hidden="true">
|
|
170
|
+
<span class={styles['axis']} data-axis="first">
|
|
171
|
+
{props.points[0]?.key}
|
|
172
|
+
</span>
|
|
173
|
+
{/* One point: its bar is both first and last, so its key is named once. */}
|
|
174
|
+
{props.points.length === 1 ? null : (
|
|
175
|
+
<span class={styles['axis']} data-axis="last">
|
|
176
|
+
{props.points.at(-1)?.key}
|
|
177
|
+
</span>
|
|
85
178
|
)}
|
|
86
|
-
</
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{maxOf(props.points)}
|
|
91
|
-
</text>
|
|
92
|
-
{first() === undefined ? null : (
|
|
93
|
-
<text class={styles['axis']} x={0} y={BAR_CHART.height - 3} text-anchor="start">
|
|
94
|
-
{first()?.key}
|
|
95
|
-
</text>
|
|
96
|
-
)}
|
|
97
|
-
{last() === undefined ? null : (
|
|
98
|
-
<text class={styles['axis']} x={BAR_CHART.width} y={BAR_CHART.height - 3} text-anchor="end">
|
|
99
|
-
{last()?.key}
|
|
100
|
-
</text>
|
|
101
|
-
)}
|
|
102
|
-
</svg>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</ChartFrame>
|
|
103
183
|
);
|
|
104
184
|
}
|
package/src/components/Card.tsx
CHANGED
|
@@ -21,20 +21,56 @@ export interface CardProps {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function Card(props: CardProps): JSX.Element {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</
|
|
39
|
-
|
|
24
|
+
const cls = (): string =>
|
|
25
|
+
cx(
|
|
26
|
+
styles['card'],
|
|
27
|
+
styles[`elevation-${props.elevation ?? 'sm'}`],
|
|
28
|
+
props.interactive === true && styles['interactive'],
|
|
29
|
+
props.class,
|
|
30
|
+
);
|
|
31
|
+
const style = (): JSX.CSSProperties => ({
|
|
32
|
+
'--card-padding': `var(--space-${props.padding ?? 5})`,
|
|
33
|
+
});
|
|
34
|
+
const header = (): JSX.Element =>
|
|
35
|
+
props.header === undefined ? null : <div class={styles['header']}>{props.header}</div>;
|
|
36
|
+
const body = (): JSX.Element => <div class={styles['body']}>{props.children}</div>;
|
|
37
|
+
const footer = (): JSX.Element =>
|
|
38
|
+
props.footer === undefined ? null : <div class={styles['footer']}>{props.footer}</div>;
|
|
39
|
+
|
|
40
|
+
// One intrinsic per case, never `const Tag = props.as; <Tag>`: the island build compiles a
|
|
41
|
+
// capitalised tag to `createComponent(Tag)` and calls the string (#488, `intrinsic-root.test.ts`).
|
|
42
|
+
switch (props.as ?? 'div') {
|
|
43
|
+
case 'article':
|
|
44
|
+
return (
|
|
45
|
+
<article class={cls()} style={style()}>
|
|
46
|
+
{header()}
|
|
47
|
+
{body()}
|
|
48
|
+
{footer()}
|
|
49
|
+
</article>
|
|
50
|
+
);
|
|
51
|
+
case 'section':
|
|
52
|
+
return (
|
|
53
|
+
<section class={cls()} style={style()}>
|
|
54
|
+
{header()}
|
|
55
|
+
{body()}
|
|
56
|
+
{footer()}
|
|
57
|
+
</section>
|
|
58
|
+
);
|
|
59
|
+
case 'li':
|
|
60
|
+
return (
|
|
61
|
+
<li class={cls()} style={style()}>
|
|
62
|
+
{header()}
|
|
63
|
+
{body()}
|
|
64
|
+
{footer()}
|
|
65
|
+
</li>
|
|
66
|
+
);
|
|
67
|
+
case 'div':
|
|
68
|
+
return (
|
|
69
|
+
<div class={cls()} style={style()}>
|
|
70
|
+
{header()}
|
|
71
|
+
{body()}
|
|
72
|
+
{footer()}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
40
76
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
@use '../tokens' as t;
|
|
2
|
+
|
|
3
|
+
// The figure is the container every chart's layout asks about: a 390px phone column and a
|
|
4
|
+
// 1440px dashboard tile are the same component at two container widths, never two components.
|
|
5
|
+
.frame {
|
|
6
|
+
@include t.container(chart-frame);
|
|
7
|
+
// The visually-hidden table is absolute; this keeps it inside the figure, not the page.
|
|
8
|
+
position: relative;
|
|
9
|
+
display: grid;
|
|
10
|
+
gap: t.space(2);
|
|
11
|
+
inline-size: 100%;
|
|
12
|
+
margin: 0;
|
|
13
|
+
|
|
14
|
+
// A series' colour is its slot's role, keyed on `data-series` so every chart (and its legend)
|
|
15
|
+
// reads one rule. `chart-1` … `chart-8` are the categorical roles, AA against both surfaces.
|
|
16
|
+
@for $slot from 1 through 8 {
|
|
17
|
+
[data-series='#{$slot}'] {
|
|
18
|
+
--series: #{t.role('chart-#{$slot}')};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.caption {
|
|
24
|
+
@include t.label-caps;
|
|
25
|
+
color: t.role('fg-muted');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.hidden {
|
|
29
|
+
@include t.visually-hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.layout {
|
|
33
|
+
display: grid;
|
|
34
|
+
gap: t.space(3);
|
|
35
|
+
min-inline-size: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.plot {
|
|
39
|
+
min-inline-size: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Narrow first: the legend is a wrapping row under the plot.
|
|
43
|
+
.legend {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: t.space(1) t.space(4);
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding: 0;
|
|
49
|
+
list-style: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Wide enough for a plot AND a column beside it: the legend stands at the plot's end edge.
|
|
53
|
+
@include t.container-query(chart-frame, 40rem) {
|
|
54
|
+
.withLegend {
|
|
55
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
56
|
+
align-items: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.withLegend .legend {
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
flex-wrap: nowrap;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.item {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: t.space(2);
|
|
69
|
+
min-inline-size: 0;
|
|
70
|
+
font-size: t.text(sm);
|
|
71
|
+
color: t.role('fg');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.swatch {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
flex: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.name {
|
|
80
|
+
@include t.truncate;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.value {
|
|
84
|
+
@include t.data-text;
|
|
85
|
+
color: t.role('fg-strong');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.detail {
|
|
89
|
+
@include t.data-text;
|
|
90
|
+
color: t.role('fg-muted');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.key {
|
|
94
|
+
display: block;
|
|
95
|
+
inline-size: t.space(6);
|
|
96
|
+
block-size: t.space(3);
|
|
97
|
+
overflow: visible;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.keyLine {
|
|
101
|
+
fill: none;
|
|
102
|
+
stroke: var(--series);
|
|
103
|
+
stroke-width: 2;
|
|
104
|
+
stroke-linecap: round;
|
|
105
|
+
vector-effect: non-scaling-stroke;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.keyMarker {
|
|
109
|
+
fill: var(--series);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- The keyboard layer ------------------------------------------------------
|
|
113
|
+
// Over the plot box, which every chart sheet makes `position: relative`. The layer lets the
|
|
114
|
+
// pointer through to nothing in particular; only the points themselves are targets.
|
|
115
|
+
.hits {
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: 0;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.hit {
|
|
122
|
+
// A small visible dot, a finger-sized target: the pseudo-element grows the hit area.
|
|
123
|
+
@include t.touch-target($extend: true);
|
|
124
|
+
position: absolute;
|
|
125
|
+
inset-block-start: var(--y);
|
|
126
|
+
inset-inline-start: var(--x);
|
|
127
|
+
inline-size: t.space(3);
|
|
128
|
+
block-size: t.space(3);
|
|
129
|
+
// Centred on the point by half its own size — margins, not a percentage translate, so the rule
|
|
130
|
+
// reads the same in either direction (`rtl-sheets.test.ts`).
|
|
131
|
+
margin-block-start: calc(#{t.space(3)} / -2);
|
|
132
|
+
margin-inline-start: calc(#{t.space(3)} / -2);
|
|
133
|
+
border-radius: t.radius(full);
|
|
134
|
+
pointer-events: auto;
|
|
135
|
+
cursor: default;
|
|
136
|
+
|
|
137
|
+
&:hover,
|
|
138
|
+
&:focus-visible {
|
|
139
|
+
background: t.role('fg-strong', 0.18);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@include t.focus-ring;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The readout: a zero-width anchor over the point whose bubble overflows it equally both ways
|
|
146
|
+
// (flex `center`), so it is centred with no translate. Visible on hover and keyboard focus only.
|
|
147
|
+
.readout {
|
|
148
|
+
position: absolute;
|
|
149
|
+
inset-block-end: calc(100% + #{t.space(2)});
|
|
150
|
+
inset-inline-start: 50%;
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
inline-size: 0;
|
|
154
|
+
visibility: hidden;
|
|
155
|
+
opacity: 0;
|
|
156
|
+
transition: opacity t.duration(fast) t.easing(out);
|
|
157
|
+
|
|
158
|
+
.hit:hover > &,
|
|
159
|
+
.hit:focus-visible > & {
|
|
160
|
+
visibility: visible;
|
|
161
|
+
opacity: 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A point at the edge of the plot opens its readout inward, so it never leaves the box.
|
|
165
|
+
.hit[data-edge='start'] > & {
|
|
166
|
+
inset-inline-start: 0;
|
|
167
|
+
justify-content: flex-start;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.hit[data-edge='end'] > & {
|
|
171
|
+
inset-inline-start: auto;
|
|
172
|
+
inset-inline-end: 0;
|
|
173
|
+
justify-content: flex-end;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.bubble {
|
|
178
|
+
@include t.data-text;
|
|
179
|
+
flex: none;
|
|
180
|
+
padding: t.space(1) t.space(2);
|
|
181
|
+
border: t.stroke(hairline) solid t.role('line');
|
|
182
|
+
border-radius: t.radius(sm);
|
|
183
|
+
background: t.role('surface-raised');
|
|
184
|
+
box-shadow: t.shadow(md);
|
|
185
|
+
color: t.role('fg-strong');
|
|
186
|
+
font-size: t.text(xs);
|
|
187
|
+
white-space: nowrap;
|
|
188
|
+
}
|