eidotter 0.36.0 → 0.37.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 (328) hide show
  1. package/README.md +19 -0
  2. package/dist/components/AIText/components/AIText.d.ts +0 -1
  3. package/dist/components/AIText/components/AIText.js +23 -0
  4. package/dist/components/AIText/components/index.d.ts +2 -2
  5. package/dist/components/AIText/components/index.js +1 -0
  6. package/dist/components/AIText/index.d.ts +2 -2
  7. package/dist/components/AIText/index.js +1 -0
  8. package/dist/components/Accordion/components/AccordionFill.d.ts +0 -1
  9. package/dist/components/Accordion/components/AccordionFill.js +12 -0
  10. package/dist/components/Accordion/components/Section.d.ts +0 -1
  11. package/dist/components/Accordion/components/Section.js +22 -0
  12. package/dist/components/Accordion/components/index.d.ts +4 -4
  13. package/dist/components/Accordion/components/index.js +2 -0
  14. package/dist/components/Accordion/index.d.ts +1 -1
  15. package/dist/components/Accordion/index.js +1 -0
  16. package/dist/components/Alert/components/Alert.d.ts +0 -1
  17. package/dist/components/Alert/components/Alert.js +51 -0
  18. package/dist/components/Alert/components/index.d.ts +2 -2
  19. package/dist/components/Alert/components/index.js +1 -0
  20. package/dist/components/Alert/index.d.ts +1 -1
  21. package/dist/components/Alert/index.js +1 -0
  22. package/dist/components/Badge/components/Badge.d.ts +0 -1
  23. package/dist/components/Badge/components/Badge.js +31 -0
  24. package/dist/components/Badge/components/index.d.ts +2 -2
  25. package/dist/components/Badge/components/index.js +1 -0
  26. package/dist/components/Badge/index.d.ts +2 -2
  27. package/dist/components/Badge/index.js +1 -0
  28. package/dist/components/Brand/components/BrandLockup.js +14 -0
  29. package/dist/components/Brand/components/Logo.d.ts +0 -1
  30. package/dist/components/Brand/components/Logo.js +22 -0
  31. package/dist/components/Brand/components/Wordmark.d.ts +0 -1
  32. package/dist/components/Brand/components/Wordmark.js +13 -0
  33. package/dist/components/Brand/components/index.d.ts +6 -6
  34. package/dist/components/Brand/components/index.js +3 -0
  35. package/dist/components/Brand/index.d.ts +1 -1
  36. package/dist/components/Brand/index.js +1 -0
  37. package/dist/components/Breadcrumb/components/Breadcrumb.d.ts +0 -1
  38. package/dist/components/Breadcrumb/components/Breadcrumb.js +29 -0
  39. package/dist/components/Breadcrumb/components/index.d.ts +2 -2
  40. package/dist/components/Breadcrumb/components/index.js +1 -0
  41. package/dist/components/Breadcrumb/index.d.ts +2 -2
  42. package/dist/components/Breadcrumb/index.js +1 -0
  43. package/dist/components/Button/components/Button.d.ts +0 -2
  44. package/dist/components/Button/components/Button.js +49 -0
  45. package/dist/components/Button/components/index.d.ts +2 -2
  46. package/dist/components/Button/components/index.js +1 -0
  47. package/dist/components/Button/index.d.ts +2 -2
  48. package/dist/components/Button/index.js +1 -0
  49. package/dist/components/Card/components/Card.d.ts +0 -1
  50. package/dist/components/Card/components/Card.js +16 -0
  51. package/dist/components/Card/components/index.d.ts +2 -2
  52. package/dist/components/Card/components/index.js +1 -0
  53. package/dist/components/Card/index.d.ts +2 -2
  54. package/dist/components/Card/index.js +1 -0
  55. package/dist/components/Chat/components/ChatContainer.d.ts +1 -1
  56. package/dist/components/Chat/components/ChatContainer.js +20 -0
  57. package/dist/components/Chat/components/ChatHistory.d.ts +0 -1
  58. package/dist/components/Chat/components/ChatHistory.js +27 -0
  59. package/dist/components/Chat/components/ChatInput.d.ts +0 -2
  60. package/dist/components/Chat/components/ChatInput.js +37 -0
  61. package/dist/components/Chat/components/ChatMessage.d.ts +0 -2
  62. package/dist/components/Chat/components/ChatMessage.js +18 -0
  63. package/dist/components/Chat/components/index.d.ts +8 -8
  64. package/dist/components/Chat/components/index.js +4 -0
  65. package/dist/components/Chat/index.d.ts +2 -2
  66. package/dist/components/Chat/index.js +1 -0
  67. package/dist/components/Checkbox/components/Checkbox.d.ts +0 -2
  68. package/dist/components/Checkbox/components/Checkbox.js +11 -0
  69. package/dist/components/Checkbox/components/index.d.ts +2 -2
  70. package/dist/components/Checkbox/components/index.js +1 -0
  71. package/dist/components/Checkbox/index.d.ts +2 -2
  72. package/dist/components/Checkbox/index.js +1 -0
  73. package/dist/components/ChecklistRow/components/ChecklistRow.js +18 -0
  74. package/dist/components/ChecklistRow/components/index.d.ts +2 -2
  75. package/dist/components/ChecklistRow/components/index.js +1 -0
  76. package/dist/components/ChecklistRow/index.d.ts +2 -2
  77. package/dist/components/ChecklistRow/index.js +1 -0
  78. package/dist/components/CmdPalette/components/CmdPalette.d.ts +0 -2
  79. package/dist/components/CmdPalette/components/CmdPalette.js +169 -0
  80. package/dist/components/CmdPalette/components/index.d.ts +2 -2
  81. package/dist/components/CmdPalette/components/index.js +1 -0
  82. package/dist/components/CmdPalette/index.d.ts +2 -2
  83. package/dist/components/CmdPalette/index.js +1 -0
  84. package/dist/components/CommandPrompt/components/CommandPrompt.d.ts +0 -2
  85. package/dist/components/CommandPrompt/components/CommandPrompt.js +34 -0
  86. package/dist/components/CommandPrompt/components/index.d.ts +2 -2
  87. package/dist/components/CommandPrompt/components/index.js +1 -0
  88. package/dist/components/CommandPrompt/index.d.ts +1 -1
  89. package/dist/components/CommandPrompt/index.js +1 -0
  90. package/dist/components/CopyButton/components/CopyButton.js +50 -0
  91. package/dist/components/CopyButton/components/index.d.ts +2 -2
  92. package/dist/components/CopyButton/components/index.js +1 -0
  93. package/dist/components/CopyButton/index.d.ts +2 -2
  94. package/dist/components/CopyButton/index.js +1 -0
  95. package/dist/components/DosFigure/components/DosFigure.d.ts +0 -1
  96. package/dist/components/DosFigure/components/DosFigure.js +26 -0
  97. package/dist/components/DosFigure/components/index.d.ts +2 -2
  98. package/dist/components/DosFigure/components/index.js +1 -0
  99. package/dist/components/DosFigure/index.d.ts +2 -2
  100. package/dist/components/DosFigure/index.js +1 -0
  101. package/dist/components/EmptyState/components/EmptyState.js +18 -0
  102. package/dist/components/EmptyState/components/index.d.ts +2 -2
  103. package/dist/components/EmptyState/components/index.js +1 -0
  104. package/dist/components/EmptyState/index.d.ts +2 -2
  105. package/dist/components/EmptyState/index.js +1 -0
  106. package/dist/components/FilterBar/components/FilterBar.d.ts +0 -1
  107. package/dist/components/FilterBar/components/FilterBar.js +120 -0
  108. package/dist/components/FilterBar/components/index.d.ts +2 -2
  109. package/dist/components/FilterBar/components/index.js +1 -0
  110. package/dist/components/FilterBar/index.d.ts +2 -2
  111. package/dist/components/FilterBar/index.js +1 -0
  112. package/dist/components/Footer/components/Footer.d.ts +0 -1
  113. package/dist/components/Footer/components/Footer.js +18 -0
  114. package/dist/components/Footer/components/index.d.ts +2 -2
  115. package/dist/components/Footer/components/index.js +1 -0
  116. package/dist/components/Footer/index.d.ts +2 -2
  117. package/dist/components/Footer/index.js +1 -0
  118. package/dist/components/Header/components/Header.d.ts +1 -2
  119. package/dist/components/Header/components/Header.js +13 -0
  120. package/dist/components/Header/components/index.d.ts +2 -2
  121. package/dist/components/Header/components/index.js +1 -0
  122. package/dist/components/Header/index.d.ts +2 -2
  123. package/dist/components/Header/index.js +1 -0
  124. package/dist/components/Icon/components/Icon.d.ts +0 -1
  125. package/dist/components/Icon/components/Icon.js +78 -0
  126. package/dist/components/Icon/components/index.d.ts +2 -2
  127. package/dist/components/Icon/components/index.js +1 -0
  128. package/dist/components/Icon/index.d.ts +1 -1
  129. package/dist/components/Icon/index.js +1 -0
  130. package/dist/components/InlineExpand/components/InlineExpand.d.ts +0 -1
  131. package/dist/components/InlineExpand/components/InlineExpand.js +50 -0
  132. package/dist/components/InlineExpand/components/index.d.ts +2 -2
  133. package/dist/components/InlineExpand/components/index.js +1 -0
  134. package/dist/components/InlineExpand/index.d.ts +2 -2
  135. package/dist/components/InlineExpand/index.js +1 -0
  136. package/dist/components/InlineLink/components/InlineLink.d.ts +0 -1
  137. package/dist/components/InlineLink/components/InlineLink.js +61 -0
  138. package/dist/components/InlineLink/components/index.d.ts +2 -2
  139. package/dist/components/InlineLink/components/index.js +1 -0
  140. package/dist/components/InlineLink/index.d.ts +2 -2
  141. package/dist/components/InlineLink/index.js +1 -0
  142. package/dist/components/Input/components/Input.d.ts +0 -1
  143. package/dist/components/Input/components/Input.js +18 -0
  144. package/dist/components/Input/components/index.d.ts +2 -2
  145. package/dist/components/Input/components/index.js +1 -0
  146. package/dist/components/Input/index.d.ts +1 -1
  147. package/dist/components/Input/index.js +1 -0
  148. package/dist/components/LabeledProgress/components/LabeledProgress.d.ts +0 -1
  149. package/dist/components/LabeledProgress/components/LabeledProgress.js +16 -0
  150. package/dist/components/LabeledProgress/components/index.d.ts +2 -2
  151. package/dist/components/LabeledProgress/components/index.js +1 -0
  152. package/dist/components/LabeledProgress/index.d.ts +2 -2
  153. package/dist/components/LabeledProgress/index.js +1 -0
  154. package/dist/components/LedgerRow/components/LedgerRow.js +15 -0
  155. package/dist/components/LedgerRow/components/index.d.ts +2 -2
  156. package/dist/components/LedgerRow/components/index.js +1 -0
  157. package/dist/components/LedgerRow/index.d.ts +2 -2
  158. package/dist/components/LedgerRow/index.js +1 -0
  159. package/dist/components/LegalPage/components/LegalPage.d.ts +0 -1
  160. package/dist/components/LegalPage/components/LegalPage.js +11 -0
  161. package/dist/components/LegalPage/components/clauses.js +28 -0
  162. package/dist/components/LegalPage/components/index.d.ts +4 -4
  163. package/dist/components/LegalPage/components/index.js +2 -0
  164. package/dist/components/LegalPage/index.d.ts +2 -2
  165. package/dist/components/LegalPage/index.js +1 -0
  166. package/dist/components/Lightbox/components/Lightbox.d.ts +1 -3
  167. package/dist/components/Lightbox/components/Lightbox.js +97 -0
  168. package/dist/components/Lightbox/components/index.d.ts +2 -2
  169. package/dist/components/Lightbox/components/index.js +1 -0
  170. package/dist/components/Lightbox/index.d.ts +2 -2
  171. package/dist/components/Lightbox/index.js +1 -0
  172. package/dist/components/Modal/components/Modal.d.ts +0 -2
  173. package/dist/components/Modal/components/Modal.js +37 -0
  174. package/dist/components/Modal/components/index.d.ts +2 -2
  175. package/dist/components/Modal/components/index.js +1 -0
  176. package/dist/components/Modal/index.d.ts +2 -2
  177. package/dist/components/Modal/index.js +1 -0
  178. package/dist/components/Nav/components/Nav.d.ts +0 -1
  179. package/dist/components/Nav/components/Nav.js +69 -0
  180. package/dist/components/Nav/components/index.d.ts +2 -2
  181. package/dist/components/Nav/components/index.js +1 -0
  182. package/dist/components/Nav/index.d.ts +2 -2
  183. package/dist/components/Nav/index.js +1 -0
  184. package/dist/components/Notification/components/Notification.d.ts +1 -2
  185. package/dist/components/Notification/components/Notification.js +34 -0
  186. package/dist/components/Notification/components/index.d.ts +2 -2
  187. package/dist/components/Notification/components/index.js +1 -0
  188. package/dist/components/Notification/index.d.ts +1 -1
  189. package/dist/components/Notification/index.js +1 -0
  190. package/dist/components/Progress/components/Progress.d.ts +0 -1
  191. package/dist/components/Progress/components/Progress.js +72 -0
  192. package/dist/components/Progress/components/index.d.ts +2 -2
  193. package/dist/components/Progress/components/index.js +1 -0
  194. package/dist/components/Progress/index.d.ts +2 -2
  195. package/dist/components/Progress/index.js +1 -0
  196. package/dist/components/RetroEffects/components/RetroEffects.d.ts +0 -1
  197. package/dist/components/RetroEffects/components/RetroEffects.js +163 -0
  198. package/dist/components/RetroEffects/components/index.d.ts +2 -2
  199. package/dist/components/RetroEffects/components/index.js +1 -0
  200. package/dist/components/RetroEffects/index.d.ts +2 -2
  201. package/dist/components/RetroEffects/index.js +1 -0
  202. package/dist/components/SectionHeading/components/SectionHeading.js +18 -0
  203. package/dist/components/SectionHeading/components/index.d.ts +2 -2
  204. package/dist/components/SectionHeading/components/index.js +1 -0
  205. package/dist/components/SectionHeading/index.d.ts +2 -2
  206. package/dist/components/SectionHeading/index.js +1 -0
  207. package/dist/components/Select/components/Select.js +14 -0
  208. package/dist/components/Select/components/index.d.ts +2 -2
  209. package/dist/components/Select/components/index.js +1 -0
  210. package/dist/components/Select/index.d.ts +2 -2
  211. package/dist/components/Select/index.js +1 -0
  212. package/dist/components/Separator/components/Separator.d.ts +0 -1
  213. package/dist/components/Separator/components/Separator.js +11 -0
  214. package/dist/components/Separator/components/index.d.ts +2 -2
  215. package/dist/components/Separator/components/index.js +1 -0
  216. package/dist/components/Separator/index.d.ts +2 -2
  217. package/dist/components/Separator/index.js +1 -0
  218. package/dist/components/Skeleton/components/Skeleton.d.ts +0 -1
  219. package/dist/components/Skeleton/components/Skeleton.js +52 -0
  220. package/dist/components/Skeleton/components/index.d.ts +2 -2
  221. package/dist/components/Skeleton/components/index.js +1 -0
  222. package/dist/components/Skeleton/index.d.ts +2 -2
  223. package/dist/components/Skeleton/index.js +1 -0
  224. package/dist/components/Stat/components/Stat.d.ts +0 -1
  225. package/dist/components/Stat/components/Stat.js +42 -0
  226. package/dist/components/Stat/components/index.d.ts +2 -2
  227. package/dist/components/Stat/components/index.js +1 -0
  228. package/dist/components/Stat/index.d.ts +2 -2
  229. package/dist/components/Stat/index.js +1 -0
  230. package/dist/components/Switch/components/Switch.d.ts +0 -1
  231. package/dist/components/Switch/components/Switch.js +11 -0
  232. package/dist/components/Switch/components/index.d.ts +2 -2
  233. package/dist/components/Switch/components/index.js +1 -0
  234. package/dist/components/Switch/index.d.ts +2 -2
  235. package/dist/components/Switch/index.js +1 -0
  236. package/dist/components/Tabs/components/Tabs.d.ts +0 -1
  237. package/dist/components/Tabs/components/Tabs.js +54 -0
  238. package/dist/components/Tabs/components/index.d.ts +2 -2
  239. package/dist/components/Tabs/components/index.js +1 -0
  240. package/dist/components/Tabs/index.d.ts +2 -2
  241. package/dist/components/Tabs/index.js +1 -0
  242. package/dist/components/Tag/components/Tag.d.ts +0 -1
  243. package/dist/components/Tag/components/Tag.js +46 -0
  244. package/dist/components/Tag/components/TagGroup.js +15 -0
  245. package/dist/components/Tag/components/index.d.ts +4 -4
  246. package/dist/components/Tag/components/index.js +2 -0
  247. package/dist/components/Tag/index.d.ts +2 -2
  248. package/dist/components/Tag/index.js +1 -0
  249. package/dist/components/Terminal/components/Terminal.d.ts +0 -2
  250. package/dist/components/Terminal/components/Terminal.js +43 -0
  251. package/dist/components/Terminal/components/index.d.ts +2 -2
  252. package/dist/components/Terminal/components/index.js +1 -0
  253. package/dist/components/Terminal/index.d.ts +1 -1
  254. package/dist/components/Terminal/index.js +1 -0
  255. package/dist/components/TextScramble/components/TextScramble.d.ts +0 -1
  256. package/dist/components/TextScramble/components/TextScramble.js +14 -0
  257. package/dist/components/TextScramble/components/index.d.ts +2 -2
  258. package/dist/components/TextScramble/components/index.js +1 -0
  259. package/dist/components/TextScramble/index.d.ts +1 -1
  260. package/dist/components/TextScramble/index.js +1 -0
  261. package/dist/components/TimelineContainer/components/MasterDetailLayout.d.ts +1 -3
  262. package/dist/components/TimelineContainer/components/MasterDetailLayout.js +77 -0
  263. package/dist/components/TimelineContainer/components/TimelineAxis.d.ts +0 -1
  264. package/dist/components/TimelineContainer/components/TimelineAxis.js +8 -0
  265. package/dist/components/TimelineContainer/components/TimelineContainer.d.ts +1 -3
  266. package/dist/components/TimelineContainer/components/TimelineContainer.js +218 -0
  267. package/dist/components/TimelineContainer/components/TimelineContent.d.ts +1 -1
  268. package/dist/components/TimelineContainer/components/TimelineContent.js +23 -0
  269. package/dist/components/TimelineContainer/components/TimelineEntryCard.d.ts +1 -2
  270. package/dist/components/TimelineContainer/components/TimelineEntryCard.js +50 -0
  271. package/dist/components/TimelineContainer/components/ZoomControls.d.ts +2 -3
  272. package/dist/components/TimelineContainer/components/ZoomControls.js +16 -0
  273. package/dist/components/TimelineContainer/components/index.d.ts +5 -5
  274. package/dist/components/TimelineContainer/components/index.js +2 -0
  275. package/dist/components/TimelineContainer/components/timelineUtils.d.ts +1 -1
  276. package/dist/components/TimelineContainer/components/timelineUtils.js +185 -0
  277. package/dist/components/TimelineContainer/components/types.js +7 -0
  278. package/dist/components/TimelineContainer/components/useDrillDown.d.ts +1 -1
  279. package/dist/components/TimelineContainer/components/useDrillDown.js +83 -0
  280. package/dist/components/TimelineContainer/components/useSelection.js +51 -0
  281. package/dist/components/TimelineContainer/components/useZoom.d.ts +1 -1
  282. package/dist/components/TimelineContainer/components/useZoom.js +54 -0
  283. package/dist/components/TimelineContainer/components/variants/TimelineEntryCardGallery.d.ts +1 -2
  284. package/dist/components/TimelineContainer/components/variants/TimelineEntryCardGallery.js +67 -0
  285. package/dist/components/TimelineContainer/components/variants/TimelineEntryCardImage.d.ts +1 -2
  286. package/dist/components/TimelineContainer/components/variants/TimelineEntryCardImage.js +31 -0
  287. package/dist/components/TimelineContainer/components/views/DayView.d.ts +1 -1
  288. package/dist/components/TimelineContainer/components/views/DayView.js +20 -0
  289. package/dist/components/TimelineContainer/components/views/HourView.d.ts +1 -1
  290. package/dist/components/TimelineContainer/components/views/HourView.js +22 -0
  291. package/dist/components/TimelineContainer/components/views/MonthView.d.ts +1 -1
  292. package/dist/components/TimelineContainer/components/views/MonthView.js +18 -0
  293. package/dist/components/TimelineContainer/components/views/YearView.d.ts +1 -1
  294. package/dist/components/TimelineContainer/components/views/YearView.js +13 -0
  295. package/dist/components/TimelineContainer/components/views/index.d.ts +4 -4
  296. package/dist/components/TimelineContainer/components/views/index.js +4 -0
  297. package/dist/components/TimelineContainer/index.d.ts +3 -3
  298. package/dist/components/TimelineContainer/index.js +2 -0
  299. package/dist/components/TimelineNode/components/TimelineNode.d.ts +0 -1
  300. package/dist/components/TimelineNode/components/TimelineNode.js +49 -0
  301. package/dist/components/TimelineNode/components/index.d.ts +2 -2
  302. package/dist/components/TimelineNode/components/index.js +1 -0
  303. package/dist/components/TimelineNode/index.d.ts +2 -2
  304. package/dist/components/TimelineNode/index.js +1 -0
  305. package/dist/components/registry.js +247 -0
  306. package/dist/eidotter.css +1 -1
  307. package/dist/hooks/index.d.ts +4 -0
  308. package/dist/hooks/index.js +3 -0
  309. package/dist/hooks/useAnimatedDismiss.d.ts +15 -0
  310. package/dist/hooks/useAnimatedDismiss.js +29 -0
  311. package/dist/hooks/useTextScramble.d.ts +21 -0
  312. package/dist/hooks/useTextScramble.js +91 -0
  313. package/dist/hooks/useTextScramble.test.d.ts +1 -0
  314. package/dist/hooks/useThemePortal.d.ts +12 -0
  315. package/dist/hooks/useThemePortal.js +62 -0
  316. package/dist/hooks/useThemePortal.test.d.ts +1 -0
  317. package/dist/utils/cn.d.ts +9 -0
  318. package/dist/utils/cn.js +12 -0
  319. package/dist/utils/index.d.ts +2 -0
  320. package/dist/utils/index.js +2 -0
  321. package/dist/utils/isSafeHref.d.ts +1 -0
  322. package/dist/utils/isSafeHref.js +22 -0
  323. package/dist/utils/isSafeUrl.d.ts +8 -0
  324. package/dist/utils/isSafeUrl.js +18 -0
  325. package/dist/utils/isSafeUrl.test.d.ts +1 -0
  326. package/dist/utils/prefersReducedMotion.d.ts +5 -0
  327. package/dist/utils/prefersReducedMotion.js +8 -0
  328. package/package.json +9 -2
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { Button as AriaButton } from 'react-aria-components';
5
+ import { cn } from '../../../utils/cn.js';
6
+ import { TimelineEntryCardImage } from './variants/TimelineEntryCardImage.js';
7
+ import { TimelineEntryCardGallery } from './variants/TimelineEntryCardGallery.js';
8
+ /**
9
+ * Dispatcher for content-aware timeline entry rendering.
10
+ *
11
+ * Branches on `entry.kind`:
12
+ * - `text` — existing trigger+panel rendering with `entry.content`.
13
+ * - `image` — placeholder for now; filled in by Task 6.
14
+ * - `gallery` — placeholder for now; filled in by Tasks 7–8.
15
+ *
16
+ * The card chrome (border, hover slide+glow, selected/expanded states)
17
+ * lives on the outer `.eidotter-timeline-card` element and applies to all
18
+ * kinds.
19
+ */
20
+ export const TimelineEntryCard = React.memo(({ entry, isSelected, isExpanded = false, onSelect, footer, children, }) => {
21
+ return (_jsxs("div", { className: cn('eidotter-timeline-card', isSelected && 'eidotter-timeline-card--selected', isExpanded && 'eidotter-timeline-card--expanded'), children: [renderBranch(entry, isExpanded, onSelect, children), footer && _jsx("div", { className: "eidotter-timeline-card__footer", children: footer })] }));
22
+ });
23
+ TimelineEntryCard.displayName = 'TimelineEntryCard';
24
+ /**
25
+ * Exhaustive switch over `entry.kind` so a future fourth kind triggers a
26
+ * compile-time error via `assertNever` rather than silently falling through.
27
+ */
28
+ function renderBranch(entry, isExpanded, onSelect, children) {
29
+ switch (entry.kind) {
30
+ case 'text':
31
+ return (_jsx(TextBranch, { entry: entry, isExpanded: isExpanded, onSelect: onSelect, children: children }));
32
+ case 'image':
33
+ return _jsx(TimelineEntryCardImage, { entry: entry, isExpanded: isExpanded, onSelect: onSelect });
34
+ case 'gallery':
35
+ return _jsx(TimelineEntryCardGallery, { entry: entry, isExpanded: isExpanded, onSelect: onSelect });
36
+ default:
37
+ return assertNever(entry);
38
+ }
39
+ }
40
+ function assertNever(value) {
41
+ throw new Error(`TimelineEntryCard: unhandled entry kind: ${JSON.stringify(value)}`);
42
+ }
43
+ const TextBranch = ({ entry, isExpanded, onSelect, children }) => {
44
+ const hasStringContent = typeof entry.content === 'string';
45
+ const hasContent = entry.content != null;
46
+ return (_jsxs(_Fragment, { children: [_jsxs(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "eidotter-timeline-card__header", children: [entry.type && (_jsx("span", { className: "eidotter-timeline-card__type", children: entry.type.toUpperCase() })), entry.tags && entry.tags.length > 0 && (_jsx("span", { className: "eidotter-timeline-card__tags", children: entry.tags.map(t => `#${t}`).join(' ') }))] }), _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }), !isExpanded && hasStringContent && (() => {
47
+ const text = String(entry.content);
48
+ return (_jsxs("p", { className: "eidotter-timeline-card__preview", children: [text.slice(0, 80), text.length > 80 ? '...' : ''] }));
49
+ })(), children] }), hasContent && (_jsx("div", { className: "eidotter-timeline-card__body", children: _jsx("div", { className: "eidotter-timeline-card__body-inner", inert: !isExpanded ? true : undefined, children: entry.content }) }))] }));
50
+ };
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
- import type { DrillDownEntry } from './useDrillDown';
3
- import type { ZoomLevel } from './types';
4
- import './ZoomControls.css';
2
+ import type { DrillDownEntry } from './useDrillDown.js';
3
+ import type { ZoomLevel } from './types.js';
5
4
  export interface ZoomControlsProps {
6
5
  zoomLevel: ZoomLevel;
7
6
  canZoomIn: boolean;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button as AriaButton } from 'react-aria-components';
4
+ import { Button } from '../../Button/components/Button.js';
5
+ import { Badge } from '../../Badge/components/Badge.js';
6
+ import { Breadcrumb } from '../../Breadcrumb/components/Breadcrumb.js';
7
+ export const ZoomControls = ({ zoomLevel, canZoomIn, canZoomOut, onZoomIn, onZoomOut, onReset, breadcrumbs = [], onBreadcrumbClick, }) => {
8
+ const hasBreadcrumbs = breadcrumbs.length > 0;
9
+ return (_jsxs("div", { className: "timeline-zoom-controls", role: "toolbar", "aria-label": "Timeline zoom controls", children: [_jsx(Button, { variant: "secondary", size: "small", onClick: onZoomOut, disabled: !canZoomOut, "aria-label": "Zoom out", children: "[-]" }), hasBreadcrumbs ? (_jsx(Breadcrumb, { trail: [
10
+ { label: 'ALL', onClick: onReset },
11
+ ...breadcrumbs.slice(0, -1).map((entry, index) => ({
12
+ label: entry.label,
13
+ onClick: () => onBreadcrumbClick?.(index),
14
+ })),
15
+ ], currentLabel: breadcrumbs[breadcrumbs.length - 1].label, showBackArrow: false, separator: ">", className: "timeline-zoom-controls__breadcrumb" })) : (_jsx(AriaButton, { className: "timeline-zoom-controls__level", onPress: onReset, "aria-label": `Current zoom: ${zoomLevel}. Click to reset.`, children: _jsx(Badge, { variant: "accent", size: "medium", children: zoomLevel.toUpperCase() }) })), _jsx(Button, { variant: "secondary", size: "small", onClick: onZoomIn, disabled: !canZoomIn, "aria-label": "Zoom in", children: "[+]" })] }));
16
+ };
@@ -1,5 +1,5 @@
1
- export { TimelineContainer } from './TimelineContainer';
2
- export type { TimelineContainerProps } from './TimelineContainer';
3
- export { ZOOM_LEVELS } from './types';
4
- export type { TimelineEntryData, TimelineEntry, TimelineEntryRenderContext, TimelineRenderEntry, TimelineImage, DateBucket, ZoomLevel, } from './types';
5
- export type { DrillDownEntry, UseDrillDownReturn } from './useDrillDown';
1
+ export { TimelineContainer } from './TimelineContainer.js';
2
+ export type { TimelineContainerProps } from './TimelineContainer.js';
3
+ export { ZOOM_LEVELS } from './types.js';
4
+ export type { TimelineEntryData, TimelineEntry, TimelineEntryRenderContext, TimelineRenderEntry, TimelineImage, DateBucket, ZoomLevel, } from './types.js';
5
+ export type { DrillDownEntry, UseDrillDownReturn } from './useDrillDown.js';
@@ -0,0 +1,2 @@
1
+ export { TimelineContainer } from './TimelineContainer.js';
2
+ export { ZOOM_LEVELS } from './types.js';
@@ -2,7 +2,7 @@
2
2
  * Timeline utility functions for grouping entries and formatting dates.
3
3
  * Pure functions with zero external dependencies — uses only native Date/Intl.
4
4
  */
5
- import type { TimelineEntryData, DateBucket, ZoomLevel } from './types';
5
+ import type { TimelineEntryData, DateBucket, ZoomLevel } from './types.js';
6
6
  /** Format a full timestamp: "Mar 15, 2024 at 2:30pm" */
7
7
  export declare function formatTimestamp(iso: string): string;
8
8
  /**
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Timeline utility functions for grouping entries and formatting dates.
3
+ * Pure functions with zero external dependencies — uses only native Date/Intl.
4
+ */
5
+ // ─── Date extraction ────────────────────────────────────────────────────────
6
+ const MONTH_NAMES = [
7
+ 'January', 'February', 'March', 'April', 'May', 'June',
8
+ 'July', 'August', 'September', 'October', 'November', 'December',
9
+ ];
10
+ const MONTH_ABBREVS = [
11
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
12
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
13
+ ];
14
+ function getUTCYear(iso) {
15
+ return new Date(iso).getUTCFullYear();
16
+ }
17
+ function getUTCMonth(iso) {
18
+ return new Date(iso).getUTCMonth();
19
+ }
20
+ function getUTCDay(iso) {
21
+ return new Date(iso).getUTCDate();
22
+ }
23
+ function getUTCHour(iso) {
24
+ return new Date(iso).getUTCHours();
25
+ }
26
+ // ─── Date formatting ────────────────────────────────────────────────────────
27
+ function formatYearLabel(year) {
28
+ return String(year);
29
+ }
30
+ function formatMonthLabel(year, month) {
31
+ return `${MONTH_NAMES[month]} ${year}`;
32
+ }
33
+ function formatDayLabel(_year, month, day) {
34
+ return `${MONTH_ABBREVS[month]} ${day}`;
35
+ }
36
+ function formatHourLabel(_year, month, day, hour) {
37
+ const period = hour >= 12 ? 'pm' : 'am';
38
+ const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
39
+ return `${MONTH_ABBREVS[month]} ${day}, ${displayHour}${period}`;
40
+ }
41
+ /** Format a full timestamp: "Mar 15, 2024 at 2:30pm" */
42
+ export function formatTimestamp(iso) {
43
+ const d = new Date(iso);
44
+ const month = MONTH_ABBREVS[d.getUTCMonth()];
45
+ const day = d.getUTCDate();
46
+ const year = d.getUTCFullYear();
47
+ const hour = d.getUTCHours();
48
+ const minute = d.getUTCMinutes();
49
+ const period = hour >= 12 ? 'pm' : 'am';
50
+ const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
51
+ const minuteStr = minute > 0 ? `:${String(minute).padStart(2, '0')}` : '';
52
+ return `${month} ${day}, ${year} at ${displayHour}${minuteStr}${period}`;
53
+ }
54
+ // ─── Period start generators ────────────────────────────────────────────────
55
+ function yearPeriodStart(year) {
56
+ return new Date(Date.UTC(year, 0, 1)).toISOString();
57
+ }
58
+ function monthPeriodStart(year, month) {
59
+ return new Date(Date.UTC(year, month, 1)).toISOString();
60
+ }
61
+ function dayPeriodStart(year, month, day) {
62
+ return new Date(Date.UTC(year, month, day)).toISOString();
63
+ }
64
+ function hourPeriodStart(year, month, day, hour) {
65
+ return new Date(Date.UTC(year, month, day, hour)).toISOString();
66
+ }
67
+ function groupBy(entries, keyFn, labelFn, periodStartFn, sortOrder) {
68
+ const map = new Map();
69
+ const keyOrder = [];
70
+ for (const entry of entries) {
71
+ const key = keyFn(entry);
72
+ const existing = map.get(key);
73
+ if (existing) {
74
+ existing.push(entry);
75
+ }
76
+ else {
77
+ map.set(key, [entry]);
78
+ keyOrder.push(key);
79
+ }
80
+ }
81
+ keyOrder.sort((a, b) => sortOrder === 'asc' ? a.localeCompare(b) : b.localeCompare(a));
82
+ for (const [, bucketEntries] of map) {
83
+ bucketEntries.sort((a, b) => {
84
+ const cmp = a.date.localeCompare(b.date);
85
+ return sortOrder === 'asc' ? cmp : -cmp;
86
+ });
87
+ }
88
+ return keyOrder.map(key => ({
89
+ label: labelFn(key),
90
+ periodStart: periodStartFn(key),
91
+ entries: map.get(key) ?? [],
92
+ }));
93
+ }
94
+ function yearKey(entry) {
95
+ return String(getUTCYear(entry.date));
96
+ }
97
+ function monthKey(entry) {
98
+ const y = getUTCYear(entry.date);
99
+ const m = getUTCMonth(entry.date);
100
+ return `${y}-${String(m).padStart(2, '0')}`;
101
+ }
102
+ function dayKey(entry) {
103
+ const y = getUTCYear(entry.date);
104
+ const m = getUTCMonth(entry.date);
105
+ const d = getUTCDay(entry.date);
106
+ return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
107
+ }
108
+ function hourKey(entry) {
109
+ const y = getUTCYear(entry.date);
110
+ const m = getUTCMonth(entry.date);
111
+ const d = getUTCDay(entry.date);
112
+ const h = getUTCHour(entry.date);
113
+ return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}-${String(h).padStart(2, '0')}`;
114
+ }
115
+ function parseYearKey(key) {
116
+ return { year: parseInt(key) };
117
+ }
118
+ function parseMonthKey(key) {
119
+ const [y, m] = key.split('-');
120
+ return { year: parseInt(y), month: parseInt(m) };
121
+ }
122
+ function parseDayKey(key) {
123
+ const [y, m, d] = key.split('-');
124
+ return { year: parseInt(y), month: parseInt(m), day: parseInt(d) };
125
+ }
126
+ function parseHourKey(key) {
127
+ const [y, m, d, h] = key.split('-');
128
+ return { year: parseInt(y), month: parseInt(m), day: parseInt(d), hour: parseInt(h) };
129
+ }
130
+ // ─── Drill-down filtering ──────────────────────────────────────────────────
131
+ /** Prefix lengths for ISO string matching by parent zoom level */
132
+ const PERIOD_PREFIX_LENGTH = {
133
+ year: 4, // "2024"
134
+ month: 7, // "2024-03"
135
+ day: 10, // "2024-03-15"
136
+ };
137
+ /**
138
+ * Filter buckets to only those within a drilled-into period.
139
+ * Uses ISO string prefix matching: a year prefix "2024" matches
140
+ * any periodStart starting with "2024", etc.
141
+ *
142
+ * @param buckets - All buckets at the current zoom level
143
+ * @param periodStart - The parent period's periodStart, or null for no filter
144
+ * @param parentZoomLevel - The zoom level of the parent period
145
+ * @returns Filtered buckets (or all buckets if no filter applies)
146
+ */
147
+ export function filterBucketsByPeriod(buckets, periodStart, parentZoomLevel) {
148
+ if (periodStart === null)
149
+ return buckets;
150
+ if (parentZoomLevel === 'hour')
151
+ return buckets;
152
+ const prefixLen = PERIOD_PREFIX_LENGTH[parentZoomLevel];
153
+ if (prefixLen === undefined)
154
+ return buckets;
155
+ const prefix = periodStart.slice(0, prefixLen);
156
+ return buckets.filter(b => b.periodStart.slice(0, prefixLen) === prefix);
157
+ }
158
+ // ─── Grouping ──────────────────────────────────────────────────────────────
159
+ /**
160
+ * Group timeline entries into DateBuckets based on the current zoom level.
161
+ */
162
+ export function groupEntriesByZoom(entries, zoomLevel, sortOrder = 'desc') {
163
+ if (entries.length === 0)
164
+ return [];
165
+ // Filter entries with invalid dates to prevent NaN bucket keys
166
+ const valid = entries.filter(e => !isNaN(new Date(e.date).getTime()));
167
+ if (valid.length === 0)
168
+ return [];
169
+ switch (zoomLevel) {
170
+ case 'year':
171
+ return groupBy(valid, yearKey, key => { const { year } = parseYearKey(key); return formatYearLabel(year); }, key => { const { year } = parseYearKey(key); return yearPeriodStart(year); }, sortOrder);
172
+ case 'month':
173
+ return groupBy(valid, monthKey, key => { const { year, month } = parseMonthKey(key); return formatMonthLabel(year, month); }, key => { const { year, month } = parseMonthKey(key); return monthPeriodStart(year, month); }, sortOrder);
174
+ case 'day':
175
+ return groupBy(valid, dayKey, key => { const { year, month, day } = parseDayKey(key); return formatDayLabel(year, month, day); }, key => { const { year, month, day } = parseDayKey(key); return dayPeriodStart(year, month, day); }, sortOrder);
176
+ case 'hour':
177
+ return groupBy(valid, hourKey, key => {
178
+ const { year, month, day, hour } = parseHourKey(key);
179
+ return formatHourLabel(year, month, day, hour);
180
+ }, key => {
181
+ const { year, month, day, hour } = parseHourKey(key);
182
+ return hourPeriodStart(year, month, day, hour);
183
+ }, sortOrder);
184
+ }
185
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared type definitions for TimelineContainer.
3
+ * Extracted to a separate file to avoid circular imports between
4
+ * the component, hooks, and view sub-components.
5
+ */
6
+ /** Ordered zoom levels from least to most detail */
7
+ export const ZOOM_LEVELS = ['year', 'month', 'day', 'hour'];
@@ -1,4 +1,4 @@
1
- import type { ZoomLevel } from './types';
1
+ import type { ZoomLevel } from './types.js';
2
2
  export interface DrillDownEntry {
3
3
  /** ISO period start for this drill-down level */
4
4
  periodStart: string;
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { ZOOM_LEVELS } from './types.js';
4
+ /**
5
+ * Manages drill-down navigation with a breadcrumb stack.
6
+ * When zoom is uncontrolled, maintains its own zoom state so that
7
+ * reset() can go directly to 'year' (the outermost level).
8
+ * When zoom is controlled externally, drill-down is disabled.
9
+ */
10
+ export function useDrillDown({ zoomLevel: controlledLevel, defaultZoomLevel = 'month', onZoomChange, } = {}) {
11
+ const isDrillDownEnabled = controlledLevel === undefined;
12
+ const [internalLevel, setInternalLevel] = useState(defaultZoomLevel);
13
+ const [breadcrumbs, setBreadcrumbs] = useState([]);
14
+ // Clear breadcrumbs when transitioning from uncontrolled to controlled mode
15
+ const prevEnabledRef = useRef(isDrillDownEnabled);
16
+ useEffect(() => {
17
+ if (prevEnabledRef.current && !isDrillDownEnabled) {
18
+ setBreadcrumbs([]);
19
+ }
20
+ prevEnabledRef.current = isDrillDownEnabled;
21
+ }, [isDrillDownEnabled]);
22
+ const currentLevel = controlledLevel ?? internalLevel;
23
+ const currentIndex = ZOOM_LEVELS.indexOf(currentLevel);
24
+ const canZoomIn = currentIndex < ZOOM_LEVELS.length - 1;
25
+ const canZoomOut = currentIndex > 0;
26
+ const currentPeriod = breadcrumbs.length > 0
27
+ ? breadcrumbs[breadcrumbs.length - 1].periodStart
28
+ : null;
29
+ // Refs for stable callbacks
30
+ const isDrillDownEnabledRef = useRef(isDrillDownEnabled);
31
+ const canZoomInRef = useRef(canZoomIn);
32
+ const currentIndexRef = useRef(currentIndex);
33
+ const breadcrumbsRef = useRef(breadcrumbs);
34
+ const onZoomChangeRef = useRef(onZoomChange);
35
+ useEffect(() => {
36
+ isDrillDownEnabledRef.current = isDrillDownEnabled;
37
+ canZoomInRef.current = canZoomIn;
38
+ currentIndexRef.current = currentIndex;
39
+ breadcrumbsRef.current = breadcrumbs;
40
+ onZoomChangeRef.current = onZoomChange;
41
+ });
42
+ const updateLevel = useCallback((level) => {
43
+ if (isDrillDownEnabledRef.current) {
44
+ setInternalLevel(level);
45
+ }
46
+ onZoomChangeRef.current?.(level);
47
+ }, []);
48
+ const drillDown = useCallback((periodStart, label) => {
49
+ if (!isDrillDownEnabledRef.current || !canZoomInRef.current)
50
+ return;
51
+ const nextLevel = ZOOM_LEVELS[currentIndexRef.current + 1];
52
+ if (nextLevel !== undefined) {
53
+ setBreadcrumbs(prev => [...prev, { periodStart, label }]);
54
+ updateLevel(nextLevel);
55
+ }
56
+ }, [updateLevel]);
57
+ const drillUp = useCallback(() => {
58
+ if (!isDrillDownEnabledRef.current || breadcrumbsRef.current.length === 0)
59
+ return;
60
+ const prevLevel = ZOOM_LEVELS[currentIndexRef.current - 1];
61
+ if (prevLevel !== undefined) {
62
+ setBreadcrumbs(prev => prev.slice(0, -1));
63
+ updateLevel(prevLevel);
64
+ }
65
+ }, [updateLevel]);
66
+ const reset = useCallback(() => {
67
+ if (!isDrillDownEnabledRef.current)
68
+ return;
69
+ setBreadcrumbs([]);
70
+ updateLevel('year');
71
+ }, [updateLevel]);
72
+ return {
73
+ zoomLevel: currentLevel,
74
+ canZoomIn,
75
+ canZoomOut,
76
+ breadcrumbs,
77
+ currentPeriod,
78
+ isDrillDownEnabled,
79
+ drillDown,
80
+ drillUp,
81
+ reset,
82
+ };
83
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ /**
4
+ * Manages entry selection with controlled/uncontrolled pattern.
5
+ */
6
+ export function useSelection({ selectedEntryId: controlledId, defaultSelectedEntryId = null, onSelectEntry, } = {}) {
7
+ const [internalId, setInternalId] = useState(defaultSelectedEntryId);
8
+ // Inline narrowing — undefined means uncontrolled, null means controlled-with-nothing-selected
9
+ const isControlled = controlledId !== undefined;
10
+ const currentId = controlledId !== undefined ? controlledId : internalId;
11
+ // Refs for stable toggle callback
12
+ const currentIdRef = useRef(currentId);
13
+ const isControlledRef = useRef(isControlled);
14
+ const onSelectEntryRef = useRef(onSelectEntry);
15
+ useEffect(() => {
16
+ currentIdRef.current = currentId;
17
+ isControlledRef.current = isControlled;
18
+ onSelectEntryRef.current = onSelectEntry;
19
+ });
20
+ const updateSelection = useCallback((id) => {
21
+ if (!isControlledRef.current) {
22
+ setInternalId(id);
23
+ }
24
+ onSelectEntryRef.current?.(id);
25
+ }, []);
26
+ const select = useCallback((entryId) => {
27
+ updateSelection(entryId);
28
+ }, [updateSelection]);
29
+ const deselect = useCallback(() => {
30
+ updateSelection(null);
31
+ }, [updateSelection]);
32
+ // Stable toggle — uses ref to avoid closing over currentId
33
+ const toggle = useCallback((entryId) => {
34
+ if (isControlledRef.current) {
35
+ onSelectEntryRef.current?.(currentIdRef.current === entryId ? null : entryId);
36
+ }
37
+ else {
38
+ setInternalId(prev => {
39
+ const newId = prev === entryId ? null : entryId;
40
+ onSelectEntryRef.current?.(newId);
41
+ return newId;
42
+ });
43
+ }
44
+ }, []);
45
+ return {
46
+ selectedEntryId: currentId,
47
+ select,
48
+ deselect,
49
+ toggle,
50
+ };
51
+ }
@@ -1,4 +1,4 @@
1
- import type { ZoomLevel } from './types';
1
+ import type { ZoomLevel } from './types.js';
2
2
  export interface UseZoomOptions {
3
3
  zoomLevel?: ZoomLevel;
4
4
  defaultZoomLevel?: ZoomLevel;
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { ZOOM_LEVELS } from './types.js';
4
+ /**
5
+ * Manages zoom level with controlled/uncontrolled pattern.
6
+ * Zoom "in" means more detail (year -> month -> day -> hour).
7
+ */
8
+ export function useZoom({ zoomLevel: controlledLevel, defaultZoomLevel = 'month', onZoomChange, } = {}) {
9
+ const [internalLevel, setInternalLevel] = useState(defaultZoomLevel);
10
+ // Inline narrowing — TS cannot narrow through an extracted boolean variable
11
+ const currentLevel = controlledLevel ?? internalLevel;
12
+ const isControlled = controlledLevel !== undefined;
13
+ const currentIndex = ZOOM_LEVELS.indexOf(currentLevel);
14
+ const canZoomIn = currentIndex < ZOOM_LEVELS.length - 1;
15
+ const canZoomOut = currentIndex > 0;
16
+ // Store current values in refs for stable callbacks (prevents listener churn)
17
+ const currentIndexRef = useRef(currentIndex);
18
+ const isControlledRef = useRef(isControlled);
19
+ const onZoomChangeRef = useRef(onZoomChange);
20
+ useEffect(() => {
21
+ currentIndexRef.current = currentIndex;
22
+ isControlledRef.current = isControlled;
23
+ onZoomChangeRef.current = onZoomChange;
24
+ });
25
+ const updateLevel = useCallback((level) => {
26
+ if (!isControlledRef.current) {
27
+ setInternalLevel(level);
28
+ }
29
+ onZoomChangeRef.current?.(level);
30
+ }, []);
31
+ const zoomIn = useCallback(() => {
32
+ const nextLevel = ZOOM_LEVELS[currentIndexRef.current + 1];
33
+ if (nextLevel !== undefined) {
34
+ updateLevel(nextLevel);
35
+ }
36
+ }, [updateLevel]);
37
+ const zoomOut = useCallback(() => {
38
+ const prevLevel = ZOOM_LEVELS[currentIndexRef.current - 1];
39
+ if (prevLevel !== undefined) {
40
+ updateLevel(prevLevel);
41
+ }
42
+ }, [updateLevel]);
43
+ const reset = useCallback(() => {
44
+ updateLevel(defaultZoomLevel);
45
+ }, [defaultZoomLevel, updateLevel]);
46
+ return {
47
+ zoomLevel: currentLevel,
48
+ zoomIn,
49
+ zoomOut,
50
+ reset,
51
+ canZoomIn,
52
+ canZoomOut,
53
+ };
54
+ }
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import type { TimelineEntryData } from '../types';
3
- import './TimelineEntryCardGallery.css';
2
+ import type { TimelineEntryData } from '../types.js';
4
3
  type GalleryEntry = Extract<TimelineEntryData, {
5
4
  kind: 'gallery';
6
5
  }>;
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { Button as AriaButton } from 'react-aria-components';
5
+ import { Lightbox } from '../../../Lightbox/index.js';
6
+ import { cn } from '../../../../utils/cn.js';
7
+ import { isSafeHref } from '../../../../utils/isSafeHref.js';
8
+ /**
9
+ * Gallery variant of TimelineEntryCard. iOS Photos two-stage interaction:
10
+ * grid → focused (grow-in-place) → lightbox. Resets to grid on parent
11
+ * collapse via useEffect on `isExpanded`.
12
+ */
13
+ export const TimelineEntryCardGallery = ({ entry, isExpanded, onSelect, }) => {
14
+ const [state, setState] = useState({ phase: 'grid' });
15
+ // Reset when the parent card collapses, so re-expanding starts at grid.
16
+ useEffect(() => {
17
+ if (!isExpanded)
18
+ setState({ phase: 'grid' });
19
+ }, [isExpanded]);
20
+ // Re-validate state.index when entry.images shrinks — otherwise a stale
21
+ // index can feed an out-of-range value into the Lightbox below.
22
+ useEffect(() => {
23
+ setState((prev) => {
24
+ if (prev.phase === 'grid')
25
+ return prev;
26
+ if (prev.index >= entry.images.length)
27
+ return { phase: 'grid' };
28
+ return prev;
29
+ });
30
+ }, [entry.images.length]);
31
+ if (entry.images.length === 0) {
32
+ if (process.env.NODE_ENV !== 'production') {
33
+ console.error(`[eidotter] TimelineEntryCard kind="gallery" entry "${entry.id}" has no images.`);
34
+ }
35
+ return (_jsxs(_Fragment, { children: [_jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }), _jsx("p", { className: "eidotter-timeline-card-gallery__empty", children: "No images." })] }));
36
+ }
37
+ const handleThumbClick = (index, image) => {
38
+ // Only short-circuit when the link will actually render as a navigable
39
+ // anchor — unsafe schemes are stripped by GalleryThumb, in which case
40
+ // the thumb falls back to the focus/lightbox interaction.
41
+ if (image.link && isSafeHref(image.link))
42
+ return;
43
+ setState((prev) => {
44
+ if (prev.phase === 'focused' && prev.index === index) {
45
+ return { phase: 'lightbox', index };
46
+ }
47
+ return { phase: 'focused', index };
48
+ });
49
+ };
50
+ const focusedIndex = state.phase === 'focused' || state.phase === 'lightbox' ? state.index : null;
51
+ return (_jsxs(_Fragment, { children: [_jsx(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }) }), _jsx("div", { className: "eidotter-timeline-card-gallery__grid", role: "list", children: entry.images.map((img, i) => (_jsx(GalleryThumb, { image: img, isFocused: focusedIndex === i, onClick: () => handleThumbClick(i, img) }, i))) }), _jsx(Lightbox, { images: entry.images, isOpen: state.phase === 'lightbox', initialIndex: state.phase === 'lightbox' ? state.index : 0, onClose: () => {
52
+ setState((prev) => prev.phase === 'lightbox' ? { phase: 'focused', index: prev.index } : prev);
53
+ }, onIndexChange: (idx) => {
54
+ setState((prev) => prev.phase === 'lightbox' ? { phase: 'lightbox', index: idx } : prev);
55
+ } })] }));
56
+ };
57
+ const GalleryThumb = ({ image, isFocused, onClick }) => {
58
+ const src = image.thumbnail || image.src;
59
+ const linkHref = image.link && isSafeHref(image.link) ? image.link : undefined;
60
+ // Outer listitem wraps either an anchor (link mode) or a button (interactive
61
+ // focus/lightbox mode). The listitem semantics live on the outer wrapper so
62
+ // both modes participate in the parent role="list" consistently.
63
+ if (linkHref) {
64
+ return (_jsx("div", { role: "listitem", className: "eidotter-timeline-card-gallery__cell eidotter-timeline-card-gallery__cell--link", children: _jsx("a", { className: "eidotter-timeline-card-gallery__link", href: linkHref, rel: "noopener noreferrer", children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, width: image.width, height: image.height }) }) }));
65
+ }
66
+ return (_jsx("div", { role: "listitem", className: cn('eidotter-timeline-card-gallery__cell', isFocused && 'eidotter-timeline-card-gallery__cell--focused'), children: _jsx(AriaButton, { className: "eidotter-timeline-card-gallery__button", onPress: onClick, "aria-label": image.alt || 'Image', "aria-pressed": isFocused, children: _jsx("img", { className: "eidotter-timeline-card-gallery__img", src: src, alt: image.alt, width: image.width, height: image.height }) }) }));
67
+ };
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import type { TimelineEntryData } from '../types';
3
- import './TimelineEntryCardImage.css';
2
+ import type { TimelineEntryData } from '../types.js';
4
3
  type ImageEntry = Extract<TimelineEntryData, {
5
4
  kind: 'image';
6
5
  }>;
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { Button as AriaButton } from 'react-aria-components';
5
+ import { Lightbox } from '../../../Lightbox/index.js';
6
+ import { isSafeHref } from '../../../../utils/isSafeHref.js';
7
+ /**
8
+ * Image variant of TimelineEntryCard.
9
+ *
10
+ * Collapsed: title + thumbnail. Expanded: title + full-width image. Click on
11
+ * the expanded image opens a Lightbox. If `entry.image.link` is set and uses
12
+ * a safe scheme (http/https/mailto/relative), the thumbnail is wrapped in a
13
+ * plain anchor and clicks navigate out — no expansion, no lightbox.
14
+ */
15
+ export const TimelineEntryCardImage = ({ entry, isExpanded, onSelect, }) => {
16
+ const [lightboxOpen, setLightboxOpen] = useState(false);
17
+ const { image } = entry;
18
+ const thumbSrc = image.thumbnail || image.src;
19
+ const linkHref = image.link && isSafeHref(image.link) ? image.link : undefined;
20
+ // Reset lightbox state when the parent card collapses, mirroring Gallery's
21
+ // useEffect — otherwise the modal stays mounted detached from a collapsed card.
22
+ useEffect(() => {
23
+ if (!isExpanded)
24
+ setLightboxOpen(false);
25
+ }, [isExpanded]);
26
+ // link mode — render as a plain anchor and exit early.
27
+ if (linkHref) {
28
+ return (_jsxs("a", { className: "eidotter-timeline-card-image eidotter-timeline-card-image--link", href: linkHref, rel: "noopener noreferrer", children: [_jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }), _jsx("img", { className: "eidotter-timeline-card-image__media", src: thumbSrc, alt: image.alt, width: image.width, height: image.height })] }));
29
+ }
30
+ return (_jsxs(_Fragment, { children: [_jsx(AriaButton, { className: "eidotter-timeline-card__trigger", onPress: () => onSelect?.(entry.id), "aria-expanded": isExpanded, children: _jsx("p", { className: "eidotter-timeline-card__title", children: entry.title }) }), _jsx("div", { className: "eidotter-timeline-card-image", children: isExpanded ? (_jsx(AriaButton, { className: "eidotter-timeline-card-image__media-button", onPress: () => setLightboxOpen(true), "aria-label": image.alt ? `Open ${image.alt} in lightbox` : 'Open image in lightbox', children: _jsx("img", { className: "eidotter-timeline-card-image__media eidotter-timeline-card-image__media--expanded", src: image.src, alt: image.alt, width: image.width, height: image.height }) })) : (_jsx("img", { className: "eidotter-timeline-card-image__media eidotter-timeline-card-image__media--thumb", src: thumbSrc, alt: image.alt, width: image.width, height: image.height })) }), _jsx(Lightbox, { images: [image], isOpen: lightboxOpen, onClose: () => setLightboxOpen(false) })] }));
31
+ };
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { TimelineViewProps } from '../types';
2
+ import type { TimelineViewProps } from '../types.js';
3
3
  /**
4
4
  * Day-level zoom view: shows day labels with entry cards and content preview.
5
5
  */