@spatika/react 1.0.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 (369) hide show
  1. package/AGENTS.md +71 -0
  2. package/README.md +4 -3
  3. package/dist/composites/AccentRule.d.ts +6 -0
  4. package/dist/composites/AccentRule.d.ts.map +1 -0
  5. package/dist/composites/AccentRule.js +6 -0
  6. package/dist/composites/AvailabilityBadge.d.ts +9 -0
  7. package/dist/composites/AvailabilityBadge.d.ts.map +1 -0
  8. package/dist/composites/AvailabilityBadge.js +9 -0
  9. package/dist/composites/Card.d.ts +7 -1
  10. package/dist/composites/Card.d.ts.map +1 -1
  11. package/dist/composites/Card.js +10 -0
  12. package/dist/composites/CareerCard.d.ts +20 -0
  13. package/dist/composites/CareerCard.d.ts.map +1 -0
  14. package/dist/composites/CareerCard.js +17 -0
  15. package/dist/composites/ChartDataGrid.d.ts +33 -0
  16. package/dist/composites/ChartDataGrid.d.ts.map +1 -0
  17. package/dist/composites/ChartDataGrid.js +77 -0
  18. package/dist/composites/ContactLink.d.ts +15 -0
  19. package/dist/composites/ContactLink.d.ts.map +1 -0
  20. package/dist/composites/ContactLink.js +8 -0
  21. package/dist/composites/CoverHero.d.ts +1 -1
  22. package/dist/composites/CoverHero.d.ts.map +1 -1
  23. package/dist/composites/CoverHero.js +2 -2
  24. package/dist/composites/DisplayHeading.d.ts +9 -0
  25. package/dist/composites/DisplayHeading.d.ts.map +1 -0
  26. package/dist/composites/DisplayHeading.js +6 -0
  27. package/dist/composites/EntityMediaCard.d.ts +1 -1
  28. package/dist/composites/EntityMediaCard.d.ts.map +1 -1
  29. package/dist/composites/EntityMediaCard.js +12 -4
  30. package/dist/composites/EventCalendar.d.ts +35 -0
  31. package/dist/composites/EventCalendar.d.ts.map +1 -0
  32. package/dist/composites/EventCalendar.js +339 -0
  33. package/dist/composites/EventEditor.d.ts +22 -0
  34. package/dist/composites/EventEditor.d.ts.map +1 -0
  35. package/dist/composites/EventEditor.js +86 -0
  36. package/dist/composites/EventTimeline.d.ts +24 -0
  37. package/dist/composites/EventTimeline.d.ts.map +1 -0
  38. package/dist/composites/EventTimeline.js +184 -0
  39. package/dist/composites/FloatChip.d.ts +15 -0
  40. package/dist/composites/FloatChip.d.ts.map +1 -0
  41. package/dist/composites/FloatChip.js +7 -0
  42. package/dist/composites/GradientText.d.ts +11 -0
  43. package/dist/composites/GradientText.d.ts.map +1 -0
  44. package/dist/composites/GradientText.js +6 -0
  45. package/dist/composites/IconTile.d.ts +10 -0
  46. package/dist/composites/IconTile.d.ts.map +1 -0
  47. package/dist/composites/IconTile.js +7 -0
  48. package/dist/composites/MapChart.d.ts +24 -0
  49. package/dist/composites/MapChart.d.ts.map +1 -0
  50. package/dist/composites/MapChart.js +58 -0
  51. package/dist/composites/MetaChip.d.ts +9 -0
  52. package/dist/composites/MetaChip.d.ts.map +1 -0
  53. package/dist/composites/MetaChip.js +6 -0
  54. package/dist/composites/PresenceDot.d.ts +10 -0
  55. package/dist/composites/PresenceDot.d.ts.map +1 -0
  56. package/dist/composites/PresenceDot.js +10 -0
  57. package/dist/composites/ProfileHero.d.ts +6 -4
  58. package/dist/composites/ProfileHero.d.ts.map +1 -1
  59. package/dist/composites/ProfileHero.js +8 -8
  60. package/dist/composites/ProjectCard.d.ts +12 -0
  61. package/dist/composites/ProjectCard.d.ts.map +1 -0
  62. package/dist/composites/ProjectCard.js +9 -0
  63. package/dist/composites/ScatterWebGL.d.ts +22 -0
  64. package/dist/composites/ScatterWebGL.d.ts.map +1 -0
  65. package/dist/composites/ScatterWebGL.js +45 -0
  66. package/dist/composites/SectionHeading.d.ts +16 -0
  67. package/dist/composites/SectionHeading.d.ts.map +1 -0
  68. package/dist/composites/SectionHeading.js +12 -0
  69. package/dist/composites/SiteFooter.d.ts +18 -0
  70. package/dist/composites/SiteFooter.d.ts.map +1 -0
  71. package/dist/composites/SiteFooter.js +15 -0
  72. package/dist/composites/SiteNav.d.ts +25 -0
  73. package/dist/composites/SiteNav.d.ts.map +1 -0
  74. package/dist/composites/SiteNav.js +44 -0
  75. package/dist/composites/StatCard.d.ts +7 -1
  76. package/dist/composites/StatCard.d.ts.map +1 -1
  77. package/dist/composites/StatCard.js +5 -2
  78. package/dist/composites/StatusDot.d.ts +1 -1
  79. package/dist/composites/Tag.d.ts +11 -0
  80. package/dist/composites/Tag.d.ts.map +1 -0
  81. package/dist/composites/Tag.js +33 -0
  82. package/dist/composites/Wordmark.d.ts +19 -0
  83. package/dist/composites/Wordmark.d.ts.map +1 -0
  84. package/dist/composites/Wordmark.js +15 -0
  85. package/dist/composites/cartesian-charts.d.ts +138 -0
  86. package/dist/composites/cartesian-charts.d.ts.map +1 -0
  87. package/dist/composites/cartesian-charts.js +401 -0
  88. package/dist/composites/chart-composition.d.ts +139 -0
  89. package/dist/composites/chart-composition.d.ts.map +1 -0
  90. package/dist/composites/chart-composition.js +486 -0
  91. package/dist/composites/chart-ui.d.ts +83 -0
  92. package/dist/composites/chart-ui.d.ts.map +1 -0
  93. package/dist/composites/chart-ui.js +108 -0
  94. package/dist/composites/charts-3d.d.ts +37 -0
  95. package/dist/composites/charts-3d.d.ts.map +1 -0
  96. package/dist/composites/charts-3d.js +121 -0
  97. package/dist/composites/flow-charts.d.ts +89 -0
  98. package/dist/composites/flow-charts.d.ts.map +1 -0
  99. package/dist/composites/flow-charts.js +186 -0
  100. package/dist/composites/radial-charts.d.ts +109 -0
  101. package/dist/composites/radial-charts.d.ts.map +1 -0
  102. package/dist/composites/radial-charts.js +176 -0
  103. package/dist/composites/scheduler-ui.d.ts +43 -0
  104. package/dist/composites/scheduler-ui.d.ts.map +1 -0
  105. package/dist/composites/scheduler-ui.js +71 -0
  106. package/dist/index.d.ts +83 -7
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +74 -7
  109. package/dist/lib/chart-export.d.ts +10 -0
  110. package/dist/lib/chart-export.d.ts.map +1 -0
  111. package/dist/lib/chart-export.js +102 -0
  112. package/dist/lib/charts.d.ts +235 -0
  113. package/dist/lib/charts.d.ts.map +1 -0
  114. package/dist/lib/charts.js +744 -0
  115. package/dist/lib/geo.d.ts +39 -0
  116. package/dist/lib/geo.d.ts.map +1 -0
  117. package/dist/lib/geo.js +101 -0
  118. package/dist/lib/iso.d.ts +21 -0
  119. package/dist/lib/iso.d.ts.map +1 -0
  120. package/dist/lib/iso.js +56 -0
  121. package/dist/lib/rrule.d.ts +20 -0
  122. package/dist/lib/rrule.d.ts.map +1 -0
  123. package/dist/lib/rrule.js +236 -0
  124. package/dist/lib/scheduler.d.ts +152 -0
  125. package/dist/lib/scheduler.d.ts.map +1 -0
  126. package/dist/lib/scheduler.js +501 -0
  127. package/dist/lib/webgl-scatter.d.ts +44 -0
  128. package/dist/lib/webgl-scatter.d.ts.map +1 -0
  129. package/dist/lib/webgl-scatter.js +185 -0
  130. package/dist/primitives/Accordion.d.ts +44 -20
  131. package/dist/primitives/Accordion.d.ts.map +1 -1
  132. package/dist/primitives/Accordion.js +82 -22
  133. package/dist/primitives/AppBar.d.ts +13 -0
  134. package/dist/primitives/AppBar.d.ts.map +1 -0
  135. package/dist/primitives/AppBar.js +27 -0
  136. package/dist/primitives/Autocomplete.d.ts +28 -0
  137. package/dist/primitives/Autocomplete.d.ts.map +1 -0
  138. package/dist/primitives/Autocomplete.js +118 -0
  139. package/dist/primitives/Avatar.d.ts +13 -1
  140. package/dist/primitives/Avatar.d.ts.map +1 -1
  141. package/dist/primitives/Avatar.js +17 -2
  142. package/dist/primitives/Backdrop.d.ts +17 -0
  143. package/dist/primitives/Backdrop.d.ts.map +1 -0
  144. package/dist/primitives/Backdrop.js +17 -0
  145. package/dist/primitives/BottomNavigation.d.ts +23 -0
  146. package/dist/primitives/BottomNavigation.d.ts.map +1 -0
  147. package/dist/primitives/BottomNavigation.js +30 -0
  148. package/dist/primitives/Button.d.ts +4 -4
  149. package/dist/primitives/Button.d.ts.map +1 -1
  150. package/dist/primitives/Button.js +2 -0
  151. package/dist/primitives/ButtonBase.d.ts +14 -0
  152. package/dist/primitives/ButtonBase.d.ts.map +1 -0
  153. package/dist/primitives/ButtonBase.js +13 -0
  154. package/dist/primitives/ButtonGroup.d.ts +25 -0
  155. package/dist/primitives/ButtonGroup.d.ts.map +1 -0
  156. package/dist/primitives/ButtonGroup.js +44 -0
  157. package/dist/primitives/CircularProgress.d.ts +16 -0
  158. package/dist/primitives/CircularProgress.d.ts.map +1 -0
  159. package/dist/primitives/CircularProgress.js +17 -0
  160. package/dist/primitives/ClickAwayListener.d.ts +16 -0
  161. package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
  162. package/dist/primitives/ClickAwayListener.js +33 -0
  163. package/dist/primitives/Collapse.d.ts +19 -0
  164. package/dist/primitives/Collapse.d.ts.map +1 -0
  165. package/dist/primitives/Collapse.js +14 -0
  166. package/dist/primitives/Container.d.ts +13 -0
  167. package/dist/primitives/Container.d.ts.map +1 -0
  168. package/dist/primitives/Container.js +26 -0
  169. package/dist/primitives/Fab.d.ts +22 -0
  170. package/dist/primitives/Fab.d.ts.map +1 -0
  171. package/dist/primitives/Fab.js +42 -0
  172. package/dist/primitives/FormControl.d.ts +27 -0
  173. package/dist/primitives/FormControl.d.ts.map +1 -0
  174. package/dist/primitives/FormControl.js +24 -0
  175. package/dist/primitives/FormControlLabel.d.ts +18 -0
  176. package/dist/primitives/FormControlLabel.d.ts.map +1 -0
  177. package/dist/primitives/FormControlLabel.js +19 -0
  178. package/dist/primitives/Grid.d.ts +32 -0
  179. package/dist/primitives/Grid.d.ts.map +1 -0
  180. package/dist/primitives/Grid.js +50 -0
  181. package/dist/primitives/IconButton.d.ts +22 -0
  182. package/dist/primitives/IconButton.d.ts.map +1 -0
  183. package/dist/primitives/IconButton.js +39 -0
  184. package/dist/primitives/ImageList.d.ts +31 -0
  185. package/dist/primitives/ImageList.d.ts.map +1 -0
  186. package/dist/primitives/ImageList.js +33 -0
  187. package/dist/primitives/InputAdornment.d.ts +28 -0
  188. package/dist/primitives/InputAdornment.d.ts.map +1 -0
  189. package/dist/primitives/InputAdornment.js +13 -0
  190. package/dist/primitives/Link.d.ts +17 -0
  191. package/dist/primitives/Link.d.ts.map +1 -0
  192. package/dist/primitives/Link.js +29 -0
  193. package/dist/primitives/List.d.ts +41 -0
  194. package/dist/primitives/List.d.ts.map +1 -0
  195. package/dist/primitives/List.js +22 -0
  196. package/dist/primitives/Masonry.d.ts +14 -0
  197. package/dist/primitives/Masonry.d.ts.map +1 -0
  198. package/dist/primitives/Masonry.js +15 -0
  199. package/dist/primitives/Menu.d.ts +28 -0
  200. package/dist/primitives/Menu.d.ts.map +1 -0
  201. package/dist/primitives/Menu.js +54 -0
  202. package/dist/primitives/MobileStepper.d.ts +15 -0
  203. package/dist/primitives/MobileStepper.d.ts.map +1 -0
  204. package/dist/primitives/MobileStepper.js +8 -0
  205. package/dist/primitives/Modal.d.ts +18 -0
  206. package/dist/primitives/Modal.d.ts.map +1 -0
  207. package/dist/primitives/Modal.js +29 -0
  208. package/dist/primitives/NoSsr.d.ts +15 -0
  209. package/dist/primitives/NoSsr.d.ts.map +1 -0
  210. package/dist/primitives/NoSsr.js +19 -0
  211. package/dist/primitives/Paper.d.ts +22 -0
  212. package/dist/primitives/Paper.d.ts.map +1 -0
  213. package/dist/primitives/Paper.js +33 -0
  214. package/dist/primitives/Progress.d.ts +6 -2
  215. package/dist/primitives/Progress.d.ts.map +1 -1
  216. package/dist/primitives/Progress.js +8 -3
  217. package/dist/primitives/Snackbar.d.ts +23 -0
  218. package/dist/primitives/Snackbar.d.ts.map +1 -0
  219. package/dist/primitives/Snackbar.js +33 -0
  220. package/dist/primitives/SpeedDial.d.ts +26 -0
  221. package/dist/primitives/SpeedDial.d.ts.map +1 -0
  222. package/dist/primitives/SpeedDial.js +26 -0
  223. package/dist/primitives/Stack.d.ts +15 -0
  224. package/dist/primitives/Stack.d.ts.map +1 -0
  225. package/dist/primitives/Stack.js +45 -0
  226. package/dist/primitives/StarRating.d.ts +19 -1
  227. package/dist/primitives/StarRating.d.ts.map +1 -1
  228. package/dist/primitives/StarRating.js +36 -6
  229. package/dist/primitives/Stepper.d.ts +36 -0
  230. package/dist/primitives/Stepper.d.ts.map +1 -0
  231. package/dist/primitives/Stepper.js +53 -0
  232. package/dist/primitives/TablePagination.d.ts +16 -0
  233. package/dist/primitives/TablePagination.d.ts.map +1 -0
  234. package/dist/primitives/TablePagination.js +12 -0
  235. package/dist/primitives/TextField.d.ts +24 -0
  236. package/dist/primitives/TextField.d.ts.map +1 -0
  237. package/dist/primitives/TextField.js +19 -0
  238. package/dist/primitives/TextareaAutosize.d.ts +11 -0
  239. package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
  240. package/dist/primitives/TextareaAutosize.js +26 -0
  241. package/dist/primitives/ToggleButton.d.ts +28 -0
  242. package/dist/primitives/ToggleButton.d.ts.map +1 -0
  243. package/dist/primitives/ToggleButton.js +56 -0
  244. package/dist/primitives/TransferList.d.ts +21 -0
  245. package/dist/primitives/TransferList.d.ts.map +1 -0
  246. package/dist/primitives/TransferList.js +40 -0
  247. package/dist/primitives/Typography.d.ts +23 -0
  248. package/dist/primitives/Typography.d.ts.map +1 -0
  249. package/dist/primitives/Typography.js +69 -0
  250. package/dist/primitives/transitions.d.ts +29 -0
  251. package/dist/primitives/transitions.d.ts.map +1 -0
  252. package/dist/primitives/transitions.js +52 -0
  253. package/package.json +30 -5
  254. package/skills/spatika-ui/SKILL.md +96 -0
  255. package/src/composites/AccentRule.tsx +12 -0
  256. package/src/composites/AvailabilityBadge.tsx +32 -0
  257. package/src/composites/Card.tsx +51 -1
  258. package/src/composites/CareerCard.tsx +111 -0
  259. package/src/composites/ChartDataGrid.test.tsx +57 -0
  260. package/src/composites/ChartDataGrid.tsx +196 -0
  261. package/src/composites/ContactLink.tsx +66 -0
  262. package/src/composites/CoverHero.test.tsx +38 -0
  263. package/src/composites/CoverHero.tsx +29 -22
  264. package/src/composites/DisplayHeading.tsx +27 -0
  265. package/src/composites/EntityMediaCard.test.tsx +58 -0
  266. package/src/composites/EntityMediaCard.tsx +31 -13
  267. package/src/composites/EventCalendar.tsx +928 -0
  268. package/src/composites/EventEditor.tsx +268 -0
  269. package/src/composites/EventTimeline.tsx +357 -0
  270. package/src/composites/FloatChip.tsx +63 -0
  271. package/src/composites/GradientText.tsx +31 -0
  272. package/src/composites/IconTile.tsx +33 -0
  273. package/src/composites/MapChart.test.tsx +39 -0
  274. package/src/composites/MapChart.tsx +130 -0
  275. package/src/composites/MetaChip.tsx +24 -0
  276. package/src/composites/PresenceDot.tsx +42 -0
  277. package/src/composites/ProfileHero.test.tsx +33 -0
  278. package/src/composites/ProfileHero.tsx +96 -79
  279. package/src/composites/ProjectCard.tsx +65 -0
  280. package/src/composites/ScatterWebGL.test.tsx +25 -0
  281. package/src/composites/ScatterWebGL.tsx +79 -0
  282. package/src/composites/SectionHeading.tsx +61 -0
  283. package/src/composites/SiteFooter.test.tsx +37 -0
  284. package/src/composites/SiteFooter.tsx +79 -0
  285. package/src/composites/SiteNav.test.tsx +46 -0
  286. package/src/composites/SiteNav.tsx +150 -0
  287. package/src/composites/StatCard.test.tsx +30 -0
  288. package/src/composites/StatCard.tsx +36 -1
  289. package/src/composites/Tag.tsx +50 -0
  290. package/src/composites/Wordmark.test.tsx +23 -0
  291. package/src/composites/Wordmark.tsx +46 -0
  292. package/src/composites/cartesian-charts.test.tsx +52 -0
  293. package/src/composites/cartesian-charts.tsx +1287 -0
  294. package/src/composites/chart-composition.test.tsx +53 -0
  295. package/src/composites/chart-composition.tsx +898 -0
  296. package/src/composites/chart-ui.tsx +328 -0
  297. package/src/composites/charts-3d.test.tsx +25 -0
  298. package/src/composites/charts-3d.tsx +318 -0
  299. package/src/composites/charts.test.tsx +254 -0
  300. package/src/composites/chrome.test.tsx +29 -0
  301. package/src/composites/flow-charts.test.tsx +79 -0
  302. package/src/composites/flow-charts.tsx +607 -0
  303. package/src/composites/marketing.test.tsx +192 -0
  304. package/src/composites/radial-charts.test.tsx +74 -0
  305. package/src/composites/radial-charts.tsx +617 -0
  306. package/src/composites/scheduler-ui.tsx +304 -0
  307. package/src/composites/scheduler.test.tsx +194 -0
  308. package/src/index.test.ts +22 -0
  309. package/src/index.ts +300 -5
  310. package/src/lib/chart-export.test.ts +20 -0
  311. package/src/lib/chart-export.ts +99 -0
  312. package/src/lib/charts-platform.test.ts +93 -0
  313. package/src/lib/charts.test.ts +132 -0
  314. package/src/lib/charts.ts +1021 -0
  315. package/src/lib/geo.test.ts +42 -0
  316. package/src/lib/geo.ts +138 -0
  317. package/src/lib/iso.test.ts +22 -0
  318. package/src/lib/iso.ts +102 -0
  319. package/src/lib/rrule.test.ts +53 -0
  320. package/src/lib/rrule.ts +281 -0
  321. package/src/lib/scheduler.test.ts +186 -0
  322. package/src/lib/scheduler.ts +688 -0
  323. package/src/lib/tokens.test.ts +2 -2
  324. package/src/lib/webgl-scatter.test.ts +36 -0
  325. package/src/lib/webgl-scatter.ts +230 -0
  326. package/src/primitives/Accordion.tsx +218 -63
  327. package/src/primitives/AppBar.tsx +39 -0
  328. package/src/primitives/Autocomplete.tsx +236 -0
  329. package/src/primitives/Avatar.tsx +52 -1
  330. package/src/primitives/Backdrop.tsx +48 -0
  331. package/src/primitives/BottomNavigation.tsx +102 -0
  332. package/src/primitives/Button.tsx +3 -0
  333. package/src/primitives/ButtonBase.tsx +34 -0
  334. package/src/primitives/ButtonGroup.tsx +90 -0
  335. package/src/primitives/CircularProgress.tsx +104 -0
  336. package/src/primitives/ClickAwayListener.tsx +46 -0
  337. package/src/primitives/Collapse.tsx +39 -0
  338. package/src/primitives/Container.tsx +40 -0
  339. package/src/primitives/Fab.tsx +62 -0
  340. package/src/primitives/FormControl.tsx +90 -0
  341. package/src/primitives/FormControlLabel.tsx +51 -0
  342. package/src/primitives/Grid.tsx +101 -0
  343. package/src/primitives/IconButton.tsx +59 -0
  344. package/src/primitives/ImageList.test.tsx +48 -0
  345. package/src/primitives/ImageList.tsx +112 -0
  346. package/src/primitives/InputAdornment.tsx +86 -0
  347. package/src/primitives/Link.tsx +48 -0
  348. package/src/primitives/List.tsx +121 -0
  349. package/src/primitives/Masonry.tsx +32 -0
  350. package/src/primitives/Menu.tsx +112 -0
  351. package/src/primitives/MobileStepper.tsx +64 -0
  352. package/src/primitives/Modal.tsx +70 -0
  353. package/src/primitives/NoSsr.tsx +28 -0
  354. package/src/primitives/Paper.tsx +55 -0
  355. package/src/primitives/Progress.tsx +25 -7
  356. package/src/primitives/Snackbar.tsx +83 -0
  357. package/src/primitives/SpeedDial.tsx +110 -0
  358. package/src/primitives/Stack.tsx +88 -0
  359. package/src/primitives/StarRating.tsx +87 -28
  360. package/src/primitives/Stepper.tsx +184 -0
  361. package/src/primitives/TablePagination.tsx +58 -0
  362. package/src/primitives/TextField.tsx +105 -0
  363. package/src/primitives/TextareaAutosize.tsx +49 -0
  364. package/src/primitives/ToggleButton.tsx +148 -0
  365. package/src/primitives/TransferList.tsx +110 -0
  366. package/src/primitives/Typography.tsx +90 -0
  367. package/src/primitives/mui-more.test.tsx +76 -0
  368. package/src/primitives/mui.test.tsx +134 -0
  369. package/src/primitives/transitions.tsx +120 -0
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { featureId, fitFeatures, geoPath, mercator } from "./geo";
3
+
4
+ describe("geo", () => {
5
+ it("projects the equator to the mercator midpoint", () => {
6
+ const [x, y] = mercator([0, 0]);
7
+ expect(x).toBeCloseTo(0.5);
8
+ expect(y).toBeCloseTo(0.5);
9
+ });
10
+
11
+ it("builds a closed polygon path fitted to a viewport", () => {
12
+ const features = [
13
+ {
14
+ type: "Feature" as const,
15
+ id: "box",
16
+ properties: { name: "Box" },
17
+ geometry: {
18
+ type: "Polygon" as const,
19
+ coordinates: [
20
+ [
21
+ [0, 0],
22
+ [10, 0],
23
+ [10, 10],
24
+ [0, 10],
25
+ [0, 0],
26
+ ],
27
+ ],
28
+ },
29
+ },
30
+ ];
31
+ const project = fitFeatures(features, 200, 100, 10);
32
+ const d = geoPath(features[0]!.geometry, project);
33
+ expect(d.startsWith("M")).toBe(true);
34
+ expect(d.includes("Z")).toBe(true);
35
+ expect(featureId(features[0]!)).toBe("box");
36
+ });
37
+
38
+ it("projects a point to a move-to command", () => {
39
+ const d = geoPath({ type: "Point", coordinates: [0, 0] }, mercator);
40
+ expect(d.startsWith("M")).toBe(true);
41
+ });
42
+ });
package/src/lib/geo.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tiny GeoJSON helpers — Mercator / equirectangular, no d3-geo.
3
+ */
4
+
5
+ export type GeoPosition = [number, number];
6
+
7
+ export type GeoJsonGeometry =
8
+ | { type: "Polygon"; coordinates: GeoPosition[][] }
9
+ | { type: "MultiPolygon"; coordinates: GeoPosition[][][] }
10
+ | { type: "Point"; coordinates: GeoPosition }
11
+ | { type: "LineString"; coordinates: GeoPosition[] }
12
+ | { type: "MultiLineString"; coordinates: GeoPosition[][] };
13
+
14
+ export type GeoJsonFeature = {
15
+ type: "Feature";
16
+ id?: string | number;
17
+ properties?: Record<string, unknown> | null;
18
+ geometry: GeoJsonGeometry | null;
19
+ };
20
+
21
+ export type GeoJsonFeatureCollection = {
22
+ type: "FeatureCollection";
23
+ features: GeoJsonFeature[];
24
+ };
25
+
26
+ export type GeoProjectionKind = "mercator" | "equirectangular";
27
+
28
+ export function mercator([lon, lat]: GeoPosition): [number, number] {
29
+ const x = (lon + 180) / 360;
30
+ const clamped = Math.min(85.051129, Math.max(-85.051129, lat));
31
+ const sin = Math.sin((clamped * Math.PI) / 180);
32
+ const y = 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
33
+ return [x, y];
34
+ }
35
+
36
+ export function equirectangular([lon, lat]: GeoPosition): [number, number] {
37
+ return [(lon + 180) / 360, (90 - lat) / 180];
38
+ }
39
+
40
+ export function projectLonLat(pos: GeoPosition, kind: GeoProjectionKind): [number, number] {
41
+ return kind === "equirectangular" ? equirectangular(pos) : mercator(pos);
42
+ }
43
+
44
+ export function featureId(feature: GeoJsonFeature, idProperty = "id"): string {
45
+ if (feature.id != null) return String(feature.id);
46
+ const value = feature.properties?.[idProperty] ?? feature.properties?.name;
47
+ return value != null ? String(value) : "";
48
+ }
49
+
50
+ function walkGeometry(geometry: GeoJsonGeometry, visit: (pos: GeoPosition) => void) {
51
+ if (geometry.type === "Point") {
52
+ visit(geometry.coordinates);
53
+ return;
54
+ }
55
+ if (geometry.type === "LineString") {
56
+ geometry.coordinates.forEach(visit);
57
+ return;
58
+ }
59
+ if (geometry.type === "MultiLineString") {
60
+ geometry.coordinates.forEach((line) => line.forEach(visit));
61
+ return;
62
+ }
63
+ if (geometry.type === "Polygon") {
64
+ geometry.coordinates.forEach((ring) => ring.forEach(visit));
65
+ return;
66
+ }
67
+ geometry.coordinates.forEach((poly) => poly.forEach((ring) => ring.forEach(visit)));
68
+ }
69
+
70
+ export function walkFeatures(features: GeoJsonFeature[], visit: (pos: GeoPosition) => void) {
71
+ for (const feature of features) {
72
+ if (feature.geometry) walkGeometry(feature.geometry, visit);
73
+ }
74
+ }
75
+
76
+ function ringPath(ring: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
77
+ if (!ring.length) return "";
78
+ const pts = ring.map(project);
79
+ return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}Z`;
80
+ }
81
+
82
+ function linePath(line: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
83
+ if (!line.length) return "";
84
+ const pts = line.map(project);
85
+ return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}`;
86
+ }
87
+
88
+ export function geoPath(
89
+ geometry: GeoJsonGeometry,
90
+ project: (pos: GeoPosition) => [number, number],
91
+ ): string {
92
+ if (geometry.type === "Polygon") {
93
+ return geometry.coordinates.map((ring) => ringPath(ring, project)).join(" ");
94
+ }
95
+ if (geometry.type === "MultiPolygon") {
96
+ return geometry.coordinates
97
+ .map((poly) => poly.map((ring) => ringPath(ring, project)).join(" "))
98
+ .join(" ");
99
+ }
100
+ if (geometry.type === "LineString") return linePath(geometry.coordinates, project);
101
+ if (geometry.type === "MultiLineString") {
102
+ return geometry.coordinates.map((line) => linePath(line, project)).join(" ");
103
+ }
104
+ const [x, y] = project(geometry.coordinates);
105
+ return `M${x} ${y}m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0`;
106
+ }
107
+
108
+ export function fitFeatures(
109
+ features: GeoJsonFeature[],
110
+ width: number,
111
+ height: number,
112
+ padding: number,
113
+ kind: GeoProjectionKind = "mercator",
114
+ ): (pos: GeoPosition) => [number, number] {
115
+ const raw = (pos: GeoPosition) => projectLonLat(pos, kind);
116
+ let minX = Infinity;
117
+ let minY = Infinity;
118
+ let maxX = -Infinity;
119
+ let maxY = -Infinity;
120
+ walkFeatures(features, (pos) => {
121
+ const [x, y] = raw(pos);
122
+ minX = Math.min(minX, x);
123
+ minY = Math.min(minY, y);
124
+ maxX = Math.max(maxX, x);
125
+ maxY = Math.max(maxY, y);
126
+ });
127
+ const bw = Math.max(1e-6, maxX - minX);
128
+ const bh = Math.max(1e-6, maxY - minY);
129
+ const innerW = Math.max(1, width - padding * 2);
130
+ const innerH = Math.max(1, height - padding * 2);
131
+ const scale = Math.min(innerW / bw, innerH / bh);
132
+ const tx = padding + (innerW - bw * scale) / 2;
133
+ const ty = padding + (innerH - bh * scale) / 2;
134
+ return (pos) => {
135
+ const [x, y] = raw(pos);
136
+ return [tx + (x - minX) * scale, ty + (y - minY) * scale];
137
+ };
138
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ellipseSlicePath, isoBoxFaces, isoPoint } from "./iso";
3
+
4
+ describe("iso", () => {
5
+ it("projects isometric points with y up", () => {
6
+ const origin = isoPoint(0, 0, 0, 40, 80);
7
+ const raised = isoPoint(0, 10, 0, 40, 80);
8
+ expect(raised.y).toBeLessThan(origin.y);
9
+ });
10
+
11
+ it("emits three faces for a box", () => {
12
+ const faces = isoBoxFaces(0, 0, 0, 10, 20, 8, 40, 80);
13
+ expect(faces.front.startsWith("M")).toBe(true);
14
+ expect(faces.top).not.toEqual(faces.front);
15
+ expect(faces.side).not.toEqual(faces.top);
16
+ });
17
+
18
+ it("draws an elliptical pie slice", () => {
19
+ const d = ellipseSlicePath(50, 40, 30, 12, 0, 90);
20
+ expect(d.includes("A30 12")).toBe(true);
21
+ });
22
+ });
package/src/lib/iso.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Isometric 2.5D helpers for SVG bar and pie charts.
3
+ */
4
+
5
+ const COS = Math.cos(Math.PI / 6);
6
+ const SIN = Math.sin(Math.PI / 6);
7
+
8
+ export function isoPoint(
9
+ x: number,
10
+ y: number,
11
+ z: number,
12
+ originX: number,
13
+ originY: number,
14
+ ): { x: number; y: number } {
15
+ return {
16
+ x: originX + (x - z) * COS,
17
+ y: originY - y + (x + z) * SIN,
18
+ };
19
+ }
20
+
21
+ function poly(points: Array<{ x: number; y: number }>): string {
22
+ if (!points.length) return "";
23
+ return `M${points.map((p) => `${p.x} ${p.y}`).join("L")}Z`;
24
+ }
25
+
26
+ export type IsoBoxFaces = {
27
+ front: string;
28
+ top: string;
29
+ side: string;
30
+ };
31
+
32
+ /** Axis-aligned box: x/z on the ground plane, y up. */
33
+ export function isoBoxFaces(
34
+ x: number,
35
+ y0: number,
36
+ z: number,
37
+ w: number,
38
+ h: number,
39
+ d: number,
40
+ originX: number,
41
+ originY: number,
42
+ ): IsoBoxFaces {
43
+ const p = (px: number, py: number, pz: number) => isoPoint(px, py, pz, originX, originY);
44
+ const a = p(x, y0, z);
45
+ const b = p(x + w, y0, z);
46
+ const c = p(x + w, y0 + h, z);
47
+ const e = p(x, y0 + h, z);
48
+ const f = p(x + w, y0, z + d);
49
+ const g = p(x + w, y0 + h, z + d);
50
+ const k = p(x, y0 + h, z + d);
51
+ return {
52
+ front: poly([a, b, c, e]),
53
+ top: poly([e, c, g, k]),
54
+ side: poly([b, f, g, c]),
55
+ };
56
+ }
57
+
58
+ export function ellipsePoint(
59
+ cx: number,
60
+ cy: number,
61
+ rx: number,
62
+ ry: number,
63
+ angleDeg: number,
64
+ ): { x: number; y: number } {
65
+ const rad = ((angleDeg - 90) * Math.PI) / 180;
66
+ return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
67
+ }
68
+
69
+ export function ellipseSlicePath(
70
+ cx: number,
71
+ cy: number,
72
+ rx: number,
73
+ ry: number,
74
+ startAngle: number,
75
+ endAngle: number,
76
+ ): string {
77
+ const start = ellipsePoint(cx, cy, rx, ry, startAngle);
78
+ const end = ellipsePoint(cx, cy, rx, ry, endAngle);
79
+ const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
80
+ if (sweep <= 0) return "";
81
+ const large = sweep > 180 ? 1 : 0;
82
+ return `M${cx} ${cy} L${start.x} ${start.y} A${rx} ${ry} 0 ${large} 1 ${end.x} ${end.y} Z`;
83
+ }
84
+
85
+ export function ellipseWallPath(
86
+ cx: number,
87
+ cy: number,
88
+ rx: number,
89
+ ry: number,
90
+ startAngle: number,
91
+ endAngle: number,
92
+ thickness: number,
93
+ ): string {
94
+ const top0 = ellipsePoint(cx, cy, rx, ry, startAngle);
95
+ const top1 = ellipsePoint(cx, cy, rx, ry, endAngle);
96
+ const bot0 = { x: top0.x, y: top0.y + thickness };
97
+ const bot1 = { x: top1.x, y: top1.y + thickness };
98
+ const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
99
+ if (sweep <= 0) return "";
100
+ const large = sweep > 180 ? 1 : 0;
101
+ return `M${bot0.x} ${bot0.y} L${top0.x} ${top0.y} A${rx} ${ry} 0 ${large} 1 ${top1.x} ${top1.y} L${bot1.x} ${bot1.y} A${rx} ${ry} 0 ${large} 0 ${bot0.x} ${bot0.y} Z`;
102
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { expandEventOccurrences, parseRRule } from "./rrule";
3
+ import { normalizeEvents } from "./scheduler";
4
+
5
+ describe("rrule", () => {
6
+ it("parses FREQ, INTERVAL, BYDAY, and COUNT", () => {
7
+ const rule = parseRRule("FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=6");
8
+ expect(rule).toMatchObject({
9
+ freq: "WEEKLY",
10
+ interval: 2,
11
+ count: 6,
12
+ });
13
+ expect(rule?.byDay?.map((item) => item.weekday)).toEqual([1, 3]);
14
+ });
15
+
16
+ it("expands weekly BYDAY into the visible range", () => {
17
+ const [event] = normalizeEvents([
18
+ {
19
+ id: "standup",
20
+ title: "Standup",
21
+ start: "2026-08-03T10:00:00",
22
+ end: "2026-08-03T10:15:00",
23
+ rrule: "FREQ=WEEKLY;BYDAY=MO,WE,FR",
24
+ },
25
+ ]);
26
+ const occurrences = expandEventOccurrences(
27
+ event,
28
+ new Date(2026, 7, 3),
29
+ new Date(2026, 7, 8),
30
+ );
31
+ expect(occurrences.map((item) => item.start.getDay())).toEqual([1, 3, 5]);
32
+ expect(occurrences.every((item) => item.id === "standup")).toBe(true);
33
+ });
34
+
35
+ it("skips exception dates", () => {
36
+ const [event] = normalizeEvents([
37
+ {
38
+ id: "daily",
39
+ title: "Daily",
40
+ start: "2026-08-03T09:00:00",
41
+ end: "2026-08-03T09:30:00",
42
+ rrule: "FREQ=DAILY;COUNT=5",
43
+ exceptionDates: ["2026-08-04"],
44
+ },
45
+ ]);
46
+ const occurrences = expandEventOccurrences(
47
+ event,
48
+ new Date(2026, 7, 3),
49
+ new Date(2026, 7, 10),
50
+ );
51
+ expect(occurrences.map((item) => item.start.getDate())).toEqual([3, 5, 6, 7]);
52
+ });
53
+ });
@@ -0,0 +1,281 @@
1
+ import type { NormalizedEvent, SchedulerEvent } from "./scheduler";
2
+
3
+ function toDate(value: Date | string): Date {
4
+ if (value instanceof Date) return new Date(value.getTime());
5
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
6
+ if (dateOnly) {
7
+ return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]));
8
+ }
9
+ return new Date(value);
10
+ }
11
+
12
+ function startOfDay(date: Date): Date {
13
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
14
+ }
15
+
16
+ function addDays(date: Date, amount: number): Date {
17
+ const next = new Date(date.getTime());
18
+ next.setDate(next.getDate() + amount);
19
+ return next;
20
+ }
21
+
22
+ function addMonths(date: Date, amount: number): Date {
23
+ const next = new Date(date.getTime());
24
+ next.setMonth(next.getMonth() + amount);
25
+ return next;
26
+ }
27
+
28
+ function addMs(date: Date, ms: number): Date {
29
+ return new Date(date.getTime() + ms);
30
+ }
31
+
32
+ const WEEKDAY: Record<string, number> = {
33
+ SU: 0,
34
+ MO: 1,
35
+ TU: 2,
36
+ WE: 3,
37
+ TH: 4,
38
+ FR: 5,
39
+ SA: 6,
40
+ };
41
+
42
+ export type ParsedRRule = {
43
+ freq: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
44
+ interval: number;
45
+ count?: number;
46
+ until?: Date;
47
+ byDay?: { weekday: number; nth?: number }[];
48
+ byMonthDay?: number[];
49
+ };
50
+
51
+ export function parseRRule(rrule: string): ParsedRRule | null {
52
+ const parts = rrule
53
+ .replace(/^RRULE:/i, "")
54
+ .split(";")
55
+ .map((part) => part.trim())
56
+ .filter(Boolean);
57
+ const map = new Map<string, string>();
58
+ for (const part of parts) {
59
+ const [key, value] = part.split("=");
60
+ if (key && value) map.set(key.toUpperCase(), value);
61
+ }
62
+ const freq = map.get("FREQ") as ParsedRRule["freq"] | undefined;
63
+ if (!freq || !["DAILY", "WEEKLY", "MONTHLY", "YEARLY"].includes(freq)) return null;
64
+
65
+ const untilRaw = map.get("UNTIL");
66
+ let until: Date | undefined;
67
+ if (untilRaw) {
68
+ const compact = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})Z?)?$/.exec(untilRaw);
69
+ until = compact
70
+ ? new Date(
71
+ Date.UTC(
72
+ Number(compact[1]),
73
+ Number(compact[2]) - 1,
74
+ Number(compact[3]),
75
+ Number(compact[4] ?? 23),
76
+ Number(compact[5] ?? 59),
77
+ Number(compact[6] ?? 59),
78
+ ),
79
+ )
80
+ : toDate(untilRaw);
81
+ }
82
+
83
+ const byDay = map.get("BYDAY")
84
+ ?.split(",")
85
+ .flatMap((token) => {
86
+ const match = /^(-?\d+)?(SU|MO|TU|WE|TH|FR|SA)$/.exec(token.trim().toUpperCase());
87
+ if (!match) return [];
88
+ return [{ weekday: WEEKDAY[match[2]], nth: match[1] ? Number(match[1]) : undefined }];
89
+ });
90
+
91
+ const byMonthDay = map
92
+ .get("BYMONTHDAY")
93
+ ?.split(",")
94
+ .map((value) => Number(value))
95
+ .filter((value) => Number.isFinite(value));
96
+
97
+ return {
98
+ freq,
99
+ interval: Math.max(1, Number(map.get("INTERVAL") ?? 1) || 1),
100
+ count: map.get("COUNT") ? Math.max(1, Number(map.get("COUNT"))) : undefined,
101
+ until,
102
+ byDay,
103
+ byMonthDay,
104
+ };
105
+ }
106
+
107
+ function nthWeekdayOfMonth(year: number, month: number, weekday: number, nth: number): Date | null {
108
+ if (nth > 0) {
109
+ const first = new Date(year, month, 1);
110
+ const shift = (weekday - first.getDay() + 7) % 7;
111
+ const day = 1 + shift + (nth - 1) * 7;
112
+ const date = new Date(year, month, day);
113
+ return date.getMonth() === month ? date : null;
114
+ }
115
+ const last = new Date(year, month + 1, 0);
116
+ const shift = (last.getDay() - weekday + 7) % 7;
117
+ const day = last.getDate() - shift + (nth + 1) * 7;
118
+ const date = new Date(year, month, day);
119
+ return date.getMonth() === month ? date : null;
120
+ }
121
+
122
+ function matchesByDay(date: Date, rule: ParsedRRule): boolean {
123
+ if (!rule.byDay?.length) return true;
124
+ return rule.byDay.some((item) => {
125
+ if (item.weekday !== date.getDay()) return false;
126
+ if (item.nth == null) return true;
127
+ const found = nthWeekdayOfMonth(date.getFullYear(), date.getMonth(), item.weekday, item.nth);
128
+ return found ? found.getDate() === date.getDate() : false;
129
+ });
130
+ }
131
+
132
+ function exceptionSet(dates: Array<Date | string> | undefined): Set<string> {
133
+ const set = new Set<string>();
134
+ for (const value of dates ?? []) {
135
+ const date = startOfDay(toDate(value));
136
+ set.add(`${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`);
137
+ }
138
+ return set;
139
+ }
140
+
141
+ function dayKey(date: Date): string {
142
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
143
+ }
144
+
145
+ /**
146
+ * Expand a recurring event into occurrences overlapping [rangeStart, rangeEnd).
147
+ * Non-recurring events are returned as a single occurrence.
148
+ */
149
+ export function expandEventOccurrences(
150
+ event: NormalizedEvent,
151
+ rangeStart: Date,
152
+ rangeEnd: Date,
153
+ source?: SchedulerEvent,
154
+ ): NormalizedEvent[] {
155
+ if (!event.rrule) return [event];
156
+ const rule = parseRRule(event.rrule);
157
+ if (!rule) return [event];
158
+
159
+ const duration = Math.max(0, event.end.getTime() - event.start.getTime());
160
+ const exceptions = exceptionSet(source?.exceptionDates ?? event.exceptionDates);
161
+ const occurrences: NormalizedEvent[] = [];
162
+ const seed = new Date(event.start.getTime());
163
+ let emitted = 0;
164
+ const hardCap = rule.count ?? 400;
165
+
166
+ const pushIfVisible = (start: Date) => {
167
+ if (start.getTime() < seed.getTime() - 60_000) return;
168
+ if (exceptions.has(dayKey(start))) return;
169
+ if (rule.until && start.getTime() > rule.until.getTime()) return;
170
+ const end = addMs(start, duration || 3_600_000);
171
+ if (end.getTime() <= rangeStart.getTime() || start.getTime() >= rangeEnd.getTime()) return;
172
+ occurrences.push({
173
+ ...event,
174
+ start,
175
+ end,
176
+ occurrenceStart: start,
177
+ });
178
+ };
179
+
180
+ if (rule.freq === "DAILY") {
181
+ let cursor = new Date(seed.getTime());
182
+ while (emitted < hardCap && cursor.getTime() < rangeEnd.getTime() + duration) {
183
+ if (!rule.until || cursor.getTime() <= rule.until.getTime()) {
184
+ pushIfVisible(new Date(cursor.getTime()));
185
+ emitted += 1;
186
+ }
187
+ cursor = addDays(cursor, rule.interval);
188
+ if (rule.count && emitted >= rule.count) break;
189
+ }
190
+ return occurrences;
191
+ }
192
+
193
+ if (rule.freq === "WEEKLY") {
194
+ const days = rule.byDay?.length
195
+ ? [...new Set(rule.byDay.map((item) => item.weekday))].sort()
196
+ : [seed.getDay()];
197
+ let weekStart = addDays(startOfDay(seed), -seed.getDay());
198
+ while (emitted < hardCap && weekStart.getTime() < rangeEnd.getTime()) {
199
+ for (const weekday of days) {
200
+ const start = new Date(
201
+ weekStart.getFullYear(),
202
+ weekStart.getMonth(),
203
+ weekStart.getDate() + weekday,
204
+ seed.getHours(),
205
+ seed.getMinutes(),
206
+ seed.getSeconds(),
207
+ );
208
+ if (start.getTime() < seed.getTime() - 60_000) continue;
209
+ if (rule.until && start.getTime() > rule.until.getTime()) continue;
210
+ pushIfVisible(start);
211
+ emitted += 1;
212
+ if (rule.count && emitted >= rule.count) return occurrences;
213
+ }
214
+ weekStart = addDays(weekStart, 7 * rule.interval);
215
+ }
216
+ return occurrences;
217
+ }
218
+
219
+ if (rule.freq === "MONTHLY") {
220
+ let year = seed.getFullYear();
221
+ let month = seed.getMonth();
222
+ while (emitted < hardCap) {
223
+ const monthStart = new Date(year, month, 1);
224
+ if (monthStart.getTime() > rangeEnd.getTime()) break;
225
+ const candidates: Date[] = [];
226
+ if (rule.byDay?.length) {
227
+ for (const item of rule.byDay) {
228
+ const found = nthWeekdayOfMonth(year, month, item.weekday, item.nth ?? 1);
229
+ if (found) {
230
+ candidates.push(
231
+ new Date(year, month, found.getDate(), seed.getHours(), seed.getMinutes(), seed.getSeconds()),
232
+ );
233
+ }
234
+ }
235
+ } else {
236
+ const days = rule.byMonthDay?.length ? rule.byMonthDay : [seed.getDate()];
237
+ for (const day of days) {
238
+ const date = new Date(year, month, day, seed.getHours(), seed.getMinutes(), seed.getSeconds());
239
+ if (date.getMonth() === month) candidates.push(date);
240
+ }
241
+ }
242
+ for (const start of candidates.sort((a, b) => a.getTime() - b.getTime())) {
243
+ if (rule.until && start.getTime() > rule.until.getTime()) continue;
244
+ pushIfVisible(start);
245
+ emitted += 1;
246
+ if (rule.count && emitted >= rule.count) return occurrences;
247
+ }
248
+ const next = addMonths(new Date(year, month, 1), rule.interval);
249
+ year = next.getFullYear();
250
+ month = next.getMonth();
251
+ }
252
+ return occurrences;
253
+ }
254
+
255
+ let cursor = new Date(seed.getTime());
256
+ while (emitted < hardCap && cursor.getTime() < rangeEnd.getTime() + duration) {
257
+ if (matchesByDay(cursor, rule) && (!rule.until || cursor.getTime() <= rule.until.getTime())) {
258
+ pushIfVisible(new Date(cursor.getTime()));
259
+ emitted += 1;
260
+ }
261
+ cursor = addMonths(cursor, 12 * rule.interval);
262
+ if (rule.count && emitted >= rule.count) break;
263
+ }
264
+ return occurrences;
265
+ }
266
+
267
+ export function expandVisibleEvents(
268
+ events: NormalizedEvent[],
269
+ rangeStart: Date,
270
+ rangeEnd: Date,
271
+ sources: SchedulerEvent[] = [],
272
+ ): NormalizedEvent[] {
273
+ return events.flatMap((event) =>
274
+ expandEventOccurrences(
275
+ event,
276
+ rangeStart,
277
+ rangeEnd,
278
+ sources.find((item) => item.id === event.id),
279
+ ),
280
+ );
281
+ }