@ultimat3/ui 29.4.0 → 30.1.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 +491 -24
- package/CLAUDE.md +23 -16
- package/README.md +338 -33
- package/package.json +16 -5
- package/src/components/AppShell.module.scss +23 -7
- package/src/components/AppShell.tsx +9 -0
- package/src/components/BarChart.module.scss +26 -0
- package/src/components/BarChart.tsx +242 -93
- package/src/components/Board.module.scss +30 -0
- package/src/components/Board.tsx +47 -0
- package/src/components/BottomNav.module.scss +49 -0
- package/src/components/BottomNav.tsx +57 -0
- package/src/components/Calendar.module.scss +118 -0
- package/src/components/Calendar.tsx +246 -0
- package/src/components/ChartFrame.module.scss +40 -3
- package/src/components/ChartFrame.tsx +84 -28
- package/src/components/CodeBlock.module.scss +101 -0
- package/src/components/CodeBlock.tsx +76 -0
- package/src/components/CommandPalette.module.scss +25 -0
- package/src/components/CommandPalette.tsx +50 -22
- package/src/components/ConfirmDialog.module.scss +36 -0
- package/src/components/ConfirmDialog.tsx +188 -0
- package/src/components/DataTable.tsx +92 -33
- package/src/components/DatePicker.module.scss +37 -0
- package/src/components/DatePicker.tsx +134 -0
- package/src/components/DateRangePicker.module.scss +54 -0
- package/src/components/DateRangePicker.tsx +156 -0
- package/src/components/DescriptionList.module.scss +46 -0
- package/src/components/DescriptionList.tsx +38 -0
- package/src/components/DiffView.module.scss +124 -0
- package/src/components/DiffView.tsx +169 -0
- package/src/components/DonutChart.tsx +13 -4
- package/src/components/Drawer.module.scss +40 -0
- package/src/components/Drawer.tsx +56 -0
- package/src/components/Gauge.module.scss +8 -0
- package/src/components/Gauge.tsx +40 -13
- package/src/components/HeatStrip.module.scss +133 -0
- package/src/components/HeatStrip.tsx +64 -0
- package/src/components/JsonView.module.scss +64 -0
- package/src/components/JsonView.tsx +68 -0
- package/src/components/LineChart.tsx +45 -18
- package/src/components/LiveIndicator.module.scss +44 -0
- package/src/components/LiveIndicator.tsx +45 -0
- package/src/components/LogViewer.module.scss +102 -0
- package/src/components/LogViewer.tsx +123 -0
- package/src/components/Markdown.module.scss +87 -0
- package/src/components/Markdown.tsx +84 -0
- package/src/components/Meter.module.scss +7 -0
- package/src/components/Meter.tsx +16 -0
- package/src/components/NavList.module.scss +66 -0
- package/src/components/NavList.tsx +95 -0
- package/src/components/Pagination.module.scss +6 -2
- package/src/components/Pagination.tsx +57 -13
- package/src/components/Popover.tsx +12 -2
- package/src/components/PullToRefresh.module.scss +41 -0
- package/src/components/PullToRefresh.tsx +108 -0
- package/src/components/Radio.module.scss +51 -0
- package/src/components/Radio.tsx +12 -1
- package/src/components/SearchableSelect.module.scss +216 -0
- package/src/components/SearchableSelect.tsx +331 -0
- package/src/components/SearchableSelectChips.tsx +41 -0
- package/src/components/SearchableSelectNative.tsx +89 -0
- package/src/components/ShortcutSheet.module.scss +33 -0
- package/src/components/ShortcutSheet.tsx +67 -0
- package/src/components/Slider.module.scss +50 -0
- package/src/components/Slider.tsx +68 -0
- package/src/components/SortableList.module.scss +72 -0
- package/src/components/SortableList.tsx +42 -0
- package/src/components/Sparkline.module.scss +10 -2
- package/src/components/Sparkline.tsx +27 -10
- package/src/components/SplitPane.module.scss +39 -0
- package/src/components/SplitPane.tsx +96 -0
- package/src/components/StatTile.module.scss +29 -3
- package/src/components/StatTile.tsx +75 -16
- package/src/components/Stepper.module.scss +78 -0
- package/src/components/Stepper.tsx +73 -0
- package/src/components/Tabs.module.scss +13 -1
- package/src/components/Tabs.tsx +77 -1
- package/src/components/TerminalView.module.scss +68 -0
- package/src/components/TerminalView.tsx +97 -0
- package/src/components/TimeRangePicker.module.scss +85 -0
- package/src/components/TimeRangePicker.tsx +91 -0
- package/src/components/Timeline.module.scss +74 -0
- package/src/components/Timeline.tsx +60 -0
- package/src/components/Tree.module.scss +98 -0
- package/src/components/Tree.tsx +185 -0
- package/src/components/VirtualList.module.scss +60 -0
- package/src/components/VirtualList.tsx +251 -0
- package/src/components/app-shell-view.ts +4 -1
- package/src/components/bar-chart-view.ts +121 -39
- package/src/components/board-view.ts +25 -0
- package/src/components/bottom-nav-view.ts +18 -0
- package/src/components/calendar-view.ts +226 -0
- package/src/components/chart-checks.ts +59 -0
- package/src/components/chart-frame-view.ts +33 -2
- package/src/components/chart-reference-view.ts +41 -0
- package/src/components/command-palette-view.ts +47 -6
- package/src/components/confirm-dialog-view.ts +47 -0
- package/src/components/data-table-rows.ts +93 -0
- package/src/components/date-picker-view.ts +38 -0
- package/src/components/date-range-view.ts +46 -0
- package/src/components/diff-view-view.ts +85 -0
- package/src/components/drawer-gesture-view.ts +33 -0
- package/src/components/gauge-view.ts +29 -0
- package/src/components/heat-strip-view.ts +68 -0
- package/src/components/json-view-view.ts +54 -0
- package/src/components/line-chart-view.ts +87 -25
- package/src/components/log-viewer-view.ts +51 -0
- package/src/components/markdown-nodes.tsx +150 -0
- package/src/components/nav-list-view.ts +37 -0
- package/src/components/pull-refresh-view.ts +93 -0
- package/src/components/rank-options.ts +61 -0
- package/src/components/searchable-select-keys.ts +118 -0
- package/src/components/searchable-select-view.ts +187 -0
- package/src/components/slider-view.ts +47 -0
- package/src/components/sort-pointer.ts +141 -0
- package/src/components/sort-surface.tsx +231 -0
- package/src/components/sparkline-view.ts +74 -14
- package/src/components/split-pane-view.ts +69 -0
- package/src/components/stat-delta.ts +54 -11
- package/src/components/stepper-view.ts +38 -0
- package/src/components/thunk.ts +9 -0
- package/src/components/timeline-view.ts +29 -0
- package/src/components/tree-node.tsx +133 -0
- package/src/components/tree-view.ts +185 -0
- package/src/components/use-command-palette.ts +153 -0
- package/src/components/virtual-list-anchor.ts +35 -0
- package/src/components/virtual-list-observer.ts +58 -0
- package/src/components/virtual-list-view.ts +145 -0
- package/src/content/ansi.ts +144 -0
- package/src/content/heading-slug.ts +32 -0
- package/src/content/highlight.ts +129 -0
- package/src/content/languages/all.ts +21 -0
- package/src/content/languages/diff.ts +14 -0
- package/src/content/languages/html.ts +16 -0
- package/src/content/languages/json.ts +15 -0
- package/src/content/languages/shell.ts +20 -0
- package/src/content/languages/sql.ts +27 -0
- package/src/content/languages/ts.ts +26 -0
- package/src/content/languages/yaml.ts +18 -0
- package/src/content/line-diff.ts +191 -0
- package/src/content/markdown-inline.ts +368 -0
- package/src/content/markdown-parse.ts +267 -0
- package/src/content/markdown-tree.ts +58 -0
- package/src/content/unified-patch.ts +53 -0
- package/src/drag/pointer-drag.ts +185 -0
- package/src/drag/sort-controller.ts +196 -0
- package/src/drag/sort-view.ts +201 -0
- package/src/errors.ts +1 -1
- package/src/form/field-path.ts +7 -33
- package/src/form/form-binding.ts +5 -8
- package/src/form/form-issue.ts +30 -36
- package/src/form/form-values.ts +3 -2
- package/src/hotkeys-view.ts +150 -0
- package/src/i18n-keys.ts +7 -50
- package/src/index.ts +214 -15
- package/src/list-url.ts +123 -0
- package/src/live-window.ts +23 -0
- package/src/roving.ts +2 -0
- package/src/theme/inert-runtime.ts +4 -0
- package/src/theme/runtime-slot.ts +2 -2
- package/src/theme/solid-adapter.ts +11 -5
- package/src/time-range-url.ts +200 -0
- package/src/tokens/_mixins.scss +1 -1
- package/src/tokens/_touch.scss +30 -1
- package/src/tokens/contrast-pairs.ts +12 -0
- package/src/ui-keys.ts +126 -0
- package/src/use-hotkeys.ts +145 -0
|
@@ -58,6 +58,27 @@
|
|
|
58
58
|
opacity: 1;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// A tone or an n-series slot: the frame's `--series` (`ChartFrame.module.scss`) is the colour.
|
|
62
|
+
.bar[data-tone],
|
|
63
|
+
.bar[data-series] {
|
|
64
|
+
fill: var(--series);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Below zero, a hatch-free cue that does not lean on colour: the bar's edge is dashed.
|
|
68
|
+
.bar[data-sign='negative'] {
|
|
69
|
+
stroke: t.role('fg-strong');
|
|
70
|
+
stroke-width: 1;
|
|
71
|
+
stroke-dasharray: 2 2;
|
|
72
|
+
vector-effect: non-scaling-stroke;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The real zero line, drawn only when the range crosses it: what a negative bar hangs from.
|
|
76
|
+
.zero {
|
|
77
|
+
stroke: t.role('fg-muted');
|
|
78
|
+
stroke-width: 1;
|
|
79
|
+
vector-effect: non-scaling-stroke;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
.axis {
|
|
62
83
|
@include t.data-text;
|
|
63
84
|
|
|
@@ -88,6 +109,11 @@
|
|
|
88
109
|
background: t.role('accent');
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
.swatch[data-tone],
|
|
113
|
+
.swatch[data-series] {
|
|
114
|
+
background: var(--series);
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
.swatchSecondary {
|
|
92
118
|
border: t.stroke(hairline) dashed t.role('fg-strong');
|
|
93
119
|
background: t.role('danger', 0.6);
|
|
@@ -1,39 +1,55 @@
|
|
|
1
|
-
// A bar chart of one series — clicks per day, signups per week —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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.
|
|
1
|
+
// A bar chart of one series — clicks per day, signups per week — two stacked (done, and failed on
|
|
2
|
+
// top), or n side by side over shared keys: SVG bars, HTML axis labels. Signed: a loss hangs below
|
|
3
|
+
// a drawn zero line, a gap is no bar. No charting library: `<rect>` bars cost nothing to hydrate,
|
|
4
|
+
// so the server-rendered shell IS the chart. It stands in a `ChartFrame`: legend, hidden table.
|
|
7
5
|
|
|
8
6
|
import type { JSX } from 'solid-js';
|
|
9
7
|
import { cx } from '../cx';
|
|
8
|
+
import { UI_KEYS } from '../i18n-keys';
|
|
10
9
|
import { useUi } from '../theme/context';
|
|
11
10
|
import styles from './BarChart.module.scss';
|
|
12
|
-
import type { ChartPoint } from './bar-chart-view';
|
|
13
|
-
import {
|
|
11
|
+
import type { BarExtent, BarRect, ChartPoint } from './bar-chart-view';
|
|
12
|
+
import {
|
|
13
|
+
BAR_CHART,
|
|
14
|
+
barExtent,
|
|
15
|
+
barRects,
|
|
16
|
+
GRID_STEPS,
|
|
17
|
+
gridY,
|
|
18
|
+
groupedBarRects,
|
|
19
|
+
secondaryRects,
|
|
20
|
+
seriesExtent,
|
|
21
|
+
valueY,
|
|
22
|
+
zeroY,
|
|
23
|
+
} from './bar-chart-view';
|
|
14
24
|
import type { ChartHit, ChartLegendItem } from './ChartFrame';
|
|
15
|
-
import { ChartFrame,
|
|
25
|
+
import { ChartFrame, ChartHits, ReferenceLabels, ReferenceRules } from './ChartFrame';
|
|
26
|
+
import { checkSeries } from './chart-checks';
|
|
16
27
|
import type { ChartSeries } from './chart-frame-view';
|
|
17
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
chartNumberFormat,
|
|
30
|
+
chartTable,
|
|
31
|
+
cssPercent,
|
|
32
|
+
hitEdge,
|
|
33
|
+
hitIds,
|
|
34
|
+
pointLabel,
|
|
35
|
+
seriesStyle,
|
|
36
|
+
} from './chart-frame-view';
|
|
37
|
+
import type { ChartReference } from './chart-reference-view';
|
|
38
|
+
import { referenceLines, referenceValues } from './chart-reference-view';
|
|
39
|
+
import type { Tone } from './variants';
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
interface BarChartBase {
|
|
20
42
|
/** The accessible name for the whole chart, already translated. */
|
|
21
43
|
label: string;
|
|
22
|
-
/** Oldest first. Every bar comes from exactly this array — the caller zero-fills gaps. */
|
|
23
|
-
points: readonly ChartPoint[];
|
|
24
44
|
/** Draw the last bar at full strength — the eye lands where the live number is. Default on. */
|
|
25
45
|
highlightLast?: boolean | undefined;
|
|
26
|
-
/**
|
|
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.
|
|
30
|
-
*/
|
|
31
|
-
seriesLabels?: { readonly primary: string; readonly secondary: string } | undefined;
|
|
32
46
|
/** The key column's head in the data table, already translated — "Day". */
|
|
33
47
|
keyLabel?: string | undefined;
|
|
34
48
|
/** Formats every value. Default: `Intl.NumberFormat` in the page's locale. */
|
|
35
49
|
format?: ((value: number) => string) | undefined;
|
|
36
|
-
/**
|
|
50
|
+
/** Dashed rules at a value — a target, a limit. Each widens the range so it stays on the plot. */
|
|
51
|
+
references?: readonly ChartReference[] | undefined;
|
|
52
|
+
/** Give every bar a tab stop and a hover/focus/tap readout. Default off: no tab stops. */
|
|
37
53
|
focusable?: boolean | undefined;
|
|
38
54
|
/** Show the caption above the plot. Default off — it names the figure either way. */
|
|
39
55
|
showCaption?: boolean | undefined;
|
|
@@ -41,58 +57,206 @@ export interface BarChartProps {
|
|
|
41
57
|
class?: string | undefined;
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
/** One series, or two stacked: a point per bar. */
|
|
61
|
+
export interface BarChartPointsProps extends BarChartBase {
|
|
62
|
+
/** Oldest first. `value: null` is unknown — no bar, and the table says so. Never zero-fill. */
|
|
63
|
+
points: readonly ChartPoint[];
|
|
64
|
+
/** Names of the two series, already translated, when a point carries `secondary`. */
|
|
65
|
+
seriesLabels?: { readonly primary: string; readonly secondary: string } | undefined;
|
|
66
|
+
/** The primary bars' colour role, in place of `accent` — a failure count in `danger`. */
|
|
67
|
+
tone?: Tone | undefined;
|
|
68
|
+
keys?: undefined;
|
|
69
|
+
series?: undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** n series side by side over shared keys; each series' `tone` overrides its slot colour. */
|
|
73
|
+
export interface BarChartSeriesProps extends BarChartBase {
|
|
74
|
+
keys: readonly string[];
|
|
75
|
+
/** `values[i]` belongs to `keys[i]`, exactly one per key — `null` for a gap. */
|
|
76
|
+
series: readonly ChartSeries[];
|
|
77
|
+
points?: undefined;
|
|
78
|
+
seriesLabels?: undefined;
|
|
79
|
+
tone?: undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type BarChartProps = BarChartPointsProps | BarChartSeriesProps;
|
|
83
|
+
|
|
84
|
+
interface BarMark {
|
|
85
|
+
readonly rect: BarRect;
|
|
86
|
+
readonly key: string;
|
|
87
|
+
readonly name: string | undefined;
|
|
88
|
+
readonly value: number;
|
|
89
|
+
readonly series: number;
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly last: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
44
94
|
export function BarChart(props: BarChartProps): JSX.Element {
|
|
45
95
|
const ui = useUi();
|
|
46
96
|
const format = (value: number): string => (props.format ?? chartNumberFormat(ui.locale))(value);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
97
|
+
const keys = (): readonly string[] =>
|
|
98
|
+
props.series === undefined ? props.points.map((p) => p.key) : props.keys;
|
|
99
|
+
const series = (): readonly ChartSeries[] => {
|
|
100
|
+
if (props.series !== undefined) {
|
|
101
|
+
checkSeries('BarChart', props.keys, props.series);
|
|
102
|
+
return props.series;
|
|
103
|
+
}
|
|
104
|
+
const points = props.points;
|
|
105
|
+
const stacked = points.some((point) => (point.secondary ?? 0) > 0);
|
|
106
|
+
const primary = {
|
|
107
|
+
label: props.seriesLabels?.primary ?? props.label,
|
|
108
|
+
values: points.map((p) => p.value),
|
|
109
|
+
};
|
|
110
|
+
return props.seriesLabels === undefined && !stacked
|
|
111
|
+
? [primary]
|
|
112
|
+
: [
|
|
113
|
+
primary,
|
|
59
114
|
{
|
|
60
115
|
label: props.seriesLabels?.secondary ?? '',
|
|
61
|
-
values:
|
|
116
|
+
values: points.map((p) => p.secondary ?? 0),
|
|
62
117
|
},
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
|
|
118
|
+
];
|
|
119
|
+
};
|
|
120
|
+
const extent = (): BarExtent => {
|
|
121
|
+
const also = referenceValues(props.references);
|
|
122
|
+
return props.series === undefined
|
|
123
|
+
? barExtent(props.points, also)
|
|
124
|
+
: seriesExtent(props.series, also);
|
|
125
|
+
};
|
|
126
|
+
const highlight = (index: number): boolean =>
|
|
127
|
+
props.highlightLast !== false && index === keys().length - 1;
|
|
128
|
+
|
|
129
|
+
/** Every drawn bar, in reading order: key by key, series by series. */
|
|
130
|
+
const marks = (): readonly BarMark[] => {
|
|
131
|
+
const scale = extent();
|
|
132
|
+
if (props.series !== undefined) {
|
|
133
|
+
const grid = groupedBarRects(props.keys.length, series(), scale);
|
|
134
|
+
return props.keys.flatMap((key, k) =>
|
|
135
|
+
series().flatMap((one, s): BarMark[] => {
|
|
136
|
+
const rect = grid[s]?.[k] ?? null;
|
|
137
|
+
const value = one.values[k] ?? null;
|
|
138
|
+
if (rect === null || value === null) return [];
|
|
139
|
+
const id = hitIds(s, props.keys)[k] ?? `${s}:${k}`;
|
|
140
|
+
return [{ rect, key, name: one.label, value, series: s, id, last: highlight(k) }];
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const points = props.points;
|
|
145
|
+
const ids = (s: number) =>
|
|
146
|
+
hitIds(
|
|
147
|
+
s,
|
|
148
|
+
points.map((p) => p.key),
|
|
149
|
+
);
|
|
150
|
+
const lower = barRects(points, scale);
|
|
151
|
+
const upper = secondaryRects(points, scale);
|
|
152
|
+
return points.flatMap((point, k): BarMark[] => {
|
|
153
|
+
const out: BarMark[] = [];
|
|
154
|
+
const base = lower[k] ?? null;
|
|
155
|
+
if (base !== null && point.value !== null) {
|
|
156
|
+
out.push({
|
|
157
|
+
rect: base,
|
|
158
|
+
key: point.key,
|
|
159
|
+
name: props.seriesLabels?.primary,
|
|
160
|
+
value: point.value,
|
|
161
|
+
series: 0,
|
|
162
|
+
id: ids(0)[k] ?? '',
|
|
163
|
+
last: highlight(k),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const top = upper[k] ?? null;
|
|
167
|
+
if (top !== null) {
|
|
168
|
+
out.push({
|
|
169
|
+
rect: top,
|
|
170
|
+
key: point.key,
|
|
171
|
+
name: props.seriesLabels?.secondary,
|
|
172
|
+
value: point.secondary ?? 0,
|
|
173
|
+
series: 1,
|
|
174
|
+
id: ids(1)[k] ?? '',
|
|
175
|
+
last: highlight(k),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
});
|
|
180
|
+
};
|
|
66
181
|
|
|
67
|
-
const legend = (): readonly ChartLegendItem[] | undefined =>
|
|
68
|
-
props.
|
|
182
|
+
const legend = (): readonly ChartLegendItem[] | undefined => {
|
|
183
|
+
if (props.series !== undefined) {
|
|
184
|
+
return props.series.length < 2
|
|
185
|
+
? undefined
|
|
186
|
+
: props.series.map((one, s) => ({
|
|
187
|
+
label: one.label,
|
|
188
|
+
swatch: (
|
|
189
|
+
<span
|
|
190
|
+
class={styles['swatch']}
|
|
191
|
+
data-series={seriesStyle(s).slot}
|
|
192
|
+
data-tone={one.tone}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
return props.seriesLabels === undefined
|
|
69
198
|
? undefined
|
|
70
199
|
: [
|
|
71
|
-
{
|
|
200
|
+
{
|
|
201
|
+
label: props.seriesLabels.primary,
|
|
202
|
+
swatch: <span class={styles['swatch']} data-tone={props.tone} />,
|
|
203
|
+
},
|
|
72
204
|
{
|
|
73
205
|
label: props.seriesLabels.secondary,
|
|
74
206
|
swatch: <span class={cx(styles['swatch'], styles['swatchSecondary'])} />,
|
|
75
207
|
},
|
|
76
208
|
];
|
|
209
|
+
};
|
|
77
210
|
|
|
78
211
|
const hits = (): readonly ChartHit[] =>
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const x = rect.x + rect.width / 2;
|
|
84
|
-
const own = (y: number, label: string | undefined, value: number): ChartHit => ({
|
|
212
|
+
marks().map((mark) => {
|
|
213
|
+
const x = mark.rect.x + mark.rect.width / 2;
|
|
214
|
+
return {
|
|
215
|
+
id: mark.id,
|
|
85
216
|
x: cssPercent(x, BAR_CHART.width),
|
|
86
|
-
y: cssPercent(
|
|
87
|
-
|
|
217
|
+
y: cssPercent(
|
|
218
|
+
mark.value < 0 ? mark.rect.y + mark.rect.height : mark.rect.y,
|
|
219
|
+
BAR_CHART.height,
|
|
220
|
+
),
|
|
221
|
+
label: pointLabel(mark.key, mark.name, format(mark.value)),
|
|
88
222
|
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)];
|
|
223
|
+
};
|
|
94
224
|
});
|
|
95
225
|
|
|
226
|
+
const lines = () => referenceLines(props.references, (value) => valueY(value, extent()));
|
|
227
|
+
|
|
228
|
+
const rectOf = (mark: BarMark): JSX.Element => {
|
|
229
|
+
const secondary = props.series === undefined && mark.series === 1;
|
|
230
|
+
return (
|
|
231
|
+
<rect
|
|
232
|
+
class={cx(secondary ? styles['secondary'] : styles['bar'], mark.last && styles['last'])}
|
|
233
|
+
x={mark.rect.x}
|
|
234
|
+
y={mark.rect.y}
|
|
235
|
+
width={mark.rect.width}
|
|
236
|
+
height={mark.rect.height}
|
|
237
|
+
rx={2}
|
|
238
|
+
data-bar={secondary ? undefined : 'true'}
|
|
239
|
+
data-series={
|
|
240
|
+
secondary
|
|
241
|
+
? 'secondary'
|
|
242
|
+
: props.series === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: seriesStyle(mark.series).slot
|
|
245
|
+
}
|
|
246
|
+
data-tone={
|
|
247
|
+
props.series === undefined
|
|
248
|
+
? secondary
|
|
249
|
+
? undefined
|
|
250
|
+
: props.tone
|
|
251
|
+
: props.series[mark.series]?.tone
|
|
252
|
+
}
|
|
253
|
+
data-sign={mark.value < 0 ? 'negative' : undefined}
|
|
254
|
+
>
|
|
255
|
+
<title>{`${mark.key}: ${mark.name === undefined ? format(mark.value) : `${mark.name} ${format(mark.value)}`}`}</title>
|
|
256
|
+
</rect>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
96
260
|
return (
|
|
97
261
|
<ChartFrame
|
|
98
262
|
label={props.label}
|
|
@@ -102,15 +266,16 @@ export function BarChart(props: BarChartProps): JSX.Element {
|
|
|
102
266
|
table={chartTable({
|
|
103
267
|
caption: props.label,
|
|
104
268
|
keyLabel: props.keyLabel,
|
|
105
|
-
keys:
|
|
269
|
+
keys: keys(),
|
|
106
270
|
series: series(),
|
|
107
271
|
format,
|
|
272
|
+
none: ui.t(UI_KEYS.chartNone),
|
|
108
273
|
})}
|
|
109
274
|
>
|
|
110
275
|
<div class={styles['chart']}>
|
|
111
276
|
{/* aria-hidden: the svg is the image and names itself; a bare "9" read aloud is noise. */}
|
|
112
277
|
<span class={cx(styles['axis'], styles['max'])} data-axis="max" aria-hidden="true">
|
|
113
|
-
{format(
|
|
278
|
+
{format(extent().max)}
|
|
114
279
|
</span>
|
|
115
280
|
<div class={styles['box']}>
|
|
116
281
|
<svg
|
|
@@ -128,52 +293,36 @@ export function BarChart(props: BarChartProps): JSX.Element {
|
|
|
128
293
|
y2={gridY(step)}
|
|
129
294
|
/>
|
|
130
295
|
))}
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
})}
|
|
296
|
+
{extent().min < 0 ? (
|
|
297
|
+
<line
|
|
298
|
+
class={styles['zero']}
|
|
299
|
+
data-zero="true"
|
|
300
|
+
x1={0}
|
|
301
|
+
x2={BAR_CHART.width}
|
|
302
|
+
y1={zeroY(extent())}
|
|
303
|
+
y2={zeroY(extent())}
|
|
304
|
+
/>
|
|
305
|
+
) : null}
|
|
306
|
+
{marks().map(rectOf)}
|
|
307
|
+
<ReferenceRules lines={lines()} width={BAR_CHART.width} />
|
|
165
308
|
</svg>
|
|
166
|
-
{
|
|
309
|
+
<ReferenceLabels lines={lines()} height={BAR_CHART.height} format={format} />
|
|
310
|
+
{props.focusable === true ? <ChartHits hits={hits()} /> : null}
|
|
167
311
|
</div>
|
|
168
|
-
{
|
|
312
|
+
{extent().min < 0 ? (
|
|
313
|
+
<span class={cx(styles['axis'], styles['max'])} data-axis="min" aria-hidden="true">
|
|
314
|
+
{format(extent().min)}
|
|
315
|
+
</span>
|
|
316
|
+
) : null}
|
|
317
|
+
{keys().length === 0 ? null : (
|
|
169
318
|
<div class={styles['keys']} aria-hidden="true">
|
|
170
319
|
<span class={styles['axis']} data-axis="first">
|
|
171
|
-
{
|
|
320
|
+
{keys()[0]}
|
|
172
321
|
</span>
|
|
173
322
|
{/* One point: its bar is both first and last, so its key is named once. */}
|
|
174
|
-
{
|
|
323
|
+
{keys().length === 1 ? null : (
|
|
175
324
|
<span class={styles['axis']} data-axis="last">
|
|
176
|
-
{
|
|
325
|
+
{keys().at(-1)}
|
|
177
326
|
</span>
|
|
178
327
|
)}
|
|
179
328
|
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@use '../tokens' as t;
|
|
2
|
+
|
|
3
|
+
// A board: columns side by side that snap-scroll sideways; one column wide on a phone.
|
|
4
|
+
.board {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-auto-flow: column;
|
|
7
|
+
grid-auto-columns: minmax(t.rem(256px), 1fr);
|
|
8
|
+
gap: t.space(4);
|
|
9
|
+
overflow-x: auto;
|
|
10
|
+
scroll-snap-type: inline mandatory;
|
|
11
|
+
overscroll-behavior-inline: contain;
|
|
12
|
+
|
|
13
|
+
@include t.respond-down(sm) {
|
|
14
|
+
grid-auto-columns: 100%;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.column {
|
|
19
|
+
@include t.column(t.space(2));
|
|
20
|
+
scroll-snap-align: start;
|
|
21
|
+
border-radius: t.radius(lg);
|
|
22
|
+
background: t.role('bg-soft');
|
|
23
|
+
padding: t.space(3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.columnLabel {
|
|
27
|
+
margin: 0;
|
|
28
|
+
color: t.role('fg-strong');
|
|
29
|
+
font-weight: t.weight(semibold);
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Columns of sortable groups sharing one gesture: a card moves within a column or across to
|
|
2
|
+
// another by pointer, touch, the arrow keys, or — with scripting off — a target `select` per card.
|
|
3
|
+
// No card look and no column rules: `renderItem` and `canDrop` are the caller's.
|
|
4
|
+
|
|
5
|
+
import type { JSX } from 'solid-js';
|
|
6
|
+
import type { CanDrop, SortMove } from '../drag/sort-view';
|
|
7
|
+
import { boardGroups } from './board-view';
|
|
8
|
+
import { type SortFields, sortSurface } from './sort-surface';
|
|
9
|
+
|
|
10
|
+
export interface BoardColumn<T> {
|
|
11
|
+
/** What `onMove` names as `fromGroup` / `toGroup` — a status, a lane id. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Already-translated column name. */
|
|
14
|
+
label: string;
|
|
15
|
+
items: readonly T[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BoardProps<T> {
|
|
19
|
+
groups: readonly BoardColumn<T>[];
|
|
20
|
+
/** Unique across EVERY column: a card is one node wherever it sits. */
|
|
21
|
+
itemKey: (item: T) => string;
|
|
22
|
+
renderItem: (item: T) => JSX.Element;
|
|
23
|
+
/** Already-translated name of the whole board. */
|
|
24
|
+
label: string;
|
|
25
|
+
onMove: (move: SortMove) => unknown;
|
|
26
|
+
/** The one seam for a business rule — a WIP limit, a status machine. */
|
|
27
|
+
canDrop?: CanDrop<T> | undefined;
|
|
28
|
+
/** The action URL the no-script target select posts to. */
|
|
29
|
+
action?: string | undefined;
|
|
30
|
+
fields?: SortFields | undefined;
|
|
31
|
+
class?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Board<T>(props: BoardProps<T>): JSX.Element {
|
|
35
|
+
return sortSurface<T>({
|
|
36
|
+
groups: () => boardGroups(props.groups, props.itemKey),
|
|
37
|
+
itemKey: (item) => props.itemKey(item),
|
|
38
|
+
renderItem: (item) => props.renderItem(item),
|
|
39
|
+
onMove: (move) => props.onMove(move),
|
|
40
|
+
canDrop: props.canDrop,
|
|
41
|
+
action: props.action,
|
|
42
|
+
fields: props.fields,
|
|
43
|
+
layout: 'board',
|
|
44
|
+
label: props.label,
|
|
45
|
+
class: props.class,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@use '../tokens' as t;
|
|
2
|
+
|
|
3
|
+
.bar {
|
|
4
|
+
@include t.safe-area-padding(block-end);
|
|
5
|
+
@include t.safe-area-padding(inline);
|
|
6
|
+
border-block-start: 1px solid t.role('line');
|
|
7
|
+
background: t.role('surface');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.list {
|
|
11
|
+
display: flex;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
list-style: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Equal shares: five items on a 320px phone are 64px each, over the 44px target.
|
|
18
|
+
.item {
|
|
19
|
+
flex: 1 1 0;
|
|
20
|
+
min-inline-size: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.link {
|
|
24
|
+
@include t.focus-ring('accent', t.stroke(thick), calc(-1 * #{t.stroke(thick)}));
|
|
25
|
+
@include t.touch-target;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
gap: t.space(1);
|
|
31
|
+
padding-block: t.space(2);
|
|
32
|
+
color: t.role('fg-muted');
|
|
33
|
+
font-size: t.text(xs);
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
|
|
36
|
+
&[aria-current='page'] {
|
|
37
|
+
color: t.role('accent');
|
|
38
|
+
font-weight: t.weight(semibold);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.icon {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.label {
|
|
47
|
+
@include t.truncate;
|
|
48
|
+
max-inline-size: 100%;
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The phone's destination bar: up to five links along the bottom edge, in thumb reach, the current
|
|
2
|
+
// one marked by `currentOf` — the same rule as `NavList`. No state and no script: links with
|
|
3
|
+
// `aria-current`, server-rendered. Its padding clears the home indicator (`safe-area-padding`);
|
|
4
|
+
// `AppShell bottomNav` shows it below `md` and reserves the space so no content hides under it.
|
|
5
|
+
|
|
6
|
+
import type { JSX } from 'solid-js';
|
|
7
|
+
import { cx } from '../cx';
|
|
8
|
+
import { UI_KEYS } from '../i18n-keys';
|
|
9
|
+
import { useUi } from '../theme/context';
|
|
10
|
+
import styles from './BottomNav.module.scss';
|
|
11
|
+
import { bottomNavItems } from './bottom-nav-view';
|
|
12
|
+
import { linkTarget } from './link-target';
|
|
13
|
+
import { currentOf } from './nav-list-view';
|
|
14
|
+
|
|
15
|
+
export interface BottomNavItem {
|
|
16
|
+
/** Already translated; shown under the icon, never only as a tooltip. */
|
|
17
|
+
label: string;
|
|
18
|
+
href: string;
|
|
19
|
+
/** An `<Icon>`, decorative. */
|
|
20
|
+
icon: JSX.Element;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BottomNavProps {
|
|
24
|
+
/** At most five; a sixth is `X_UI_INVALID_VALUE`. */
|
|
25
|
+
items: readonly BottomNavItem[];
|
|
26
|
+
currentHref: string;
|
|
27
|
+
/** The landmark's name. Defaults to the translated `ui.navigation`. */
|
|
28
|
+
label?: string | undefined;
|
|
29
|
+
class?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function BottomNav(props: BottomNavProps): JSX.Element {
|
|
33
|
+
const ui = useUi();
|
|
34
|
+
const items = bottomNavItems(props.items);
|
|
35
|
+
const current = currentOf(items, props.currentHref);
|
|
36
|
+
return (
|
|
37
|
+
<nav
|
|
38
|
+
class={cx(styles['bar'], props.class)}
|
|
39
|
+
aria-label={props.label ?? ui.t(UI_KEYS.navigation)}
|
|
40
|
+
>
|
|
41
|
+
<ul class={styles['list']}>
|
|
42
|
+
{items.map((item, index) => (
|
|
43
|
+
<li class={styles['item']}>
|
|
44
|
+
<a
|
|
45
|
+
class={styles['link']}
|
|
46
|
+
href={linkTarget(item.href).href}
|
|
47
|
+
aria-current={index === current ? 'page' : undefined}
|
|
48
|
+
>
|
|
49
|
+
<span class={styles['icon']}>{item.icon}</span>
|
|
50
|
+
<span class={styles['label']}>{item.label}</span>
|
|
51
|
+
</a>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
</nav>
|
|
56
|
+
);
|
|
57
|
+
}
|