@otfdashkit/ui 0.1.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 (658) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +14886 -0
  3. package/dist/index.mjs +14389 -0
  4. package/package.json +124 -0
  5. package/src/advanced/BulkActions/BulkActions.stories.tsx +14 -0
  6. package/src/advanced/BulkActions/BulkActions.test.tsx +9 -0
  7. package/src/advanced/BulkActions/BulkActions.tsx +46 -0
  8. package/src/advanced/BulkActions/index.ts +2 -0
  9. package/src/advanced/CommandBar/CommandBar.stories.tsx +14 -0
  10. package/src/advanced/CommandBar/CommandBar.test.tsx +9 -0
  11. package/src/advanced/CommandBar/CommandBar.tsx +115 -0
  12. package/src/advanced/CommandBar/index.ts +2 -0
  13. package/src/advanced/Filters/Filters.stories.tsx +14 -0
  14. package/src/advanced/Filters/Filters.test.tsx +9 -0
  15. package/src/advanced/Filters/Filters.tsx +103 -0
  16. package/src/advanced/Filters/index.ts +2 -0
  17. package/src/advanced/Kanban/Kanban.stories.tsx +14 -0
  18. package/src/advanced/Kanban/Kanban.test.tsx +9 -0
  19. package/src/advanced/Kanban/Kanban.tsx +95 -0
  20. package/src/advanced/Kanban/index.ts +2 -0
  21. package/src/advanced/ToggleButton/ToggleButton.stories.tsx +14 -0
  22. package/src/advanced/ToggleButton/ToggleButton.test.tsx +9 -0
  23. package/src/advanced/ToggleButton/ToggleButton.tsx +78 -0
  24. package/src/advanced/ToggleButton/index.ts +2 -0
  25. package/src/advanced/bulk-actions.tsx +46 -0
  26. package/src/advanced/command-bar.tsx +115 -0
  27. package/src/advanced/filters.tsx +103 -0
  28. package/src/advanced/gantt.tsx +824 -0
  29. package/src/advanced/kanban.tsx +95 -0
  30. package/src/advanced/rich-editor.tsx +700 -0
  31. package/src/advanced/toggle-button.tsx +78 -0
  32. package/src/advanced/utils/cn.ts +1 -0
  33. package/src/app-shell/AppShell.tsx +24 -0
  34. package/src/app-shell/BillingStatus.tsx +63 -0
  35. package/src/app-shell/Sidebar.tsx +172 -0
  36. package/src/app-shell/SidebarResizer.tsx +109 -0
  37. package/src/app-shell/SortableNav.tsx +122 -0
  38. package/src/app-shell/UserMenu.tsx +135 -0
  39. package/src/app-shell/WorkspacesMenu.tsx +114 -0
  40. package/src/app-shell/index.ts +18 -0
  41. package/src/backgrounds/AnimatedGradient.tsx +151 -0
  42. package/src/backgrounds/DottedBackground.tsx +64 -0
  43. package/src/backgrounds/GradientBackground.tsx +113 -0
  44. package/src/backgrounds/HeroShaderA.tsx +255 -0
  45. package/src/backgrounds/NoiseBackground.tsx +51 -0
  46. package/src/backgrounds/index.ts +5 -0
  47. package/src/blocks/ChatDetail/ChatDetail.stories.tsx +14 -0
  48. package/src/blocks/ChatDetail/ChatDetail.test.tsx +9 -0
  49. package/src/blocks/ChatDetail/ChatDetail.tsx +73 -0
  50. package/src/blocks/ChatDetail/index.ts +2 -0
  51. package/src/blocks/FeedbackModal/FeedbackModal.stories.tsx +14 -0
  52. package/src/blocks/FeedbackModal/FeedbackModal.test.tsx +9 -0
  53. package/src/blocks/FeedbackModal/FeedbackModal.tsx +68 -0
  54. package/src/blocks/FeedbackModal/index.ts +2 -0
  55. package/src/blocks/FileCards/FileCards.stories.tsx +14 -0
  56. package/src/blocks/FileCards/FileCards.test.tsx +9 -0
  57. package/src/blocks/FileCards/FileCards.tsx +56 -0
  58. package/src/blocks/FileCards/index.ts +2 -0
  59. package/src/blocks/FilesList/FilesList.stories.tsx +14 -0
  60. package/src/blocks/FilesList/FilesList.test.tsx +9 -0
  61. package/src/blocks/FilesList/FilesList.tsx +72 -0
  62. package/src/blocks/FilesList/index.ts +2 -0
  63. package/src/blocks/FloatingActionButton/FloatingActionButton.stories.tsx +223 -0
  64. package/src/blocks/FloatingActionButton/FloatingActionButton.test.tsx +47 -0
  65. package/src/blocks/FloatingActionButton/FloatingActionButton.tsx +253 -0
  66. package/src/blocks/FloatingActionButton/index.ts +1 -0
  67. package/src/blocks/IntegrationCard/IntegrationCard.stories.tsx +14 -0
  68. package/src/blocks/IntegrationCard/IntegrationCard.test.tsx +9 -0
  69. package/src/blocks/IntegrationCard/IntegrationCard.tsx +50 -0
  70. package/src/blocks/IntegrationCard/index.ts +2 -0
  71. package/src/blocks/InviteModal/InviteModal.stories.tsx +14 -0
  72. package/src/blocks/InviteModal/InviteModal.test.tsx +9 -0
  73. package/src/blocks/InviteModal/InviteModal.tsx +69 -0
  74. package/src/blocks/InviteModal/index.ts +2 -0
  75. package/src/blocks/ManageTagsModal/ManageTagsModal.stories.tsx +14 -0
  76. package/src/blocks/ManageTagsModal/ManageTagsModal.test.tsx +9 -0
  77. package/src/blocks/ManageTagsModal/ManageTagsModal.tsx +63 -0
  78. package/src/blocks/ManageTagsModal/index.ts +2 -0
  79. package/src/blocks/MessagesCard/MessagesCard.stories.tsx +14 -0
  80. package/src/blocks/MessagesCard/MessagesCard.test.tsx +9 -0
  81. package/src/blocks/MessagesCard/MessagesCard.tsx +59 -0
  82. package/src/blocks/MessagesCard/index.ts +2 -0
  83. package/src/blocks/MetricCard/MetricCard.stories.tsx +14 -0
  84. package/src/blocks/MetricCard/MetricCard.test.tsx +9 -0
  85. package/src/blocks/MetricCard/MetricCard.tsx +79 -0
  86. package/src/blocks/MetricCard/index.ts +2 -0
  87. package/src/blocks/NotificationSettings/NotificationSettings.stories.tsx +14 -0
  88. package/src/blocks/NotificationSettings/NotificationSettings.test.tsx +9 -0
  89. package/src/blocks/NotificationSettings/NotificationSettings.tsx +61 -0
  90. package/src/blocks/NotificationSettings/index.ts +2 -0
  91. package/src/blocks/OrganizationMenu/OrganizationMenu.stories.tsx +14 -0
  92. package/src/blocks/OrganizationMenu/OrganizationMenu.test.tsx +9 -0
  93. package/src/blocks/OrganizationMenu/OrganizationMenu.tsx +66 -0
  94. package/src/blocks/OrganizationMenu/index.ts +2 -0
  95. package/src/blocks/RolesMenu/RolesMenu.stories.tsx +14 -0
  96. package/src/blocks/RolesMenu/RolesMenu.test.tsx +9 -0
  97. package/src/blocks/RolesMenu/RolesMenu.tsx +61 -0
  98. package/src/blocks/RolesMenu/index.ts +2 -0
  99. package/src/blocks/SelectUsersModal/SelectUsersModal.stories.tsx +14 -0
  100. package/src/blocks/SelectUsersModal/SelectUsersModal.test.tsx +9 -0
  101. package/src/blocks/SelectUsersModal/SelectUsersModal.tsx +64 -0
  102. package/src/blocks/SelectUsersModal/index.ts +2 -0
  103. package/src/blocks/SidebarLayoutDashboard/SidebarLayoutDashboard.stories.tsx +14 -0
  104. package/src/blocks/SidebarLayoutDashboard/SidebarLayoutDashboard.test.tsx +9 -0
  105. package/src/blocks/SidebarLayoutDashboard/SidebarLayoutDashboard.tsx +97 -0
  106. package/src/blocks/SidebarLayoutDashboard/index.ts +2 -0
  107. package/src/blocks/SidebarLayoutGroups/SidebarLayoutGroups.stories.tsx +14 -0
  108. package/src/blocks/SidebarLayoutGroups/SidebarLayoutGroups.test.tsx +9 -0
  109. package/src/blocks/SidebarLayoutGroups/SidebarLayoutGroups.tsx +79 -0
  110. package/src/blocks/SidebarLayoutGroups/index.ts +2 -0
  111. package/src/blocks/SidebarLayoutMinimal/SidebarLayoutMinimal.stories.tsx +14 -0
  112. package/src/blocks/SidebarLayoutMinimal/SidebarLayoutMinimal.test.tsx +9 -0
  113. package/src/blocks/SidebarLayoutMinimal/SidebarLayoutMinimal.tsx +73 -0
  114. package/src/blocks/SidebarLayoutMinimal/index.ts +2 -0
  115. package/src/blocks/SidebarLayoutSearch/SidebarLayoutSearch.stories.tsx +14 -0
  116. package/src/blocks/SidebarLayoutSearch/SidebarLayoutSearch.test.tsx +9 -0
  117. package/src/blocks/SidebarLayoutSearch/SidebarLayoutSearch.tsx +79 -0
  118. package/src/blocks/SidebarLayoutSearch/index.ts +2 -0
  119. package/src/blocks/SidebarLayoutUser/SidebarLayoutUser.stories.tsx +14 -0
  120. package/src/blocks/SidebarLayoutUser/SidebarLayoutUser.test.tsx +9 -0
  121. package/src/blocks/SidebarLayoutUser/SidebarLayoutUser.tsx +82 -0
  122. package/src/blocks/SidebarLayoutUser/index.ts +2 -0
  123. package/src/blocks/SortableTaskList/SortableTaskList.stories.tsx +14 -0
  124. package/src/blocks/SortableTaskList/SortableTaskList.test.tsx +9 -0
  125. package/src/blocks/SortableTaskList/SortableTaskList.tsx +70 -0
  126. package/src/blocks/SortableTaskList/index.ts +2 -0
  127. package/src/blocks/StackedLayoutBranded/StackedLayoutBranded.stories.tsx +14 -0
  128. package/src/blocks/StackedLayoutBranded/StackedLayoutBranded.test.tsx +9 -0
  129. package/src/blocks/StackedLayoutBranded/StackedLayoutBranded.tsx +64 -0
  130. package/src/blocks/StackedLayoutBranded/index.ts +2 -0
  131. package/src/blocks/StackedLayoutTabs/StackedLayoutTabs.stories.tsx +14 -0
  132. package/src/blocks/StackedLayoutTabs/StackedLayoutTabs.test.tsx +9 -0
  133. package/src/blocks/StackedLayoutTabs/StackedLayoutTabs.tsx +80 -0
  134. package/src/blocks/StackedLayoutTabs/index.ts +2 -0
  135. package/src/blocks/TaskCard/TaskCard.stories.tsx +14 -0
  136. package/src/blocks/TaskCard/TaskCard.test.tsx +9 -0
  137. package/src/blocks/TaskCard/TaskCard.tsx +88 -0
  138. package/src/blocks/TaskCard/index.ts +2 -0
  139. package/src/blocks/UserMenu/UserMenu.stories.tsx +14 -0
  140. package/src/blocks/UserMenu/UserMenu.test.tsx +9 -0
  141. package/src/blocks/UserMenu/UserMenu.tsx +62 -0
  142. package/src/blocks/UserMenu/index.ts +2 -0
  143. package/src/blocks/WorkspaceMembers/WorkspaceMembers.stories.tsx +14 -0
  144. package/src/blocks/WorkspaceMembers/WorkspaceMembers.test.tsx +9 -0
  145. package/src/blocks/WorkspaceMembers/WorkspaceMembers.tsx +61 -0
  146. package/src/blocks/WorkspaceMembers/index.ts +2 -0
  147. package/src/blocks/activity-heatmap.tsx +122 -0
  148. package/src/blocks/bar-list.tsx +75 -0
  149. package/src/blocks/chat-detail.tsx +73 -0
  150. package/src/blocks/feedback-modal.tsx +68 -0
  151. package/src/blocks/file-cards.tsx +56 -0
  152. package/src/blocks/files-list.tsx +72 -0
  153. package/src/blocks/integration-card.tsx +50 -0
  154. package/src/blocks/invite-modal.tsx +69 -0
  155. package/src/blocks/manage-tags-modal.tsx +63 -0
  156. package/src/blocks/messages-card.tsx +59 -0
  157. package/src/blocks/metric-card.tsx +79 -0
  158. package/src/blocks/notification-settings.tsx +61 -0
  159. package/src/blocks/organization-menu.tsx +85 -0
  160. package/src/blocks/roles-menu.tsx +61 -0
  161. package/src/blocks/select-users-modal.tsx +64 -0
  162. package/src/blocks/sidebar-layout-dashboard.tsx +97 -0
  163. package/src/blocks/sidebar-layout-groups.tsx +79 -0
  164. package/src/blocks/sidebar-layout-minimal.tsx +73 -0
  165. package/src/blocks/sidebar-layout-search.tsx +79 -0
  166. package/src/blocks/sidebar-layout-user.tsx +82 -0
  167. package/src/blocks/sortable-task-list.tsx +70 -0
  168. package/src/blocks/stacked-layout-branded.tsx +64 -0
  169. package/src/blocks/stacked-layout-tabs.tsx +80 -0
  170. package/src/blocks/task-card.tsx +88 -0
  171. package/src/blocks/user-menu.tsx +62 -0
  172. package/src/blocks/utils/cn.ts +1 -0
  173. package/src/blocks/workspace-members.tsx +61 -0
  174. package/src/charts/area-chart.tsx +83 -0
  175. package/src/charts/bar-chart.tsx +146 -0
  176. package/src/charts/heatmap.tsx +185 -0
  177. package/src/charts/line-chart.tsx +89 -0
  178. package/src/charts/sparkline.tsx +62 -0
  179. package/src/charts/utils/cn.ts +1 -0
  180. package/src/components/Banner/Banner.stories.tsx +14 -0
  181. package/src/components/Banner/Banner.test.tsx +9 -0
  182. package/src/components/Banner/Banner.tsx +203 -0
  183. package/src/components/Banner/index.ts +2 -0
  184. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +14 -0
  185. package/src/components/Breadcrumb/Breadcrumb.test.tsx +9 -0
  186. package/src/components/Breadcrumb/Breadcrumb.tsx +58 -0
  187. package/src/components/Breadcrumb/index.ts +2 -0
  188. package/src/components/CommandItem/CommandItem.stories.tsx +14 -0
  189. package/src/components/CommandItem/CommandItem.test.tsx +9 -0
  190. package/src/components/CommandItem/CommandItem.tsx +36 -0
  191. package/src/components/CommandItem/index.ts +2 -0
  192. package/src/components/DataGrid/DataGrid.stories.tsx +14 -0
  193. package/src/components/DataGrid/DataGrid.test.tsx +9 -0
  194. package/src/components/DataGrid/DataGrid.tsx +125 -0
  195. package/src/components/DataGrid/index.ts +2 -0
  196. package/src/components/DataTable/DataTable.stories.tsx +14 -0
  197. package/src/components/DataTable/DataTable.test.tsx +9 -0
  198. package/src/components/DataTable/DataTable.tsx +139 -0
  199. package/src/components/DataTable/index.ts +2 -0
  200. package/src/components/EmptyState/EmptyState.stories.tsx +14 -0
  201. package/src/components/EmptyState/EmptyState.test.tsx +9 -0
  202. package/src/components/EmptyState/EmptyState.tsx +106 -0
  203. package/src/components/EmptyState/index.ts +2 -0
  204. package/src/components/Hotkeys/Hotkeys.stories.tsx +14 -0
  205. package/src/components/Hotkeys/Hotkeys.test.tsx +9 -0
  206. package/src/components/Hotkeys/Hotkeys.tsx +31 -0
  207. package/src/components/Hotkeys/index.ts +2 -0
  208. package/src/components/IconBadge/IconBadge.stories.tsx +14 -0
  209. package/src/components/IconBadge/IconBadge.test.tsx +9 -0
  210. package/src/components/IconBadge/IconBadge.tsx +35 -0
  211. package/src/components/IconBadge/index.ts +2 -0
  212. package/src/components/LoadingOverlay/LoadingOverlay.stories.tsx +14 -0
  213. package/src/components/LoadingOverlay/LoadingOverlay.test.tsx +9 -0
  214. package/src/components/LoadingOverlay/LoadingOverlay.tsx +38 -0
  215. package/src/components/LoadingOverlay/index.ts +2 -0
  216. package/src/components/Persona/Persona.stories.tsx +14 -0
  217. package/src/components/Persona/Persona.test.tsx +9 -0
  218. package/src/components/Persona/Persona.tsx +38 -0
  219. package/src/components/Persona/index.ts +2 -0
  220. package/src/components/PropertyList/PropertyList.stories.tsx +14 -0
  221. package/src/components/PropertyList/PropertyList.test.tsx +9 -0
  222. package/src/components/PropertyList/PropertyList.tsx +29 -0
  223. package/src/components/PropertyList/index.ts +2 -0
  224. package/src/components/Stat/Stat.stories.tsx +14 -0
  225. package/src/components/Stat/Stat.test.tsx +9 -0
  226. package/src/components/Stat/Stat.tsx +51 -0
  227. package/src/components/Stat/index.ts +2 -0
  228. package/src/components/Stepper/Stepper.stories.tsx +14 -0
  229. package/src/components/Stepper/Stepper.test.tsx +9 -0
  230. package/src/components/Stepper/Stepper.tsx +58 -0
  231. package/src/components/Stepper/index.ts +2 -0
  232. package/src/components/StructuredList/StructuredList.stories.tsx +14 -0
  233. package/src/components/StructuredList/StructuredList.test.tsx +9 -0
  234. package/src/components/StructuredList/StructuredList.tsx +51 -0
  235. package/src/components/StructuredList/index.ts +2 -0
  236. package/src/components/Timeline/Timeline.stories.tsx +14 -0
  237. package/src/components/Timeline/Timeline.test.tsx +9 -0
  238. package/src/components/Timeline/Timeline.tsx +69 -0
  239. package/src/components/Timeline/index.ts +2 -0
  240. package/src/components/Toaster/Toaster.stories.tsx +14 -0
  241. package/src/components/Toaster/Toaster.test.tsx +9 -0
  242. package/src/components/Toaster/Toaster.tsx +42 -0
  243. package/src/components/Toaster/index.ts +2 -0
  244. package/src/components/banner.tsx +203 -0
  245. package/src/components/breadcrumb.tsx +58 -0
  246. package/src/components/command-item.tsx +36 -0
  247. package/src/components/data-grid.tsx +125 -0
  248. package/src/components/data-table.tsx +139 -0
  249. package/src/components/empty-state.tsx +31 -0
  250. package/src/components/hotkeys.tsx +31 -0
  251. package/src/components/icon-badge.tsx +35 -0
  252. package/src/components/loading-overlay.tsx +38 -0
  253. package/src/components/persona.tsx +38 -0
  254. package/src/components/property-list.tsx +29 -0
  255. package/src/components/stat.tsx +51 -0
  256. package/src/components/stepper.tsx +69 -0
  257. package/src/components/structured-list.tsx +51 -0
  258. package/src/components/timeline.tsx +69 -0
  259. package/src/components/toaster.tsx +42 -0
  260. package/src/components/utils/cn.ts +1 -0
  261. package/src/data/ActionBar.tsx +115 -0
  262. package/src/data/DataGrid.tsx +453 -0
  263. package/src/data/DataList.tsx +79 -0
  264. package/src/data/EventCalendar.tsx +794 -0
  265. package/src/data/Filters.tsx +271 -0
  266. package/src/data/GridList.tsx +67 -0
  267. package/src/data/ImageCrop.tsx +198 -0
  268. package/src/data/SortableList.tsx +258 -0
  269. package/src/data/Tree.tsx +510 -0
  270. package/src/data/VideoPlayer.tsx +564 -0
  271. package/src/data/index.ts +6 -0
  272. package/src/engagement/Beacon/Beacon.stories.tsx +14 -0
  273. package/src/engagement/Beacon/Beacon.test.tsx +9 -0
  274. package/src/engagement/Beacon/Beacon.tsx +50 -0
  275. package/src/engagement/Beacon/index.ts +2 -0
  276. package/src/engagement/Tour/Tour.stories.tsx +14 -0
  277. package/src/engagement/Tour/Tour.test.tsx +9 -0
  278. package/src/engagement/Tour/Tour.tsx +106 -0
  279. package/src/engagement/Tour/index.ts +2 -0
  280. package/src/engagement/beacon.tsx +50 -0
  281. package/src/engagement/checklist.tsx +259 -0
  282. package/src/engagement/intro-disclosure.tsx +210 -0
  283. package/src/engagement/tour.tsx +106 -0
  284. package/src/engagement/utils/cn.ts +1 -0
  285. package/src/feedback/Toaster.tsx +141 -0
  286. package/src/feedback/index.ts +2 -0
  287. package/src/feedback/toast-emitter.ts +74 -0
  288. package/src/forms/ArrayField/ArrayField.stories.tsx +14 -0
  289. package/src/forms/ArrayField/ArrayField.test.tsx +9 -0
  290. package/src/forms/ArrayField/ArrayField.tsx +61 -0
  291. package/src/forms/ArrayField/index.ts +2 -0
  292. package/src/forms/AutoForm/AutoForm.stories.tsx +14 -0
  293. package/src/forms/AutoForm/AutoForm.test.tsx +9 -0
  294. package/src/forms/AutoForm/AutoForm.tsx +133 -0
  295. package/src/forms/AutoForm/index.ts +2 -0
  296. package/src/forms/DatePicker/DatePicker.stories.tsx +14 -0
  297. package/src/forms/DatePicker/DatePicker.test.tsx +9 -0
  298. package/src/forms/DatePicker/DatePicker.tsx +109 -0
  299. package/src/forms/DatePicker/index.ts +2 -0
  300. package/src/forms/Field/Field.stories.tsx +14 -0
  301. package/src/forms/Field/Field.test.tsx +9 -0
  302. package/src/forms/Field/Field.tsx +62 -0
  303. package/src/forms/Field/index.ts +2 -0
  304. package/src/forms/FileUpload/FileUpload.stories.tsx +14 -0
  305. package/src/forms/FileUpload/FileUpload.test.tsx +9 -0
  306. package/src/forms/FileUpload/FileUpload.tsx +101 -0
  307. package/src/forms/FileUpload/index.ts +2 -0
  308. package/src/forms/Form/Form.stories.tsx +14 -0
  309. package/src/forms/Form/Form.test.tsx +9 -0
  310. package/src/forms/Form/Form.tsx +57 -0
  311. package/src/forms/Form/index.ts +2 -0
  312. package/src/forms/ObjectField/ObjectField.stories.tsx +14 -0
  313. package/src/forms/ObjectField/ObjectField.test.tsx +9 -0
  314. package/src/forms/ObjectField/ObjectField.tsx +53 -0
  315. package/src/forms/ObjectField/index.ts +2 -0
  316. package/src/forms/PasswordInput/PasswordInput.stories.tsx +14 -0
  317. package/src/forms/PasswordInput/PasswordInput.test.tsx +9 -0
  318. package/src/forms/PasswordInput/PasswordInput.tsx +54 -0
  319. package/src/forms/PasswordInput/index.ts +2 -0
  320. package/src/forms/SearchInput/SearchInput.stories.tsx +14 -0
  321. package/src/forms/SearchInput/SearchInput.test.tsx +9 -0
  322. package/src/forms/SearchInput/SearchInput.tsx +64 -0
  323. package/src/forms/SearchInput/index.ts +2 -0
  324. package/src/forms/StepForm/StepForm.stories.tsx +14 -0
  325. package/src/forms/StepForm/StepForm.test.tsx +9 -0
  326. package/src/forms/StepForm/StepForm.tsx +113 -0
  327. package/src/forms/StepForm/index.ts +2 -0
  328. package/src/forms/array-field.tsx +61 -0
  329. package/src/forms/auto-form.tsx +133 -0
  330. package/src/forms/color-picker.tsx +258 -0
  331. package/src/forms/combobox.tsx +106 -0
  332. package/src/forms/date-picker.tsx +109 -0
  333. package/src/forms/field.tsx +62 -0
  334. package/src/forms/file-upload.tsx +101 -0
  335. package/src/forms/form.tsx +57 -0
  336. package/src/forms/object-field.tsx +53 -0
  337. package/src/forms/password-input.tsx +54 -0
  338. package/src/forms/search-input.tsx +64 -0
  339. package/src/forms/step-form.tsx +113 -0
  340. package/src/forms/tags.tsx +270 -0
  341. package/src/forms/utils/cn.ts +1 -0
  342. package/src/headers/ScrollHeader.tsx +110 -0
  343. package/src/headers/index.ts +1 -0
  344. package/src/index.ts +171 -0
  345. package/src/layout/PageContainer.tsx +40 -0
  346. package/src/layout/PageLayout.tsx +55 -0
  347. package/src/layout/index.ts +2 -0
  348. package/src/layouts/AppShell/AppShell.stories.tsx +14 -0
  349. package/src/layouts/AppShell/AppShell.test.tsx +9 -0
  350. package/src/layouts/AppShell/AppShell.tsx +129 -0
  351. package/src/layouts/AppShell/index.ts +2 -0
  352. package/src/layouts/Container/Container.stories.tsx +14 -0
  353. package/src/layouts/Container/Container.test.tsx +9 -0
  354. package/src/layouts/Container/Container.tsx +33 -0
  355. package/src/layouts/Container/index.ts +2 -0
  356. package/src/layouts/Navbar/Navbar.stories.tsx +14 -0
  357. package/src/layouts/Navbar/Navbar.test.tsx +9 -0
  358. package/src/layouts/Navbar/Navbar.tsx +60 -0
  359. package/src/layouts/Navbar/index.ts +2 -0
  360. package/src/layouts/Page/Page.stories.tsx +14 -0
  361. package/src/layouts/Page/Page.test.tsx +9 -0
  362. package/src/layouts/Page/Page.tsx +61 -0
  363. package/src/layouts/Page/index.ts +2 -0
  364. package/src/layouts/ResizeBox/ResizeBox.stories.tsx +14 -0
  365. package/src/layouts/ResizeBox/ResizeBox.test.tsx +9 -0
  366. package/src/layouts/ResizeBox/ResizeBox.tsx +45 -0
  367. package/src/layouts/ResizeBox/index.ts +2 -0
  368. package/src/layouts/Sidebar/Sidebar.stories.tsx +14 -0
  369. package/src/layouts/Sidebar/Sidebar.test.tsx +9 -0
  370. package/src/layouts/Sidebar/Sidebar.tsx +99 -0
  371. package/src/layouts/Sidebar/index.ts +2 -0
  372. package/src/layouts/SplitPage/SplitPage.stories.tsx +14 -0
  373. package/src/layouts/SplitPage/SplitPage.test.tsx +9 -0
  374. package/src/layouts/SplitPage/SplitPage.tsx +32 -0
  375. package/src/layouts/SplitPage/index.ts +2 -0
  376. package/src/layouts/Stack/Stack.stories.tsx +14 -0
  377. package/src/layouts/Stack/Stack.test.tsx +9 -0
  378. package/src/layouts/Stack/Stack.tsx +56 -0
  379. package/src/layouts/Stack/index.ts +2 -0
  380. package/src/layouts/app-shell.tsx +129 -0
  381. package/src/layouts/container.tsx +33 -0
  382. package/src/layouts/navbar.tsx +60 -0
  383. package/src/layouts/page.tsx +61 -0
  384. package/src/layouts/resize-box.tsx +45 -0
  385. package/src/layouts/sidebar.tsx +99 -0
  386. package/src/layouts/split-page.tsx +32 -0
  387. package/src/layouts/stack.tsx +56 -0
  388. package/src/layouts/utils/cn.ts +1 -0
  389. package/src/motion/FadeIn.tsx +33 -0
  390. package/src/motion/Morph.tsx +17 -0
  391. package/src/motion/Parallax.tsx +37 -0
  392. package/src/motion/Reveal.tsx +32 -0
  393. package/src/motion/Ripple.tsx +78 -0
  394. package/src/motion/ScaleIn.tsx +33 -0
  395. package/src/motion/SlideIn.tsx +42 -0
  396. package/src/motion/Stagger.tsx +48 -0
  397. package/src/motion/Transition.tsx +13 -0
  398. package/src/motion/index.ts +13 -0
  399. package/src/motion/presets.ts +33 -0
  400. package/src/motion/useLayoutId.ts +6 -0
  401. package/src/nav/CommandPalette.tsx +197 -0
  402. package/src/nav/HotkeysProvider.tsx +223 -0
  403. package/src/nav/index.ts +2 -0
  404. package/src/primitives/Accordion/Accordion.stories.tsx +14 -0
  405. package/src/primitives/Accordion/Accordion.test.tsx +9 -0
  406. package/src/primitives/Accordion/Accordion.tsx +56 -0
  407. package/src/primitives/Accordion/index.ts +2 -0
  408. package/src/primitives/Alert/Alert.stories.tsx +14 -0
  409. package/src/primitives/Alert/Alert.test.tsx +9 -0
  410. package/src/primitives/Alert/Alert.tsx +60 -0
  411. package/src/primitives/Alert/index.ts +2 -0
  412. package/src/primitives/AlertDialog/AlertDialog.stories.tsx +14 -0
  413. package/src/primitives/AlertDialog/AlertDialog.test.tsx +9 -0
  414. package/src/primitives/AlertDialog/AlertDialog.tsx +140 -0
  415. package/src/primitives/AlertDialog/index.ts +2 -0
  416. package/src/primitives/AspectRatio/AspectRatio.stories.tsx +14 -0
  417. package/src/primitives/AspectRatio/AspectRatio.test.tsx +9 -0
  418. package/src/primitives/AspectRatio/AspectRatio.tsx +6 -0
  419. package/src/primitives/AspectRatio/index.ts +2 -0
  420. package/src/primitives/Avatar/Avatar.stories.tsx +14 -0
  421. package/src/primitives/Avatar/Avatar.test.tsx +9 -0
  422. package/src/primitives/Avatar/Avatar.tsx +35 -0
  423. package/src/primitives/Avatar/index.ts +2 -0
  424. package/src/primitives/Badge/Badge.stories.tsx +14 -0
  425. package/src/primitives/Badge/Badge.test.tsx +9 -0
  426. package/src/primitives/Badge/Badge.tsx +28 -0
  427. package/src/primitives/Badge/index.ts +2 -0
  428. package/src/primitives/Button/Button.stories.tsx +14 -0
  429. package/src/primitives/Button/Button.test.tsx +9 -0
  430. package/src/primitives/Button/Button.tsx +113 -0
  431. package/src/primitives/Button/index.ts +2 -0
  432. package/src/primitives/Calendar/Calendar.stories.tsx +14 -0
  433. package/src/primitives/Calendar/Calendar.test.tsx +9 -0
  434. package/src/primitives/Calendar/Calendar.tsx +214 -0
  435. package/src/primitives/Calendar/index.ts +2 -0
  436. package/src/primitives/Card/Card.stories.tsx +14 -0
  437. package/src/primitives/Card/Card.test.tsx +9 -0
  438. package/src/primitives/Card/Card.tsx +48 -0
  439. package/src/primitives/Card/TextureCard.tsx +93 -0
  440. package/src/primitives/Card/index.ts +3 -0
  441. package/src/primitives/Carousel/Carousel.stories.tsx +14 -0
  442. package/src/primitives/Carousel/Carousel.test.tsx +9 -0
  443. package/src/primitives/Carousel/Carousel.tsx +261 -0
  444. package/src/primitives/Carousel/index.ts +2 -0
  445. package/src/primitives/Chart/Chart.stories.tsx +14 -0
  446. package/src/primitives/Chart/Chart.test.tsx +9 -0
  447. package/src/primitives/Chart/Chart.tsx +368 -0
  448. package/src/primitives/Chart/index.ts +2 -0
  449. package/src/primitives/Checkbox/Checkbox.stories.tsx +14 -0
  450. package/src/primitives/Checkbox/Checkbox.test.tsx +9 -0
  451. package/src/primitives/Checkbox/Checkbox.tsx +27 -0
  452. package/src/primitives/Checkbox/index.ts +2 -0
  453. package/src/primitives/Collapsible/Collapsible.stories.tsx +14 -0
  454. package/src/primitives/Collapsible/Collapsible.test.tsx +9 -0
  455. package/src/primitives/Collapsible/Collapsible.tsx +12 -0
  456. package/src/primitives/Collapsible/index.ts +2 -0
  457. package/src/primitives/Command/Command.stories.tsx +14 -0
  458. package/src/primitives/Command/Command.test.tsx +9 -0
  459. package/src/primitives/Command/Command.tsx +154 -0
  460. package/src/primitives/Command/index.ts +2 -0
  461. package/src/primitives/ContextMenu/ContextMenu.stories.tsx +14 -0
  462. package/src/primitives/ContextMenu/ContextMenu.test.tsx +9 -0
  463. package/src/primitives/ContextMenu/ContextMenu.tsx +101 -0
  464. package/src/primitives/ContextMenu/index.ts +2 -0
  465. package/src/primitives/Dialog/Dialog.stories.tsx +14 -0
  466. package/src/primitives/Dialog/Dialog.test.tsx +9 -0
  467. package/src/primitives/Dialog/Dialog.tsx +88 -0
  468. package/src/primitives/Dialog/DialogProvider.tsx +130 -0
  469. package/src/primitives/Dialog/index.ts +3 -0
  470. package/src/primitives/Drawer/Drawer.stories.tsx +14 -0
  471. package/src/primitives/Drawer/Drawer.test.tsx +9 -0
  472. package/src/primitives/Drawer/Drawer.tsx +123 -0
  473. package/src/primitives/Drawer/index.ts +2 -0
  474. package/src/primitives/DropdownMenu/DropdownMenu.stories.tsx +14 -0
  475. package/src/primitives/DropdownMenu/DropdownMenu.test.tsx +9 -0
  476. package/src/primitives/DropdownMenu/DropdownMenu.tsx +111 -0
  477. package/src/primitives/DropdownMenu/index.ts +2 -0
  478. package/src/primitives/Form/Form.stories.tsx +14 -0
  479. package/src/primitives/Form/Form.test.tsx +9 -0
  480. package/src/primitives/Form/Form.tsx +179 -0
  481. package/src/primitives/Form/index.ts +2 -0
  482. package/src/primitives/HoverCard/HoverCard.stories.tsx +14 -0
  483. package/src/primitives/HoverCard/HoverCard.test.tsx +9 -0
  484. package/src/primitives/HoverCard/HoverCard.tsx +31 -0
  485. package/src/primitives/HoverCard/index.ts +2 -0
  486. package/src/primitives/Input/Input.stories.tsx +14 -0
  487. package/src/primitives/Input/Input.test.tsx +9 -0
  488. package/src/primitives/Input/Input.tsx +110 -0
  489. package/src/primitives/Input/index.ts +2 -0
  490. package/src/primitives/InputOtp/InputOtp.stories.tsx +14 -0
  491. package/src/primitives/InputOtp/InputOtp.test.tsx +9 -0
  492. package/src/primitives/InputOtp/InputOtp.tsx +70 -0
  493. package/src/primitives/InputOtp/index.ts +2 -0
  494. package/src/primitives/Label/Label.stories.tsx +14 -0
  495. package/src/primitives/Label/Label.test.tsx +9 -0
  496. package/src/primitives/Label/Label.tsx +27 -0
  497. package/src/primitives/Label/index.ts +2 -0
  498. package/src/primitives/Menubar/Menubar.stories.tsx +14 -0
  499. package/src/primitives/Menubar/Menubar.test.tsx +9 -0
  500. package/src/primitives/Menubar/Menubar.tsx +255 -0
  501. package/src/primitives/Menubar/index.ts +2 -0
  502. package/src/primitives/NavigationMenu/NavigationMenu.stories.tsx +14 -0
  503. package/src/primitives/NavigationMenu/NavigationMenu.test.tsx +9 -0
  504. package/src/primitives/NavigationMenu/NavigationMenu.tsx +129 -0
  505. package/src/primitives/NavigationMenu/index.ts +2 -0
  506. package/src/primitives/Pagination/Pagination.stories.tsx +14 -0
  507. package/src/primitives/Pagination/Pagination.test.tsx +9 -0
  508. package/src/primitives/Pagination/Pagination.tsx +118 -0
  509. package/src/primitives/Pagination/index.ts +2 -0
  510. package/src/primitives/Popover/Popover.stories.tsx +14 -0
  511. package/src/primitives/Popover/Popover.test.tsx +9 -0
  512. package/src/primitives/Popover/Popover.tsx +30 -0
  513. package/src/primitives/Popover/index.ts +2 -0
  514. package/src/primitives/Progress/Progress.stories.tsx +14 -0
  515. package/src/primitives/Progress/Progress.test.tsx +9 -0
  516. package/src/primitives/Progress/Progress.tsx +20 -0
  517. package/src/primitives/Progress/index.ts +2 -0
  518. package/src/primitives/Radio/Radio.stories.tsx +14 -0
  519. package/src/primitives/Radio/Radio.test.tsx +9 -0
  520. package/src/primitives/Radio/Radio.tsx +33 -0
  521. package/src/primitives/Radio/index.ts +2 -0
  522. package/src/primitives/Resizable/Resizable.stories.tsx +14 -0
  523. package/src/primitives/Resizable/Resizable.test.tsx +9 -0
  524. package/src/primitives/Resizable/Resizable.tsx +44 -0
  525. package/src/primitives/Resizable/index.ts +2 -0
  526. package/src/primitives/ScrollArea/ScrollArea.stories.tsx +14 -0
  527. package/src/primitives/ScrollArea/ScrollArea.test.tsx +9 -0
  528. package/src/primitives/ScrollArea/ScrollArea.tsx +49 -0
  529. package/src/primitives/ScrollArea/index.ts +2 -0
  530. package/src/primitives/Select/Select.stories.tsx +14 -0
  531. package/src/primitives/Select/Select.test.tsx +9 -0
  532. package/src/primitives/Select/Select.tsx +140 -0
  533. package/src/primitives/Select/index.ts +2 -0
  534. package/src/primitives/Separator/Separator.stories.tsx +14 -0
  535. package/src/primitives/Separator/Separator.test.tsx +9 -0
  536. package/src/primitives/Separator/Separator.tsx +21 -0
  537. package/src/primitives/Separator/index.ts +2 -0
  538. package/src/primitives/Sheet/Sheet.stories.tsx +14 -0
  539. package/src/primitives/Sheet/Sheet.test.tsx +9 -0
  540. package/src/primitives/Sheet/Sheet.tsx +88 -0
  541. package/src/primitives/Sheet/index.ts +2 -0
  542. package/src/primitives/Skeleton/Skeleton.stories.tsx +14 -0
  543. package/src/primitives/Skeleton/Skeleton.test.tsx +9 -0
  544. package/src/primitives/Skeleton/Skeleton.tsx +71 -0
  545. package/src/primitives/Skeleton/index.ts +2 -0
  546. package/src/primitives/Slider/Slider.stories.tsx +14 -0
  547. package/src/primitives/Slider/Slider.test.tsx +9 -0
  548. package/src/primitives/Slider/Slider.tsx +20 -0
  549. package/src/primitives/Slider/index.ts +2 -0
  550. package/src/primitives/Sonner/Sonner.stories.tsx +14 -0
  551. package/src/primitives/Sonner/Sonner.test.tsx +9 -0
  552. package/src/primitives/Sonner/Sonner.tsx +30 -0
  553. package/src/primitives/Sonner/index.ts +2 -0
  554. package/src/primitives/Spinner/Spinner.stories.tsx +14 -0
  555. package/src/primitives/Spinner/Spinner.test.tsx +9 -0
  556. package/src/primitives/Spinner/Spinner.tsx +17 -0
  557. package/src/primitives/Spinner/index.ts +2 -0
  558. package/src/primitives/Switch/Switch.stories.tsx +14 -0
  559. package/src/primitives/Switch/Switch.test.tsx +9 -0
  560. package/src/primitives/Switch/Switch.tsx +31 -0
  561. package/src/primitives/Switch/index.ts +2 -0
  562. package/src/primitives/Table/Table.stories.tsx +14 -0
  563. package/src/primitives/Table/Table.test.tsx +9 -0
  564. package/src/primitives/Table/Table.tsx +121 -0
  565. package/src/primitives/Table/index.ts +2 -0
  566. package/src/primitives/Tabs/Tabs.stories.tsx +14 -0
  567. package/src/primitives/Tabs/Tabs.test.tsx +9 -0
  568. package/src/primitives/Tabs/Tabs.tsx +53 -0
  569. package/src/primitives/Tabs/index.ts +2 -0
  570. package/src/primitives/Textarea/Textarea.stories.tsx +14 -0
  571. package/src/primitives/Textarea/Textarea.test.tsx +9 -0
  572. package/src/primitives/Textarea/Textarea.tsx +22 -0
  573. package/src/primitives/Textarea/index.ts +2 -0
  574. package/src/primitives/Toggle/Toggle.stories.tsx +14 -0
  575. package/src/primitives/Toggle/Toggle.test.tsx +9 -0
  576. package/src/primitives/Toggle/Toggle.tsx +44 -0
  577. package/src/primitives/Toggle/index.ts +2 -0
  578. package/src/primitives/ToggleGroup/ToggleGroup.stories.tsx +14 -0
  579. package/src/primitives/ToggleGroup/ToggleGroup.test.tsx +9 -0
  580. package/src/primitives/ToggleGroup/ToggleGroup.tsx +62 -0
  581. package/src/primitives/ToggleGroup/index.ts +2 -0
  582. package/src/primitives/Tooltip/Tooltip.stories.tsx +14 -0
  583. package/src/primitives/Tooltip/Tooltip.test.tsx +9 -0
  584. package/src/primitives/Tooltip/Tooltip.tsx +29 -0
  585. package/src/primitives/Tooltip/index.ts +2 -0
  586. package/src/primitives/accordion.tsx +56 -0
  587. package/src/primitives/alert-dialog.tsx +140 -0
  588. package/src/primitives/alert.tsx +60 -0
  589. package/src/primitives/aspect-ratio.tsx +6 -0
  590. package/src/primitives/avatar.tsx +100 -0
  591. package/src/primitives/badge.tsx +28 -0
  592. package/src/primitives/button.tsx +43 -0
  593. package/src/primitives/calendar.tsx +214 -0
  594. package/src/primitives/card.tsx +48 -0
  595. package/src/primitives/carousel.tsx +261 -0
  596. package/src/primitives/chart.tsx +368 -0
  597. package/src/primitives/checkbox.tsx +26 -0
  598. package/src/primitives/code-block.tsx +173 -0
  599. package/src/primitives/collapsible.tsx +12 -0
  600. package/src/primitives/command.tsx +154 -0
  601. package/src/primitives/comparison.tsx +184 -0
  602. package/src/primitives/context-menu.tsx +101 -0
  603. package/src/primitives/dialog.tsx +88 -0
  604. package/src/primitives/drawer.tsx +117 -0
  605. package/src/primitives/dropdown-menu.tsx +111 -0
  606. package/src/primitives/form.tsx +179 -0
  607. package/src/primitives/hover-card.tsx +30 -0
  608. package/src/primitives/input-otp.tsx +70 -0
  609. package/src/primitives/input.tsx +23 -0
  610. package/src/primitives/label.tsx +27 -0
  611. package/src/primitives/logo-carousel.tsx +110 -0
  612. package/src/primitives/marquee.tsx +84 -0
  613. package/src/primitives/menubar.tsx +255 -0
  614. package/src/primitives/navigation-menu.tsx +129 -0
  615. package/src/primitives/pagination.tsx +118 -0
  616. package/src/primitives/popover.tsx +29 -0
  617. package/src/primitives/progress.tsx +20 -0
  618. package/src/primitives/qr-code.tsx +89 -0
  619. package/src/primitives/radio.tsx +33 -0
  620. package/src/primitives/rating.tsx +157 -0
  621. package/src/primitives/resizable.tsx +43 -0
  622. package/src/primitives/scroll-area.tsx +49 -0
  623. package/src/primitives/select.tsx +137 -0
  624. package/src/primitives/separator.tsx +21 -0
  625. package/src/primitives/sheet.tsx +87 -0
  626. package/src/primitives/skeleton.tsx +11 -0
  627. package/src/primitives/slider.tsx +20 -0
  628. package/src/primitives/sonner.tsx +27 -0
  629. package/src/primitives/spinner.tsx +17 -0
  630. package/src/primitives/switch.tsx +30 -0
  631. package/src/primitives/table.tsx +121 -0
  632. package/src/primitives/tabs.tsx +53 -0
  633. package/src/primitives/textarea.tsx +22 -0
  634. package/src/primitives/texture-card.tsx +52 -0
  635. package/src/primitives/toggle-group.tsx +62 -0
  636. package/src/primitives/toggle.tsx +44 -0
  637. package/src/primitives/tooltip.tsx +28 -0
  638. package/src/primitives/utils/cn.ts +1 -0
  639. package/src/provider/otf-ui-provider.tsx +66 -0
  640. package/src/styles.css +6 -0
  641. package/src/text/AnimatedNumber.tsx +157 -0
  642. package/src/text/Body.tsx +47 -0
  643. package/src/text/Caption.tsx +32 -0
  644. package/src/text/Code.tsx +43 -0
  645. package/src/text/GradientHeading.tsx +90 -0
  646. package/src/text/Heading.tsx +44 -0
  647. package/src/text/TextReveal.tsx +96 -0
  648. package/src/text/Typewriter.tsx +107 -0
  649. package/src/text/index.ts +8 -0
  650. package/src/theme/ColorSchemeSelect.tsx +74 -0
  651. package/src/theme/ThemeSwitch.tsx +116 -0
  652. package/src/theme/index.ts +2 -0
  653. package/src/themes/base.css +25 -0
  654. package/src/themes/glass.css +142 -0
  655. package/src/themes/linear.css +123 -0
  656. package/src/themes/midnight.css +131 -0
  657. package/src/themes/minimal.css +121 -0
  658. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,824 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { ChevronDown, ChevronRight } from 'lucide-react'
5
+ import {
6
+ addDays,
7
+ addMonths,
8
+ addWeeks,
9
+ differenceInCalendarDays,
10
+ differenceInCalendarMonths,
11
+ differenceInCalendarWeeks,
12
+ endOfMonth,
13
+ endOfWeek,
14
+ format,
15
+ startOfDay,
16
+ startOfMonth,
17
+ startOfWeek,
18
+ } from 'date-fns'
19
+ import { cn } from '../utils/cn'
20
+
21
+ // ── Types ────────────────────────────────────────────────────────
22
+
23
+ export type GanttItem = {
24
+ id: string
25
+ title: string
26
+ start: Date
27
+ end: Date
28
+ /** 0..100. Default 0. */
29
+ progress?: number
30
+ /** CSS color (e.g. 'hsl(var(--chart-1))' or '#hex'). */
31
+ color?: string
32
+ /** ids of upstream items — render arrows */
33
+ dependsOn?: string[]
34
+ /** optional row grouping label */
35
+ group?: string
36
+ }
37
+
38
+ export type GanttScale = 'days' | 'weeks' | 'months'
39
+
40
+ export type GanttProps = Omit<
41
+ React.HTMLAttributes<HTMLDivElement>,
42
+ 'onMove'
43
+ > & {
44
+ items: GanttItem[]
45
+ scale?: GanttScale
46
+ startDate?: Date
47
+ endDate?: Date
48
+ rowHeight?: number
49
+ /** Omit = read-only. */
50
+ onItemMove?: (id: string, newStart: Date, newEnd: Date) => void
51
+ /** Omit = no resize. */
52
+ onItemResize?: (id: string, newEnd: Date) => void
53
+ onItemClick?: (item: GanttItem) => void
54
+ showDependencies?: boolean
55
+ showProgress?: boolean
56
+ showToday?: boolean
57
+ }
58
+
59
+ // ── Constants ────────────────────────────────────────────────────
60
+
61
+ const DEFAULT_COLUMN_WIDTH: Record<GanttScale, number> = {
62
+ days: 56,
63
+ weeks: 80,
64
+ months: 120,
65
+ }
66
+ const SIDEBAR_WIDTH = 280
67
+ const HEADER_HEIGHT = 44
68
+ const DEFAULT_ROW_HEIGHT = 40
69
+ const DEFAULT_COLOR = 'hsl(var(--chart-1))'
70
+ const EDGE_HIT_PX = 8
71
+ const LONG_PRESS_MS = 300
72
+
73
+ // ── Scale helpers ────────────────────────────────────────────────
74
+
75
+ function startOfScale(date: Date, scale: GanttScale): Date {
76
+ if (scale === 'days') return startOfDay(date)
77
+ if (scale === 'weeks') return startOfWeek(date, { weekStartsOn: 1 })
78
+ return startOfMonth(date)
79
+ }
80
+
81
+ function endOfScale(date: Date, scale: GanttScale): Date {
82
+ if (scale === 'days') return startOfDay(date)
83
+ if (scale === 'weeks') return endOfWeek(date, { weekStartsOn: 1 })
84
+ return endOfMonth(date)
85
+ }
86
+
87
+ function addUnits(date: Date, n: number, scale: GanttScale): Date {
88
+ if (scale === 'days') return addDays(date, n)
89
+ if (scale === 'weeks') return addWeeks(date, n)
90
+ return addMonths(date, n)
91
+ }
92
+
93
+ /** Float distance in scale-units between two dates (a -> b). */
94
+ function unitsBetween(a: Date, b: Date, scale: GanttScale): number {
95
+ if (scale === 'days') {
96
+ return (b.getTime() - a.getTime()) / 86_400_000
97
+ }
98
+ if (scale === 'weeks') {
99
+ return (b.getTime() - a.getTime()) / (7 * 86_400_000)
100
+ }
101
+ // Months: blend integer month delta + fractional day-in-month.
102
+ const wholeMonths = differenceInCalendarMonths(b, a)
103
+ const monthAnchor = addMonths(a, wholeMonths)
104
+ const monthLen =
105
+ (endOfMonth(monthAnchor).getTime() - startOfMonth(monthAnchor).getTime()) /
106
+ 86_400_000 +
107
+ 1
108
+ const remainderDays =
109
+ (b.getTime() - monthAnchor.getTime()) / 86_400_000
110
+ return wholeMonths + remainderDays / monthLen
111
+ }
112
+
113
+ /** Total integer columns spanning [viewportStart, viewportEnd]. */
114
+ function totalColumns(
115
+ viewportStart: Date,
116
+ viewportEnd: Date,
117
+ scale: GanttScale
118
+ ): number {
119
+ if (scale === 'days') {
120
+ return Math.max(1, differenceInCalendarDays(viewportEnd, viewportStart) + 1)
121
+ }
122
+ if (scale === 'weeks') {
123
+ return Math.max(1, differenceInCalendarWeeks(viewportEnd, viewportStart, { weekStartsOn: 1 }) + 1)
124
+ }
125
+ return Math.max(1, differenceInCalendarMonths(viewportEnd, viewportStart) + 1)
126
+ }
127
+
128
+ function formatColumnLabel(date: Date, scale: GanttScale): string {
129
+ if (scale === 'days') return format(date, 'EEE d')
130
+ if (scale === 'weeks') return `Week ${format(date, 'w')}`
131
+ return format(date, 'MMM yyyy')
132
+ }
133
+
134
+ function formatColumnSubLabel(date: Date, scale: GanttScale): string | null {
135
+ if (scale === 'days') return format(date, 'MMM')
136
+ if (scale === 'weeks') return format(date, 'MMM yyyy')
137
+ return null
138
+ }
139
+
140
+ function snapDate(date: Date, scale: GanttScale): Date {
141
+ return startOfScale(date, scale)
142
+ }
143
+
144
+ // ── Gantt ────────────────────────────────────────────────────────
145
+
146
+ export const Gantt = React.forwardRef<HTMLDivElement, GanttProps>(
147
+ (
148
+ {
149
+ items,
150
+ scale = 'days',
151
+ startDate,
152
+ endDate,
153
+ rowHeight = DEFAULT_ROW_HEIGHT,
154
+ onItemMove,
155
+ onItemResize,
156
+ onItemClick,
157
+ showDependencies = true,
158
+ showProgress = true,
159
+ showToday = true,
160
+ className,
161
+ ...props
162
+ },
163
+ ref
164
+ ) => {
165
+ const interactive = !!onItemMove || !!onItemResize
166
+ const columnWidth = DEFAULT_COLUMN_WIDTH[scale]
167
+
168
+ // ── Viewport range ──────────────────────────────────────────
169
+ const viewport = React.useMemo(() => {
170
+ const earliest =
171
+ startDate ??
172
+ (items.length
173
+ ? new Date(Math.min(...items.map((i) => i.start.getTime())))
174
+ : new Date())
175
+ const latest =
176
+ endDate ??
177
+ (items.length
178
+ ? new Date(Math.max(...items.map((i) => i.end.getTime())))
179
+ : addDays(new Date(), 14))
180
+ return {
181
+ start: startOfScale(earliest, scale),
182
+ end: endOfScale(latest, scale),
183
+ }
184
+ }, [items, startDate, endDate, scale])
185
+
186
+ const colCount = totalColumns(viewport.start, viewport.end, scale)
187
+ const timelineWidth = colCount * columnWidth
188
+
189
+ // ── Group rows ──────────────────────────────────────────────
190
+ const grouped = React.useMemo(() => {
191
+ const map = new Map<string | undefined, GanttItem[]>()
192
+ for (const it of items) {
193
+ const k = it.group ?? undefined
194
+ if (!map.has(k)) map.set(k, [])
195
+ map.get(k)!.push(it)
196
+ }
197
+ return Array.from(map.entries()) // preserves insertion order
198
+ }, [items])
199
+
200
+ const hasGroups = grouped.some(([k]) => k !== undefined)
201
+
202
+ const [collapsed, setCollapsed] = React.useState<Set<string>>(() => new Set())
203
+ const toggleGroup = (key: string) => {
204
+ setCollapsed((prev) => {
205
+ const next = new Set(prev)
206
+ if (next.has(key)) next.delete(key)
207
+ else next.add(key)
208
+ return next
209
+ })
210
+ }
211
+
212
+ // Build flat row layout: each row is a header or an item.
213
+ type Row =
214
+ | { kind: 'group'; key: string; count: number }
215
+ | { kind: 'item'; item: GanttItem }
216
+ const rows = React.useMemo<Row[]>(() => {
217
+ const out: Row[] = []
218
+ for (const [groupKey, list] of grouped) {
219
+ if (groupKey !== undefined) {
220
+ out.push({ kind: 'group', key: groupKey, count: list.length })
221
+ if (collapsed.has(groupKey)) continue
222
+ }
223
+ for (const it of list) out.push({ kind: 'item', item: it })
224
+ }
225
+ return out
226
+ }, [grouped, collapsed])
227
+
228
+ // ── Item position lookup (for dependency arrows) ────────────
229
+ const itemRowIndex = React.useMemo(() => {
230
+ const m = new Map<string, number>()
231
+ rows.forEach((r, i) => {
232
+ if (r.kind === 'item') m.set(r.item.id, i)
233
+ })
234
+ return m
235
+ }, [rows])
236
+
237
+ // ── Today line ──────────────────────────────────────────────
238
+ const todayOffsetPx = React.useMemo(() => {
239
+ if (!showToday) return null
240
+ const now = new Date()
241
+ const u = unitsBetween(viewport.start, now, scale)
242
+ if (u < 0 || u > colCount) return null
243
+ return u * columnWidth
244
+ }, [showToday, viewport.start, scale, colCount, columnWidth])
245
+
246
+ // ── Column header cells ─────────────────────────────────────
247
+ const columnDates = React.useMemo(() => {
248
+ return Array.from({ length: colCount }, (_, i) =>
249
+ addUnits(viewport.start, i, scale)
250
+ )
251
+ }, [viewport.start, colCount, scale])
252
+
253
+ return (
254
+ <div
255
+ ref={ref}
256
+ data-slot="gantt"
257
+ className={cn(
258
+ 'flex w-full overflow-hidden rounded-lg border border-border bg-card text-foreground',
259
+ className
260
+ )}
261
+ {...props}
262
+ >
263
+ {/* ── LEFT: sidebar (sticky-ish via flex layout) ─────── */}
264
+ <div
265
+ data-slot="gantt-sidebar"
266
+ className="flex shrink-0 flex-col border-r border-border bg-card"
267
+ style={{ width: SIDEBAR_WIDTH }}
268
+ >
269
+ <div
270
+ className="flex items-center border-b border-border px-3 text-[11px] font-medium uppercase tracking-wide text-muted-foreground"
271
+ style={{ height: HEADER_HEIGHT }}
272
+ >
273
+ Task
274
+ </div>
275
+ <div className="flex flex-col">
276
+ {rows.map((row, idx) => {
277
+ if (row.kind === 'group') {
278
+ const isCollapsed = collapsed.has(row.key)
279
+ return (
280
+ <button
281
+ key={`g:${row.key}`}
282
+ type="button"
283
+ onClick={() => toggleGroup(row.key)}
284
+ aria-expanded={!isCollapsed}
285
+ className="flex items-center gap-1.5 border-b border-border bg-muted/40 px-3 text-left text-xs font-semibold text-foreground hover:bg-muted/60"
286
+ style={{ height: rowHeight }}
287
+ >
288
+ {isCollapsed ? (
289
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
290
+ ) : (
291
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
292
+ )}
293
+ <span className="truncate">{row.key}</span>
294
+ <span className="ml-auto text-[10px] font-normal tabular-nums text-muted-foreground">
295
+ {row.count}
296
+ </span>
297
+ </button>
298
+ )
299
+ }
300
+ return (
301
+ <div
302
+ key={`i:${row.item.id}`}
303
+ className={cn(
304
+ 'flex items-center gap-2 border-b border-border px-3 text-xs text-foreground',
305
+ hasGroups && 'pl-6'
306
+ )}
307
+ style={{ height: rowHeight }}
308
+ title={row.item.title}
309
+ >
310
+ <span
311
+ aria-hidden="true"
312
+ className="h-2 w-2 shrink-0 rounded-full"
313
+ style={{ backgroundColor: row.item.color ?? DEFAULT_COLOR }}
314
+ />
315
+ <span className="truncate font-medium">{row.item.title}</span>
316
+ </div>
317
+ )
318
+ })}
319
+ </div>
320
+ </div>
321
+
322
+ {/* ── RIGHT: timeline ─────────────────────────────────── */}
323
+ <div
324
+ data-slot="gantt-timeline"
325
+ className="relative flex-1 overflow-x-auto overflow-y-hidden"
326
+ >
327
+ <div style={{ width: timelineWidth, minWidth: '100%' }}>
328
+ {/* Header */}
329
+ <div
330
+ data-slot="gantt-timeline-header"
331
+ className="grid border-b border-border bg-card"
332
+ style={{
333
+ gridTemplateColumns: `repeat(${colCount}, ${columnWidth}px)`,
334
+ height: HEADER_HEIGHT,
335
+ }}
336
+ >
337
+ {columnDates.map((d, i) => {
338
+ const sub = formatColumnSubLabel(d, scale)
339
+ const isMonthBoundary =
340
+ scale === 'days' && d.getDate() === 1
341
+ return (
342
+ <div
343
+ key={i}
344
+ className={cn(
345
+ 'flex flex-col items-center justify-center border-r border-border text-[11px]',
346
+ isMonthBoundary && 'bg-muted/30'
347
+ )}
348
+ >
349
+ <span className="font-medium tabular-nums text-foreground">
350
+ {formatColumnLabel(d, scale)}
351
+ </span>
352
+ {sub && (
353
+ <span className="text-[10px] uppercase tracking-wide text-muted-foreground">
354
+ {sub}
355
+ </span>
356
+ )}
357
+ </div>
358
+ )
359
+ })}
360
+ </div>
361
+
362
+ {/* Body */}
363
+ <div
364
+ role="grid"
365
+ aria-label="Gantt timeline"
366
+ data-slot="gantt-timeline-body"
367
+ className="relative"
368
+ style={{ height: rows.length * rowHeight }}
369
+ >
370
+ {/* Vertical column rules */}
371
+ <div
372
+ className="pointer-events-none absolute inset-0 grid"
373
+ style={{
374
+ gridTemplateColumns: `repeat(${colCount}, ${columnWidth}px)`,
375
+ }}
376
+ aria-hidden="true"
377
+ >
378
+ {Array.from({ length: colCount }, (_, i) => (
379
+ <div key={i} className="border-r border-border/60" />
380
+ ))}
381
+ </div>
382
+
383
+ {/* Horizontal row rules */}
384
+ <div
385
+ className="pointer-events-none absolute inset-0 flex flex-col"
386
+ aria-hidden="true"
387
+ >
388
+ {rows.map((row, i) => (
389
+ <div
390
+ key={i}
391
+ className={cn(
392
+ 'border-b border-border',
393
+ row.kind === 'group' && 'bg-muted/40'
394
+ )}
395
+ style={{ height: rowHeight }}
396
+ />
397
+ ))}
398
+ </div>
399
+
400
+ {/* Today line */}
401
+ {todayOffsetPx !== null && (
402
+ <div
403
+ data-slot="gantt-today"
404
+ aria-hidden="true"
405
+ className="pointer-events-none absolute top-0 bottom-0 w-px bg-primary/60"
406
+ style={{ left: todayOffsetPx }}
407
+ >
408
+ <div className="absolute -top-1 -left-[3px] h-1.5 w-1.5 rounded-full bg-primary" />
409
+ </div>
410
+ )}
411
+
412
+ {/* Bars */}
413
+ {rows.map((row, i) => {
414
+ if (row.kind !== 'item') return null
415
+ return (
416
+ <GanttBar
417
+ key={row.item.id}
418
+ item={row.item}
419
+ rowIndex={i}
420
+ rowHeight={rowHeight}
421
+ viewportStart={viewport.start}
422
+ scale={scale}
423
+ columnWidth={columnWidth}
424
+ timelineWidth={timelineWidth}
425
+ showProgress={showProgress}
426
+ interactive={interactive}
427
+ onItemMove={onItemMove}
428
+ onItemResize={onItemResize}
429
+ onItemClick={onItemClick}
430
+ />
431
+ )
432
+ })}
433
+
434
+ {/* Dependency arrows */}
435
+ {showDependencies && (
436
+ <DependencyLayer
437
+ items={items}
438
+ itemRowIndex={itemRowIndex}
439
+ rowHeight={rowHeight}
440
+ viewportStart={viewport.start}
441
+ scale={scale}
442
+ columnWidth={columnWidth}
443
+ timelineWidth={timelineWidth}
444
+ totalHeight={rows.length * rowHeight}
445
+ />
446
+ )}
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ )
452
+ }
453
+ )
454
+ Gantt.displayName = 'Gantt'
455
+
456
+ // ── GanttBar ─────────────────────────────────────────────────────
457
+
458
+ type DragMode = 'move' | 'resize-left' | 'resize-right' | null
459
+
460
+ interface GanttBarProps {
461
+ item: GanttItem
462
+ rowIndex: number
463
+ rowHeight: number
464
+ viewportStart: Date
465
+ scale: GanttScale
466
+ columnWidth: number
467
+ timelineWidth: number
468
+ showProgress: boolean
469
+ interactive: boolean
470
+ onItemMove?: (id: string, newStart: Date, newEnd: Date) => void
471
+ onItemResize?: (id: string, newEnd: Date) => void
472
+ onItemClick?: (item: GanttItem) => void
473
+ }
474
+
475
+ function GanttBar({
476
+ item,
477
+ rowIndex,
478
+ rowHeight,
479
+ viewportStart,
480
+ scale,
481
+ columnWidth,
482
+ timelineWidth,
483
+ showProgress,
484
+ interactive,
485
+ onItemMove,
486
+ onItemResize,
487
+ onItemClick,
488
+ }: GanttBarProps) {
489
+ const color = item.color ?? DEFAULT_COLOR
490
+ const progress = Math.max(0, Math.min(100, item.progress ?? 0))
491
+
492
+ // Drag-preview deltas (in pixels). Committed on pointerup.
493
+ const [previewLeftDelta, setPreviewLeftDelta] = React.useState(0)
494
+ const [previewRightDelta, setPreviewRightDelta] = React.useState(0)
495
+
496
+ const dragMode = React.useRef<DragMode>(null)
497
+ const startX = React.useRef(0)
498
+ const longPressTimer = React.useRef<number | null>(null)
499
+ const moved = React.useRef(false)
500
+
501
+ // Base position (in px) before any drag preview.
502
+ const baseLeft = unitsBetween(viewportStart, item.start, scale) * columnWidth
503
+ const baseRight =
504
+ unitsBetween(viewportStart, item.end, scale) * columnWidth
505
+ const baseWidth = Math.max(columnWidth * 0.25, baseRight - baseLeft)
506
+
507
+ // Apply preview deltas based on which mode is active.
508
+ const left = baseLeft + previewLeftDelta
509
+ const width = Math.max(
510
+ columnWidth * 0.25,
511
+ baseWidth - previewLeftDelta + previewRightDelta
512
+ )
513
+
514
+ const top = rowIndex * rowHeight + 4
515
+ const barHeight = rowHeight - 8
516
+
517
+ const cancelLongPress = () => {
518
+ if (longPressTimer.current !== null) {
519
+ window.clearTimeout(longPressTimer.current)
520
+ longPressTimer.current = null
521
+ }
522
+ }
523
+
524
+ const beginDrag = (
525
+ e: React.PointerEvent<HTMLDivElement>,
526
+ mode: DragMode
527
+ ) => {
528
+ if (!interactive || !mode) return
529
+ if (mode === 'move' && !onItemMove) return
530
+ if (mode === 'resize-left' && !onItemMove) return
531
+ if (mode === 'resize-right' && !onItemResize && !onItemMove) return
532
+ dragMode.current = mode
533
+ startX.current = e.clientX
534
+ moved.current = false
535
+ setPreviewLeftDelta(0)
536
+ setPreviewRightDelta(0)
537
+ try {
538
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
539
+ } catch {
540
+ // ignore
541
+ }
542
+ }
543
+
544
+ const handlePointerDown = (
545
+ e: React.PointerEvent<HTMLDivElement>,
546
+ mode: Exclude<DragMode, null>
547
+ ) => {
548
+ if (!interactive) return
549
+ e.stopPropagation()
550
+ // Touch → long-press to start drag (avoids scroll hijack).
551
+ if (e.pointerType === 'touch') {
552
+ cancelLongPress()
553
+ longPressTimer.current = window.setTimeout(() => {
554
+ beginDrag(e, mode)
555
+ }, LONG_PRESS_MS)
556
+ return
557
+ }
558
+ e.preventDefault()
559
+ beginDrag(e, mode)
560
+ }
561
+
562
+ const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
563
+ if (!dragMode.current) return
564
+ const delta = e.clientX - startX.current
565
+ if (Math.abs(delta) > 2) moved.current = true
566
+ if (dragMode.current === 'move') {
567
+ setPreviewLeftDelta(delta)
568
+ setPreviewRightDelta(delta)
569
+ } else if (dragMode.current === 'resize-left') {
570
+ // Don't let bar invert.
571
+ const clamped = Math.min(delta, baseWidth - columnWidth * 0.25)
572
+ setPreviewLeftDelta(clamped)
573
+ } else if (dragMode.current === 'resize-right') {
574
+ const clamped = Math.max(delta, -(baseWidth - columnWidth * 0.25))
575
+ setPreviewRightDelta(clamped)
576
+ }
577
+ }
578
+
579
+ const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
580
+ cancelLongPress()
581
+ const mode = dragMode.current
582
+ if (!mode) return
583
+ dragMode.current = null
584
+ const delta = e.clientX - startX.current
585
+ setPreviewLeftDelta(0)
586
+ setPreviewRightDelta(0)
587
+ try {
588
+ ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
589
+ } catch {
590
+ // ignore
591
+ }
592
+ // Snap delta to nearest unit.
593
+ const unitsDelta = Math.round(delta / columnWidth)
594
+ if (unitsDelta === 0) return
595
+
596
+ if (mode === 'move' && onItemMove) {
597
+ const newStart = snapDate(addUnits(item.start, unitsDelta, scale), scale)
598
+ const span =
599
+ unitsBetween(item.start, item.end, scale)
600
+ const newEnd = addUnits(newStart, span, scale)
601
+ onItemMove(item.id, newStart, newEnd)
602
+ } else if (mode === 'resize-left' && onItemMove) {
603
+ // Resizing from left moves the start; end stays put. Re-use onItemMove.
604
+ const newStart = snapDate(addUnits(item.start, unitsDelta, scale), scale)
605
+ // Don't let start cross end.
606
+ if (newStart >= item.end) return
607
+ onItemMove(item.id, newStart, item.end)
608
+ } else if (mode === 'resize-right') {
609
+ const newEnd = snapDate(addUnits(item.end, unitsDelta, scale), scale)
610
+ if (newEnd <= item.start) return
611
+ if (onItemResize) onItemResize(item.id, newEnd)
612
+ else if (onItemMove) onItemMove(item.id, item.start, newEnd)
613
+ }
614
+ }
615
+
616
+ const handleClick = () => {
617
+ if (moved.current) return
618
+ onItemClick?.(item)
619
+ }
620
+
621
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
622
+ if (e.key === 'Enter' || e.key === ' ') {
623
+ e.preventDefault()
624
+ onItemClick?.(item)
625
+ return
626
+ }
627
+ const isLeft = e.key === 'ArrowLeft'
628
+ const isRight = e.key === 'ArrowRight'
629
+ if (!isLeft && !isRight) return
630
+ if (!onItemMove) return
631
+ e.preventDefault()
632
+ const dir = isLeft ? -1 : 1
633
+ // Shift+arrow = larger step (1 week in days mode, otherwise just 1).
634
+ let units = dir
635
+ if (e.shiftKey) {
636
+ units = scale === 'days' ? dir * 7 : dir
637
+ }
638
+ const newStart = addUnits(item.start, units, scale)
639
+ const newEnd = addUnits(item.end, units, scale)
640
+ onItemMove(item.id, newStart, newEnd)
641
+ }
642
+
643
+ const ariaLabel = `${item.title}, ${format(item.start, 'PP')} to ${format(
644
+ item.end,
645
+ 'PP'
646
+ )}`
647
+
648
+ // Clamp visual bounds to timeline width so off-viewport bars don't overflow.
649
+ const clampedLeft = Math.max(0, left)
650
+ const clampedRight = Math.min(timelineWidth, left + width)
651
+ const visibleWidth = Math.max(0, clampedRight - clampedLeft)
652
+
653
+ return (
654
+ <div
655
+ data-slot="gantt-bar"
656
+ data-item-id={item.id}
657
+ role="button"
658
+ tabIndex={0}
659
+ aria-label={ariaLabel}
660
+ onClick={handleClick}
661
+ onKeyDown={handleKeyDown}
662
+ onPointerDown={(e) => handlePointerDown(e, 'move')}
663
+ onPointerMove={handlePointerMove}
664
+ onPointerUp={handlePointerUp}
665
+ onPointerCancel={handlePointerUp}
666
+ title={item.title}
667
+ style={{
668
+ left: clampedLeft,
669
+ top,
670
+ width: visibleWidth,
671
+ height: barHeight,
672
+ backgroundColor: `color-mix(in oklab, ${color} 35%, transparent)`,
673
+ borderColor: color,
674
+ }}
675
+ className={cn(
676
+ 'group/bar absolute z-10 flex items-center overflow-hidden rounded-md border-l-[3px] px-2 text-[11px] font-medium text-foreground shadow-sm outline-none transition-shadow hover:brightness-110 focus-visible:ring-2 focus-visible:ring-ring',
677
+ interactive && onItemMove && 'cursor-grab active:cursor-grabbing',
678
+ !interactive && onItemClick && 'cursor-pointer'
679
+ )}
680
+ >
681
+ {/* Progress fill */}
682
+ {showProgress && progress > 0 && (
683
+ <div
684
+ aria-hidden="true"
685
+ className="pointer-events-none absolute inset-y-0 left-0"
686
+ style={{
687
+ width: `${progress}%`,
688
+ backgroundColor: `color-mix(in oklab, ${color} 55%, transparent)`,
689
+ }}
690
+ />
691
+ )}
692
+
693
+ <span className="relative z-10 truncate">{item.title}</span>
694
+ {showProgress && (
695
+ <span className="relative z-10 ml-auto pl-2 text-[10px] tabular-nums text-muted-foreground">
696
+ {progress}%
697
+ </span>
698
+ )}
699
+
700
+ {/* Left edge handle */}
701
+ {interactive && onItemMove && (
702
+ <div
703
+ aria-label="Resize start"
704
+ onPointerDown={(e) => handlePointerDown(e, 'resize-left')}
705
+ onPointerMove={handlePointerMove}
706
+ onPointerUp={handlePointerUp}
707
+ onPointerCancel={handlePointerUp}
708
+ className="absolute inset-y-0 left-0 z-20 w-2 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100"
709
+ style={{ width: EDGE_HIT_PX }}
710
+ />
711
+ )}
712
+ {/* Right edge handle */}
713
+ {interactive && (onItemResize || onItemMove) && (
714
+ <div
715
+ aria-label="Resize end"
716
+ onPointerDown={(e) => handlePointerDown(e, 'resize-right')}
717
+ onPointerMove={handlePointerMove}
718
+ onPointerUp={handlePointerUp}
719
+ onPointerCancel={handlePointerUp}
720
+ className="absolute inset-y-0 right-0 z-20 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100"
721
+ style={{ width: EDGE_HIT_PX }}
722
+ />
723
+ )}
724
+ </div>
725
+ )
726
+ }
727
+
728
+ // ── DependencyLayer ─────────────────────────────────────────────
729
+
730
+ interface DependencyLayerProps {
731
+ items: GanttItem[]
732
+ itemRowIndex: Map<string, number>
733
+ rowHeight: number
734
+ viewportStart: Date
735
+ scale: GanttScale
736
+ columnWidth: number
737
+ timelineWidth: number
738
+ totalHeight: number
739
+ }
740
+
741
+ function DependencyLayer({
742
+ items,
743
+ itemRowIndex,
744
+ rowHeight,
745
+ viewportStart,
746
+ scale,
747
+ columnWidth,
748
+ timelineWidth,
749
+ totalHeight,
750
+ }: DependencyLayerProps) {
751
+ const itemById = React.useMemo(() => {
752
+ const m = new Map<string, GanttItem>()
753
+ for (const it of items) m.set(it.id, it)
754
+ return m
755
+ }, [items])
756
+
757
+ const arrows: Array<{ id: string; d: string; endX: number; endY: number }> = []
758
+ for (const downstream of items) {
759
+ if (!downstream.dependsOn?.length) continue
760
+ const dRow = itemRowIndex.get(downstream.id)
761
+ if (dRow === undefined) continue
762
+ const dStartX =
763
+ unitsBetween(viewportStart, downstream.start, scale) * columnWidth
764
+ const dY = dRow * rowHeight + rowHeight / 2
765
+
766
+ for (const upId of downstream.dependsOn) {
767
+ const up = itemById.get(upId)
768
+ if (!up) continue
769
+ const uRow = itemRowIndex.get(upId)
770
+ if (uRow === undefined) continue
771
+ const uEndX = unitsBetween(viewportStart, up.end, scale) * columnWidth
772
+ const uY = uRow * rowHeight + rowHeight / 2
773
+
774
+ // L-shaped polyline: out from upstream end, then down/up, then into downstream start.
775
+ const stub = 12
776
+ const midX = Math.max(uEndX + stub, dStartX - stub)
777
+ const d = `M ${uEndX} ${uY} L ${midX} ${uY} L ${midX} ${dY} L ${dStartX} ${dY}`
778
+ arrows.push({
779
+ id: `${upId}->${downstream.id}`,
780
+ d,
781
+ endX: dStartX,
782
+ endY: dY,
783
+ })
784
+ }
785
+ }
786
+
787
+ if (arrows.length === 0) return null
788
+
789
+ return (
790
+ <svg
791
+ className="pointer-events-none absolute inset-0 z-20"
792
+ width={timelineWidth}
793
+ height={totalHeight}
794
+ aria-hidden="true"
795
+ >
796
+ <defs>
797
+ <marker
798
+ id="gantt-arrow-head"
799
+ viewBox="0 0 10 10"
800
+ refX="8"
801
+ refY="5"
802
+ markerWidth="6"
803
+ markerHeight="6"
804
+ orient="auto-start-reverse"
805
+ >
806
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
807
+ </marker>
808
+ </defs>
809
+ <g className="text-muted-foreground">
810
+ {arrows.map((a) => (
811
+ <path
812
+ key={a.id}
813
+ d={a.d}
814
+ fill="none"
815
+ stroke="currentColor"
816
+ strokeWidth="1.25"
817
+ strokeOpacity="0.5"
818
+ markerEnd="url(#gantt-arrow-head)"
819
+ />
820
+ ))}
821
+ </g>
822
+ </svg>
823
+ )
824
+ }