@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,111 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { Badge } from "../primitives/Badge";
4
+ import { Card } from "./Card";
5
+ import { Tag } from "./Tag";
6
+
7
+ export type CareerCardProps = {
8
+ role: ReactNode;
9
+ company: ReactNode;
10
+ location?: ReactNode;
11
+ period?: ReactNode;
12
+ current?: boolean;
13
+ bullets?: ReactNode[];
14
+ tags?: string[];
15
+ className?: string;
16
+ };
17
+
18
+ export type CareerTimelineProps = {
19
+ children: ReactNode;
20
+ className?: string;
21
+ };
22
+
23
+ /** Vertical career rail with a hairline and slot for CareerCard rows. */
24
+ export function CareerTimeline({ children, className }: CareerTimelineProps) {
25
+ return (
26
+ <div data-slot="career-timeline" className={cn("relative", className)}>
27
+ <div className="absolute top-2 bottom-2 left-[5px] hidden w-px bg-border sm:block" aria-hidden />
28
+ <div className="space-y-3">{children}</div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ /** Resume / experience card: role, company, period pill, bullets, tech tags. */
34
+ export function CareerCard({
35
+ role,
36
+ company,
37
+ location,
38
+ period,
39
+ current = false,
40
+ bullets = [],
41
+ tags = [],
42
+ className,
43
+ }: CareerCardProps) {
44
+ return (
45
+ <article data-slot="career-card" className={cn("relative sm:pl-8", className)}>
46
+ <div
47
+ className={cn(
48
+ "absolute top-4 left-0 hidden h-2.5 w-2.5 rounded-full border-2 sm:block",
49
+ current ? "border-primary bg-primary" : "border-border bg-background",
50
+ )}
51
+ aria-hidden
52
+ />
53
+ <Card
54
+ variant="panel"
55
+ padding="md"
56
+ className={
57
+ current
58
+ ? "border-primary/25 bg-gradient-to-br from-[var(--background)] to-primary/[0.04] shadow-[0_8px_28px_color-mix(in_srgb,var(--primary)_7%,transparent)]"
59
+ : "hover:border-primary/20"
60
+ }
61
+ >
62
+ <div className="mb-2.5 flex flex-wrap items-start justify-between gap-2">
63
+ <div className="min-w-0">
64
+ <h3 className="text-[15px] font-[900] leading-snug text-foreground sm:text-[16px]">{role}</h3>
65
+ <p className="mt-0.5 text-[13px] font-bold text-primary">
66
+ {company}
67
+ {location ? (
68
+ <span className="font-semibold text-muted-foreground"> · {location}</span>
69
+ ) : null}
70
+ </p>
71
+ </div>
72
+ {period ? (
73
+ <Badge
74
+ variant={current ? "default" : "secondary"}
75
+ className={
76
+ current
77
+ ? "rounded-full border-primary bg-primary px-2.5 py-1 text-[11px] font-black text-primary-foreground"
78
+ : "rounded-full px-2.5 py-1 text-[11px] font-black text-muted-foreground"
79
+ }
80
+ >
81
+ {current ? <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary-foreground" /> : null}
82
+ {period}
83
+ </Badge>
84
+ ) : null}
85
+ </div>
86
+
87
+ {bullets.length > 0 ? (
88
+ <ul className="mb-3 space-y-1.5">
89
+ {bullets.map((bullet, index) => (
90
+ <li
91
+ key={index}
92
+ className="flex gap-2 text-[13px] leading-relaxed text-muted-foreground"
93
+ >
94
+ <span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-primary/50" />
95
+ {bullet}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ ) : null}
100
+
101
+ {tags.length > 0 ? (
102
+ <div className="flex flex-wrap gap-1.5">
103
+ {tags.map((tag) => (
104
+ <Tag key={tag}>{tag}</Tag>
105
+ ))}
106
+ </div>
107
+ ) : null}
108
+ </Card>
109
+ </article>
110
+ );
111
+ }
@@ -0,0 +1,57 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it } from "vitest";
4
+ import { ChartDataGrid, renderChartCell } from "./ChartDataGrid";
5
+
6
+ describe("ChartDataGrid", () => {
7
+ it("renders a linked table and highlights a selected row", async () => {
8
+ const user = userEvent.setup();
9
+ const { container } = render(
10
+ <ChartDataGrid
11
+ height={140}
12
+ categoryField="name"
13
+ valueField="views"
14
+ rows={[
15
+ { id: "web", name: "Web", views: 10, trend: [1, 2, 3] },
16
+ { id: "ios", name: "iOS", views: 6, trend: [2, 2, 1] },
17
+ ]}
18
+ columns={[
19
+ { field: "name", headerName: "Channel" },
20
+ { field: "views", headerName: "Views", type: "number" },
21
+ { field: "trend", headerName: "Trend", type: "sparkline" },
22
+ ]}
23
+ />,
24
+ );
25
+ expect(container.querySelector('[data-slot="chart-data-grid"]')).toBeTruthy();
26
+ expect(screen.getByText("Channel")).toBeInTheDocument();
27
+ const row = container.querySelector(
28
+ '[data-slot="chart-data-grid"] tbody [data-slot="table-row"]:nth-child(2)',
29
+ );
30
+ await user.click(row!);
31
+ expect(row).toHaveAttribute("data-state", "selected");
32
+ });
33
+
34
+ it("hides the chart when requested", () => {
35
+ const { container } = render(
36
+ <ChartDataGrid
37
+ hideChart
38
+ categoryField="name"
39
+ valueField="views"
40
+ rows={[{ id: "web", name: "Web", views: 10 }]}
41
+ columns={[{ field: "name", headerName: "Channel" }]}
42
+ />,
43
+ );
44
+ expect(container.querySelector('[data-slot="chart-container"]')).toBeNull();
45
+ });
46
+ });
47
+
48
+ describe("renderChartCell", () => {
49
+ it("returns a dash for non-numeric values", () => {
50
+ expect(renderChartCell("n/a")).toBe("—");
51
+ });
52
+
53
+ it("renders a sparkline for numeric arrays", () => {
54
+ const { container } = render(<>{renderChartCell([1, 4, 2], { variant: "sparkline" })}</>);
55
+ expect(container.querySelector('[data-slot="sparkline"]')).toBeTruthy();
56
+ });
57
+ });
@@ -0,0 +1,196 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { chartColor, formatChartNumber } from "../lib/charts";
3
+ import { cn } from "../lib/cn";
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from "../primitives/Table";
12
+ import { ChartContainer } from "./chart-composition";
13
+ import { SparkLineChart } from "./cartesian-charts";
14
+ import { PieChart } from "./radial-charts";
15
+
16
+ export type ChartGridColumnType = "text" | "number" | "sparkline" | "bar";
17
+
18
+ export type ChartGridColumn = {
19
+ field: string;
20
+ headerName: string;
21
+ type?: ChartGridColumnType;
22
+ width?: number;
23
+ };
24
+
25
+ export type ChartDataGridRow = { id: string } & Record<string, unknown>;
26
+
27
+ export type ChartDataGridProps = {
28
+ rows: ChartDataGridRow[];
29
+ columns: ChartGridColumn[];
30
+ chartType?: "bar" | "line" | "pie";
31
+ categoryField: string;
32
+ valueField: string;
33
+ height?: number;
34
+ className?: string;
35
+ selectedIds?: string[];
36
+ defaultSelectedIds?: string[];
37
+ onSelectionChange?: (ids: string[]) => void;
38
+ hideChart?: boolean;
39
+ hideLegend?: boolean;
40
+ };
41
+
42
+ export function renderChartCell(
43
+ value: unknown,
44
+ options?: {
45
+ variant?: "sparkline" | "bar";
46
+ color?: string;
47
+ height?: number;
48
+ max?: number;
49
+ },
50
+ ): ReactNode {
51
+ const color = options?.color ?? "var(--chart-1)";
52
+ if (Array.isArray(value) && value.every((item) => typeof item === "number")) {
53
+ return (
54
+ <SparkLineChart
55
+ data={value as number[]}
56
+ plotType={options?.variant === "bar" ? "bar" : "line"}
57
+ color={color}
58
+ height={options?.height ?? 32}
59
+ showHighlight={false}
60
+ area={options?.variant !== "bar"}
61
+ />
62
+ );
63
+ }
64
+ const n = typeof value === "number" ? value : Number(value);
65
+ if (!Number.isFinite(n)) return "—";
66
+ const max = options?.max && options.max > 0 ? options.max : Math.max(n, 1);
67
+ const pct = Math.max(0, Math.min(100, (n / max) * 100));
68
+ return (
69
+ <div className="spk-chart-cell-bar" title={formatChartNumber(n)}>
70
+ <span style={{ width: `${pct}%`, background: color }} />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export function ChartDataGrid({
76
+ rows,
77
+ columns,
78
+ chartType = "bar",
79
+ categoryField,
80
+ valueField,
81
+ height = 200,
82
+ className,
83
+ selectedIds,
84
+ defaultSelectedIds,
85
+ onSelectionChange,
86
+ hideChart,
87
+ hideLegend = true,
88
+ }: ChartDataGridProps) {
89
+ const [internal, setInternal] = useState<string[]>(defaultSelectedIds ?? []);
90
+ const selected = selectedIds ?? internal;
91
+ const setSelected = (ids: string[]) => {
92
+ if (selectedIds == null) setInternal(ids);
93
+ onSelectionChange?.(ids);
94
+ };
95
+ const selectedIndex = selected.length ? rows.findIndex((row) => row.id === selected[0]) : -1;
96
+ const categories = rows.map((row) => String(row[categoryField] ?? row.id));
97
+ const values = rows.map((row) => Number(row[valueField] ?? 0));
98
+ const maxByField = new Map<string, number>();
99
+ for (const col of columns) {
100
+ if (col.type === "bar" || col.type === "number") {
101
+ const nums = rows.map((row) => Number(row[col.field] ?? 0)).filter((n) => Number.isFinite(n));
102
+ maxByField.set(col.field, nums.length ? Math.max(...nums) : 1);
103
+ }
104
+ }
105
+
106
+ const toggleRow = (id: string) => {
107
+ setSelected(selected[0] === id ? [] : [id]);
108
+ };
109
+
110
+ return (
111
+ <div data-slot="chart-data-grid" className={cn("spk-chart-data-grid", className)}>
112
+ {hideChart ? null : chartType === "pie" ? (
113
+ <PieChart
114
+ height={height}
115
+ hideLegend={hideLegend}
116
+ series={[
117
+ {
118
+ innerRadius: 36,
119
+ paddingAngle: 2,
120
+ data: rows.map((row, i) => ({
121
+ id: row.id,
122
+ value: values[i] ?? 0,
123
+ label: categories[i],
124
+ color:
125
+ selected.length && !selected.includes(row.id)
126
+ ? `color-mix(in srgb, ${chartColor(i)} 28%, transparent)`
127
+ : undefined,
128
+ })),
129
+ },
130
+ ]}
131
+ />
132
+ ) : (
133
+ <ChartContainer
134
+ height={height}
135
+ hideLegend={hideLegend}
136
+ highlightScope="item"
137
+ selected={
138
+ selectedIndex >= 0 ? { seriesId: "value", dataIndex: selectedIndex } : null
139
+ }
140
+ xAxis={[{ data: categories }]}
141
+ series={[{ type: chartType, id: "value", label: valueField, data: values }]}
142
+ aria-label="Linked chart"
143
+ />
144
+ )}
145
+ <Table containerClassName="spk-chart-grid-table">
146
+ <TableHeader>
147
+ <TableRow>
148
+ {columns.map((col) => (
149
+ <TableHead key={col.field} style={col.width ? { width: col.width } : undefined}>
150
+ {col.headerName}
151
+ </TableHead>
152
+ ))}
153
+ </TableRow>
154
+ </TableHeader>
155
+ <TableBody>
156
+ {rows.map((row, rowIndex) => (
157
+ <TableRow
158
+ key={row.id}
159
+ data-state={selected.includes(row.id) ? "selected" : undefined}
160
+ onClick={() => toggleRow(row.id)}
161
+ style={{ cursor: "pointer" }}
162
+ >
163
+ {columns.map((col, colIndex) => {
164
+ const value = row[col.field];
165
+ const color = chartColor(rowIndex);
166
+ let content: ReactNode;
167
+ if (col.type === "sparkline") {
168
+ content = renderChartCell(value, { variant: "sparkline", color, height: 32 });
169
+ } else if (col.type === "bar") {
170
+ content = renderChartCell(value, {
171
+ variant: "bar",
172
+ color,
173
+ max: maxByField.get(col.field),
174
+ });
175
+ } else if (col.type === "number" && typeof value === "number") {
176
+ content = formatChartNumber(value);
177
+ } else {
178
+ content = value == null ? "—" : String(value);
179
+ }
180
+ return (
181
+ <TableCell key={col.field} data-field={col.field}>
182
+ {colIndex === 0 ? (
183
+ <span className="spk-chart-grid-name">{content}</span>
184
+ ) : (
185
+ content
186
+ )}
187
+ </TableCell>
188
+ );
189
+ })}
190
+ </TableRow>
191
+ ))}
192
+ </TableBody>
193
+ </Table>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { Card } from "./Card";
4
+
5
+ export type ContactLinkProps = {
6
+ href: string;
7
+ name: ReactNode;
8
+ label?: ReactNode;
9
+ icon: ReactNode;
10
+ iconBg?: string;
11
+ iconColor?: string;
12
+ className?: string;
13
+ target?: string;
14
+ rel?: string;
15
+ };
16
+
17
+ /** Contact / social row: tinted icon well, name, handle, trailing arrow. */
18
+ export function ContactLink({
19
+ href,
20
+ name,
21
+ label,
22
+ icon,
23
+ iconBg,
24
+ iconColor,
25
+ className,
26
+ target,
27
+ rel,
28
+ }: ContactLinkProps) {
29
+ const external = href.startsWith("http");
30
+ return (
31
+ <a
32
+ data-slot="contact-link"
33
+ href={href}
34
+ target={target ?? (external ? "_blank" : undefined)}
35
+ rel={rel ?? (external ? "noopener noreferrer" : undefined)}
36
+ className={cn("group block rounded-2xl", className)}
37
+ >
38
+ <Card
39
+ variant="panel"
40
+ padding="md"
41
+ className="flex items-center gap-4 p-4 group-hover:border-primary/25 group-hover:shadow-[0_8px_28px_color-mix(in_srgb,var(--chart-3)_18%,transparent)]"
42
+ >
43
+ <div
44
+ className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-transform duration-200 group-hover:scale-110"
45
+ style={{ background: iconBg, color: iconColor }}
46
+ >
47
+ {icon}
48
+ </div>
49
+ <div className="min-w-0 flex-1">
50
+ <p className="text-[13px] font-semibold text-foreground">{name}</p>
51
+ {label ? <p className="truncate text-[12px] text-muted-foreground">{label}</p> : null}
52
+ </div>
53
+ <svg
54
+ className="h-4 w-4 shrink-0 text-muted-foreground transition-colors group-hover:text-primary"
55
+ fill="none"
56
+ viewBox="0 0 24 24"
57
+ stroke="currentColor"
58
+ strokeWidth={2}
59
+ aria-hidden
60
+ >
61
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
62
+ </svg>
63
+ </Card>
64
+ </a>
65
+ );
66
+ }
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { CoverHero } from "./CoverHero";
5
+
6
+ describe("CoverHero", () => {
7
+ it("renders title, kicker, facts, and avatars", () => {
8
+ render(
9
+ <CoverHero
10
+ title="Friday night"
11
+ kicker={<span>Meetup</span>}
12
+ facts={[{ key: "when", label: "When", value: "Fri 14 Mar" }]}
13
+ avatars={[{ name: "Alex Rivera" }, { name: "Sam Lee" }, { name: "Jo Park" }, { name: "Riley" }]}
14
+ bleed={false}
15
+ />,
16
+ );
17
+ expect(screen.getByRole("heading", { name: "Friday night" })).toBeInTheDocument();
18
+ expect(screen.getByText("Meetup")).toBeInTheDocument();
19
+ expect(screen.getByText("When")).toBeInTheDocument();
20
+ expect(screen.getByText("Fri 14 Mar")).toBeInTheDocument();
21
+ expect(screen.getByLabelText("Alex Rivera")).toBeInTheDocument();
22
+ expect(screen.getByText("+1")).toBeInTheDocument();
23
+ });
24
+
25
+ it("calls onBack from the floating control and skips bleed attributes when disabled", async () => {
26
+ const user = userEvent.setup();
27
+ const onBack = vi.fn();
28
+ const { container } = render(
29
+ <CoverHero title="Studio" onBack={onBack} backLabel="Go back" bleed={false} />,
30
+ );
31
+ expect(container.querySelector("[data-slot='cover-hero']")).not.toHaveAttribute(
32
+ "data-cover-bleed",
33
+ );
34
+ expect(screen.queryByRole("button", { name: "Go back" })).toBeInTheDocument();
35
+ await user.click(screen.getByRole("button", { name: "Go back" }));
36
+ expect(onBack).toHaveBeenCalledTimes(1);
37
+ });
38
+ });
@@ -60,7 +60,7 @@ export type CoverHeroProps = {
60
60
  className?: string;
61
61
  /** Offset used when the compact bar pins under app chrome. */
62
62
  compactOffsetClassName?: string;
63
- /** When false, skip writing cover-bleed attributes on `<html>` (catalog previews). */
63
+ /** When false, keep the cover inside this frame (docs / device previews). */
64
64
  bleed?: boolean;
65
65
  };
66
66
 
@@ -141,9 +141,14 @@ export function CoverHero({
141
141
  <header
142
142
  ref={heroRef}
143
143
  data-slot="cover-hero"
144
- className={cn("relative z-10 w-full overflow-visible", className)}
144
+ data-cover-bleed={bleed ? "true" : undefined}
145
+ className={cn(
146
+ "relative z-10 w-full",
147
+ bleed ? "overflow-visible" : "overflow-hidden rounded-[1.35rem] sm:rounded-[1.75rem]",
148
+ className,
149
+ )}
145
150
  >
146
- <div className={cn(mediaClass, "sm:rounded-b-[1.75rem]")} aria-hidden>
151
+ <div className={cn(mediaClass, bleed && "sm:rounded-b-[1.75rem]")} aria-hidden>
147
152
  {!hideImages && coverUrl ? (
148
153
  <img src={coverUrl} alt="" className="absolute inset-0 size-full object-cover" />
149
154
  ) : (
@@ -332,26 +337,28 @@ export function CoverHero({
332
337
  </div>
333
338
  </header>
334
339
 
335
- <div className={cn("sticky z-[60] h-0", compactOffsetClassName)}>
336
- {!heroInView ? (
337
- <div className="glass-header cover-chrome-bar absolute inset-x-0 top-0 border-b border-border/50 bg-background/85 backdrop-blur-md dark:bg-background/94">
338
- <div className="app-frame-pad flex items-center gap-2 py-1.5 sm:py-2">
339
- {onBack ? (
340
- <button
341
- type="button"
342
- onClick={onBack}
343
- aria-label={backLabel}
344
- className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-muted"
345
- >
346
- <ChevronLeft className="h-4 w-4" aria-hidden />
347
- </button>
348
- ) : null}
349
- <h2 className="page-header-title min-w-0 flex-1 truncate">{title}</h2>
350
- {compactActions}
340
+ {bleed ? (
341
+ <div className={cn("sticky z-[60] h-0", compactOffsetClassName)}>
342
+ {!heroInView ? (
343
+ <div className="glass-header cover-chrome-bar absolute inset-x-0 top-0 border-b border-border/50 bg-background/85 backdrop-blur-md dark:bg-background/94">
344
+ <div className="app-frame-pad flex items-center gap-2 py-1.5 sm:py-2">
345
+ {onBack ? (
346
+ <button
347
+ type="button"
348
+ onClick={onBack}
349
+ aria-label={backLabel}
350
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-muted"
351
+ >
352
+ <ChevronLeft className="h-4 w-4" aria-hidden />
353
+ </button>
354
+ ) : null}
355
+ <h2 className="page-header-title min-w-0 flex-1 truncate">{title}</h2>
356
+ {compactActions}
357
+ </div>
351
358
  </div>
352
- </div>
353
- ) : null}
354
- </div>
359
+ ) : null}
360
+ </div>
361
+ ) : null}
355
362
  </>
356
363
  );
357
364
  }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ export type DisplayHeadingProps = {
5
+ children: ReactNode;
6
+ as?: "h1" | "h2";
7
+ className?: string;
8
+ };
9
+
10
+ /** Oversized marketing hero title. Compose with Wordmark / GradientText. */
11
+ export function DisplayHeading({
12
+ children,
13
+ as: Tag = "h1",
14
+ className,
15
+ }: DisplayHeadingProps) {
16
+ return (
17
+ <Tag
18
+ data-slot="display-heading"
19
+ className={cn(
20
+ "text-[clamp(2.5rem,8vw,5.5rem)] font-[900] leading-[0.95] tracking-tight",
21
+ className,
22
+ )}
23
+ >
24
+ {children}
25
+ </Tag>
26
+ );
27
+ }
@@ -0,0 +1,58 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { EntityMediaCard } from "./EntityMediaCard";
5
+
6
+ describe("EntityMediaCard", () => {
7
+ it("renders title, subtitle, and status on a stacked tile", () => {
8
+ const { container } = render(
9
+ <EntityMediaCard title="Alex Chen" subtitle="Product designer" statusLine="Active" />,
10
+ );
11
+ expect(screen.getByText("Alex Chen")).toBeInTheDocument();
12
+ expect(screen.getByText("Product designer")).toBeInTheDocument();
13
+ expect(screen.getByText("Active")).toBeInTheDocument();
14
+ expect(screen.getByText("AC")).toBeInTheDocument();
15
+ const card = container.querySelector("[data-slot='entity-media-card']");
16
+ expect(card).toHaveAttribute("data-size", "large");
17
+ expect(card).toHaveAttribute("data-orientation", "stack");
18
+ expect(container.querySelector("[data-cover-pattern]")).toBeTruthy();
19
+ });
20
+
21
+ it("uses a photo when imageUrl is set", () => {
22
+ const { container } = render(
23
+ <EntityMediaCard title="Jordan Lee" imageUrl="/jordan.jpg" />,
24
+ );
25
+ expect(container.querySelector("img")).toHaveAttribute("src", "/jordan.jpg");
26
+ expect(screen.queryByText("JL")).not.toBeInTheDocument();
27
+ });
28
+
29
+ it("renders as a link and calls onClick on a button card", async () => {
30
+ const user = userEvent.setup();
31
+ const onClick = vi.fn();
32
+ const { rerender } = render(
33
+ <EntityMediaCard title="Alex Chen" href="/people/alex" />,
34
+ );
35
+ expect(screen.getByRole("link", { name: /Alex Chen/ })).toHaveAttribute(
36
+ "href",
37
+ "/people/alex",
38
+ );
39
+
40
+ rerender(<EntityMediaCard title="Alex Chen" onClick={onClick} />);
41
+ await user.click(screen.getByRole("button", { name: /Alex Chen/ }));
42
+ expect(onClick).toHaveBeenCalledTimes(1);
43
+ });
44
+
45
+ it("keeps row cards stretched so the photo matches the copy height", () => {
46
+ const { container } = render(
47
+ <EntityMediaCard
48
+ title="Jordan Lee"
49
+ subtitle="Account lead"
50
+ orientation="row"
51
+ size="small"
52
+ />,
53
+ );
54
+ const card = container.querySelector("[data-slot='entity-media-card']");
55
+ expect(card).toHaveAttribute("data-orientation", "row");
56
+ expect(card).toHaveClass("items-stretch");
57
+ });
58
+ });