@ultimat3/ui 29.3.2 → 30.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.
Files changed (168) hide show
  1. package/CATALOG.md +491 -24
  2. package/CLAUDE.md +23 -16
  3. package/README.md +338 -33
  4. package/package.json +16 -5
  5. package/src/components/AppShell.module.scss +23 -7
  6. package/src/components/AppShell.tsx +9 -0
  7. package/src/components/BarChart.module.scss +26 -0
  8. package/src/components/BarChart.tsx +242 -93
  9. package/src/components/Board.module.scss +30 -0
  10. package/src/components/Board.tsx +47 -0
  11. package/src/components/BottomNav.module.scss +49 -0
  12. package/src/components/BottomNav.tsx +57 -0
  13. package/src/components/Calendar.module.scss +118 -0
  14. package/src/components/Calendar.tsx +246 -0
  15. package/src/components/ChartFrame.module.scss +40 -3
  16. package/src/components/ChartFrame.tsx +84 -28
  17. package/src/components/CodeBlock.module.scss +101 -0
  18. package/src/components/CodeBlock.tsx +76 -0
  19. package/src/components/CommandPalette.module.scss +25 -0
  20. package/src/components/CommandPalette.tsx +50 -22
  21. package/src/components/ConfirmDialog.module.scss +36 -0
  22. package/src/components/ConfirmDialog.tsx +188 -0
  23. package/src/components/DataTable.tsx +92 -33
  24. package/src/components/DatePicker.module.scss +37 -0
  25. package/src/components/DatePicker.tsx +134 -0
  26. package/src/components/DateRangePicker.module.scss +54 -0
  27. package/src/components/DateRangePicker.tsx +156 -0
  28. package/src/components/DescriptionList.module.scss +46 -0
  29. package/src/components/DescriptionList.tsx +38 -0
  30. package/src/components/DiffView.module.scss +124 -0
  31. package/src/components/DiffView.tsx +169 -0
  32. package/src/components/DonutChart.tsx +13 -4
  33. package/src/components/Drawer.module.scss +40 -0
  34. package/src/components/Drawer.tsx +56 -0
  35. package/src/components/Gauge.module.scss +8 -0
  36. package/src/components/Gauge.tsx +40 -13
  37. package/src/components/HeatStrip.module.scss +133 -0
  38. package/src/components/HeatStrip.tsx +64 -0
  39. package/src/components/JsonView.module.scss +64 -0
  40. package/src/components/JsonView.tsx +68 -0
  41. package/src/components/LineChart.tsx +45 -18
  42. package/src/components/LiveIndicator.module.scss +44 -0
  43. package/src/components/LiveIndicator.tsx +45 -0
  44. package/src/components/LogViewer.module.scss +102 -0
  45. package/src/components/LogViewer.tsx +123 -0
  46. package/src/components/Markdown.module.scss +87 -0
  47. package/src/components/Markdown.tsx +84 -0
  48. package/src/components/Meter.module.scss +7 -0
  49. package/src/components/Meter.tsx +16 -0
  50. package/src/components/NavList.module.scss +66 -0
  51. package/src/components/NavList.tsx +95 -0
  52. package/src/components/Pagination.module.scss +6 -2
  53. package/src/components/Pagination.tsx +57 -13
  54. package/src/components/Popover.tsx +12 -2
  55. package/src/components/PullToRefresh.module.scss +41 -0
  56. package/src/components/PullToRefresh.tsx +108 -0
  57. package/src/components/Radio.module.scss +51 -0
  58. package/src/components/Radio.tsx +12 -1
  59. package/src/components/SearchableSelect.module.scss +216 -0
  60. package/src/components/SearchableSelect.tsx +331 -0
  61. package/src/components/SearchableSelectChips.tsx +41 -0
  62. package/src/components/SearchableSelectNative.tsx +89 -0
  63. package/src/components/ShortcutSheet.module.scss +33 -0
  64. package/src/components/ShortcutSheet.tsx +67 -0
  65. package/src/components/Slider.module.scss +50 -0
  66. package/src/components/Slider.tsx +68 -0
  67. package/src/components/SortableList.module.scss +72 -0
  68. package/src/components/SortableList.tsx +42 -0
  69. package/src/components/Sparkline.module.scss +10 -2
  70. package/src/components/Sparkline.tsx +27 -10
  71. package/src/components/SplitPane.module.scss +39 -0
  72. package/src/components/SplitPane.tsx +96 -0
  73. package/src/components/StatTile.module.scss +29 -3
  74. package/src/components/StatTile.tsx +75 -16
  75. package/src/components/Stepper.module.scss +78 -0
  76. package/src/components/Stepper.tsx +73 -0
  77. package/src/components/Tabs.module.scss +13 -1
  78. package/src/components/Tabs.tsx +77 -1
  79. package/src/components/TerminalView.module.scss +68 -0
  80. package/src/components/TerminalView.tsx +97 -0
  81. package/src/components/TimeRangePicker.module.scss +85 -0
  82. package/src/components/TimeRangePicker.tsx +91 -0
  83. package/src/components/Timeline.module.scss +74 -0
  84. package/src/components/Timeline.tsx +60 -0
  85. package/src/components/Tree.module.scss +98 -0
  86. package/src/components/Tree.tsx +185 -0
  87. package/src/components/VirtualList.module.scss +60 -0
  88. package/src/components/VirtualList.tsx +251 -0
  89. package/src/components/app-shell-view.ts +4 -1
  90. package/src/components/bar-chart-view.ts +121 -39
  91. package/src/components/board-view.ts +25 -0
  92. package/src/components/bottom-nav-view.ts +18 -0
  93. package/src/components/calendar-view.ts +226 -0
  94. package/src/components/chart-checks.ts +59 -0
  95. package/src/components/chart-frame-view.ts +33 -2
  96. package/src/components/chart-reference-view.ts +41 -0
  97. package/src/components/command-palette-view.ts +47 -6
  98. package/src/components/confirm-dialog-view.ts +47 -0
  99. package/src/components/data-table-rows.ts +93 -0
  100. package/src/components/date-picker-view.ts +38 -0
  101. package/src/components/date-range-view.ts +46 -0
  102. package/src/components/diff-view-view.ts +85 -0
  103. package/src/components/drawer-gesture-view.ts +33 -0
  104. package/src/components/gauge-view.ts +29 -0
  105. package/src/components/heat-strip-view.ts +68 -0
  106. package/src/components/json-view-view.ts +54 -0
  107. package/src/components/line-chart-view.ts +87 -25
  108. package/src/components/log-viewer-view.ts +51 -0
  109. package/src/components/markdown-nodes.tsx +150 -0
  110. package/src/components/nav-list-view.ts +37 -0
  111. package/src/components/pull-refresh-view.ts +93 -0
  112. package/src/components/rank-options.ts +61 -0
  113. package/src/components/searchable-select-keys.ts +118 -0
  114. package/src/components/searchable-select-view.ts +187 -0
  115. package/src/components/slider-view.ts +47 -0
  116. package/src/components/sort-pointer.ts +141 -0
  117. package/src/components/sort-surface.tsx +231 -0
  118. package/src/components/sparkline-view.ts +74 -14
  119. package/src/components/split-pane-view.ts +69 -0
  120. package/src/components/stat-delta.ts +54 -11
  121. package/src/components/stepper-view.ts +38 -0
  122. package/src/components/thunk.ts +9 -0
  123. package/src/components/timeline-view.ts +29 -0
  124. package/src/components/tree-node.tsx +133 -0
  125. package/src/components/tree-view.ts +185 -0
  126. package/src/components/use-command-palette.ts +153 -0
  127. package/src/components/virtual-list-anchor.ts +35 -0
  128. package/src/components/virtual-list-observer.ts +58 -0
  129. package/src/components/virtual-list-view.ts +145 -0
  130. package/src/content/ansi.ts +144 -0
  131. package/src/content/heading-slug.ts +32 -0
  132. package/src/content/highlight.ts +129 -0
  133. package/src/content/languages/all.ts +21 -0
  134. package/src/content/languages/diff.ts +14 -0
  135. package/src/content/languages/html.ts +16 -0
  136. package/src/content/languages/json.ts +15 -0
  137. package/src/content/languages/shell.ts +20 -0
  138. package/src/content/languages/sql.ts +27 -0
  139. package/src/content/languages/ts.ts +26 -0
  140. package/src/content/languages/yaml.ts +18 -0
  141. package/src/content/line-diff.ts +191 -0
  142. package/src/content/markdown-inline.ts +368 -0
  143. package/src/content/markdown-parse.ts +267 -0
  144. package/src/content/markdown-tree.ts +58 -0
  145. package/src/content/unified-patch.ts +53 -0
  146. package/src/drag/pointer-drag.ts +185 -0
  147. package/src/drag/sort-controller.ts +196 -0
  148. package/src/drag/sort-view.ts +201 -0
  149. package/src/errors.ts +1 -1
  150. package/src/form/field-path.ts +7 -33
  151. package/src/form/form-binding.ts +5 -8
  152. package/src/form/form-issue.ts +30 -36
  153. package/src/form/form-values.ts +3 -2
  154. package/src/hotkeys-view.ts +150 -0
  155. package/src/i18n-keys.ts +7 -50
  156. package/src/index.ts +214 -15
  157. package/src/list-url.ts +123 -0
  158. package/src/live-window.ts +23 -0
  159. package/src/roving.ts +2 -0
  160. package/src/theme/inert-runtime.ts +4 -0
  161. package/src/theme/runtime-slot.ts +2 -2
  162. package/src/theme/solid-adapter.ts +11 -5
  163. package/src/time-range-url.ts +200 -0
  164. package/src/tokens/_mixins.scss +1 -1
  165. package/src/tokens/_touch.scss +30 -1
  166. package/src/tokens/contrast-pairs.ts +12 -0
  167. package/src/ui-keys.ts +126 -0
  168. 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 — or two stacked (done, and failed
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.
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 { BAR_CHART, barRects, GRID_STEPS, gridY, maxOf, secondaryRects } from './bar-chart-view';
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, chartHits } from './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 { chartNumberFormat, chartTable, cssPercent, hitEdge, pointLabel } from './chart-frame-view';
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
- export interface BarChartProps {
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
- /** Give every bar a tab stop and a hover/focus readout. Default off: no tab stops. */
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 rects = (): readonly ReturnType<typeof barRects>[number][] => barRects(props.points);
48
- const tops = (): ReturnType<typeof secondaryRects> => secondaryRects(props.points);
49
- const named = (label: string | undefined, value: number): string =>
50
- label === undefined ? format(value) : `${label} ${format(value)}`;
51
- const lastIndex = (): number => props.points.length - 1;
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
- ? [
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: props.points.map((p) => p.secondary ?? 0),
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.seriesLabels === undefined
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
- { label: props.seriesLabels.primary, swatch: <span class={styles['swatch']} /> },
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
- 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 => ({
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(y, BAR_CHART.height),
87
- label: pointLabel(point.key, label, format(value)),
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: props.points.map((point) => point.key),
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(maxOf(props.points))}
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
- {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
- })}
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
- {props.focusable === true ? chartHits(hits()) : null}
309
+ <ReferenceLabels lines={lines()} height={BAR_CHART.height} format={format} />
310
+ {props.focusable === true ? <ChartHits hits={hits()} /> : null}
167
311
  </div>
168
- {props.points.length === 0 ? null : (
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
- {props.points[0]?.key}
320
+ {keys()[0]}
172
321
  </span>
173
322
  {/* One point: its bar is both first and last, so its key is named once. */}
174
- {props.points.length === 1 ? null : (
323
+ {keys().length === 1 ? null : (
175
324
  <span class={styles['axis']} data-axis="last">
176
- {props.points.at(-1)?.key}
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
+ }