@spatika/react 1.1.0 → 1.2.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 (311) hide show
  1. package/AGENTS.md +71 -0
  2. package/README.md +4 -3
  3. package/dist/composites/Card.d.ts +7 -1
  4. package/dist/composites/Card.d.ts.map +1 -1
  5. package/dist/composites/Card.js +10 -0
  6. package/dist/composites/ChartDataGrid.d.ts +33 -0
  7. package/dist/composites/ChartDataGrid.d.ts.map +1 -0
  8. package/dist/composites/ChartDataGrid.js +77 -0
  9. package/dist/composites/CoverHero.d.ts +1 -1
  10. package/dist/composites/CoverHero.d.ts.map +1 -1
  11. package/dist/composites/CoverHero.js +2 -2
  12. package/dist/composites/EntityMediaCard.d.ts +1 -1
  13. package/dist/composites/EntityMediaCard.d.ts.map +1 -1
  14. package/dist/composites/EntityMediaCard.js +12 -4
  15. package/dist/composites/EventCalendar.d.ts +35 -0
  16. package/dist/composites/EventCalendar.d.ts.map +1 -0
  17. package/dist/composites/EventCalendar.js +339 -0
  18. package/dist/composites/EventEditor.d.ts +22 -0
  19. package/dist/composites/EventEditor.d.ts.map +1 -0
  20. package/dist/composites/EventEditor.js +86 -0
  21. package/dist/composites/EventTimeline.d.ts +24 -0
  22. package/dist/composites/EventTimeline.d.ts.map +1 -0
  23. package/dist/composites/EventTimeline.js +184 -0
  24. package/dist/composites/MapChart.d.ts +24 -0
  25. package/dist/composites/MapChart.d.ts.map +1 -0
  26. package/dist/composites/MapChart.js +58 -0
  27. package/dist/composites/ProfileHero.d.ts +6 -4
  28. package/dist/composites/ProfileHero.d.ts.map +1 -1
  29. package/dist/composites/ProfileHero.js +8 -8
  30. package/dist/composites/ScatterWebGL.d.ts +22 -0
  31. package/dist/composites/ScatterWebGL.d.ts.map +1 -0
  32. package/dist/composites/ScatterWebGL.js +45 -0
  33. package/dist/composites/SiteFooter.d.ts +1 -1
  34. package/dist/composites/SiteFooter.js +1 -1
  35. package/dist/composites/SiteNav.d.ts +7 -2
  36. package/dist/composites/SiteNav.d.ts.map +1 -1
  37. package/dist/composites/SiteNav.js +9 -5
  38. package/dist/composites/StatCard.d.ts +2 -1
  39. package/dist/composites/StatCard.d.ts.map +1 -1
  40. package/dist/composites/StatCard.js +3 -3
  41. package/dist/composites/StatusDot.d.ts +1 -1
  42. package/dist/composites/Wordmark.d.ts +2 -2
  43. package/dist/composites/Wordmark.d.ts.map +1 -1
  44. package/dist/composites/cartesian-charts.d.ts +138 -0
  45. package/dist/composites/cartesian-charts.d.ts.map +1 -0
  46. package/dist/composites/cartesian-charts.js +401 -0
  47. package/dist/composites/chart-composition.d.ts +139 -0
  48. package/dist/composites/chart-composition.d.ts.map +1 -0
  49. package/dist/composites/chart-composition.js +486 -0
  50. package/dist/composites/chart-ui.d.ts +83 -0
  51. package/dist/composites/chart-ui.d.ts.map +1 -0
  52. package/dist/composites/chart-ui.js +108 -0
  53. package/dist/composites/charts-3d.d.ts +37 -0
  54. package/dist/composites/charts-3d.d.ts.map +1 -0
  55. package/dist/composites/charts-3d.js +121 -0
  56. package/dist/composites/flow-charts.d.ts +89 -0
  57. package/dist/composites/flow-charts.d.ts.map +1 -0
  58. package/dist/composites/flow-charts.js +186 -0
  59. package/dist/composites/radial-charts.d.ts +109 -0
  60. package/dist/composites/radial-charts.d.ts.map +1 -0
  61. package/dist/composites/radial-charts.js +176 -0
  62. package/dist/composites/scheduler-ui.d.ts +43 -0
  63. package/dist/composites/scheduler-ui.d.ts.map +1 -0
  64. package/dist/composites/scheduler-ui.js +71 -0
  65. package/dist/index.d.ts +67 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +57 -7
  68. package/dist/lib/chart-export.d.ts +10 -0
  69. package/dist/lib/chart-export.d.ts.map +1 -0
  70. package/dist/lib/chart-export.js +102 -0
  71. package/dist/lib/charts.d.ts +235 -0
  72. package/dist/lib/charts.d.ts.map +1 -0
  73. package/dist/lib/charts.js +744 -0
  74. package/dist/lib/geo.d.ts +39 -0
  75. package/dist/lib/geo.d.ts.map +1 -0
  76. package/dist/lib/geo.js +101 -0
  77. package/dist/lib/iso.d.ts +21 -0
  78. package/dist/lib/iso.d.ts.map +1 -0
  79. package/dist/lib/iso.js +56 -0
  80. package/dist/lib/rrule.d.ts +20 -0
  81. package/dist/lib/rrule.d.ts.map +1 -0
  82. package/dist/lib/rrule.js +236 -0
  83. package/dist/lib/scheduler.d.ts +152 -0
  84. package/dist/lib/scheduler.d.ts.map +1 -0
  85. package/dist/lib/scheduler.js +501 -0
  86. package/dist/lib/webgl-scatter.d.ts +44 -0
  87. package/dist/lib/webgl-scatter.d.ts.map +1 -0
  88. package/dist/lib/webgl-scatter.js +185 -0
  89. package/dist/primitives/Accordion.d.ts +44 -20
  90. package/dist/primitives/Accordion.d.ts.map +1 -1
  91. package/dist/primitives/Accordion.js +82 -22
  92. package/dist/primitives/AppBar.d.ts +13 -0
  93. package/dist/primitives/AppBar.d.ts.map +1 -0
  94. package/dist/primitives/AppBar.js +27 -0
  95. package/dist/primitives/Autocomplete.d.ts +28 -0
  96. package/dist/primitives/Autocomplete.d.ts.map +1 -0
  97. package/dist/primitives/Autocomplete.js +118 -0
  98. package/dist/primitives/Avatar.d.ts +13 -1
  99. package/dist/primitives/Avatar.d.ts.map +1 -1
  100. package/dist/primitives/Avatar.js +17 -2
  101. package/dist/primitives/Backdrop.d.ts +17 -0
  102. package/dist/primitives/Backdrop.d.ts.map +1 -0
  103. package/dist/primitives/Backdrop.js +17 -0
  104. package/dist/primitives/BottomNavigation.d.ts +23 -0
  105. package/dist/primitives/BottomNavigation.d.ts.map +1 -0
  106. package/dist/primitives/BottomNavigation.js +30 -0
  107. package/dist/primitives/ButtonBase.d.ts +14 -0
  108. package/dist/primitives/ButtonBase.d.ts.map +1 -0
  109. package/dist/primitives/ButtonBase.js +13 -0
  110. package/dist/primitives/ButtonGroup.d.ts +25 -0
  111. package/dist/primitives/ButtonGroup.d.ts.map +1 -0
  112. package/dist/primitives/ButtonGroup.js +44 -0
  113. package/dist/primitives/CircularProgress.d.ts +16 -0
  114. package/dist/primitives/CircularProgress.d.ts.map +1 -0
  115. package/dist/primitives/CircularProgress.js +17 -0
  116. package/dist/primitives/ClickAwayListener.d.ts +16 -0
  117. package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
  118. package/dist/primitives/ClickAwayListener.js +33 -0
  119. package/dist/primitives/Collapse.d.ts +19 -0
  120. package/dist/primitives/Collapse.d.ts.map +1 -0
  121. package/dist/primitives/Collapse.js +14 -0
  122. package/dist/primitives/Container.d.ts +13 -0
  123. package/dist/primitives/Container.d.ts.map +1 -0
  124. package/dist/primitives/Container.js +26 -0
  125. package/dist/primitives/Fab.d.ts +22 -0
  126. package/dist/primitives/Fab.d.ts.map +1 -0
  127. package/dist/primitives/Fab.js +42 -0
  128. package/dist/primitives/FormControl.d.ts +27 -0
  129. package/dist/primitives/FormControl.d.ts.map +1 -0
  130. package/dist/primitives/FormControl.js +24 -0
  131. package/dist/primitives/FormControlLabel.d.ts +18 -0
  132. package/dist/primitives/FormControlLabel.d.ts.map +1 -0
  133. package/dist/primitives/FormControlLabel.js +19 -0
  134. package/dist/primitives/Grid.d.ts +32 -0
  135. package/dist/primitives/Grid.d.ts.map +1 -0
  136. package/dist/primitives/Grid.js +50 -0
  137. package/dist/primitives/IconButton.d.ts +22 -0
  138. package/dist/primitives/IconButton.d.ts.map +1 -0
  139. package/dist/primitives/IconButton.js +39 -0
  140. package/dist/primitives/ImageList.d.ts +31 -0
  141. package/dist/primitives/ImageList.d.ts.map +1 -0
  142. package/dist/primitives/ImageList.js +33 -0
  143. package/dist/primitives/InputAdornment.d.ts +28 -0
  144. package/dist/primitives/InputAdornment.d.ts.map +1 -0
  145. package/dist/primitives/InputAdornment.js +13 -0
  146. package/dist/primitives/Link.d.ts +17 -0
  147. package/dist/primitives/Link.d.ts.map +1 -0
  148. package/dist/primitives/Link.js +29 -0
  149. package/dist/primitives/List.d.ts +41 -0
  150. package/dist/primitives/List.d.ts.map +1 -0
  151. package/dist/primitives/List.js +22 -0
  152. package/dist/primitives/Masonry.d.ts +14 -0
  153. package/dist/primitives/Masonry.d.ts.map +1 -0
  154. package/dist/primitives/Masonry.js +15 -0
  155. package/dist/primitives/Menu.d.ts +28 -0
  156. package/dist/primitives/Menu.d.ts.map +1 -0
  157. package/dist/primitives/Menu.js +54 -0
  158. package/dist/primitives/MobileStepper.d.ts +15 -0
  159. package/dist/primitives/MobileStepper.d.ts.map +1 -0
  160. package/dist/primitives/MobileStepper.js +8 -0
  161. package/dist/primitives/Modal.d.ts +18 -0
  162. package/dist/primitives/Modal.d.ts.map +1 -0
  163. package/dist/primitives/Modal.js +29 -0
  164. package/dist/primitives/NoSsr.d.ts +15 -0
  165. package/dist/primitives/NoSsr.d.ts.map +1 -0
  166. package/dist/primitives/NoSsr.js +19 -0
  167. package/dist/primitives/Paper.d.ts +22 -0
  168. package/dist/primitives/Paper.d.ts.map +1 -0
  169. package/dist/primitives/Paper.js +33 -0
  170. package/dist/primitives/Progress.d.ts +6 -2
  171. package/dist/primitives/Progress.d.ts.map +1 -1
  172. package/dist/primitives/Progress.js +8 -3
  173. package/dist/primitives/Snackbar.d.ts +23 -0
  174. package/dist/primitives/Snackbar.d.ts.map +1 -0
  175. package/dist/primitives/Snackbar.js +33 -0
  176. package/dist/primitives/SpeedDial.d.ts +26 -0
  177. package/dist/primitives/SpeedDial.d.ts.map +1 -0
  178. package/dist/primitives/SpeedDial.js +26 -0
  179. package/dist/primitives/Stack.d.ts +15 -0
  180. package/dist/primitives/Stack.d.ts.map +1 -0
  181. package/dist/primitives/Stack.js +45 -0
  182. package/dist/primitives/StarRating.d.ts +19 -1
  183. package/dist/primitives/StarRating.d.ts.map +1 -1
  184. package/dist/primitives/StarRating.js +36 -6
  185. package/dist/primitives/Stepper.d.ts +36 -0
  186. package/dist/primitives/Stepper.d.ts.map +1 -0
  187. package/dist/primitives/Stepper.js +53 -0
  188. package/dist/primitives/TablePagination.d.ts +16 -0
  189. package/dist/primitives/TablePagination.d.ts.map +1 -0
  190. package/dist/primitives/TablePagination.js +12 -0
  191. package/dist/primitives/TextField.d.ts +24 -0
  192. package/dist/primitives/TextField.d.ts.map +1 -0
  193. package/dist/primitives/TextField.js +19 -0
  194. package/dist/primitives/TextareaAutosize.d.ts +11 -0
  195. package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
  196. package/dist/primitives/TextareaAutosize.js +26 -0
  197. package/dist/primitives/ToggleButton.d.ts +28 -0
  198. package/dist/primitives/ToggleButton.d.ts.map +1 -0
  199. package/dist/primitives/ToggleButton.js +56 -0
  200. package/dist/primitives/TransferList.d.ts +21 -0
  201. package/dist/primitives/TransferList.d.ts.map +1 -0
  202. package/dist/primitives/TransferList.js +40 -0
  203. package/dist/primitives/Typography.d.ts +23 -0
  204. package/dist/primitives/Typography.d.ts.map +1 -0
  205. package/dist/primitives/Typography.js +69 -0
  206. package/dist/primitives/transitions.d.ts +29 -0
  207. package/dist/primitives/transitions.d.ts.map +1 -0
  208. package/dist/primitives/transitions.js +52 -0
  209. package/package.json +28 -4
  210. package/skills/spatika-ui/SKILL.md +96 -0
  211. package/src/composites/Card.tsx +51 -1
  212. package/src/composites/ChartDataGrid.test.tsx +57 -0
  213. package/src/composites/ChartDataGrid.tsx +196 -0
  214. package/src/composites/CoverHero.test.tsx +38 -0
  215. package/src/composites/CoverHero.tsx +29 -22
  216. package/src/composites/EntityMediaCard.test.tsx +58 -0
  217. package/src/composites/EntityMediaCard.tsx +31 -13
  218. package/src/composites/EventCalendar.tsx +928 -0
  219. package/src/composites/EventEditor.tsx +268 -0
  220. package/src/composites/EventTimeline.tsx +357 -0
  221. package/src/composites/MapChart.test.tsx +39 -0
  222. package/src/composites/MapChart.tsx +130 -0
  223. package/src/composites/ProfileHero.test.tsx +33 -0
  224. package/src/composites/ProfileHero.tsx +96 -79
  225. package/src/composites/ScatterWebGL.test.tsx +25 -0
  226. package/src/composites/ScatterWebGL.tsx +79 -0
  227. package/src/composites/SiteFooter.test.tsx +37 -0
  228. package/src/composites/SiteFooter.tsx +1 -1
  229. package/src/composites/SiteNav.test.tsx +46 -0
  230. package/src/composites/SiteNav.tsx +14 -4
  231. package/src/composites/StatCard.test.tsx +30 -0
  232. package/src/composites/StatCard.tsx +4 -0
  233. package/src/composites/Wordmark.test.tsx +23 -0
  234. package/src/composites/Wordmark.tsx +2 -2
  235. package/src/composites/cartesian-charts.test.tsx +52 -0
  236. package/src/composites/cartesian-charts.tsx +1287 -0
  237. package/src/composites/chart-composition.test.tsx +53 -0
  238. package/src/composites/chart-composition.tsx +898 -0
  239. package/src/composites/chart-ui.tsx +328 -0
  240. package/src/composites/charts-3d.test.tsx +25 -0
  241. package/src/composites/charts-3d.tsx +318 -0
  242. package/src/composites/charts.test.tsx +254 -0
  243. package/src/composites/chrome.test.tsx +29 -0
  244. package/src/composites/flow-charts.test.tsx +79 -0
  245. package/src/composites/flow-charts.tsx +607 -0
  246. package/src/composites/marketing.test.tsx +38 -34
  247. package/src/composites/radial-charts.test.tsx +74 -0
  248. package/src/composites/radial-charts.tsx +617 -0
  249. package/src/composites/scheduler-ui.tsx +304 -0
  250. package/src/composites/scheduler.test.tsx +194 -0
  251. package/src/index.test.ts +22 -0
  252. package/src/index.ts +277 -5
  253. package/src/lib/chart-export.test.ts +20 -0
  254. package/src/lib/chart-export.ts +99 -0
  255. package/src/lib/charts-platform.test.ts +93 -0
  256. package/src/lib/charts.test.ts +132 -0
  257. package/src/lib/charts.ts +1021 -0
  258. package/src/lib/geo.test.ts +42 -0
  259. package/src/lib/geo.ts +138 -0
  260. package/src/lib/iso.test.ts +22 -0
  261. package/src/lib/iso.ts +102 -0
  262. package/src/lib/rrule.test.ts +53 -0
  263. package/src/lib/rrule.ts +281 -0
  264. package/src/lib/scheduler.test.ts +186 -0
  265. package/src/lib/scheduler.ts +688 -0
  266. package/src/lib/tokens.test.ts +2 -2
  267. package/src/lib/webgl-scatter.test.ts +36 -0
  268. package/src/lib/webgl-scatter.ts +230 -0
  269. package/src/primitives/Accordion.tsx +218 -63
  270. package/src/primitives/AppBar.tsx +39 -0
  271. package/src/primitives/Autocomplete.tsx +236 -0
  272. package/src/primitives/Avatar.tsx +52 -1
  273. package/src/primitives/Backdrop.tsx +48 -0
  274. package/src/primitives/BottomNavigation.tsx +102 -0
  275. package/src/primitives/ButtonBase.tsx +34 -0
  276. package/src/primitives/ButtonGroup.tsx +90 -0
  277. package/src/primitives/CircularProgress.tsx +104 -0
  278. package/src/primitives/ClickAwayListener.tsx +46 -0
  279. package/src/primitives/Collapse.tsx +39 -0
  280. package/src/primitives/Container.tsx +40 -0
  281. package/src/primitives/Fab.tsx +62 -0
  282. package/src/primitives/FormControl.tsx +90 -0
  283. package/src/primitives/FormControlLabel.tsx +51 -0
  284. package/src/primitives/Grid.tsx +101 -0
  285. package/src/primitives/IconButton.tsx +59 -0
  286. package/src/primitives/ImageList.test.tsx +48 -0
  287. package/src/primitives/ImageList.tsx +112 -0
  288. package/src/primitives/InputAdornment.tsx +86 -0
  289. package/src/primitives/Link.tsx +48 -0
  290. package/src/primitives/List.tsx +121 -0
  291. package/src/primitives/Masonry.tsx +32 -0
  292. package/src/primitives/Menu.tsx +112 -0
  293. package/src/primitives/MobileStepper.tsx +64 -0
  294. package/src/primitives/Modal.tsx +70 -0
  295. package/src/primitives/NoSsr.tsx +28 -0
  296. package/src/primitives/Paper.tsx +55 -0
  297. package/src/primitives/Progress.tsx +25 -7
  298. package/src/primitives/Snackbar.tsx +83 -0
  299. package/src/primitives/SpeedDial.tsx +110 -0
  300. package/src/primitives/Stack.tsx +88 -0
  301. package/src/primitives/StarRating.tsx +87 -28
  302. package/src/primitives/Stepper.tsx +184 -0
  303. package/src/primitives/TablePagination.tsx +58 -0
  304. package/src/primitives/TextField.tsx +105 -0
  305. package/src/primitives/TextareaAutosize.tsx +49 -0
  306. package/src/primitives/ToggleButton.tsx +148 -0
  307. package/src/primitives/TransferList.tsx +110 -0
  308. package/src/primitives/Typography.tsx +90 -0
  309. package/src/primitives/mui-more.test.tsx +76 -0
  310. package/src/primitives/mui.test.tsx +134 -0
  311. package/src/primitives/transitions.tsx +120 -0
@@ -0,0 +1,196 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { chartColor, formatChartNumber } from "../lib/charts";
3
+ import { cn } from "../lib/cn";
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from "../primitives/Table";
12
+ import { ChartContainer } from "./chart-composition";
13
+ import { SparkLineChart } from "./cartesian-charts";
14
+ import { PieChart } from "./radial-charts";
15
+
16
+ export type ChartGridColumnType = "text" | "number" | "sparkline" | "bar";
17
+
18
+ export type ChartGridColumn = {
19
+ field: string;
20
+ headerName: string;
21
+ type?: ChartGridColumnType;
22
+ width?: number;
23
+ };
24
+
25
+ export type ChartDataGridRow = { id: string } & Record<string, unknown>;
26
+
27
+ export type ChartDataGridProps = {
28
+ rows: ChartDataGridRow[];
29
+ columns: ChartGridColumn[];
30
+ chartType?: "bar" | "line" | "pie";
31
+ categoryField: string;
32
+ valueField: string;
33
+ height?: number;
34
+ className?: string;
35
+ selectedIds?: string[];
36
+ defaultSelectedIds?: string[];
37
+ onSelectionChange?: (ids: string[]) => void;
38
+ hideChart?: boolean;
39
+ hideLegend?: boolean;
40
+ };
41
+
42
+ export function renderChartCell(
43
+ value: unknown,
44
+ options?: {
45
+ variant?: "sparkline" | "bar";
46
+ color?: string;
47
+ height?: number;
48
+ max?: number;
49
+ },
50
+ ): ReactNode {
51
+ const color = options?.color ?? "var(--chart-1)";
52
+ if (Array.isArray(value) && value.every((item) => typeof item === "number")) {
53
+ return (
54
+ <SparkLineChart
55
+ data={value as number[]}
56
+ plotType={options?.variant === "bar" ? "bar" : "line"}
57
+ color={color}
58
+ height={options?.height ?? 32}
59
+ showHighlight={false}
60
+ area={options?.variant !== "bar"}
61
+ />
62
+ );
63
+ }
64
+ const n = typeof value === "number" ? value : Number(value);
65
+ if (!Number.isFinite(n)) return "—";
66
+ const max = options?.max && options.max > 0 ? options.max : Math.max(n, 1);
67
+ const pct = Math.max(0, Math.min(100, (n / max) * 100));
68
+ return (
69
+ <div className="spk-chart-cell-bar" title={formatChartNumber(n)}>
70
+ <span style={{ width: `${pct}%`, background: color }} />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export function ChartDataGrid({
76
+ rows,
77
+ columns,
78
+ chartType = "bar",
79
+ categoryField,
80
+ valueField,
81
+ height = 200,
82
+ className,
83
+ selectedIds,
84
+ defaultSelectedIds,
85
+ onSelectionChange,
86
+ hideChart,
87
+ hideLegend = true,
88
+ }: ChartDataGridProps) {
89
+ const [internal, setInternal] = useState<string[]>(defaultSelectedIds ?? []);
90
+ const selected = selectedIds ?? internal;
91
+ const setSelected = (ids: string[]) => {
92
+ if (selectedIds == null) setInternal(ids);
93
+ onSelectionChange?.(ids);
94
+ };
95
+ const selectedIndex = selected.length ? rows.findIndex((row) => row.id === selected[0]) : -1;
96
+ const categories = rows.map((row) => String(row[categoryField] ?? row.id));
97
+ const values = rows.map((row) => Number(row[valueField] ?? 0));
98
+ const maxByField = new Map<string, number>();
99
+ for (const col of columns) {
100
+ if (col.type === "bar" || col.type === "number") {
101
+ const nums = rows.map((row) => Number(row[col.field] ?? 0)).filter((n) => Number.isFinite(n));
102
+ maxByField.set(col.field, nums.length ? Math.max(...nums) : 1);
103
+ }
104
+ }
105
+
106
+ const toggleRow = (id: string) => {
107
+ setSelected(selected[0] === id ? [] : [id]);
108
+ };
109
+
110
+ return (
111
+ <div data-slot="chart-data-grid" className={cn("spk-chart-data-grid", className)}>
112
+ {hideChart ? null : chartType === "pie" ? (
113
+ <PieChart
114
+ height={height}
115
+ hideLegend={hideLegend}
116
+ series={[
117
+ {
118
+ innerRadius: 36,
119
+ paddingAngle: 2,
120
+ data: rows.map((row, i) => ({
121
+ id: row.id,
122
+ value: values[i] ?? 0,
123
+ label: categories[i],
124
+ color:
125
+ selected.length && !selected.includes(row.id)
126
+ ? `color-mix(in srgb, ${chartColor(i)} 28%, transparent)`
127
+ : undefined,
128
+ })),
129
+ },
130
+ ]}
131
+ />
132
+ ) : (
133
+ <ChartContainer
134
+ height={height}
135
+ hideLegend={hideLegend}
136
+ highlightScope="item"
137
+ selected={
138
+ selectedIndex >= 0 ? { seriesId: "value", dataIndex: selectedIndex } : null
139
+ }
140
+ xAxis={[{ data: categories }]}
141
+ series={[{ type: chartType, id: "value", label: valueField, data: values }]}
142
+ aria-label="Linked chart"
143
+ />
144
+ )}
145
+ <Table containerClassName="spk-chart-grid-table">
146
+ <TableHeader>
147
+ <TableRow>
148
+ {columns.map((col) => (
149
+ <TableHead key={col.field} style={col.width ? { width: col.width } : undefined}>
150
+ {col.headerName}
151
+ </TableHead>
152
+ ))}
153
+ </TableRow>
154
+ </TableHeader>
155
+ <TableBody>
156
+ {rows.map((row, rowIndex) => (
157
+ <TableRow
158
+ key={row.id}
159
+ data-state={selected.includes(row.id) ? "selected" : undefined}
160
+ onClick={() => toggleRow(row.id)}
161
+ style={{ cursor: "pointer" }}
162
+ >
163
+ {columns.map((col, colIndex) => {
164
+ const value = row[col.field];
165
+ const color = chartColor(rowIndex);
166
+ let content: ReactNode;
167
+ if (col.type === "sparkline") {
168
+ content = renderChartCell(value, { variant: "sparkline", color, height: 32 });
169
+ } else if (col.type === "bar") {
170
+ content = renderChartCell(value, {
171
+ variant: "bar",
172
+ color,
173
+ max: maxByField.get(col.field),
174
+ });
175
+ } else if (col.type === "number" && typeof value === "number") {
176
+ content = formatChartNumber(value);
177
+ } else {
178
+ content = value == null ? "—" : String(value);
179
+ }
180
+ return (
181
+ <TableCell key={col.field} data-field={col.field}>
182
+ {colIndex === 0 ? (
183
+ <span className="spk-chart-grid-name">{content}</span>
184
+ ) : (
185
+ content
186
+ )}
187
+ </TableCell>
188
+ );
189
+ })}
190
+ </TableRow>
191
+ ))}
192
+ </TableBody>
193
+ </Table>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { CoverHero } from "./CoverHero";
5
+
6
+ describe("CoverHero", () => {
7
+ it("renders title, kicker, facts, and avatars", () => {
8
+ render(
9
+ <CoverHero
10
+ title="Friday night"
11
+ kicker={<span>Meetup</span>}
12
+ facts={[{ key: "when", label: "When", value: "Fri 14 Mar" }]}
13
+ avatars={[{ name: "Alex Rivera" }, { name: "Sam Lee" }, { name: "Jo Park" }, { name: "Riley" }]}
14
+ bleed={false}
15
+ />,
16
+ );
17
+ expect(screen.getByRole("heading", { name: "Friday night" })).toBeInTheDocument();
18
+ expect(screen.getByText("Meetup")).toBeInTheDocument();
19
+ expect(screen.getByText("When")).toBeInTheDocument();
20
+ expect(screen.getByText("Fri 14 Mar")).toBeInTheDocument();
21
+ expect(screen.getByLabelText("Alex Rivera")).toBeInTheDocument();
22
+ expect(screen.getByText("+1")).toBeInTheDocument();
23
+ });
24
+
25
+ it("calls onBack from the floating control and skips bleed attributes when disabled", async () => {
26
+ const user = userEvent.setup();
27
+ const onBack = vi.fn();
28
+ const { container } = render(
29
+ <CoverHero title="Studio" onBack={onBack} backLabel="Go back" bleed={false} />,
30
+ );
31
+ expect(container.querySelector("[data-slot='cover-hero']")).not.toHaveAttribute(
32
+ "data-cover-bleed",
33
+ );
34
+ expect(screen.queryByRole("button", { name: "Go back" })).toBeInTheDocument();
35
+ await user.click(screen.getByRole("button", { name: "Go back" }));
36
+ expect(onBack).toHaveBeenCalledTimes(1);
37
+ });
38
+ });
@@ -60,7 +60,7 @@ export type CoverHeroProps = {
60
60
  className?: string;
61
61
  /** Offset used when the compact bar pins under app chrome. */
62
62
  compactOffsetClassName?: string;
63
- /** When false, skip writing cover-bleed attributes on `<html>` (catalog previews). */
63
+ /** When false, keep the cover inside this frame (docs / device previews). */
64
64
  bleed?: boolean;
65
65
  };
66
66
 
@@ -141,9 +141,14 @@ export function CoverHero({
141
141
  <header
142
142
  ref={heroRef}
143
143
  data-slot="cover-hero"
144
- className={cn("relative z-10 w-full overflow-visible", className)}
144
+ data-cover-bleed={bleed ? "true" : undefined}
145
+ className={cn(
146
+ "relative z-10 w-full",
147
+ bleed ? "overflow-visible" : "overflow-hidden rounded-[1.35rem] sm:rounded-[1.75rem]",
148
+ className,
149
+ )}
145
150
  >
146
- <div className={cn(mediaClass, "sm:rounded-b-[1.75rem]")} aria-hidden>
151
+ <div className={cn(mediaClass, bleed && "sm:rounded-b-[1.75rem]")} aria-hidden>
147
152
  {!hideImages && coverUrl ? (
148
153
  <img src={coverUrl} alt="" className="absolute inset-0 size-full object-cover" />
149
154
  ) : (
@@ -332,26 +337,28 @@ export function CoverHero({
332
337
  </div>
333
338
  </header>
334
339
 
335
- <div className={cn("sticky z-[60] h-0", compactOffsetClassName)}>
336
- {!heroInView ? (
337
- <div className="glass-header cover-chrome-bar absolute inset-x-0 top-0 border-b border-border/50 bg-background/85 backdrop-blur-md dark:bg-background/94">
338
- <div className="app-frame-pad flex items-center gap-2 py-1.5 sm:py-2">
339
- {onBack ? (
340
- <button
341
- type="button"
342
- onClick={onBack}
343
- aria-label={backLabel}
344
- className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-muted"
345
- >
346
- <ChevronLeft className="h-4 w-4" aria-hidden />
347
- </button>
348
- ) : null}
349
- <h2 className="page-header-title min-w-0 flex-1 truncate">{title}</h2>
350
- {compactActions}
340
+ {bleed ? (
341
+ <div className={cn("sticky z-[60] h-0", compactOffsetClassName)}>
342
+ {!heroInView ? (
343
+ <div className="glass-header cover-chrome-bar absolute inset-x-0 top-0 border-b border-border/50 bg-background/85 backdrop-blur-md dark:bg-background/94">
344
+ <div className="app-frame-pad flex items-center gap-2 py-1.5 sm:py-2">
345
+ {onBack ? (
346
+ <button
347
+ type="button"
348
+ onClick={onBack}
349
+ aria-label={backLabel}
350
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-muted"
351
+ >
352
+ <ChevronLeft className="h-4 w-4" aria-hidden />
353
+ </button>
354
+ ) : null}
355
+ <h2 className="page-header-title min-w-0 flex-1 truncate">{title}</h2>
356
+ {compactActions}
357
+ </div>
351
358
  </div>
352
- </div>
353
- ) : null}
354
- </div>
359
+ ) : null}
360
+ </div>
361
+ ) : null}
355
362
  </>
356
363
  );
357
364
  }
@@ -0,0 +1,58 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { EntityMediaCard } from "./EntityMediaCard";
5
+
6
+ describe("EntityMediaCard", () => {
7
+ it("renders title, subtitle, and status on a stacked tile", () => {
8
+ const { container } = render(
9
+ <EntityMediaCard title="Alex Chen" subtitle="Product designer" statusLine="Active" />,
10
+ );
11
+ expect(screen.getByText("Alex Chen")).toBeInTheDocument();
12
+ expect(screen.getByText("Product designer")).toBeInTheDocument();
13
+ expect(screen.getByText("Active")).toBeInTheDocument();
14
+ expect(screen.getByText("AC")).toBeInTheDocument();
15
+ const card = container.querySelector("[data-slot='entity-media-card']");
16
+ expect(card).toHaveAttribute("data-size", "large");
17
+ expect(card).toHaveAttribute("data-orientation", "stack");
18
+ expect(container.querySelector("[data-cover-pattern]")).toBeTruthy();
19
+ });
20
+
21
+ it("uses a photo when imageUrl is set", () => {
22
+ const { container } = render(
23
+ <EntityMediaCard title="Jordan Lee" imageUrl="/jordan.jpg" />,
24
+ );
25
+ expect(container.querySelector("img")).toHaveAttribute("src", "/jordan.jpg");
26
+ expect(screen.queryByText("JL")).not.toBeInTheDocument();
27
+ });
28
+
29
+ it("renders as a link and calls onClick on a button card", async () => {
30
+ const user = userEvent.setup();
31
+ const onClick = vi.fn();
32
+ const { rerender } = render(
33
+ <EntityMediaCard title="Alex Chen" href="/people/alex" />,
34
+ );
35
+ expect(screen.getByRole("link", { name: /Alex Chen/ })).toHaveAttribute(
36
+ "href",
37
+ "/people/alex",
38
+ );
39
+
40
+ rerender(<EntityMediaCard title="Alex Chen" onClick={onClick} />);
41
+ await user.click(screen.getByRole("button", { name: /Alex Chen/ }));
42
+ expect(onClick).toHaveBeenCalledTimes(1);
43
+ });
44
+
45
+ it("keeps row cards stretched so the photo matches the copy height", () => {
46
+ const { container } = render(
47
+ <EntityMediaCard
48
+ title="Jordan Lee"
49
+ subtitle="Account lead"
50
+ orientation="row"
51
+ size="small"
52
+ />,
53
+ );
54
+ const card = container.querySelector("[data-slot='entity-media-card']");
55
+ expect(card).toHaveAttribute("data-orientation", "row");
56
+ expect(card).toHaveClass("items-stretch");
57
+ });
58
+ });
@@ -1,5 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
+ import { coverTitleClass } from "../lib/cover-chrome";
4
+ import { getCoverPattern } from "../lib/cover-pattern";
5
+ import { CoverPattern } from "./CoverPattern";
3
6
 
4
7
  export type EntityMediaCardProps = {
5
8
  title: string;
@@ -22,7 +25,7 @@ export type EntityMediaCardProps = {
22
25
  };
23
26
 
24
27
  /**
25
- * Entity card with photo (or initials fallback) — contact / directory style.
28
+ * Entity card with photo (or generated cover + initials) — contact / album tile.
26
29
  */
27
30
  export function EntityMediaCard({
28
31
  title,
@@ -51,9 +54,14 @@ export function EntityMediaCard({
51
54
  .map((w) => w[0]?.toUpperCase() ?? "")
52
55
  .join("") ||
53
56
  "?";
57
+ const scene = getCoverPattern({ seed: title, hue: 210 });
54
58
 
55
59
  const photoAspect =
56
- size === "compact" ? "aspect-square" : size === "small" ? "aspect-[4/3]" : "aspect-square";
60
+ orientation === "row"
61
+ ? "h-full min-h-[6.5rem]"
62
+ : size === "compact"
63
+ ? "aspect-square"
64
+ : "aspect-[4/3]";
57
65
 
58
66
  return (
59
67
  <Comp
@@ -61,18 +69,20 @@ export function EntityMediaCard({
61
69
  type={onClick && !href ? "button" : undefined}
62
70
  onClick={onClick}
63
71
  data-slot="entity-media-card"
72
+ data-size={size}
73
+ data-orientation={orientation}
64
74
  className={cn(
65
- "group flex overflow-hidden rounded-2xl border border-white/45 bg-card text-left shadow-[0_8px_30px_rgb(0,0,0,0.04)] transition-[transform,border-color] duration-200 dark:border-white/12",
66
- "hover:-translate-y-0.5 hover:border-primary/15",
67
- orientation === "row" ? "flex-row" : "flex-col",
75
+ "group flex h-full min-w-0 overflow-hidden rounded-2xl border border-border/60 bg-card text-left shadow-[0_8px_30px_color-mix(in_srgb,var(--foreground)_6%,transparent)] transition-[transform,border-color] duration-200",
76
+ "hover:-translate-y-0.5 hover:border-primary/25",
77
+ orientation === "row" ? "flex-row items-stretch" : "flex-col",
68
78
  className,
69
79
  )}
70
80
  >
71
81
  <div
72
82
  className={cn(
73
83
  "relative shrink-0 overflow-hidden bg-muted",
74
- orientation === "row" ? "w-[42%] min-w-[7rem]" : "w-full",
75
- orientation === "stack" && photoAspect,
84
+ orientation === "row" ? "w-[38%] min-w-[6.5rem] max-w-[9.5rem]" : "w-full",
85
+ photoAspect,
76
86
  imageClassName,
77
87
  )}
78
88
  >
@@ -80,20 +90,28 @@ export function EntityMediaCard({
80
90
  <img
81
91
  src={imageUrl}
82
92
  alt=""
83
- className="size-full object-cover transition-transform duration-700 group-hover:scale-110"
93
+ className="absolute inset-0 size-full object-cover transition-transform duration-700 group-hover:scale-110"
84
94
  />
85
95
  ) : (
86
- <div className="flex size-full items-center justify-center bg-gradient-to-br from-primary/20 via-muted to-accent-highlight/20 text-2xl font-black text-primary">
87
- {initials}
88
- </div>
96
+ <>
97
+ <CoverPattern seed={title} hue={210} />
98
+ <div
99
+ className={cn(
100
+ "absolute inset-0 flex items-center justify-center text-2xl font-black",
101
+ coverTitleClass(scene.headerInk),
102
+ )}
103
+ >
104
+ {initials}
105
+ </div>
106
+ </>
89
107
  )}
90
108
  {overlays ? <div className="absolute inset-0 p-2">{overlays}</div> : null}
91
109
  </div>
92
110
 
93
111
  <div
94
112
  className={cn(
95
- "relative flex min-w-0 flex-1 flex-col bg-white dark:bg-card",
96
- size === "compact" ? "gap-1 p-2.5" : "gap-1.5 p-3 sm:p-4",
113
+ "relative flex min-w-0 flex-1 flex-col justify-center bg-card",
114
+ size === "compact" ? "gap-1 p-2.5" : "gap-1 p-3 sm:p-3.5",
97
115
  )}
98
116
  >
99
117
  <div className="flex items-start justify-between gap-2">