@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,304 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { ChevronDown, ChevronLeft, ChevronRight, Settings } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+ import {
5
+ eventColorStyle,
6
+ formatDateRange,
7
+ snapMs,
8
+ startOfDay,
9
+ type CalendarView,
10
+ type NormalizedEvent,
11
+ type SchedulerPreferences,
12
+ type SchedulerResource,
13
+ type TimelineScale,
14
+ } from "../lib/scheduler";
15
+ import { Button } from "../primitives/Button";
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuTrigger,
21
+ } from "../primitives/DropdownMenu";
22
+ import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
23
+
24
+ export const CALENDAR_VIEW_LABEL: Record<CalendarView, string> = {
25
+ month: "Month",
26
+ week: "Week",
27
+ day: "Day",
28
+ agenda: "Agenda",
29
+ };
30
+
31
+ export const TIMELINE_SCALE_LABEL: Record<TimelineScale, string> = {
32
+ hours: "Hours",
33
+ days: "Days",
34
+ weeks: "Weeks",
35
+ months: "Months",
36
+ years: "Years",
37
+ };
38
+
39
+ export function SchedulerToolbar({
40
+ title,
41
+ onPrev,
42
+ onNext,
43
+ onToday,
44
+ menuLabel,
45
+ menu,
46
+ trailing,
47
+ }: {
48
+ title: string;
49
+ onPrev: () => void;
50
+ onNext: () => void;
51
+ onToday: () => void;
52
+ menuLabel: string;
53
+ menu: ReactNode;
54
+ trailing?: ReactNode;
55
+ }) {
56
+ return (
57
+ <div
58
+ data-slot="scheduler-toolbar"
59
+ className="flex flex-col gap-3 border-b border-border/50 px-3 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-4"
60
+ >
61
+ <h2 className="min-w-0 truncate text-xl font-black tracking-tight text-foreground sm:text-2xl">
62
+ {title}
63
+ </h2>
64
+ <div className="flex flex-wrap items-center gap-2">
65
+ <div className="flex items-center gap-1">
66
+ <Button
67
+ type="button"
68
+ variant="ghost"
69
+ size="icon"
70
+ aria-label="Previous"
71
+ onClick={onPrev}
72
+ >
73
+ <ChevronLeft className="size-4" />
74
+ </Button>
75
+ <Button type="button" variant="outline" size="sm" className="uppercase tracking-wide" onClick={onToday}>
76
+ Today
77
+ </Button>
78
+ <Button type="button" variant="ghost" size="icon" aria-label="Next" onClick={onNext}>
79
+ <ChevronRight className="size-4" />
80
+ </Button>
81
+ </div>
82
+ <DropdownMenu>
83
+ <DropdownMenuTrigger asChild>
84
+ <Button type="button" variant="outline" size="sm" className="min-w-[6.5rem] justify-between gap-2 uppercase tracking-wide">
85
+ {menuLabel}
86
+ <ChevronDown className="size-3.5 opacity-70" />
87
+ </Button>
88
+ </DropdownMenuTrigger>
89
+ <DropdownMenuContent align="end">{menu}</DropdownMenuContent>
90
+ </DropdownMenu>
91
+ {trailing}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export function SchedulerPreferencesMenu({
98
+ value,
99
+ onChange,
100
+ disabled,
101
+ }: {
102
+ value: Required<SchedulerPreferences>;
103
+ onChange: (next: Required<SchedulerPreferences>) => void;
104
+ disabled?: boolean;
105
+ }) {
106
+ return (
107
+ <DropdownMenu>
108
+ <DropdownMenuTrigger asChild>
109
+ <Button type="button" variant="ghost" size="icon" aria-label="Preferences" disabled={disabled}>
110
+ <Settings className="size-4" />
111
+ </Button>
112
+ </DropdownMenuTrigger>
113
+ <DropdownMenuContent align="end" className="w-52">
114
+ <DropdownMenuItem
115
+ onClick={() => onChange({ ...value, showWeekends: !value.showWeekends })}
116
+ >
117
+ {value.showWeekends ? "Hide weekends" : "Show weekends"}
118
+ </DropdownMenuItem>
119
+ <DropdownMenuItem
120
+ onClick={() => onChange({ ...value, showWeekNumber: !value.showWeekNumber })}
121
+ >
122
+ {value.showWeekNumber ? "Hide week numbers" : "Show week numbers"}
123
+ </DropdownMenuItem>
124
+ <DropdownMenuItem onClick={() => onChange({ ...value, ampm: !value.ampm })}>
125
+ {value.ampm ? "Use 24-hour time" : "Use 12-hour time"}
126
+ </DropdownMenuItem>
127
+ <DropdownMenuItem
128
+ onClick={() =>
129
+ onChange({ ...value, weekStartsOn: value.weekStartsOn === 0 ? 1 : 0 })
130
+ }
131
+ >
132
+ Week starts on {value.weekStartsOn === 0 ? "Monday" : "Sunday"}
133
+ </DropdownMenuItem>
134
+ </DropdownMenuContent>
135
+ </DropdownMenu>
136
+ );
137
+ }
138
+
139
+ export function EventResizeHandle({
140
+ event,
141
+ edge,
142
+ axis,
143
+ hourStart,
144
+ hourEnd,
145
+ onCommit,
146
+ }: {
147
+ event: NormalizedEvent;
148
+ edge: "start" | "end";
149
+ axis: "ns" | "ew";
150
+ hourStart?: number;
151
+ hourEnd?: number;
152
+ onCommit: (event: NormalizedEvent, edge: "start" | "end", instant: Date) => void;
153
+ }) {
154
+ const ns = axis === "ns";
155
+ return (
156
+ <button
157
+ type="button"
158
+ aria-label={edge === "start" ? "Resize start" : "Resize end"}
159
+ className={cn(
160
+ "absolute z-10 touch-none bg-transparent",
161
+ ns ? "inset-x-1 h-1.5 cursor-ns-resize" : "inset-y-1 w-1.5 cursor-ew-resize",
162
+ ns && edge === "start" && "top-0",
163
+ ns && edge === "end" && "bottom-0",
164
+ !ns && edge === "start" && "left-0",
165
+ !ns && edge === "end" && "right-0",
166
+ )}
167
+ onClick={(click) => click.stopPropagation()}
168
+ onPointerDown={(pointer) => {
169
+ pointer.stopPropagation();
170
+ (pointer.currentTarget as HTMLElement).setPointerCapture?.(pointer.pointerId);
171
+ }}
172
+ onPointerUp={(pointer) => {
173
+ pointer.stopPropagation();
174
+ if (ns) {
175
+ const column = (pointer.currentTarget as HTMLElement).closest(
176
+ "[data-scheduler-day]",
177
+ ) as HTMLElement | null;
178
+ if (!column || hourStart == null || hourEnd == null) return;
179
+ const rect = column.getBoundingClientRect();
180
+ const ratio = Math.min(1, Math.max(0, (pointer.clientY - rect.top) / rect.height));
181
+ const ms =
182
+ startOfDay(event.start).getTime() + (hourStart + ratio * (hourEnd - hourStart)) * 3_600_000;
183
+ onCommit(event, edge, new Date(snapMs(ms)));
184
+ return;
185
+ }
186
+ const grid = (pointer.currentTarget as HTMLElement).closest(
187
+ "[data-scheduler-grid]",
188
+ ) as HTMLElement | null;
189
+ if (!grid) return;
190
+ const start = Number(grid.dataset.rangeStart);
191
+ const end = Number(grid.dataset.rangeEnd);
192
+ const rect = grid.getBoundingClientRect();
193
+ const ratio = Math.min(1, Math.max(0, (pointer.clientX - rect.left) / rect.width));
194
+ onCommit(event, edge, new Date(snapMs(start + ratio * (end - start))));
195
+ }}
196
+ />
197
+ );
198
+ }
199
+
200
+
201
+ export function ResourceLegend({
202
+ resources,
203
+ hiddenIds,
204
+ onToggle,
205
+ }: {
206
+ resources: SchedulerResource[];
207
+ hiddenIds: ReadonlySet<string>;
208
+ onToggle: (id: string) => void;
209
+ }) {
210
+ if (resources.length === 0) return null;
211
+ return (
212
+ <div
213
+ data-slot="scheduler-resources"
214
+ className="flex flex-wrap items-center gap-1.5 border-t border-border/50 px-3 py-2.5 sm:px-4"
215
+ >
216
+ <p className="mr-1 text-[10px] font-black uppercase tracking-widest text-muted-foreground">
217
+ Resources
218
+ </p>
219
+ {resources.map((resource) => {
220
+ const active = !hiddenIds.has(resource.id);
221
+ const tone = resource.color ?? "blue";
222
+ return (
223
+ <button
224
+ key={resource.id}
225
+ type="button"
226
+ onClick={() => onToggle(resource.id)}
227
+ aria-pressed={active}
228
+ className={cn(
229
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold transition-colors",
230
+ active
231
+ ? "border-border/60 bg-background/80 text-foreground"
232
+ : "border-transparent bg-muted/30 text-muted-foreground line-through",
233
+ )}
234
+ >
235
+ <span
236
+ className="size-2 rounded-full"
237
+ style={eventColorStyle(tone, "dot")}
238
+ aria-hidden
239
+ />
240
+ {resource.title}
241
+ </button>
242
+ );
243
+ })}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ export function EventPopover({
249
+ event,
250
+ resources,
251
+ children,
252
+ onOpenChange,
253
+ }: {
254
+ event: NormalizedEvent;
255
+ resources: SchedulerResource[];
256
+ children: ReactNode;
257
+ onOpenChange?: (open: boolean) => void;
258
+ }) {
259
+ const resource = resources.find((item) => item.id === event.resourceId);
260
+ return (
261
+ <Popover onOpenChange={onOpenChange}>
262
+ <PopoverTrigger asChild>{children}</PopoverTrigger>
263
+ <PopoverContent align="start" sideOffset={6} className="w-72 p-3">
264
+ <p className="text-sm font-bold text-foreground">{event.title}</p>
265
+ <p className="mt-1 text-xs text-muted-foreground">
266
+ {formatDateRange(event.start, event.end, event.allDay)}
267
+ </p>
268
+ {resource ? (
269
+ <p className="mt-2 inline-flex items-center gap-1.5 text-xs font-semibold text-foreground">
270
+ <span className="size-2 rounded-full" style={eventColorStyle(event.color, "dot")} />
271
+ {resource.title}
272
+ </p>
273
+ ) : null}
274
+ {event.description ? (
275
+ <p className="mt-2 text-xs leading-relaxed text-muted-foreground">{event.description}</p>
276
+ ) : null}
277
+ </PopoverContent>
278
+ </Popover>
279
+ );
280
+ }
281
+
282
+ export function SchedulerShell({
283
+ className,
284
+ slot,
285
+ children,
286
+ ...props
287
+ }: {
288
+ className?: string;
289
+ slot: string;
290
+ children: ReactNode;
291
+ } & HTMLAttributes<HTMLDivElement>) {
292
+ return (
293
+ <div
294
+ data-slot={slot}
295
+ className={cn(
296
+ "relative overflow-hidden rounded-[var(--radius-card)] border border-border/60 bg-background/80 shadow-[var(--shadow-soft)]",
297
+ className,
298
+ )}
299
+ {...props}
300
+ >
301
+ {children}
302
+ </div>
303
+ );
304
+ }
@@ -0,0 +1,194 @@
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 { EventCalendar } from "./EventCalendar";
5
+ import { EventTimeline } from "./EventTimeline";
6
+ import type { SchedulerEvent, SchedulerResource } from "../lib/scheduler";
7
+
8
+ const resources: SchedulerResource[] = [
9
+ { id: "work", title: "Work", color: "violet" },
10
+ { id: "health", title: "Health", color: "teal" },
11
+ ];
12
+
13
+ const events: SchedulerEvent[] = [
14
+ {
15
+ id: "run",
16
+ title: "Morning Run",
17
+ start: "2026-08-03T07:00:00",
18
+ end: "2026-08-03T07:45:00",
19
+ resourceId: "health",
20
+ },
21
+ {
22
+ id: "bday",
23
+ title: "Alice's Birthday",
24
+ start: "2026-08-06",
25
+ end: "2026-08-08",
26
+ allDay: true,
27
+ resourceId: "work",
28
+ color: "rose",
29
+ },
30
+ {
31
+ id: "review",
32
+ title: "Design Review",
33
+ start: "2026-08-09T14:00:00",
34
+ end: "2026-08-09T16:00:00",
35
+ resourceId: "work",
36
+ },
37
+ ];
38
+
39
+ describe("EventCalendar", () => {
40
+ it("renders the month heading, weekday labels, and events", () => {
41
+ render(
42
+ <EventCalendar
43
+ defaultDate={new Date(2026, 7, 15)}
44
+ events={events}
45
+ resources={resources}
46
+ locale="en-US"
47
+ />,
48
+ );
49
+ expect(screen.getByRole("heading", { name: /August 2026/i })).toBeInTheDocument();
50
+ expect(screen.getByText(/Sun/)).toBeInTheDocument();
51
+ expect(screen.getByText("Morning Run")).toBeInTheDocument();
52
+ expect(screen.getByText("Alice's Birthday")).toBeInTheDocument();
53
+ expect(screen.getByText("Work")).toBeInTheDocument();
54
+ });
55
+
56
+ it("jumps to today from the toolbar", async () => {
57
+ const user = userEvent.setup();
58
+ const onDateChange = vi.fn();
59
+ render(
60
+ <EventCalendar
61
+ defaultDate={new Date(2025, 0, 1)}
62
+ onDateChange={onDateChange}
63
+ events={events}
64
+ />,
65
+ );
66
+ await user.click(screen.getByRole("button", { name: "Today" }));
67
+ expect(onDateChange).toHaveBeenCalled();
68
+ const next = onDateChange.mock.calls[0][0] as Date;
69
+ const now = new Date();
70
+ expect(next.getFullYear()).toBe(now.getFullYear());
71
+ expect(next.getMonth()).toBe(now.getMonth());
72
+ });
73
+
74
+ it("fires onEventClick for a timed event", async () => {
75
+ const user = userEvent.setup();
76
+ const onEventClick = vi.fn();
77
+ render(
78
+ <EventCalendar
79
+ defaultDate={new Date(2026, 7, 15)}
80
+ events={events}
81
+ resources={resources}
82
+ locale="en-US"
83
+ onEventClick={onEventClick}
84
+ />,
85
+ );
86
+ await user.click(screen.getByText("Morning Run"));
87
+ expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: "run" }));
88
+ });
89
+
90
+ it("switches to week view from the menu", async () => {
91
+ const user = userEvent.setup();
92
+ render(
93
+ <EventCalendar defaultDate={new Date(2026, 7, 15)} events={events} locale="en-US" />,
94
+ );
95
+ await user.click(screen.getByRole("button", { name: /month/i }));
96
+ await user.click(screen.getByRole("menuitem", { name: "Week" }));
97
+ expect(screen.getByRole("heading", { name: /2026/ })).toBeInTheDocument();
98
+ });
99
+
100
+ it("lists events in agenda view", () => {
101
+ render(
102
+ <EventCalendar
103
+ defaultView="agenda"
104
+ defaultDate={new Date(2026, 7, 3)}
105
+ events={events}
106
+ locale="en-US"
107
+ />,
108
+ );
109
+ expect(screen.getByRole("heading", { name: /Aug 3/ })).toBeInTheDocument();
110
+ expect(screen.getAllByText("No events").length).toBeGreaterThan(0);
111
+ expect(screen.getByText("Morning Run")).toBeInTheDocument();
112
+ expect(screen.getAllByText("Alice's Birthday").length).toBeGreaterThan(0);
113
+ });
114
+
115
+ it("opens the editor and saves title changes", async () => {
116
+ const user = userEvent.setup();
117
+ const onEventChange = vi.fn();
118
+ render(
119
+ <EventCalendar
120
+ defaultDate={new Date(2026, 7, 15)}
121
+ events={events}
122
+ resources={resources}
123
+ locale="en-US"
124
+ onEventChange={onEventChange}
125
+ />,
126
+ );
127
+ await user.click(screen.getByText("Morning Run"));
128
+ const title = screen.getByRole("textbox", { name: /title/i });
129
+ await user.clear(title);
130
+ await user.type(title, "Jog");
131
+ await user.click(screen.getByRole("button", { name: "Save" }));
132
+ expect(onEventChange).toHaveBeenCalledWith(
133
+ expect.objectContaining({ id: "run" }),
134
+ expect.objectContaining({ title: "Jog" }),
135
+ );
136
+ });
137
+
138
+ it("creates an event from a day slot", async () => {
139
+ const user = userEvent.setup();
140
+ const onEventCreate = vi.fn();
141
+ render(
142
+ <EventCalendar
143
+ defaultDate={new Date(2026, 7, 15)}
144
+ events={events}
145
+ locale="en-US"
146
+ onEventCreate={onEventCreate}
147
+ />,
148
+ );
149
+ await user.click(screen.getByRole("button", { name: "15" }));
150
+ expect(screen.getByRole("heading", { name: "New event" })).toBeInTheDocument();
151
+ await user.type(screen.getByRole("textbox", { name: /title/i }), "Focus time");
152
+ await user.click(screen.getByRole("button", { name: "Save" }));
153
+ expect(onEventCreate).toHaveBeenCalledWith(expect.objectContaining({ title: "Focus time" }));
154
+ });
155
+ });
156
+
157
+ describe("EventTimeline", () => {
158
+ const timelineEvents: SchedulerEvent[] = [
159
+ {
160
+ id: "api",
161
+ title: "API V3 Development",
162
+ start: "2026-07-01",
163
+ end: "2026-09-20",
164
+ allDay: true,
165
+ resourceId: "eng",
166
+ },
167
+ {
168
+ id: "ui",
169
+ title: "Mobile App UI/UX",
170
+ start: "2026-08-01",
171
+ end: "2026-10-15",
172
+ allDay: true,
173
+ resourceId: "design",
174
+ },
175
+ ];
176
+ const timelineResources: SchedulerResource[] = [
177
+ { id: "design", title: "Design", color: "rose" },
178
+ { id: "eng", title: "Engineering", color: "blue" },
179
+ ];
180
+
181
+ it("renders resource rows and event bars", () => {
182
+ render(
183
+ <EventTimeline
184
+ defaultDate={new Date(2026, 7, 1)}
185
+ events={timelineEvents}
186
+ resources={timelineResources}
187
+ />,
188
+ );
189
+ expect(screen.getAllByText("Design").length).toBeGreaterThan(0);
190
+ expect(screen.getAllByText("Engineering").length).toBeGreaterThan(0);
191
+ expect(screen.getByText("API V3 Development")).toBeInTheDocument();
192
+ expect(screen.getByText("Mobile App UI/UX")).toBeInTheDocument();
193
+ });
194
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ BarChart,
4
+ BarChart3D,
5
+ Button,
6
+ ChartContainer,
7
+ ChartDataGrid,
8
+ MapChart,
9
+ mercator,
10
+ } from "./index";
11
+
12
+ describe("@spatika/react public exports", () => {
13
+ it("exports primitives, chart composites, and helpers", () => {
14
+ expect(Button.displayName).toBe("Button");
15
+ expect(BarChart).toBeTypeOf("function");
16
+ expect(ChartContainer).toBeTypeOf("function");
17
+ expect(ChartDataGrid).toBeTypeOf("function");
18
+ expect(MapChart).toBeTypeOf("function");
19
+ expect(BarChart3D).toBeTypeOf("function");
20
+ expect(mercator([0, 0])[0]).toBeCloseTo(0.5);
21
+ });
22
+ });