@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,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ nearestScatterHit,
4
+ parseCssColor,
5
+ shouldUseWebGL,
6
+ WEBGL_AUTO_THRESHOLD,
7
+ } from "./webgl-scatter";
8
+
9
+ describe("webgl scatter helpers", () => {
10
+ it("parses hex and rgb colors", () => {
11
+ expect(parseCssColor("#ff0000")).toEqual([1, 0, 0, 1]);
12
+ expect(parseCssColor("rgb(0, 128, 255)")).toEqual([0, 128 / 255, 1, 1]);
13
+ expect(parseCssColor("not-a-color")).toBeNull();
14
+ });
15
+
16
+ it("selects webgl for auto clouds above the threshold", () => {
17
+ expect(shouldUseWebGL("svg", 10_000)).toBe(false);
18
+ expect(shouldUseWebGL("webgl", 3)).toBe(true);
19
+ expect(shouldUseWebGL("auto", WEBGL_AUTO_THRESHOLD)).toBe(true);
20
+ expect(shouldUseWebGL("auto", 10)).toBe(false);
21
+ });
22
+
23
+ it("finds the nearest point within the hit radius", () => {
24
+ const hit = nearestScatterHit(
25
+ [
26
+ { x: 10, y: 10, r: 4, seriesIndex: 0, pointIndex: 0 },
27
+ { x: 40, y: 40, r: 4, seriesIndex: 1, pointIndex: 2 },
28
+ ],
29
+ 41,
30
+ 39,
31
+ );
32
+ expect(hit?.seriesIndex).toBe(1);
33
+ expect(hit?.pointIndex).toBe(2);
34
+ expect(nearestScatterHit([{ x: 0, y: 0, r: 2, seriesIndex: 0, pointIndex: 0 }], 80, 80)).toBeNull();
35
+ });
36
+ });
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Raw WebGL scatter renderer — no three.js.
3
+ * Canvas sits behind SVG axes; hit-testing stays on the CPU.
4
+ */
5
+
6
+ export type ScatterRenderer = "svg" | "webgl" | "auto";
7
+
8
+ export const WEBGL_AUTO_THRESHOLD = 2000;
9
+
10
+ export function shouldUseWebGL(renderer: ScatterRenderer | undefined, pointCount: number): boolean {
11
+ if (renderer === "webgl") return true;
12
+ if (renderer === "auto") return pointCount >= WEBGL_AUTO_THRESHOLD;
13
+ return false;
14
+ }
15
+
16
+ export type Rgba = [number, number, number, number];
17
+
18
+ export function parseCssColor(css: string): Rgba | null {
19
+ const hex = css.trim();
20
+ const hexMatch = /^#([0-9a-f]{3,8})$/i.exec(hex);
21
+ if (hexMatch) {
22
+ let h = hexMatch[1]!;
23
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
24
+ const n = Number.parseInt(h.slice(0, 6), 16);
25
+ const a = h.length === 8 ? Number.parseInt(h.slice(6, 8), 16) / 255 : 1;
26
+ return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, a];
27
+ }
28
+ const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(hex);
29
+ if (rgb) {
30
+ return [
31
+ Number(rgb[1]) / 255,
32
+ Number(rgb[2]) / 255,
33
+ Number(rgb[3]) / 255,
34
+ rgb[4] != null ? Number(rgb[4]) : 1,
35
+ ];
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export function resolveCssColor(color: string, el?: Element | null): Rgba {
41
+ const parsed = parseCssColor(color);
42
+ if (parsed) return parsed;
43
+ if (typeof document === "undefined" || !el) return [0.35, 0.55, 0.95, 1];
44
+ const probe = document.createElement("span");
45
+ probe.style.color = color;
46
+ probe.style.position = "absolute";
47
+ probe.style.pointerEvents = "none";
48
+ probe.style.opacity = "0";
49
+ el.appendChild(probe);
50
+ const computed = getComputedStyle(probe).color;
51
+ probe.remove();
52
+ return parseCssColor(computed) ?? [0.35, 0.55, 0.95, 1];
53
+ }
54
+
55
+ export type ScatterHitCandidate = {
56
+ x: number;
57
+ y: number;
58
+ r: number;
59
+ seriesIndex: number;
60
+ pointIndex: number;
61
+ };
62
+
63
+ export function nearestScatterHit(
64
+ candidates: ScatterHitCandidate[],
65
+ px: number,
66
+ py: number,
67
+ pad = 4,
68
+ ): ScatterHitCandidate | null {
69
+ let best: ScatterHitCandidate | null = null;
70
+ let bestDist = Infinity;
71
+ for (const item of candidates) {
72
+ const dx = item.x - px;
73
+ const dy = item.y - py;
74
+ const dist = Math.hypot(dx, dy);
75
+ if (dist <= item.r + pad && dist < bestDist) {
76
+ best = item;
77
+ bestDist = dist;
78
+ }
79
+ }
80
+ return best;
81
+ }
82
+
83
+ export function svgPointerPosition(
84
+ svg: SVGSVGElement,
85
+ clientX: number,
86
+ clientY: number,
87
+ width: number,
88
+ height: number,
89
+ ): { x: number; y: number } {
90
+ const bounds = svg.getBoundingClientRect();
91
+ const w = bounds.width || width;
92
+ const h = bounds.height || height;
93
+ return {
94
+ x: ((clientX - bounds.left) / w) * width,
95
+ y: ((clientY - bounds.top) / h) * height,
96
+ };
97
+ }
98
+
99
+ const VS = `
100
+ attribute vec2 a_position;
101
+ attribute float a_size;
102
+ uniform vec2 u_resolution;
103
+ uniform float u_dpr;
104
+ void main() {
105
+ vec2 zeroToOne = a_position / u_resolution;
106
+ vec2 clip = zeroToOne * 2.0 - 1.0;
107
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
108
+ gl_PointSize = max(1.0, a_size * u_dpr);
109
+ }
110
+ `;
111
+
112
+ const FS = `
113
+ precision mediump float;
114
+ uniform vec4 u_color;
115
+ void main() {
116
+ vec2 c = gl_PointCoord - vec2(0.5);
117
+ if (dot(c, c) > 0.25) discard;
118
+ gl_FragColor = u_color;
119
+ }
120
+ `;
121
+
122
+ export type ScatterGL = {
123
+ gl: WebGLRenderingContext;
124
+ program: WebGLProgram;
125
+ buffer: WebGLBuffer;
126
+ aPosition: number;
127
+ aSize: number;
128
+ uResolution: WebGLUniformLocation | null;
129
+ uDpr: WebGLUniformLocation | null;
130
+ uColor: WebGLUniformLocation | null;
131
+ };
132
+
133
+ function compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
134
+ const shader = gl.createShader(type);
135
+ if (!shader) return null;
136
+ gl.shaderSource(shader, source);
137
+ gl.compileShader(shader);
138
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
139
+ gl.deleteShader(shader);
140
+ return null;
141
+ }
142
+ return shader;
143
+ }
144
+
145
+ export function createScatterGL(canvas: HTMLCanvasElement): ScatterGL | null {
146
+ const gl = canvas.getContext("webgl", {
147
+ alpha: true,
148
+ antialias: true,
149
+ premultipliedAlpha: true,
150
+ });
151
+ if (!gl) return null;
152
+ const vs = compile(gl, gl.VERTEX_SHADER, VS);
153
+ const fs = compile(gl, gl.FRAGMENT_SHADER, FS);
154
+ if (!vs || !fs) return null;
155
+ const program = gl.createProgram();
156
+ if (!program) return null;
157
+ gl.attachShader(program, vs);
158
+ gl.attachShader(program, fs);
159
+ gl.linkProgram(program);
160
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;
161
+ const buffer = gl.createBuffer();
162
+ if (!buffer) return null;
163
+ gl.enable(gl.BLEND);
164
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
165
+ return {
166
+ gl,
167
+ program,
168
+ buffer,
169
+ aPosition: gl.getAttribLocation(program, "a_position"),
170
+ aSize: gl.getAttribLocation(program, "a_size"),
171
+ uResolution: gl.getUniformLocation(program, "u_resolution"),
172
+ uDpr: gl.getUniformLocation(program, "u_dpr"),
173
+ uColor: gl.getUniformLocation(program, "u_color"),
174
+ };
175
+ }
176
+
177
+ export type ScatterGLSeries = {
178
+ positions: Array<{ x: number; y: number; size: number }>;
179
+ color: string;
180
+ };
181
+
182
+ export function drawScatterGL(
183
+ gpu: ScatterGL,
184
+ canvas: HTMLCanvasElement,
185
+ width: number,
186
+ height: number,
187
+ series: ScatterGLSeries[],
188
+ ): void {
189
+ const dpr = typeof window !== "undefined" ? Math.min(window.devicePixelRatio || 1, 2) : 1;
190
+ const pixelW = Math.max(1, Math.round(width * dpr));
191
+ const pixelH = Math.max(1, Math.round(height * dpr));
192
+ if (canvas.width !== pixelW) canvas.width = pixelW;
193
+ if (canvas.height !== pixelH) canvas.height = pixelH;
194
+ canvas.style.width = `${width}px`;
195
+ canvas.style.height = `${height}px`;
196
+
197
+ const { gl } = gpu;
198
+ gl.viewport(0, 0, pixelW, pixelH);
199
+ gl.clearColor(0, 0, 0, 0);
200
+ gl.clear(gl.COLOR_BUFFER_BIT);
201
+ gl.useProgram(gpu.program);
202
+ gl.uniform2f(gpu.uResolution, width, height);
203
+ gl.uniform1f(gpu.uDpr, dpr);
204
+ gl.bindBuffer(gl.ARRAY_BUFFER, gpu.buffer);
205
+
206
+ const stride = 3 * 4;
207
+ for (const item of series) {
208
+ if (!item.positions.length) continue;
209
+ const data = new Float32Array(item.positions.length * 3);
210
+ item.positions.forEach((point, i) => {
211
+ data[i * 3] = point.x;
212
+ data[i * 3 + 1] = point.y;
213
+ data[i * 3 + 2] = point.size;
214
+ });
215
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
216
+ gl.enableVertexAttribArray(gpu.aPosition);
217
+ gl.vertexAttribPointer(gpu.aPosition, 2, gl.FLOAT, false, stride, 0);
218
+ gl.enableVertexAttribArray(gpu.aSize);
219
+ gl.vertexAttribPointer(gpu.aSize, 1, gl.FLOAT, false, stride, 8);
220
+ const [r, g, b, a] = resolveCssColor(item.color, canvas.parentElement ?? canvas);
221
+ gl.uniform4f(gpu.uColor, r, g, b, a);
222
+ gl.drawArrays(gl.POINTS, 0, item.positions.length);
223
+ }
224
+ }
225
+
226
+ export function destroyScatterGL(gpu: ScatterGL): void {
227
+ const { gl } = gpu;
228
+ gl.deleteBuffer(gpu.buffer);
229
+ gl.deleteProgram(gpu.program);
230
+ }
@@ -1,114 +1,269 @@
1
1
  import * as React from "react";
2
2
  import { ChevronDown } from "lucide-react";
3
+ import { useControllableState } from "../lib/use-controllable-state";
3
4
  import { cn } from "../lib/cn";
5
+ import { Collapse } from "./Collapse";
6
+
7
+ type AccordionType = "single" | "multiple";
4
8
 
5
9
  type AccordionContextValue = {
6
10
  open: string[];
7
11
  toggle: (id: string) => void;
8
- type: "single" | "multiple";
12
+ type: AccordionType;
13
+ disabled: boolean;
14
+ disableGutters: boolean;
15
+ };
16
+
17
+ type AccordionItemContextValue = {
18
+ value: string;
19
+ disabled: boolean;
20
+ expanded: boolean;
21
+ triggerId: string;
22
+ contentId: string;
9
23
  };
10
24
 
11
25
  const AccordionContext = React.createContext<AccordionContextValue | null>(null);
26
+ const AccordionItemContext = React.createContext<AccordionItemContextValue | null>(null);
27
+
28
+ function useAccordion(component: string) {
29
+ const ctx = React.useContext(AccordionContext);
30
+ if (!ctx) throw new Error(`${component} must be used within <Accordion>`);
31
+ return ctx;
32
+ }
33
+
34
+ function useAccordionItem(component: string) {
35
+ const ctx = React.useContext(AccordionItemContext);
36
+ if (!ctx) throw new Error(`${component} must be used within <AccordionItem>`);
37
+ return ctx;
38
+ }
39
+
40
+ export type AccordionProps = Omit<React.ComponentPropsWithoutRef<"div">, "onChange"> & {
41
+ type?: AccordionType;
42
+ value?: string | string[];
43
+ defaultValue?: string | string[];
44
+ onValueChange?: (value: string | string[]) => void;
45
+ disabled?: boolean;
46
+ /** Remove the gap between items and stack them as a single surface. */
47
+ disableGutters?: boolean;
48
+ };
49
+
50
+ function toArray(value?: string | string[]) {
51
+ if (value == null || value === "") return [];
52
+ return Array.isArray(value) ? value : [value];
53
+ }
12
54
 
13
55
  function Accordion({
14
56
  type = "single",
57
+ value: valueProp,
15
58
  defaultValue,
59
+ onValueChange,
60
+ disabled = false,
61
+ disableGutters = false,
16
62
  className,
17
63
  children,
18
- }: {
19
- type?: "single" | "multiple";
20
- defaultValue?: string | string[];
21
- className?: string;
22
- children: React.ReactNode;
23
- }) {
24
- const initial = Array.isArray(defaultValue)
25
- ? defaultValue
26
- : defaultValue
27
- ? [defaultValue]
28
- : [];
29
- const [open, setOpen] = React.useState<string[]>(initial);
64
+ ...props
65
+ }: AccordionProps) {
66
+ const [open = [], setOpen] = useControllableState<string[]>({
67
+ prop: valueProp === undefined ? undefined : toArray(valueProp),
68
+ defaultProp: toArray(defaultValue),
69
+ onChange: (next) => {
70
+ onValueChange?.(type === "single" ? (next[0] ?? "") : next);
71
+ },
72
+ });
73
+
30
74
  const toggle = (id: string) => {
75
+ if (disabled) return;
31
76
  setOpen((prev) => {
32
- const isOpen = prev.includes(id);
77
+ const current = prev ?? [];
78
+ const isOpen = current.includes(id);
33
79
  if (type === "single") return isOpen ? [] : [id];
34
- return isOpen ? prev.filter((x) => x !== id) : [...prev, id];
80
+ return isOpen ? current.filter((x) => x !== id) : [...current, id];
35
81
  });
36
82
  };
83
+
37
84
  return (
38
- <AccordionContext.Provider value={{ open, toggle, type }}>
39
- <div data-slot="accordion" className={cn("space-y-2", className)}>
85
+ <AccordionContext.Provider
86
+ value={{ open: open ?? [], toggle, type, disabled, disableGutters }}
87
+ >
88
+ <div
89
+ data-slot="accordion"
90
+ data-disabled={disabled ? "" : undefined}
91
+ data-gutters={disableGutters ? "false" : "true"}
92
+ className={cn(disableGutters ? "overflow-hidden rounded-xl" : "space-y-2", className)}
93
+ {...props}
94
+ >
40
95
  {children}
41
96
  </div>
42
97
  </AccordionContext.Provider>
43
98
  );
44
99
  }
100
+ Accordion.displayName = "Accordion";
101
+
102
+ export type AccordionItemProps = React.ComponentPropsWithoutRef<"div"> & {
103
+ value: string;
104
+ disabled?: boolean;
105
+ };
45
106
 
46
107
  function AccordionItem({
47
108
  value,
109
+ disabled = false,
48
110
  className,
49
111
  children,
50
- }: {
51
- value: string;
52
- className?: string;
53
- children: React.ReactNode;
54
- }) {
112
+ ...props
113
+ }: AccordionItemProps) {
114
+ const accordion = useAccordion("AccordionItem");
115
+ const reactId = React.useId();
116
+ const expanded = accordion.open.includes(value);
117
+ const isDisabled = disabled || accordion.disabled;
118
+
55
119
  return (
56
- <div
57
- data-slot="accordion-item"
58
- data-value={value}
59
- className={cn("overflow-hidden rounded-xl border border-border/40 bg-card/40", className)}
120
+ <AccordionItemContext.Provider
121
+ value={{
122
+ value,
123
+ disabled: isDisabled,
124
+ expanded,
125
+ triggerId: `${reactId}-trigger`,
126
+ contentId: `${reactId}-content`,
127
+ }}
60
128
  >
61
- {children}
62
- </div>
129
+ <div
130
+ data-slot="accordion-item"
131
+ data-value={value}
132
+ data-state={expanded ? "open" : "closed"}
133
+ data-disabled={isDisabled ? "" : undefined}
134
+ className={cn(
135
+ "overflow-hidden border-border/40 bg-card/40",
136
+ accordion.disableGutters
137
+ ? "border-x border-b first:rounded-t-xl first:border-t last:rounded-b-xl"
138
+ : "rounded-xl border",
139
+ isDisabled && "pointer-events-none opacity-50",
140
+ className,
141
+ )}
142
+ {...props}
143
+ >
144
+ {children}
145
+ </div>
146
+ </AccordionItemContext.Provider>
63
147
  );
64
148
  }
149
+ AccordionItem.displayName = "AccordionItem";
150
+
151
+ export type AccordionTriggerProps = React.ComponentPropsWithoutRef<"button"> & {
152
+ /** @deprecated Prefer wrapping with AccordionItem — value is inherited. */
153
+ value?: string;
154
+ };
65
155
 
66
156
  function AccordionTrigger({
67
- value,
157
+ value: valueProp,
68
158
  className,
69
159
  children,
70
- }: {
71
- value: string;
72
- className?: string;
73
- children: React.ReactNode;
74
- }) {
75
- const ctx = React.useContext(AccordionContext);
76
- const isOpen = ctx?.open.includes(value);
160
+ onClick,
161
+ ...props
162
+ }: AccordionTriggerProps) {
163
+ const accordion = useAccordion("AccordionTrigger");
164
+ const item = React.useContext(AccordionItemContext);
165
+ const value = valueProp ?? item?.value;
166
+ if (!value) throw new Error("AccordionTrigger requires AccordionItem or a value prop");
167
+
168
+ const expanded = item?.expanded ?? accordion.open.includes(value);
169
+ const disabled = item?.disabled ?? accordion.disabled;
170
+
77
171
  return (
78
- <button
79
- type="button"
80
- data-slot="accordion-trigger"
81
- aria-expanded={isOpen}
82
- className={cn(
83
- "flex w-full items-center justify-between gap-2 px-4 py-3 text-left text-sm font-bold",
84
- className,
85
- )}
86
- onClick={() => ctx?.toggle(value)}
87
- >
88
- {children}
89
- <ChevronDown
90
- className={cn("size-4 shrink-0 transition-transform", isOpen && "rotate-180")}
91
- />
92
- </button>
172
+ <h3 className="m-0">
173
+ <button
174
+ type="button"
175
+ id={item?.triggerId}
176
+ data-slot="accordion-trigger"
177
+ data-state={expanded ? "open" : "closed"}
178
+ aria-expanded={expanded}
179
+ aria-controls={item?.contentId}
180
+ disabled={disabled}
181
+ className={cn(
182
+ "flex w-full items-center justify-between gap-2 px-4 py-3 text-left text-sm font-bold outline-none transition-colors",
183
+ "hover:bg-muted/30 focus-visible:ring-ring/50 focus-visible:ring-[3px]",
184
+ "disabled:cursor-not-allowed",
185
+ className,
186
+ )}
187
+ onClick={(event) => {
188
+ onClick?.(event);
189
+ if (event.defaultPrevented || disabled) return;
190
+ accordion.toggle(value);
191
+ }}
192
+ {...props}
193
+ >
194
+ {children}
195
+ <ChevronDown
196
+ aria-hidden
197
+ className={cn(
198
+ "size-4 shrink-0 text-muted-foreground transition-transform duration-300",
199
+ expanded && "rotate-180",
200
+ )}
201
+ />
202
+ </button>
203
+ </h3>
93
204
  );
94
205
  }
206
+ AccordionTrigger.displayName = "AccordionTrigger";
207
+
208
+ export type AccordionContentProps = React.ComponentPropsWithoutRef<"div"> & {
209
+ /** @deprecated Prefer wrapping with AccordionItem — value is inherited. */
210
+ value?: string;
211
+ };
95
212
 
96
213
  function AccordionContent({
97
- value,
214
+ value: valueProp,
98
215
  className,
99
216
  children,
100
- }: {
101
- value: string;
102
- className?: string;
103
- children: React.ReactNode;
104
- }) {
105
- const ctx = React.useContext(AccordionContext);
106
- if (!ctx?.open.includes(value)) return null;
217
+ ...props
218
+ }: AccordionContentProps) {
219
+ const accordion = useAccordion("AccordionContent");
220
+ const item = React.useContext(AccordionItemContext);
221
+ const value = valueProp ?? item?.value;
222
+ if (!value) throw new Error("AccordionContent requires AccordionItem or a value prop");
223
+
224
+ const expanded = item?.expanded ?? accordion.open.includes(value);
225
+
226
+ return (
227
+ <Collapse in={expanded}>
228
+ <div
229
+ id={item?.contentId}
230
+ role="region"
231
+ aria-labelledby={item?.triggerId}
232
+ data-slot="accordion-content"
233
+ className={cn("border-t border-border/30 px-4 py-3 text-sm", className)}
234
+ {...props}
235
+ >
236
+ {children}
237
+ </div>
238
+ </Collapse>
239
+ );
240
+ }
241
+ AccordionContent.displayName = "AccordionContent";
242
+
243
+ function AccordionActions({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
107
244
  return (
108
- <div data-slot="accordion-content" className={cn("border-t border-border/30 px-4 py-3 text-sm", className)}>
109
- {children}
110
- </div>
245
+ <div
246
+ data-slot="accordion-actions"
247
+ className={cn(
248
+ "flex flex-wrap items-center justify-end gap-2 border-t border-border/30 px-4 py-2.5",
249
+ className,
250
+ )}
251
+ {...props}
252
+ />
111
253
  );
112
254
  }
255
+ AccordionActions.displayName = "AccordionActions";
256
+
257
+ /** MUI-compatible aliases. */
258
+ const AccordionSummary = AccordionTrigger;
259
+ const AccordionDetails = AccordionContent;
113
260
 
114
- export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
261
+ export {
262
+ Accordion,
263
+ AccordionItem,
264
+ AccordionTrigger,
265
+ AccordionContent,
266
+ AccordionActions,
267
+ AccordionSummary,
268
+ AccordionDetails,
269
+ };
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../lib/cn";
4
+
5
+ const appBarVariants = cva("z-40 flex w-full items-center border-b border-border/50", {
6
+ variants: {
7
+ position: {
8
+ static: "relative",
9
+ sticky: "sticky top-0",
10
+ fixed: "fixed inset-x-0 top-0",
11
+ relative: "relative",
12
+ },
13
+ color: {
14
+ default: "glass-header text-foreground",
15
+ primary: "bg-primary text-primary-foreground border-transparent",
16
+ transparent: "border-transparent bg-transparent",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ position: "sticky",
21
+ color: "default",
22
+ },
23
+ });
24
+
25
+ export type AppBarProps = Omit<React.ComponentPropsWithoutRef<"header">, "color"> &
26
+ VariantProps<typeof appBarVariants>;
27
+
28
+ function AppBar({ className, position, color, ...props }: AppBarProps) {
29
+ return (
30
+ <header
31
+ data-slot="app-bar"
32
+ className={cn(appBarVariants({ position, color }), className)}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+ AppBar.displayName = "AppBar";
38
+
39
+ export { AppBar, appBarVariants };