@multiplatform.one/components 7.7.6 → 7.8.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 (565) hide show
  1. package/dist/cjs/Button.native.js.map +1 -1
  2. package/dist/cjs/Card.cjs +7 -1
  3. package/dist/cjs/Card.native.js +11 -1
  4. package/dist/cjs/Card.native.js.map +1 -1
  5. package/dist/cjs/CardFooter.cjs +1 -0
  6. package/dist/cjs/CardFooter.native.js +1 -0
  7. package/dist/cjs/CardFooter.native.js.map +1 -1
  8. package/dist/cjs/CopyField/index.cjs +6 -1
  9. package/dist/cjs/CopyField/index.native.js +6 -1
  10. package/dist/cjs/CopyField/index.native.js.map +1 -1
  11. package/dist/cjs/DropdownMenu/index.cjs +31 -1
  12. package/dist/cjs/DropdownMenu/index.native.js +31 -1
  13. package/dist/cjs/DropdownMenu/index.native.js.map +1 -1
  14. package/dist/cjs/Meter/index.cjs +12 -5
  15. package/dist/cjs/Meter/index.native.js +12 -5
  16. package/dist/cjs/Meter/index.native.js.map +1 -1
  17. package/dist/cjs/Tabs/index.cjs +7 -6
  18. package/dist/cjs/Tabs/index.native.js +8 -7
  19. package/dist/cjs/Tabs/index.native.js.map +1 -1
  20. package/dist/cjs/Toast/index.cjs +53 -2
  21. package/dist/cjs/Toast/index.native.js +55 -2
  22. package/dist/cjs/Toast/index.native.js.map +1 -1
  23. package/dist/cjs/actions/DestructiveAction.cjs +7 -13
  24. package/dist/cjs/actions/DestructiveAction.native.js +7 -15
  25. package/dist/cjs/actions/DestructiveAction.native.js.map +1 -1
  26. package/dist/cjs/charts/Axis.cjs +5 -0
  27. package/dist/cjs/charts/Axis.native.js +5 -0
  28. package/dist/cjs/charts/Axis.native.js.map +1 -1
  29. package/dist/cjs/charts/BarChart.cjs +71 -57
  30. package/dist/cjs/charts/BarChart.native.js +81 -67
  31. package/dist/cjs/charts/BarChart.native.js.map +1 -1
  32. package/dist/cjs/charts/ChartDataView.cjs +212 -0
  33. package/dist/cjs/charts/ChartDataView.native.js +224 -0
  34. package/dist/cjs/charts/ChartDataView.native.js.map +1 -0
  35. package/dist/cjs/charts/ChartSurface.cjs +5 -1
  36. package/dist/cjs/charts/ChartSurface.native.js +5 -1
  37. package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
  38. package/dist/cjs/charts/LineChart.cjs +81 -67
  39. package/dist/cjs/charts/LineChart.native.js +94 -80
  40. package/dist/cjs/charts/LineChart.native.js.map +1 -1
  41. package/dist/cjs/charts/PieChart.cjs +98 -73
  42. package/dist/cjs/charts/PieChart.native.js +109 -84
  43. package/dist/cjs/charts/PieChart.native.js.map +1 -1
  44. package/dist/cjs/charts/Sparkline.cjs +10 -1
  45. package/dist/cjs/charts/Sparkline.native.js +10 -1
  46. package/dist/cjs/charts/Sparkline.native.js.map +1 -1
  47. package/dist/cjs/charts/accessibility.cjs +4 -0
  48. package/dist/cjs/charts/accessibility.native.js +7 -0
  49. package/dist/cjs/charts/accessibility.native.js.map +1 -1
  50. package/dist/cjs/charts/index.cjs +3 -1
  51. package/dist/cjs/charts/index.native.js +2 -0
  52. package/dist/cjs/charts/index.native.js.map +1 -1
  53. package/dist/cjs/charts/math.cjs +16 -0
  54. package/dist/cjs/charts/math.native.js +18 -0
  55. package/dist/cjs/charts/math.native.js.map +1 -1
  56. package/dist/cjs/charts/series.cjs +49 -7
  57. package/dist/cjs/charts/series.native.js +53 -11
  58. package/dist/cjs/charts/series.native.js.map +1 -1
  59. package/dist/cjs/charts/svg.cjs +46 -3
  60. package/dist/cjs/charts/svg.native.js +46 -3
  61. package/dist/cjs/charts/svg.native.js.map +1 -1
  62. package/dist/cjs/code/CodeBlock/index.cjs +6 -1
  63. package/dist/cjs/code/CodeBlock/index.native.js +6 -1
  64. package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
  65. package/dist/cjs/feedback/ConfirmDialog.cjs +7 -21
  66. package/dist/cjs/feedback/ConfirmDialog.native.js +7 -22
  67. package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
  68. package/dist/cjs/feedback/NotifyHost.cjs +2 -1
  69. package/dist/cjs/feedback/NotifyHost.native.js +3 -2
  70. package/dist/cjs/feedback/NotifyHost.native.js.map +1 -1
  71. package/dist/cjs/feedback/copy.cjs +36 -4
  72. package/dist/cjs/feedback/copy.native.js +38 -4
  73. package/dist/cjs/feedback/copy.native.js.map +1 -1
  74. package/dist/cjs/feedback/index.cjs +1 -0
  75. package/dist/cjs/feedback/index.native.js +1 -0
  76. package/dist/cjs/feedback/index.native.js.map +1 -1
  77. package/dist/cjs/feedback/notify.cjs +1 -0
  78. package/dist/cjs/feedback/notify.native.js +1 -0
  79. package/dist/cjs/feedback/notify.native.js.map +1 -1
  80. package/dist/cjs/feedback/route.cjs +3 -0
  81. package/dist/cjs/feedback/route.native.js +3 -0
  82. package/dist/cjs/feedback/route.native.js.map +1 -1
  83. package/dist/cjs/feedback/store.cjs +6 -0
  84. package/dist/cjs/feedback/store.native.js +8 -0
  85. package/dist/cjs/feedback/store.native.js.map +1 -1
  86. package/dist/cjs/icons/lucide.cjs +2 -1
  87. package/dist/cjs/icons/lucide.native.js +2 -1
  88. package/dist/cjs/icons/lucide.native.js.map +1 -1
  89. package/dist/cjs/layouts/PaneScaffold.cjs +96 -41
  90. package/dist/cjs/layouts/PaneScaffold.native.js +97 -41
  91. package/dist/cjs/layouts/PaneScaffold.native.js.map +1 -1
  92. package/dist/cjs/layouts/index.cjs +2 -0
  93. package/dist/cjs/layouts/index.native.js +2 -0
  94. package/dist/cjs/layouts/index.native.js.map +1 -1
  95. package/dist/cjs/layouts/page.cjs +48 -7
  96. package/dist/cjs/layouts/page.native.js +50 -7
  97. package/dist/cjs/layouts/page.native.js.map +1 -1
  98. package/dist/cjs/shared/useKeyboardInset.cjs +1 -19
  99. package/dist/cjs/shared/useKeyboardInset.native.js +3 -3
  100. package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
  101. package/dist/cjs/views/Dashboard.cjs +110 -17
  102. package/dist/cjs/views/Dashboard.native.js +151 -45
  103. package/dist/cjs/views/Dashboard.native.js.map +1 -1
  104. package/dist/cjs/views/Gantt.cjs +374 -91
  105. package/dist/cjs/views/Gantt.native.js +443 -94
  106. package/dist/cjs/views/Gantt.native.js.map +1 -1
  107. package/dist/cjs/views/ganttMath.cjs +91 -2
  108. package/dist/cjs/views/ganttMath.native.js +113 -3
  109. package/dist/cjs/views/ganttMath.native.js.map +1 -1
  110. package/dist/cjs/views/index.cjs +20 -1
  111. package/dist/cjs/views/index.native.js +20 -1
  112. package/dist/cjs/views/index.native.js.map +1 -1
  113. package/dist/esm/Button.mjs.map +1 -1
  114. package/dist/esm/Button.native.js.map +1 -1
  115. package/dist/esm/Card.mjs +8 -2
  116. package/dist/esm/Card.mjs.map +1 -1
  117. package/dist/esm/Card.native.js +12 -2
  118. package/dist/esm/Card.native.js.map +1 -1
  119. package/dist/esm/CardFooter.mjs +1 -0
  120. package/dist/esm/CardFooter.mjs.map +1 -1
  121. package/dist/esm/CardFooter.native.js +1 -0
  122. package/dist/esm/CardFooter.native.js.map +1 -1
  123. package/dist/esm/CopyField/index.mjs +7 -2
  124. package/dist/esm/CopyField/index.mjs.map +1 -1
  125. package/dist/esm/CopyField/index.native.js +7 -2
  126. package/dist/esm/CopyField/index.native.js.map +1 -1
  127. package/dist/esm/DropdownMenu/index.mjs +32 -2
  128. package/dist/esm/DropdownMenu/index.mjs.map +1 -1
  129. package/dist/esm/DropdownMenu/index.native.js +32 -2
  130. package/dist/esm/DropdownMenu/index.native.js.map +1 -1
  131. package/dist/esm/Meter/index.mjs +12 -5
  132. package/dist/esm/Meter/index.mjs.map +1 -1
  133. package/dist/esm/Meter/index.native.js +12 -5
  134. package/dist/esm/Meter/index.native.js.map +1 -1
  135. package/dist/esm/Tabs/index.mjs +7 -6
  136. package/dist/esm/Tabs/index.mjs.map +1 -1
  137. package/dist/esm/Tabs/index.native.js +8 -7
  138. package/dist/esm/Tabs/index.native.js.map +1 -1
  139. package/dist/esm/Toast/index.mjs +52 -3
  140. package/dist/esm/Toast/index.mjs.map +1 -1
  141. package/dist/esm/Toast/index.native.js +54 -3
  142. package/dist/esm/Toast/index.native.js.map +1 -1
  143. package/dist/esm/actions/DestructiveAction.mjs +8 -14
  144. package/dist/esm/actions/DestructiveAction.mjs.map +1 -1
  145. package/dist/esm/actions/DestructiveAction.native.js +8 -16
  146. package/dist/esm/actions/DestructiveAction.native.js.map +1 -1
  147. package/dist/esm/charts/Axis.mjs +6 -1
  148. package/dist/esm/charts/Axis.mjs.map +1 -1
  149. package/dist/esm/charts/Axis.native.js +6 -1
  150. package/dist/esm/charts/Axis.native.js.map +1 -1
  151. package/dist/esm/charts/BarChart.mjs +72 -58
  152. package/dist/esm/charts/BarChart.mjs.map +1 -1
  153. package/dist/esm/charts/BarChart.native.js +82 -68
  154. package/dist/esm/charts/BarChart.native.js.map +1 -1
  155. package/dist/esm/charts/ChartDataView.mjs +185 -0
  156. package/dist/esm/charts/ChartDataView.mjs.map +1 -0
  157. package/dist/esm/charts/ChartDataView.native.js +194 -0
  158. package/dist/esm/charts/ChartDataView.native.js.map +1 -0
  159. package/dist/esm/charts/ChartSurface.mjs +5 -1
  160. package/dist/esm/charts/ChartSurface.mjs.map +1 -1
  161. package/dist/esm/charts/ChartSurface.native.js +5 -1
  162. package/dist/esm/charts/ChartSurface.native.js.map +1 -1
  163. package/dist/esm/charts/LineChart.mjs +82 -68
  164. package/dist/esm/charts/LineChart.mjs.map +1 -1
  165. package/dist/esm/charts/LineChart.native.js +95 -81
  166. package/dist/esm/charts/LineChart.native.js.map +1 -1
  167. package/dist/esm/charts/PieChart.mjs +100 -75
  168. package/dist/esm/charts/PieChart.mjs.map +1 -1
  169. package/dist/esm/charts/PieChart.native.js +111 -86
  170. package/dist/esm/charts/PieChart.native.js.map +1 -1
  171. package/dist/esm/charts/Sparkline.mjs +11 -2
  172. package/dist/esm/charts/Sparkline.mjs.map +1 -1
  173. package/dist/esm/charts/Sparkline.native.js +11 -2
  174. package/dist/esm/charts/Sparkline.native.js.map +1 -1
  175. package/dist/esm/charts/accessibility.mjs +4 -1
  176. package/dist/esm/charts/accessibility.mjs.map +1 -1
  177. package/dist/esm/charts/accessibility.native.js +7 -1
  178. package/dist/esm/charts/accessibility.native.js.map +1 -1
  179. package/dist/esm/charts/index.mjs +2 -1
  180. package/dist/esm/charts/index.mjs.map +1 -1
  181. package/dist/esm/charts/index.native.js +2 -1
  182. package/dist/esm/charts/index.native.js.map +1 -1
  183. package/dist/esm/charts/math.mjs +16 -1
  184. package/dist/esm/charts/math.mjs.map +1 -1
  185. package/dist/esm/charts/math.native.js +18 -1
  186. package/dist/esm/charts/math.native.js.map +1 -1
  187. package/dist/esm/charts/series.mjs +51 -9
  188. package/dist/esm/charts/series.mjs.map +1 -1
  189. package/dist/esm/charts/series.native.js +55 -13
  190. package/dist/esm/charts/series.native.js.map +1 -1
  191. package/dist/esm/charts/svg.mjs +40 -4
  192. package/dist/esm/charts/svg.mjs.map +1 -1
  193. package/dist/esm/charts/svg.native.js +40 -4
  194. package/dist/esm/charts/svg.native.js.map +1 -1
  195. package/dist/esm/code/CodeBlock/index.mjs +7 -2
  196. package/dist/esm/code/CodeBlock/index.mjs.map +1 -1
  197. package/dist/esm/code/CodeBlock/index.native.js +7 -2
  198. package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
  199. package/dist/esm/feedback/ConfirmDialog.mjs +8 -22
  200. package/dist/esm/feedback/ConfirmDialog.mjs.map +1 -1
  201. package/dist/esm/feedback/ConfirmDialog.native.js +8 -23
  202. package/dist/esm/feedback/ConfirmDialog.native.js.map +1 -1
  203. package/dist/esm/feedback/NotifyHost.mjs +2 -1
  204. package/dist/esm/feedback/NotifyHost.mjs.map +1 -1
  205. package/dist/esm/feedback/NotifyHost.native.js +3 -2
  206. package/dist/esm/feedback/NotifyHost.native.js.map +1 -1
  207. package/dist/esm/feedback/copy.mjs +35 -4
  208. package/dist/esm/feedback/copy.mjs.map +1 -1
  209. package/dist/esm/feedback/copy.native.js +37 -4
  210. package/dist/esm/feedback/copy.native.js.map +1 -1
  211. package/dist/esm/feedback/index.mjs +2 -2
  212. package/dist/esm/feedback/index.mjs.map +1 -1
  213. package/dist/esm/feedback/index.native.js +2 -2
  214. package/dist/esm/feedback/index.native.js.map +1 -1
  215. package/dist/esm/feedback/notify.mjs +1 -0
  216. package/dist/esm/feedback/notify.mjs.map +1 -1
  217. package/dist/esm/feedback/notify.native.js +1 -0
  218. package/dist/esm/feedback/notify.native.js.map +1 -1
  219. package/dist/esm/feedback/route.mjs +3 -0
  220. package/dist/esm/feedback/route.mjs.map +1 -1
  221. package/dist/esm/feedback/route.native.js +3 -0
  222. package/dist/esm/feedback/route.native.js.map +1 -1
  223. package/dist/esm/feedback/store.mjs +6 -1
  224. package/dist/esm/feedback/store.mjs.map +1 -1
  225. package/dist/esm/feedback/store.native.js +8 -1
  226. package/dist/esm/feedback/store.native.js.map +1 -1
  227. package/dist/esm/icons/lucide.mjs +2 -2
  228. package/dist/esm/icons/lucide.mjs.map +1 -1
  229. package/dist/esm/icons/lucide.native.js +2 -2
  230. package/dist/esm/icons/lucide.native.js.map +1 -1
  231. package/dist/esm/layouts/PaneScaffold.mjs +97 -42
  232. package/dist/esm/layouts/PaneScaffold.mjs.map +1 -1
  233. package/dist/esm/layouts/PaneScaffold.native.js +98 -42
  234. package/dist/esm/layouts/PaneScaffold.native.js.map +1 -1
  235. package/dist/esm/layouts/index.mjs +2 -1
  236. package/dist/esm/layouts/index.mjs.map +1 -1
  237. package/dist/esm/layouts/index.native.js +2 -1
  238. package/dist/esm/layouts/index.native.js.map +1 -1
  239. package/dist/esm/layouts/page.mjs +49 -8
  240. package/dist/esm/layouts/page.mjs.map +1 -1
  241. package/dist/esm/layouts/page.native.js +51 -8
  242. package/dist/esm/layouts/page.native.js.map +1 -1
  243. package/dist/esm/shared/useKeyboardInset.mjs +1 -19
  244. package/dist/esm/shared/useKeyboardInset.mjs.map +1 -1
  245. package/dist/esm/shared/useKeyboardInset.native.js.map +1 -1
  246. package/dist/esm/views/Dashboard.mjs +112 -19
  247. package/dist/esm/views/Dashboard.mjs.map +1 -1
  248. package/dist/esm/views/Dashboard.native.js +153 -47
  249. package/dist/esm/views/Dashboard.native.js.map +1 -1
  250. package/dist/esm/views/Gantt.mjs +376 -93
  251. package/dist/esm/views/Gantt.mjs.map +1 -1
  252. package/dist/esm/views/Gantt.native.js +445 -96
  253. package/dist/esm/views/Gantt.native.js.map +1 -1
  254. package/dist/esm/views/ganttMath.mjs +84 -3
  255. package/dist/esm/views/ganttMath.mjs.map +1 -1
  256. package/dist/esm/views/ganttMath.native.js +106 -4
  257. package/dist/esm/views/ganttMath.native.js.map +1 -1
  258. package/dist/esm/views/index.mjs +2 -2
  259. package/dist/esm/views/index.mjs.map +1 -1
  260. package/dist/esm/views/index.native.js +2 -2
  261. package/dist/esm/views/index.native.js.map +1 -1
  262. package/dist/jsx/Button.mjs.map +1 -1
  263. package/dist/jsx/Button.native.js.map +1 -1
  264. package/dist/jsx/Card.mjs +8 -2
  265. package/dist/jsx/Card.mjs.map +1 -1
  266. package/dist/jsx/Card.native.js +11 -1
  267. package/dist/jsx/Card.native.js.map +1 -1
  268. package/dist/jsx/CardFooter.mjs +1 -0
  269. package/dist/jsx/CardFooter.mjs.map +1 -1
  270. package/dist/jsx/CardFooter.native.js +1 -0
  271. package/dist/jsx/CardFooter.native.js.map +1 -1
  272. package/dist/jsx/CopyField/index.mjs +7 -2
  273. package/dist/jsx/CopyField/index.mjs.map +1 -1
  274. package/dist/jsx/CopyField/index.native.js +6 -1
  275. package/dist/jsx/CopyField/index.native.js.map +1 -1
  276. package/dist/jsx/DropdownMenu/index.mjs +32 -2
  277. package/dist/jsx/DropdownMenu/index.mjs.map +1 -1
  278. package/dist/jsx/DropdownMenu/index.native.js +31 -1
  279. package/dist/jsx/DropdownMenu/index.native.js.map +1 -1
  280. package/dist/jsx/Meter/index.mjs +12 -5
  281. package/dist/jsx/Meter/index.mjs.map +1 -1
  282. package/dist/jsx/Meter/index.native.js +12 -5
  283. package/dist/jsx/Meter/index.native.js.map +1 -1
  284. package/dist/jsx/Tabs/index.mjs +7 -6
  285. package/dist/jsx/Tabs/index.mjs.map +1 -1
  286. package/dist/jsx/Tabs/index.native.js +8 -7
  287. package/dist/jsx/Tabs/index.native.js.map +1 -1
  288. package/dist/jsx/Toast/index.mjs +52 -3
  289. package/dist/jsx/Toast/index.mjs.map +1 -1
  290. package/dist/jsx/Toast/index.native.js +55 -2
  291. package/dist/jsx/Toast/index.native.js.map +1 -1
  292. package/dist/jsx/actions/DestructiveAction.mjs +8 -14
  293. package/dist/jsx/actions/DestructiveAction.mjs.map +1 -1
  294. package/dist/jsx/actions/DestructiveAction.native.js +7 -15
  295. package/dist/jsx/actions/DestructiveAction.native.js.map +1 -1
  296. package/dist/jsx/charts/Axis.mjs +6 -1
  297. package/dist/jsx/charts/Axis.mjs.map +1 -1
  298. package/dist/jsx/charts/Axis.native.js +5 -0
  299. package/dist/jsx/charts/Axis.native.js.map +1 -1
  300. package/dist/jsx/charts/BarChart.mjs +72 -58
  301. package/dist/jsx/charts/BarChart.mjs.map +1 -1
  302. package/dist/jsx/charts/BarChart.native.js +81 -67
  303. package/dist/jsx/charts/BarChart.native.js.map +1 -1
  304. package/dist/jsx/charts/ChartDataView.mjs +185 -0
  305. package/dist/jsx/charts/ChartDataView.mjs.map +1 -0
  306. package/dist/jsx/charts/ChartDataView.native.js +224 -0
  307. package/dist/jsx/charts/ChartDataView.native.js.map +1 -0
  308. package/dist/jsx/charts/ChartSurface.mjs +5 -1
  309. package/dist/jsx/charts/ChartSurface.mjs.map +1 -1
  310. package/dist/jsx/charts/ChartSurface.native.js +5 -1
  311. package/dist/jsx/charts/ChartSurface.native.js.map +1 -1
  312. package/dist/jsx/charts/LineChart.mjs +82 -68
  313. package/dist/jsx/charts/LineChart.mjs.map +1 -1
  314. package/dist/jsx/charts/LineChart.native.js +94 -80
  315. package/dist/jsx/charts/LineChart.native.js.map +1 -1
  316. package/dist/jsx/charts/PieChart.mjs +100 -75
  317. package/dist/jsx/charts/PieChart.mjs.map +1 -1
  318. package/dist/jsx/charts/PieChart.native.js +109 -84
  319. package/dist/jsx/charts/PieChart.native.js.map +1 -1
  320. package/dist/jsx/charts/Sparkline.mjs +11 -2
  321. package/dist/jsx/charts/Sparkline.mjs.map +1 -1
  322. package/dist/jsx/charts/Sparkline.native.js +10 -1
  323. package/dist/jsx/charts/Sparkline.native.js.map +1 -1
  324. package/dist/jsx/charts/accessibility.mjs +4 -1
  325. package/dist/jsx/charts/accessibility.mjs.map +1 -1
  326. package/dist/jsx/charts/accessibility.native.js +7 -0
  327. package/dist/jsx/charts/accessibility.native.js.map +1 -1
  328. package/dist/jsx/charts/index.mjs +2 -1
  329. package/dist/jsx/charts/index.mjs.map +1 -1
  330. package/dist/jsx/charts/index.native.js +2 -0
  331. package/dist/jsx/charts/index.native.js.map +1 -1
  332. package/dist/jsx/charts/math.mjs +16 -1
  333. package/dist/jsx/charts/math.mjs.map +1 -1
  334. package/dist/jsx/charts/math.native.js +18 -0
  335. package/dist/jsx/charts/math.native.js.map +1 -1
  336. package/dist/jsx/charts/series.mjs +51 -9
  337. package/dist/jsx/charts/series.mjs.map +1 -1
  338. package/dist/jsx/charts/series.native.js +53 -11
  339. package/dist/jsx/charts/series.native.js.map +1 -1
  340. package/dist/jsx/charts/svg.mjs +40 -4
  341. package/dist/jsx/charts/svg.mjs.map +1 -1
  342. package/dist/jsx/charts/svg.native.js +46 -3
  343. package/dist/jsx/charts/svg.native.js.map +1 -1
  344. package/dist/jsx/code/CodeBlock/index.mjs +7 -2
  345. package/dist/jsx/code/CodeBlock/index.mjs.map +1 -1
  346. package/dist/jsx/code/CodeBlock/index.native.js +6 -1
  347. package/dist/jsx/code/CodeBlock/index.native.js.map +1 -1
  348. package/dist/jsx/feedback/ConfirmDialog.mjs +8 -22
  349. package/dist/jsx/feedback/ConfirmDialog.mjs.map +1 -1
  350. package/dist/jsx/feedback/ConfirmDialog.native.js +7 -22
  351. package/dist/jsx/feedback/ConfirmDialog.native.js.map +1 -1
  352. package/dist/jsx/feedback/NotifyHost.mjs +2 -1
  353. package/dist/jsx/feedback/NotifyHost.mjs.map +1 -1
  354. package/dist/jsx/feedback/NotifyHost.native.js +3 -2
  355. package/dist/jsx/feedback/NotifyHost.native.js.map +1 -1
  356. package/dist/jsx/feedback/copy.mjs +35 -4
  357. package/dist/jsx/feedback/copy.mjs.map +1 -1
  358. package/dist/jsx/feedback/copy.native.js +38 -4
  359. package/dist/jsx/feedback/copy.native.js.map +1 -1
  360. package/dist/jsx/feedback/index.mjs +2 -2
  361. package/dist/jsx/feedback/index.mjs.map +1 -1
  362. package/dist/jsx/feedback/index.native.js +1 -0
  363. package/dist/jsx/feedback/index.native.js.map +1 -1
  364. package/dist/jsx/feedback/notify.mjs +1 -0
  365. package/dist/jsx/feedback/notify.mjs.map +1 -1
  366. package/dist/jsx/feedback/notify.native.js +1 -0
  367. package/dist/jsx/feedback/notify.native.js.map +1 -1
  368. package/dist/jsx/feedback/route.mjs +3 -0
  369. package/dist/jsx/feedback/route.mjs.map +1 -1
  370. package/dist/jsx/feedback/route.native.js +3 -0
  371. package/dist/jsx/feedback/route.native.js.map +1 -1
  372. package/dist/jsx/feedback/store.mjs +6 -1
  373. package/dist/jsx/feedback/store.mjs.map +1 -1
  374. package/dist/jsx/feedback/store.native.js +8 -0
  375. package/dist/jsx/feedback/store.native.js.map +1 -1
  376. package/dist/jsx/icons/lucide.mjs +2 -2
  377. package/dist/jsx/icons/lucide.mjs.map +1 -1
  378. package/dist/jsx/icons/lucide.native.js +2 -1
  379. package/dist/jsx/layouts/PaneScaffold.mjs +97 -42
  380. package/dist/jsx/layouts/PaneScaffold.mjs.map +1 -1
  381. package/dist/jsx/layouts/PaneScaffold.native.js +97 -41
  382. package/dist/jsx/layouts/PaneScaffold.native.js.map +1 -1
  383. package/dist/jsx/layouts/index.mjs +2 -1
  384. package/dist/jsx/layouts/index.mjs.map +1 -1
  385. package/dist/jsx/layouts/index.native.js +2 -0
  386. package/dist/jsx/layouts/index.native.js.map +1 -1
  387. package/dist/jsx/layouts/page.mjs +49 -8
  388. package/dist/jsx/layouts/page.mjs.map +1 -1
  389. package/dist/jsx/layouts/page.native.js +50 -7
  390. package/dist/jsx/layouts/page.native.js.map +1 -1
  391. package/dist/jsx/shared/useKeyboardInset.mjs +1 -19
  392. package/dist/jsx/shared/useKeyboardInset.mjs.map +1 -1
  393. package/dist/jsx/shared/useKeyboardInset.native.js +3 -3
  394. package/dist/jsx/shared/useKeyboardInset.native.js.map +1 -1
  395. package/dist/jsx/views/Dashboard.mjs +112 -19
  396. package/dist/jsx/views/Dashboard.mjs.map +1 -1
  397. package/dist/jsx/views/Dashboard.native.js +151 -45
  398. package/dist/jsx/views/Dashboard.native.js.map +1 -1
  399. package/dist/jsx/views/Gantt.mjs +376 -93
  400. package/dist/jsx/views/Gantt.mjs.map +1 -1
  401. package/dist/jsx/views/Gantt.native.js +443 -94
  402. package/dist/jsx/views/Gantt.native.js.map +1 -1
  403. package/dist/jsx/views/ganttMath.mjs +84 -3
  404. package/dist/jsx/views/ganttMath.mjs.map +1 -1
  405. package/dist/jsx/views/ganttMath.native.js +113 -3
  406. package/dist/jsx/views/ganttMath.native.js.map +1 -1
  407. package/dist/jsx/views/index.mjs +2 -2
  408. package/dist/jsx/views/index.mjs.map +1 -1
  409. package/dist/jsx/views/index.native.js +20 -1
  410. package/dist/jsx/views/index.native.js.map +1 -1
  411. package/package.json +11 -9
  412. package/src/Button.tsx +10 -2
  413. package/src/Card.spec.tsx +76 -3
  414. package/src/Card.stories.tsx +1 -1
  415. package/src/Card.tsx +13 -1
  416. package/src/CardFooter.stories.tsx +3 -7
  417. package/src/CardFooter.tsx +3 -1
  418. package/src/CardHeader.stories.tsx +1 -3
  419. package/src/Collapsible.stories.tsx +2 -2
  420. package/src/CopyField/CopyField.spec.tsx +66 -1
  421. package/src/CopyField/index.tsx +4 -3
  422. package/src/DropdownMenu/DropdownMenu.spec.tsx +112 -1
  423. package/src/DropdownMenu/DropdownMenu.stories.tsx +1 -3
  424. package/src/DropdownMenu/index.tsx +67 -6
  425. package/src/LinearGradient.stories.tsx +2 -2
  426. package/src/Meter/Meter.spec.tsx +59 -2
  427. package/src/Meter/Meter.stories.tsx +38 -0
  428. package/src/Meter/index.tsx +9 -4
  429. package/src/Panel.stories.tsx +1 -1
  430. package/src/SortableList/SortableList.spec.tsx +3 -3
  431. package/src/Tabs/Tabs.spec.tsx +43 -3
  432. package/src/Tabs/Tabs.stories.tsx +10 -1
  433. package/src/Tabs/index.tsx +19 -13
  434. package/src/Toast/Toast.spec.tsx +56 -1
  435. package/src/Toast/index.tsx +68 -3
  436. package/src/Unspaced.stories.tsx +2 -2
  437. package/src/VisuallyHidden.stories.tsx +2 -2
  438. package/src/actions/DestructiveAction.spec.tsx +19 -3
  439. package/src/actions/DestructiveAction.tsx +9 -14
  440. package/src/charts/Axis.tsx +6 -1
  441. package/src/charts/BarChart.tsx +82 -67
  442. package/src/charts/ChartDataView.spec.tsx +104 -0
  443. package/src/charts/ChartDataView.tsx +158 -0
  444. package/src/charts/ChartSurface.tsx +7 -2
  445. package/src/charts/Charts.stories.tsx +213 -9
  446. package/src/charts/LineChart.tsx +90 -75
  447. package/src/charts/PieChart.knobs.spec.tsx +25 -0
  448. package/src/charts/PieChart.tsx +51 -9
  449. package/src/charts/Sparkline.tsx +25 -3
  450. package/src/charts/accessibility.ts +8 -0
  451. package/src/charts/charts.spec.tsx +77 -3
  452. package/src/charts/index.ts +2 -0
  453. package/src/charts/math.spec.ts +19 -0
  454. package/src/charts/math.ts +36 -0
  455. package/src/charts/series.tsx +101 -21
  456. package/src/charts/svg.spec.ts +28 -0
  457. package/src/charts/svg.ts +56 -2
  458. package/src/code/CodeBlock/CodeBlock.copy.spec.tsx +23 -0
  459. package/src/code/CodeBlock/index.tsx +4 -3
  460. package/src/feedback/ConfirmDialog.nativeFocus.spec.tsx +52 -0
  461. package/src/feedback/ConfirmDialog.spec.tsx +20 -0
  462. package/src/feedback/ConfirmDialog.tsx +9 -15
  463. package/src/feedback/NotifyHost.tsx +11 -2
  464. package/src/feedback/copy.ts +36 -4
  465. package/src/feedback/index.ts +1 -0
  466. package/src/feedback/notify.spec.ts +15 -1
  467. package/src/feedback/notify.ts +1 -0
  468. package/src/feedback/route.ts +4 -0
  469. package/src/feedback/store.ts +7 -0
  470. package/src/icons/lucide.ts +1 -1
  471. package/src/images/SvgUri/index.native.ts +11 -3
  472. package/src/layouts/EmptyState.stories.tsx +2 -2
  473. package/src/layouts/Page.weight.spec.tsx +75 -0
  474. package/src/layouts/PageHeader.stories.tsx +4 -4
  475. package/src/layouts/PageTemplates.stories.tsx +2 -2
  476. package/src/layouts/PaneScaffold.spec.tsx +144 -4
  477. package/src/layouts/PaneScaffold.stories.tsx +58 -2
  478. package/src/layouts/PaneScaffold.tsx +156 -58
  479. package/src/layouts/ScreenToolbar.stories.tsx +6 -23
  480. package/src/layouts/index.tsx +11 -0
  481. package/src/layouts/layouts.spec.tsx +51 -0
  482. package/src/layouts/page.designLaw.spec.tsx +25 -0
  483. package/src/layouts/page.tsx +46 -9
  484. package/src/shared/useKeyboardInset.native.ts +43 -0
  485. package/src/shared/useKeyboardInset.ts +2 -41
  486. package/src/tamagui.spec.ts +63 -0
  487. package/src/views/Dashboard.stories.tsx +38 -1
  488. package/src/views/Dashboard.tsx +119 -21
  489. package/src/views/Gantt.scroll.spec.tsx +120 -0
  490. package/src/views/Gantt.spec.tsx +376 -0
  491. package/src/views/Gantt.stories.tsx +75 -2
  492. package/src/views/Gantt.tsx +564 -96
  493. package/src/views/Kanban.spec.tsx +11 -0
  494. package/src/views/ganttMath.ts +142 -4
  495. package/src/views/index.ts +44 -1
  496. package/src/views/views.spec.tsx +39 -0
  497. package/types/Button.d.ts +12 -2
  498. package/types/Button.d.ts.map +1 -1
  499. package/types/Card.d.ts.map +1 -1
  500. package/types/CardFooter.d.ts +2 -1
  501. package/types/CardFooter.d.ts.map +1 -1
  502. package/types/CopyField/index.d.ts.map +1 -1
  503. package/types/DropdownMenu/index.d.ts +22 -0
  504. package/types/DropdownMenu/index.d.ts.map +1 -1
  505. package/types/Meter/index.d.ts.map +1 -1
  506. package/types/Tabs/index.d.ts +1 -1
  507. package/types/Tabs/index.d.ts.map +1 -1
  508. package/types/Toast/index.d.ts +16 -0
  509. package/types/Toast/index.d.ts.map +1 -1
  510. package/types/actions/DestructiveAction.d.ts.map +1 -1
  511. package/types/charts/Axis.d.ts.map +1 -1
  512. package/types/charts/BarChart.d.ts.map +1 -1
  513. package/types/charts/ChartDataView.d.ts +22 -0
  514. package/types/charts/ChartDataView.d.ts.map +1 -0
  515. package/types/charts/ChartSurface.d.ts +4 -1
  516. package/types/charts/ChartSurface.d.ts.map +1 -1
  517. package/types/charts/LineChart.d.ts.map +1 -1
  518. package/types/charts/PieChart.d.ts +1 -1
  519. package/types/charts/PieChart.d.ts.map +1 -1
  520. package/types/charts/Sparkline.d.ts +3 -1
  521. package/types/charts/Sparkline.d.ts.map +1 -1
  522. package/types/charts/accessibility.d.ts +3 -0
  523. package/types/charts/accessibility.d.ts.map +1 -1
  524. package/types/charts/index.d.ts +1 -0
  525. package/types/charts/index.d.ts.map +1 -1
  526. package/types/charts/math.d.ts +12 -0
  527. package/types/charts/math.d.ts.map +1 -1
  528. package/types/charts/series.d.ts +1 -1
  529. package/types/charts/series.d.ts.map +1 -1
  530. package/types/charts/svg.d.ts +18 -2
  531. package/types/charts/svg.d.ts.map +1 -1
  532. package/types/code/CodeBlock/index.d.ts.map +1 -1
  533. package/types/feedback/ConfirmDialog.d.ts.map +1 -1
  534. package/types/feedback/NotifyHost.d.ts +3 -1
  535. package/types/feedback/NotifyHost.d.ts.map +1 -1
  536. package/types/feedback/copy.d.ts +12 -2
  537. package/types/feedback/copy.d.ts.map +1 -1
  538. package/types/feedback/index.d.ts +1 -1
  539. package/types/feedback/index.d.ts.map +1 -1
  540. package/types/feedback/notify.d.ts.map +1 -1
  541. package/types/feedback/route.d.ts +3 -0
  542. package/types/feedback/route.d.ts.map +1 -1
  543. package/types/feedback/store.d.ts +2 -0
  544. package/types/feedback/store.d.ts.map +1 -1
  545. package/types/icons/lucide.d.ts +1 -1
  546. package/types/icons/lucide.d.ts.map +1 -1
  547. package/types/images/SvgUri/index.native.d.ts +4 -3
  548. package/types/images/SvgUri/index.native.d.ts.map +1 -1
  549. package/types/layouts/PaneScaffold.d.ts +27 -5
  550. package/types/layouts/PaneScaffold.d.ts.map +1 -1
  551. package/types/layouts/index.d.ts +1 -0
  552. package/types/layouts/index.d.ts.map +1 -1
  553. package/types/layouts/page.d.ts +10 -5
  554. package/types/layouts/page.d.ts.map +1 -1
  555. package/types/shared/useKeyboardInset.d.ts +1 -14
  556. package/types/shared/useKeyboardInset.d.ts.map +1 -1
  557. package/types/shared/useKeyboardInset.native.d.ts +16 -0
  558. package/types/shared/useKeyboardInset.native.d.ts.map +1 -0
  559. package/types/views/Dashboard.d.ts.map +1 -1
  560. package/types/views/Gantt.d.ts +132 -13
  561. package/types/views/Gantt.d.ts.map +1 -1
  562. package/types/views/ganttMath.d.ts +73 -0
  563. package/types/views/ganttMath.d.ts.map +1 -1
  564. package/types/views/index.d.ts +2 -2
  565. package/types/views/index.d.ts.map +1 -1
@@ -8,6 +8,7 @@ import {
8
8
  Intent,
9
9
  MIN_PRESS_TARGET,
10
10
  ensureKeyboardModalityTracking,
11
+ formatAbsoluteDate,
11
12
  formatDayOfMonth,
12
13
  formatMonthYear,
13
14
  hairline,
@@ -27,6 +28,7 @@ import {
27
28
  barGeometry,
28
29
  clampResizeDayDelta,
29
30
  computeGanttRange,
31
+ dateLineX,
30
32
  dateToX,
31
33
  dayDiff,
32
34
  dxToDayDelta,
@@ -34,12 +36,18 @@ import {
34
36
  formatSpanLabel,
35
37
  ganttTaskAccessibleLabel,
36
38
  generateTicks,
39
+ isBoundedSpan,
40
+ openBarGeometry,
41
+ openSpanExtents,
42
+ openSpanLabelParts,
37
43
  rangeDays,
38
44
  resizeSpanByDays,
45
+ scrollAnchorX,
39
46
  shiftSpanByDays,
40
47
  spanInclusiveDays,
41
48
  startOfDay,
42
49
  todayLineX,
50
+ type GanttOpenSpan,
43
51
  type GanttRange,
44
52
  type GanttResizeEdge,
45
53
  type GanttSpan,
@@ -52,17 +60,39 @@ import {
52
60
  const CanvasScroller = (isWeb ? ScrollView : RNScrollView) as typeof ScrollView;
53
61
 
54
62
  /**
55
- * A Gantt task rendered as one row with a duration bar.
63
+ * One drawn span on a Gantt row: a duration bar, or an open-ended one.
64
+ *
65
+ * BOTH BOUNDS ARE OPTIONAL, and an absent bound means the span is open at
66
+ * that end — not that the caller forgot it. Each of the three shapes paints
67
+ * differently, because a viewer must never read an edge nobody chose:
68
+ *
69
+ * | `start` | `end` | drawn as |
70
+ * |---|---|---|
71
+ * | set | set | a capped bar between the two dates (inclusive whole days) |
72
+ * | set | absent | a bar from `start`, uncapped, running off the chart's end edge with a `»` |
73
+ * | absent | set | a bar to `end`, uncapped, running off the chart's start edge with a `«` |
74
+ * | absent | absent | a thin half-height RAIL across the whole chart, `«` and `»` at both ends |
75
+ *
76
+ * The rail is a different SHAPE on purpose. Painting an eternal span as a
77
+ * full-width BAR would put two capped edges on screen at the chart's own
78
+ * padding, and the reader would take that padding for a decision.
79
+ *
80
+ * Rescheduling needs both bounds. A span missing either one gets no drag
81
+ * session, no resize handles and no keyboard moves — there is no second
82
+ * edge to preserve a duration against. `progress` is likewise ignored on an
83
+ * open span: a percentage of an unknown duration says nothing.
56
84
  */
57
- export interface GanttTask {
58
- /** Unique task identifier */
59
- id: string;
60
- /** Task title shown in the label column and the bar's accessible name */
61
- title: string;
62
- /** Task start date */
63
- start: Date;
64
- /** Task end date (midnight ends are inclusive whole days — Frappe date fields) */
65
- end: Date;
85
+ export interface GanttTaskSpan {
86
+ /**
87
+ * Stable id, unique within the task. Used as the React key, the drag
88
+ * session key and the `data-gantt-bar` attribute. Optional: a row's spans
89
+ * fall back to `<task id>#<index>`.
90
+ */
91
+ id?: string;
92
+ /** Span start date. Absent = open at the start. */
93
+ start?: Date;
94
+ /** Span end date (midnight ends are inclusive whole days — Frappe date fields). Absent = open at the end. */
95
+ end?: Date;
66
96
  /**
67
97
  * Explicit bar color (hex/rgb/token). Passes through untouched — the
68
98
  * literal is the value (Axiom 11). Wins over `intent`.
@@ -72,13 +102,54 @@ export interface GanttTask {
72
102
  intent?: "accent" | "error" | "warning" | "success";
73
103
  /**
74
104
  * Optional completion 0–100. Paints a darker overlay on the bar (Frappe
75
- * Gantt / MS Project progress). Omitted or 0 shows an empty bar.
105
+ * Gantt / MS Project progress). Omitted or 0 shows an empty bar. Ignored
106
+ * on an open-ended span.
76
107
  */
77
108
  progress?: number;
109
+ /** Bar label, when it should differ from the row's title. */
110
+ title?: string;
111
+ }
112
+
113
+ /**
114
+ * A Gantt task rendered as one row.
115
+ *
116
+ * The row carries ONE span by default — the task's own `start`/`end`, which
117
+ * is what every caller before `spans` existed passes — or SEVERAL when
118
+ * `spans` is set. Several is for one logical subject that ships a variant
119
+ * per period: three rules named `..._2026`, `..._2027`, `..._2028` are one
120
+ * emitter and want one row carrying three strips, not three unrelated rows.
121
+ */
122
+ export interface GanttTask extends GanttTaskSpan {
123
+ /** Unique task identifier */
124
+ id: string;
125
+ /** Task title shown in the label column and the bar's accessible name */
126
+ title: string;
127
+ /**
128
+ * Several spans on this one row. When set and non-empty it REPLACES the
129
+ * task's own `start`/`end` as the thing drawn; the label column still
130
+ * shows one row. Leave it off for the one-bar-per-row shape, which is
131
+ * unchanged in every respect down to the `data-gantt-bar` attribute.
132
+ */
133
+ spans?: GanttTaskSpan[];
78
134
  /** Any additional data */
79
135
  [key: string]: unknown;
80
136
  }
81
137
 
138
+ /**
139
+ * Which span of a row a callback is about.
140
+ *
141
+ * A single-span row reports `index: 0` and `key: task.id`, so a caller that
142
+ * never sets `spans` can keep ignoring this argument entirely.
143
+ */
144
+ export interface GanttSpanRef {
145
+ /** The span itself — the task object for a single-span row. */
146
+ span: GanttTaskSpan;
147
+ /** Position within `task.spans`, or 0 for a single-span row. */
148
+ index: number;
149
+ /** Stable key: `span.id`, else `task.id` (single-span) or `<task id>#<index>`. */
150
+ key: string;
151
+ }
152
+
82
153
  /** New start/end range produced by a bar drag-move. */
83
154
  export interface GanttItemMoveRange {
84
155
  start: Date;
@@ -105,8 +176,16 @@ export interface GanttViewProps extends Omit<YStackProps, "children"> {
105
176
  * pointer only). All paths preserve wall-clock times and snap to whole
106
177
  * days. Web drags use pointer tracking (Escape cancels); native uses the
107
178
  * responder system (best-effort — scroll locks while dragging).
179
+ *
180
+ * `span` names WHICH span of a multi-span row moved. A single-span row
181
+ * reports `{ index: 0, key: task.id }` and the span object IS the task, so
182
+ * a two-argument handler stays correct and needs no change.
183
+ *
184
+ * Only fully-bounded spans reschedule: an open-ended one has no second
185
+ * edge to hold a duration against, so it is never dragged and this never
186
+ * fires for it.
108
187
  */
109
- onItemMove?: (task: GanttTask, range: GanttItemMoveRange) => void;
188
+ onItemMove?: (task: GanttTask, range: GanttItemMoveRange, span?: GanttSpanRef) => void;
110
189
  /**
111
190
  * Enable bar rescheduling — pointer drag, keyboard arrows, and edge
112
191
  * resize (default: enabled when onItemMove is provided).
@@ -139,6 +218,50 @@ export interface GanttViewProps extends Omit<YStackProps, "children"> {
139
218
  firstDayOfWeek?: 0 | 1;
140
219
  /** Date scrolled into view on mount (defaults to today) */
141
220
  initialDate?: Date;
221
+ /**
222
+ * Controlled marker date — a playhead the caller owns.
223
+ *
224
+ * Independent of both `showToday`, which pins a line to the real today and
225
+ * cannot move, and `initialDate`, which only picks the mount scroll
226
+ * position. Set it and a marker line paints across the axis and the rows
227
+ * with a grab handle on the axis; leave it unset and nothing renders. The
228
+ * component never moves it on its own — it draws exactly the date it is
229
+ * given, which is what makes an as-of view possible.
230
+ */
231
+ markerDate?: Date;
232
+ /**
233
+ * Called when the marker is scrubbed: drag its axis handle, or focus the
234
+ * handle and press ArrowLeft/ArrowRight (Shift = 7 days). Fires per whole
235
+ * day crossed, not per pointer move, and Escape mid-drag fires once more
236
+ * with the date the drag started from.
237
+ */
238
+ onMarkerDateChange?: (date: Date) => void;
239
+ /**
240
+ * Enable scrubbing the marker (default: enabled when `onMarkerDateChange`
241
+ * is provided). A marker with no callback still renders, read-only.
242
+ */
243
+ enableMarkerDrag?: boolean;
244
+ /** Accessible name for the marker handle (default: "Marker"). */
245
+ markerLabel?: string;
246
+ }
247
+
248
+ /**
249
+ * The spans a row draws, in paint order.
250
+ *
251
+ * A task with no `spans` IS its own single span and takes the task id as its
252
+ * key, so every data attribute, drag session and callback on the
253
+ * one-bar-per-row path is exactly what it was before `spans` existed.
254
+ */
255
+ /** The span as a closed duration, or null when either bound is open. */
256
+ function boundedSpan(ref: GanttSpanRef): GanttSpan | null {
257
+ const span = ref.span;
258
+ return isBoundedSpan(span) ? { start: span.start, end: span.end } : null;
259
+ }
260
+
261
+ function resolveSpans(task: GanttTask): GanttSpanRef[] {
262
+ const list = task.spans;
263
+ if (!list || list.length === 0) return [{ span: task, index: 0, key: task.id }];
264
+ return list.map((span, index) => ({ span, index, key: span.id ?? `${task.id}#${index}` }));
142
265
  }
143
266
 
144
267
  // ── Interaction constants (gesture recognition, not animation timing) ──
@@ -345,6 +468,15 @@ function GanttTaskName({
345
468
  * invert so "toward earlier dates" always matches the visual direction.
346
469
  * Native RTL follows `I18nManager` (best-effort — not device-verified).
347
470
  *
471
+ * Three things make it a TRACK view as well as a schedule. A span's bounds
472
+ * are both optional, and the missing side paints as an uncapped edge (or, with
473
+ * neither bound, a half-height rail) instead of an invented date. A row can
474
+ * carry SEVERAL spans via `task.spans`, so one subject that ships a variant
475
+ * per period reads as one row. And `markerDate` is a controlled playhead the
476
+ * caller owns — scrubbed by dragging its handle on the axis or by arrows on
477
+ * the focused handle, reported through `onMarkerDateChange` — independent of
478
+ * the today line, which stays pinned to the real today.
479
+ *
348
480
  * @example
349
481
  * ```tsx
350
482
  * <GanttView
@@ -354,6 +486,22 @@ function GanttTaskName({
354
486
  * onItemMove={(task, range) => reschedule(task, range)}
355
487
  * />
356
488
  * ```
489
+ *
490
+ * @example A track view: open-ended spans, several per row, a scrubbed marker.
491
+ * ```tsx
492
+ * <GanttView
493
+ * tasks={[
494
+ * { id: "fed", title: "Federal true-up", spans: [
495
+ * { id: "2026", start: jan(2026), end: dec(2026) },
496
+ * { id: "2027", start: jan(2027), end: dec(2027) },
497
+ * ] },
498
+ * { id: "vat", title: "VAT", start: jan(2025) }, // no end: open at the end
499
+ * { id: "base", title: "Base policy" }, // no dates: a rail
500
+ * ]}
501
+ * markerDate={asOf}
502
+ * onMarkerDateChange={setAsOf}
503
+ * />
504
+ * ```
357
505
  */
358
506
  export function GanttView({
359
507
  tasks,
@@ -374,6 +522,10 @@ export function GanttView({
374
522
  showZoomSwitcher = true,
375
523
  firstDayOfWeek = 0,
376
524
  initialDate,
525
+ markerDate,
526
+ onMarkerDateChange,
527
+ enableMarkerDrag,
528
+ markerLabel,
377
529
  ...stackProps
378
530
  }: GanttViewProps) {
379
531
  const { t } = useTranslation();
@@ -424,16 +576,26 @@ export function GanttView({
424
576
  const space = knobProps.space;
425
577
  const barInset = space === "small" ? 4 : space === "large" ? 9 : 6;
426
578
  const barHeight = Math.max(10, rowHeight - barInset * 2);
579
+ // An eternal span is a RAIL, not a bar: half the height, no caps. The
580
+ // shape is the message — a full-width capped bar would put two chosen-
581
+ // looking edges on the chart's own padding.
582
+ const railHeight = Math.max(4, Math.round(barHeight / 2));
427
583
  const transition = knobProps.transition;
428
584
  // Layout props (left) only tween on web — the RN driver cannot animate
429
585
  // layout, so native keeps committed jumps instant (Axiom 4 note).
430
586
  const webLayoutTransition = isWeb ? transition : undefined;
431
587
 
432
588
  // ── Chart range + axis ───────────────────────────────────────────────────
433
- const spans = useMemo<GanttSpan[]>(
434
- () => tasks.map((task) => ({ start: task.start, end: task.end })),
435
- [tasks],
436
- );
589
+ // Every span on every row, in one flat list. Open spans contribute only
590
+ // the date they actually name (an eternal one contributes nothing, so it
591
+ // cannot stretch the axis), and the marker joins them so it always has
592
+ // somewhere to land — the same courtesy computeGanttRange pays today.
593
+ const spans = useMemo<GanttSpan[]>(() => {
594
+ const flat: GanttOpenSpan[] = [];
595
+ for (const task of tasks) for (const ref of resolveSpans(task)) flat.push(ref.span);
596
+ if (markerDate !== undefined) flat.push({ start: markerDate, end: markerDate });
597
+ return openSpanExtents(flat);
598
+ }, [tasks, markerDate]);
437
599
  const range = useMemo(
438
600
  () => computeGanttRange(spans, zoom, new Date(), firstDayOfWeek),
439
601
  [spans, zoom, firstDayOfWeek],
@@ -461,20 +623,19 @@ export function GanttView({
461
623
  const hScrollRef = useRef<any>(null);
462
624
  const axisScrollRef = useRef<any>(null);
463
625
  const canvasViewportRef = useRef<number | null>(null);
464
- // RTL initial anchor needs the viewport width; re-run once layout reports it.
465
- const pendingRtlAnchorRef = useRef<Date | null>(null);
626
+ // Re-anchor in either direction once the canvas viewport has been measured.
627
+ const pendingAnchorRef = useRef<Date | null>(null);
466
628
 
467
629
  const scrollToDate = useCallback(
468
630
  (date: Date, animated: boolean) => {
469
- const ltrX = Math.max(0, dateToX(date, range.start, pxPerDay) - labelWidth);
470
- let x = ltrX;
471
- if (rtl) {
472
- // Mirrored canvas: the anchor sits `ltrX` from the content's END.
473
- const viewport = canvasViewportRef.current;
474
- if (viewport === null) pendingRtlAnchorRef.current = date;
475
- const max = Math.max(0, chartWidth - (viewport ?? 0));
476
- x = Math.min(max, Math.max(0, max - ltrX));
477
- }
631
+ const viewportWidth = canvasViewportRef.current;
632
+ if (viewportWidth === null) pendingAnchorRef.current = date;
633
+ const x = scrollAnchorX(date, range.start, pxPerDay, {
634
+ labelWidth,
635
+ chartWidth,
636
+ viewportWidth,
637
+ rtl,
638
+ });
478
639
  hScrollRef.current?.scrollTo?.({ x, animated });
479
640
  // Keep the pinned axis strip in step even where programmatic scrolls
480
641
  // don't emit onScroll.
@@ -500,11 +661,11 @@ export function GanttView({
500
661
 
501
662
  const handleCanvasLayout = useCallback((event: any) => {
502
663
  const width = event?.nativeEvent?.layout?.width;
503
- if (typeof width !== "number") return;
664
+ if (typeof width !== "number" || !Number.isFinite(width) || width <= 0) return;
504
665
  canvasViewportRef.current = width;
505
- const pending = pendingRtlAnchorRef.current;
666
+ const pending = pendingAnchorRef.current;
506
667
  if (pending) {
507
- pendingRtlAnchorRef.current = null;
668
+ pendingAnchorRef.current = null;
508
669
  scrollToDateRef.current(pending, false);
509
670
  }
510
671
  }, []);
@@ -515,11 +676,14 @@ export function GanttView({
515
676
  const webDragEnabled = dragEnabled && hasDom;
516
677
  const nativeDragEnabled = dragEnabled && !isWeb;
517
678
 
518
- const [drag, setDrag] = useState<{ id: string; dx: number; dayDelta: number } | null>(null);
679
+ // Sessions key on the SPAN key, not the task id — a row can carry several
680
+ // spans and only the grabbed one moves. For a single-span row the span key
681
+ // IS the task id, so this is the same value it always was.
682
+ const [drag, setDrag] = useState<{ key: string; dx: number; dayDelta: number } | null>(null);
519
683
  // Edge-resize session (web pointer only): logicalDx is clamped so the bar
520
684
  // never shrinks below one inclusive day.
521
685
  const [resize, setResize] = useState<{
522
- id: string;
686
+ key: string;
523
687
  edge: GanttResizeEdge;
524
688
  logicalDx: number;
525
689
  dayDelta: number;
@@ -529,6 +693,8 @@ export function GanttView({
529
693
  const dragCleanupRef = useRef<(() => void) | null>(null);
530
694
  const nativeSessionRef = useRef<{
531
695
  task: GanttTask;
696
+ ref: GanttSpanRef;
697
+ span: GanttSpan;
532
698
  startX: number;
533
699
  dragging: boolean;
534
700
  liftTimer: ReturnType<typeof setTimeout> | null;
@@ -544,10 +710,8 @@ export function GanttView({
544
710
  }, []);
545
711
 
546
712
  const commitDrag = useCallback(
547
- (task: GanttTask, dayDelta: number) => {
548
- if (dayDelta !== 0) {
549
- onItemMove?.(task, shiftSpanByDays({ start: task.start, end: task.end }, dayDelta));
550
- }
713
+ (task: GanttTask, ref: GanttSpanRef, span: GanttSpan, dayDelta: number) => {
714
+ if (dayDelta !== 0) onItemMove?.(task, shiftSpanByDays(span, dayDelta), ref);
551
715
  },
552
716
  [onItemMove],
553
717
  );
@@ -558,6 +722,7 @@ export function GanttView({
558
722
  const handleBarPointerDown = useCallback(
559
723
  (
560
724
  task: GanttTask,
725
+ ref: GanttSpanRef,
561
726
  e: {
562
727
  button?: number;
563
728
  clientX: number;
@@ -567,6 +732,8 @@ export function GanttView({
567
732
  },
568
733
  ) => {
569
734
  if (!webDragEnabled) return;
735
+ const span = boundedSpan(ref);
736
+ if (!span) return;
570
737
  if (e.button !== undefined && e.button !== 0) return;
571
738
  e.stopPropagation?.();
572
739
  dragCleanupRef.current?.();
@@ -588,7 +755,7 @@ export function GanttView({
588
755
  active = true;
589
756
  prevUserSelect = document.body.style.userSelect;
590
757
  document.body.style.userSelect = "none";
591
- setDrag({ id: task.id, dx, dayDelta: toDayDelta(dx) });
758
+ setDrag({ key: ref.key, dx, dayDelta: toDayDelta(dx) });
592
759
  };
593
760
 
594
761
  const teardown = () => {
@@ -616,7 +783,7 @@ export function GanttView({
616
783
  activate(dx);
617
784
  return;
618
785
  }
619
- setDrag({ id: task.id, dx, dayDelta: toDayDelta(dx) });
786
+ setDrag({ key: ref.key, dx, dayDelta: toDayDelta(dx) });
620
787
  };
621
788
 
622
789
  const onUp = (ue: PointerEvent) => {
@@ -625,7 +792,7 @@ export function GanttView({
625
792
  teardown();
626
793
  if (!wasActive) return;
627
794
  suppressNextClick();
628
- commitDrag(task, dayDelta);
795
+ commitDrag(task, ref, span, dayDelta);
629
796
  };
630
797
 
631
798
  const onKey = (ke: KeyboardEvent) => {
@@ -663,14 +830,18 @@ export function GanttView({
663
830
  // mapping (no hit-testing needed), so this is a full best-effort path —
664
831
  // scroll locks once the drag activates.
665
832
  const handleNativeGrant = useCallback(
666
- (task: GanttTask, e: { nativeEvent?: { pageX?: number } }) => {
833
+ (task: GanttTask, ref: GanttSpanRef, e: { nativeEvent?: { pageX?: number } }) => {
667
834
  if (!nativeDragEnabled) return;
835
+ const span = boundedSpan(ref);
836
+ if (!span) return;
668
837
  // Long-press lift (Kanban/Calendar parity): a stationary hold arms the
669
838
  // drag and locks scroll BEFORE any movement, because on iOS the
670
839
  // ScrollView's pan gesture is recognized natively and would win any
671
840
  // move-threshold race against the JS responder system.
672
841
  const session = {
673
842
  task,
843
+ ref,
844
+ span,
674
845
  startX: e.nativeEvent?.pageX ?? 0,
675
846
  dragging: false,
676
847
  liftTimer: null as ReturnType<typeof setTimeout> | null,
@@ -679,7 +850,7 @@ export function GanttView({
679
850
  session.liftTimer = null;
680
851
  if (nativeSessionRef.current !== session) return;
681
852
  session.dragging = true;
682
- setDrag({ id: task.id, dx: 0, dayDelta: 0 });
853
+ setDrag({ key: ref.key, dx: 0, dayDelta: 0 });
683
854
  }, TOUCH_LONG_PRESS_MS);
684
855
  nativeSessionRef.current = session;
685
856
  },
@@ -701,7 +872,7 @@ export function GanttView({
701
872
  return;
702
873
  }
703
874
  setDrag({
704
- id: session.task.id,
875
+ key: session.ref.key,
705
876
  dx,
706
877
  dayDelta: dirSignRef.current * dxToDayDelta(dx, pxPerDay),
707
878
  });
@@ -723,7 +894,7 @@ export function GanttView({
723
894
  const dayDelta =
724
895
  dirSignRef.current * dxToDayDelta((e.nativeEvent?.pageX ?? 0) - session.startX, pxPerDay);
725
896
  setDrag(null);
726
- commitDrag(session.task, dayDelta);
897
+ commitDrag(session.task, session.ref, session.span, dayDelta);
727
898
  },
728
899
  [onItemPress, pxPerDay, commitDrag],
729
900
  );
@@ -742,6 +913,7 @@ export function GanttView({
742
913
  const handleResizePointerDown = useCallback(
743
914
  (
744
915
  task: GanttTask,
916
+ ref: GanttSpanRef,
745
917
  edge: GanttResizeEdge,
746
918
  e: {
747
919
  button?: number;
@@ -751,12 +923,13 @@ export function GanttView({
751
923
  },
752
924
  ) => {
753
925
  if (!webDragEnabled) return;
926
+ const span = boundedSpan(ref);
927
+ if (!span) return;
754
928
  if (e.button !== undefined && e.button !== 0) return;
755
929
  // A handle press must not start a whole-bar move session.
756
930
  e.stopPropagation?.();
757
931
  dragCleanupRef.current?.();
758
932
 
759
- const span: GanttSpan = { start: task.start, end: task.end };
760
933
  const maxShrinkPx = (spanInclusiveDays(span) - 1) * pxPerDay;
761
934
  const startX = e.clientX;
762
935
  const sign = dirSignRef.current;
@@ -793,7 +966,7 @@ export function GanttView({
793
966
  prevUserSelect = document.body.style.userSelect;
794
967
  document.body.style.userSelect = "none";
795
968
  }
796
- setResize({ id: task.id, edge, logicalDx: clampLogicalDx(dx), dayDelta: toDayDelta(dx) });
969
+ setResize({ key: ref.key, edge, logicalDx: clampLogicalDx(dx), dayDelta: toDayDelta(dx) });
797
970
  };
798
971
 
799
972
  const onUp = (ue: PointerEvent) => {
@@ -802,9 +975,7 @@ export function GanttView({
802
975
  teardown();
803
976
  if (!wasActive) return;
804
977
  suppressNextClick();
805
- if (dayDelta !== 0) {
806
- onItemMove?.(task, resizeSpanByDays(span, edge, dayDelta));
807
- }
978
+ if (dayDelta !== 0) onItemMove?.(task, resizeSpanByDays(span, edge, dayDelta), ref);
808
979
  };
809
980
 
810
981
  const onKey = (ke: KeyboardEvent) => {
@@ -832,9 +1003,12 @@ export function GanttView({
832
1003
  );
833
1004
 
834
1005
  const getBarDragProps = useCallback(
835
- (task: GanttTask): Record<string, unknown> => {
1006
+ (task: GanttTask, ref: GanttSpanRef): Record<string, unknown> => {
1007
+ // An open-ended span has no second edge to hold a duration against, so
1008
+ // it gets no session at all rather than a drag that invents one.
1009
+ if (!boundedSpan(ref)) return {};
836
1010
  if (webDragEnabled) {
837
- return { onPointerDown: (e: any) => handleBarPointerDown(task, e) };
1011
+ return { onPointerDown: (e: any) => handleBarPointerDown(task, ref, e) };
838
1012
  }
839
1013
  if (nativeDragEnabled) {
840
1014
  return {
@@ -842,7 +1016,7 @@ export function GanttView({
842
1016
  onStartShouldSetResponder: () => true,
843
1017
  onMoveShouldSetResponder: () => true,
844
1018
  onResponderTerminationRequest: () => !nativeSessionRef.current?.dragging,
845
- onResponderGrant: (e: any) => handleNativeGrant(task, e),
1019
+ onResponderGrant: (e: any) => handleNativeGrant(task, ref, e),
846
1020
  onResponderMove: handleNativeMove,
847
1021
  onResponderRelease: handleNativeRelease,
848
1022
  onResponderTerminate: handleNativeTerminate,
@@ -863,15 +1037,17 @@ export function GanttView({
863
1037
 
864
1038
  // ── Keyboard rescheduling (focused bar, discrete jumps — LC-25) ──────────
865
1039
  const handleBarKeyboardMove = useCallback(
866
- (task: GanttTask, key: "ArrowLeft" | "ArrowRight", shiftKey: boolean) => {
1040
+ (task: GanttTask, ref: GanttSpanRef, key: "ArrowLeft" | "ArrowRight", shiftKey: boolean) => {
1041
+ const span = boundedSpan(ref);
1042
+ if (!span) return;
867
1043
  const physical = key === "ArrowRight" ? 1 : -1;
868
1044
  // Physical arrow direction maps to a logical day delta (inverts in RTL).
869
1045
  const dayDelta = physical * dirSignRef.current * (shiftKey ? 7 : 1);
870
- const next = shiftSpanByDays({ start: task.start, end: task.end }, dayDelta);
871
- onItemMove?.(task, next);
1046
+ const next = shiftSpanByDays(span, dayDelta);
1047
+ onItemMove?.(task, next, ref);
872
1048
  setAnnouncement(
873
1049
  withInterp(t("{{title}} moved to {{range}}"), {
874
- title: task.title,
1050
+ title: ref.span.title ?? task.title,
875
1051
  range: formatSpanLabel(next),
876
1052
  }),
877
1053
  );
@@ -879,21 +1055,183 @@ export function GanttView({
879
1055
  [onItemMove, t],
880
1056
  );
881
1057
 
1058
+ // ── Controlled marker (a playhead the caller owns) ───────────────────────
1059
+ const markerDragEnabled =
1060
+ (enableMarkerDrag ?? onMarkerDateChange !== undefined) && onMarkerDateChange !== undefined;
1061
+ const [markerDragging, setMarkerDragging] = useState(false);
1062
+ // The date the scrub STARTED from. Every emitted date is that date plus a
1063
+ // whole-day delta, never the last emitted one — otherwise a controlled
1064
+ // caller that applies each scrub would compound its own updates.
1065
+ const markerGrantRef = useRef<Date | null>(null);
1066
+ const markerEmittedRef = useRef<number | null>(null);
1067
+ const markerSessionRef = useRef<{ startX: number } | null>(null);
1068
+
1069
+ /** Emit a whole-day offset from the grant date, once per day crossed. */
1070
+ const emitMarker = useCallback(
1071
+ (dayDelta: number) => {
1072
+ const base = markerGrantRef.current;
1073
+ if (!base) return;
1074
+ const next = addDays(base, dayDelta);
1075
+ if (markerEmittedRef.current === next.getTime()) return;
1076
+ markerEmittedRef.current = next.getTime();
1077
+ onMarkerDateChange?.(next);
1078
+ },
1079
+ [onMarkerDateChange],
1080
+ );
1081
+
1082
+ const openMarkerSession = useCallback(() => {
1083
+ if (markerDate === undefined) return false;
1084
+ const base = startOfDay(markerDate);
1085
+ markerGrantRef.current = base;
1086
+ markerEmittedRef.current = base.getTime();
1087
+ return true;
1088
+ }, [markerDate]);
1089
+
1090
+ // Web: the bar drag's session contract on a single control — travel
1091
+ // threshold, 1:1 tracking, Escape restores the date the drag started from.
1092
+ const handleMarkerPointerDown = useCallback(
1093
+ (e: { button?: number; clientX: number; stopPropagation?: () => void }) => {
1094
+ if (!markerDragEnabled || !hasDom) return;
1095
+ if (e.button !== undefined && e.button !== 0) return;
1096
+ if (!openMarkerSession()) return;
1097
+ e.stopPropagation?.();
1098
+ dragCleanupRef.current?.();
1099
+
1100
+ const startX = e.clientX;
1101
+ let active = false;
1102
+ let ended = false;
1103
+ let prevUserSelect = "";
1104
+ const toDayDelta = (dx: number) => dirSignRef.current * dxToDayDelta(dx, pxPerDay);
1105
+
1106
+ const teardown = () => {
1107
+ if (ended) return;
1108
+ ended = true;
1109
+ window.removeEventListener("pointermove", onMove);
1110
+ window.removeEventListener("pointerup", onUp);
1111
+ window.removeEventListener("keydown", onKey, true);
1112
+ if (active) document.body.style.userSelect = prevUserSelect;
1113
+ setMarkerDragging(false);
1114
+ markerGrantRef.current = null;
1115
+ dragCleanupRef.current = null;
1116
+ };
1117
+
1118
+ const onMove = (me: PointerEvent) => {
1119
+ const dx = me.clientX - startX;
1120
+ if (!active) {
1121
+ if (Math.abs(dx) < DRAG_THRESHOLD_PX) return;
1122
+ active = true;
1123
+ prevUserSelect = document.body.style.userSelect;
1124
+ document.body.style.userSelect = "none";
1125
+ setMarkerDragging(true);
1126
+ }
1127
+ emitMarker(toDayDelta(dx));
1128
+ };
1129
+
1130
+ const onUp = () => teardown();
1131
+
1132
+ const onKey = (ke: KeyboardEvent) => {
1133
+ if (ke.key !== "Escape" || !active) return;
1134
+ ke.stopPropagation();
1135
+ emitMarker(0);
1136
+ teardown();
1137
+ };
1138
+
1139
+ window.addEventListener("pointermove", onMove);
1140
+ window.addEventListener("pointerup", onUp);
1141
+ window.addEventListener("keydown", onKey, true);
1142
+ dragCleanupRef.current = teardown;
1143
+ },
1144
+ [markerDragEnabled, hasDom, openMarkerSession, emitMarker, pxPerDay],
1145
+ );
1146
+
1147
+ // Native: the axis strip does not scroll, so the handle can claim the touch
1148
+ // immediately — no long-press lift to arbitrate against a scroller.
1149
+ const markerNativeProps = useMemo<Record<string, unknown>>(() => {
1150
+ if (!markerDragEnabled || isWeb) return {};
1151
+ return {
1152
+ onStartShouldSetResponder: () => true,
1153
+ onMoveShouldSetResponder: () => true,
1154
+ onResponderTerminationRequest: () => false,
1155
+ onResponderGrant: (e: any) => {
1156
+ if (!openMarkerSession()) return;
1157
+ markerSessionRef.current = { startX: e?.nativeEvent?.pageX ?? 0 };
1158
+ setMarkerDragging(true);
1159
+ },
1160
+ onResponderMove: (e: any) => {
1161
+ const session = markerSessionRef.current;
1162
+ if (!session) return;
1163
+ const dx = (e?.nativeEvent?.pageX ?? 0) - session.startX;
1164
+ emitMarker(dirSignRef.current * dxToDayDelta(dx, pxPerDay));
1165
+ },
1166
+ onResponderRelease: () => {
1167
+ markerSessionRef.current = null;
1168
+ markerGrantRef.current = null;
1169
+ setMarkerDragging(false);
1170
+ },
1171
+ onResponderTerminate: () => {
1172
+ markerSessionRef.current = null;
1173
+ markerGrantRef.current = null;
1174
+ setMarkerDragging(false);
1175
+ },
1176
+ };
1177
+ }, [markerDragEnabled, openMarkerSession, emitMarker, pxPerDay]);
1178
+
1179
+ const handleMarkerKeyboardMove = useCallback(
1180
+ (key: "ArrowLeft" | "ArrowRight", shiftKey: boolean) => {
1181
+ if (!markerDragEnabled || markerDate === undefined) return;
1182
+ const physical = key === "ArrowRight" ? 1 : -1;
1183
+ const dayDelta = physical * dirSignRef.current * (shiftKey ? 7 : 1);
1184
+ const next = addDays(startOfDay(markerDate), dayDelta);
1185
+ markerEmittedRef.current = next.getTime();
1186
+ onMarkerDateChange?.(next);
1187
+ setAnnouncement(
1188
+ withInterp(t("Marker moved to {{date}}"), { date: formatAbsoluteDate(next) }),
1189
+ );
1190
+ },
1191
+ [markerDragEnabled, markerDate, onMarkerDateChange, t],
1192
+ );
1193
+
1194
+ const markerX = markerDate === undefined ? null : dateLineX(range, pxPerDay, markerDate);
1195
+ const markerName = markerLabel ?? t("Marker");
1196
+
882
1197
  // ── Bar rendering ─────────────────────────────────────────────────────────
883
- const renderBar = (task: GanttTask) => {
884
- const geom = barGeometry(task, range.start, pxPerDay);
885
- const activeDrag = drag && drag.id === task.id ? drag : null;
1198
+
1199
+ /**
1200
+ * The bar's spoken name. Bounded spans keep the exact wording they had
1201
+ * ("Design review, Mar 10 – Mar 12"); the open shapes say which end is
1202
+ * open in words, because "…" is silence to a screen reader.
1203
+ */
1204
+ const spanAccessibleName = (title: string, span: GanttOpenSpan): string => {
1205
+ if (isBoundedSpan(span)) {
1206
+ return ganttTaskAccessibleLabel({ title, start: span.start, end: span.end });
1207
+ }
1208
+ const parts = openSpanLabelParts(span);
1209
+ if (parts.start !== undefined) {
1210
+ return withInterp(t("{{title}}, from {{start}}, no end"), { title, start: parts.start });
1211
+ }
1212
+ if (parts.end !== undefined) {
1213
+ return withInterp(t("{{title}}, until {{end}}, no start"), { title, end: parts.end });
1214
+ }
1215
+ return withInterp(t("{{title}}, always, no dates"), { title });
1216
+ };
1217
+
1218
+ const renderBar = (task: GanttTask, ref: GanttSpanRef) => {
1219
+ const span = ref.span;
1220
+ const barKey = ref.key;
1221
+ const bounded = boundedSpan(ref);
1222
+ const geom = openBarGeometry(span, range, pxPerDay);
1223
+ const activeDrag = drag && drag.key === barKey ? drag : null;
886
1224
  const dragging = activeDrag !== null;
887
- const activeResize = resize && resize.id === task.id ? resize : null;
1225
+ const activeResize = resize && resize.key === barKey ? resize : null;
888
1226
  const resizing = activeResize !== null;
889
1227
  const sessionLive = dragging || resizing;
890
1228
  const dayDelta = activeDrag?.dayDelta ?? activeResize?.dayDelta ?? 0;
891
- const span: GanttSpan = { start: task.start, end: task.end };
892
- const previewSpan = activeDrag
893
- ? shiftSpanByDays(span, dayDelta)
894
- : activeResize
895
- ? resizeSpanByDays(span, activeResize.edge, activeResize.dayDelta)
896
- : null;
1229
+ const previewSpan =
1230
+ bounded && activeDrag
1231
+ ? shiftSpanByDays(bounded, dayDelta)
1232
+ : bounded && activeResize
1233
+ ? resizeSpanByDays(bounded, activeResize.edge, activeResize.dayDelta)
1234
+ : null;
897
1235
  const previewGeom = previewSpan ? barGeometry(previewSpan, range.start, pxPerDay) : null;
898
1236
  // Live geometry: a move rides a pure transform; a resize tracks the
899
1237
  // dragged edge 1:1 (the opposite edge stays put in both directions).
@@ -910,19 +1248,35 @@ export function GanttView({
910
1248
  : activeResize && activeResize.edge === "start"
911
1249
  ? activeResize.logicalDx * dirSign
912
1250
  : 0;
1251
+ const barColor = span.color ?? task.color;
1252
+ const barIntent = span.intent ?? task.intent;
1253
+ const barTitle = span.title ?? task.title;
913
1254
  const fill =
914
- task.color ?? (task.intent && task.intent !== "accent" ? "$color9" : "$accentBackground");
915
- const progress = clampProgress(task.progress);
916
- const showLabel = liveWidth >= 44;
917
- const isBarFocused = focusedBarId === task.id;
1255
+ barColor ?? (barIntent && barIntent !== "accent" ? "$color9" : "$accentBackground");
1256
+ // Progress is a fraction of a DURATION; an open span has none, so a bar
1257
+ // that claimed 40% of forever would be inventing the denominator.
1258
+ const progress = bounded ? clampProgress(span.progress ?? task.progress) : null;
1259
+ const showLabel = !geom.eternal && liveWidth >= 44;
1260
+ const isBarFocused = focusedBarId === barKey;
918
1261
  const showHandles = hoveredId === task.id || sessionLive;
919
1262
  // Edge handles map physical sides to logical edges (mirrored under RTL).
920
- const handleWidth = geom.width >= 24 ? 8 : geom.width >= 12 ? 4 : 0;
1263
+ const handleWidth = bounded ? (geom.width >= 24 ? 8 : geom.width >= 12 ? 4 : 0) : 0;
921
1264
  const physicalLeftEdge: GanttResizeEdge = rtl ? "end" : "start";
922
1265
  const physicalRightEdge: GanttResizeEdge = rtl ? "start" : "end";
1266
+ // An open END is physically on the right in LTR and on the left in RTL —
1267
+ // the chevron follows the mirror, and so does which glyph it is.
1268
+ const openStartSide = rtl ? { right: 2 } : { left: 2 };
1269
+ const openEndSide = rtl ? { left: 2 } : { right: 2 };
1270
+ const squareStart = rtl
1271
+ ? { borderTopRightRadius: 0, borderBottomRightRadius: 0 }
1272
+ : { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 };
1273
+ const squareEnd = rtl
1274
+ ? { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }
1275
+ : { borderTopRightRadius: 0, borderBottomRightRadius: 0 };
1276
+ const height = geom.eternal ? railHeight : barHeight;
923
1277
 
924
1278
  return (
925
- <BarIntentWrap key={task.id} intent={task.color ? undefined : task.intent}>
1279
+ <BarIntentWrap key={barKey} intent={barColor ? undefined : barIntent}>
926
1280
  {/* Snapped drop preview (the raw bar tracks the pointer 1:1). */}
927
1281
  {sessionLive && dayDelta !== 0 && previewGeom && (
928
1282
  <YStack
@@ -937,23 +1291,26 @@ export function GanttView({
937
1291
  backgroundColor="$accentBackground"
938
1292
  opacity={0.25}
939
1293
  pointerEvents="none"
940
- {...{ "data-gantt-drop-preview": task.id }}
1294
+ {...{ "data-gantt-drop-preview": barKey }}
941
1295
  />
942
1296
  )}
943
1297
  <YStack
944
1298
  position="absolute"
945
1299
  {...canvasPos(geom.x)}
946
1300
  width={liveWidth}
947
- top={barInset}
948
- height={barHeight}
1301
+ top={barInset + (barHeight - height) / 2}
1302
+ height={height}
949
1303
  x={liveTransformX}
950
1304
  zIndex={sessionLive ? 3 : 1}
951
1305
  {...knobProps.borderRadius}
1306
+ {...(geom.openStart ? squareStart : null)}
1307
+ {...(geom.openEnd ? squareEnd : null)}
952
1308
  backgroundColor={fill as any}
953
- opacity={sessionLive ? 0.85 : 1}
954
- hoverStyle={{ opacity: sessionLive ? 0.85 : 0.9 }}
1309
+ // A rail reads as chrome, not as a duration somebody set.
1310
+ opacity={sessionLive ? 0.85 : geom.eternal ? 0.55 : 1}
1311
+ hoverStyle={{ opacity: sessionLive ? 0.85 : geom.eternal ? 0.7 : 0.9 }}
955
1312
  pressStyle={{ opacity: 0.8 }}
956
- cursor={webDragEnabled ? "grab" : onItemPress ? "pointer" : undefined}
1313
+ cursor={bounded && webDragEnabled ? "grab" : onItemPress ? "pointer" : undefined}
957
1314
  justifyContent="center"
958
1315
  // LC-25: committed/reverted geometry jumps tween on web; the live
959
1316
  // drag/resize tracking stays 1:1 (no transition while the session
@@ -961,15 +1318,15 @@ export function GanttView({
961
1318
  transition={(sessionLive ? undefined : webLayoutTransition) as any}
962
1319
  role="button"
963
1320
  tabIndex={0}
964
- aria-label={ganttTaskAccessibleLabel(task)}
1321
+ aria-label={spanAccessibleName(barTitle, span)}
965
1322
  // LC-71: one ring on the bar (the perceived control), keyboard-only.
966
1323
  {...(isBarFocused && barKeyboardRing ? keyboardFocusRingProps : { outlineWidth: 0 })}
967
1324
  onFocus={() => {
968
- setFocusedBarId(task.id);
1325
+ setFocusedBarId(barKey);
969
1326
  setBarKeyboardRing(wasKeyboardFocus());
970
1327
  }}
971
1328
  onBlur={() => {
972
- setFocusedBarId((id) => (id === task.id ? null : id));
1329
+ setFocusedBarId((id) => (id === barKey ? null : id));
973
1330
  setBarKeyboardRing(false);
974
1331
  }}
975
1332
  // Tamagui forwards onHoverIn/onHoverOut at runtime; the RN-flavored
@@ -986,18 +1343,24 @@ export function GanttView({
986
1343
  onItemPress?.(task);
987
1344
  return;
988
1345
  }
989
- if ((ke.key === "ArrowLeft" || ke.key === "ArrowRight") && dragEnabled) {
1346
+ if ((ke.key === "ArrowLeft" || ke.key === "ArrowRight") && dragEnabled && bounded) {
990
1347
  ke.preventDefault?.();
991
1348
  ke.stopPropagation?.();
992
- handleBarKeyboardMove(task, ke.key, ke.shiftKey === true);
1349
+ handleBarKeyboardMove(task, ref, ke.key, ke.shiftKey === true);
993
1350
  }
994
1351
  }) as any
995
1352
  }
996
- {...{ "data-gantt-bar": task.id }}
997
- {...(nativeDragEnabled ? null : getBarDragProps(task))}
1353
+ {...{ "data-gantt-bar": barKey }}
1354
+ {...(geom.eternal ? { "data-gantt-rail": "true" } : null)}
1355
+ {...(geom.openStart || geom.openEnd
1356
+ ? {
1357
+ "data-gantt-span-open": geom.eternal ? "both" : geom.openStart ? "start" : "end",
1358
+ }
1359
+ : null)}
1360
+ {...(nativeDragEnabled ? null : getBarDragProps(task, ref))}
998
1361
  // Native drag responders own the tap → press is handled on release.
999
1362
  onPress={
1000
- nativeDragEnabled
1363
+ nativeDragEnabled && bounded
1001
1364
  ? undefined
1002
1365
  : (e) => {
1003
1366
  (e as any)?.stopPropagation?.();
@@ -1019,7 +1382,37 @@ export function GanttView({
1019
1382
  {...{ "data-gantt-progress": String(progress) }}
1020
1383
  />
1021
1384
  )}
1022
- {showLabel && <GanttBarLabel title={task.title} fill={fill} />}
1385
+ {showLabel && <GanttBarLabel title={barTitle} fill={fill} />}
1386
+
1387
+ {/* "This end is not an end" — a mark, never a cap. */}
1388
+ {geom.openStart && (
1389
+ <Text
1390
+ position="absolute"
1391
+ {...openStartSide}
1392
+ {...knobProps.label}
1393
+ color="$color12"
1394
+ opacity={0.75}
1395
+ pointerEvents="none"
1396
+ aria-hidden
1397
+ {...{ "data-gantt-open-start": "true" }}
1398
+ >
1399
+ {rtl ? "»" : "«"}
1400
+ </Text>
1401
+ )}
1402
+ {geom.openEnd && (
1403
+ <Text
1404
+ position="absolute"
1405
+ {...openEndSide}
1406
+ {...knobProps.label}
1407
+ color="$color12"
1408
+ opacity={0.75}
1409
+ pointerEvents="none"
1410
+ aria-hidden
1411
+ {...{ "data-gantt-open-end": "true" }}
1412
+ >
1413
+ {rtl ? "«" : "»"}
1414
+ </Text>
1415
+ )}
1023
1416
 
1024
1417
  {/* Live date label while dragging (honest preview of the commit). */}
1025
1418
  {previewSpan && (
@@ -1054,7 +1447,7 @@ export function GanttView({
1054
1447
  cursor="ew-resize"
1055
1448
  backgroundColor={showHandles ? "$color12" : "transparent"}
1056
1449
  opacity={showHandles ? 0.28 : 0}
1057
- onPointerDown={(e: any) => handleResizePointerDown(task, physicalLeftEdge, e)}
1450
+ onPointerDown={(e: any) => handleResizePointerDown(task, ref, physicalLeftEdge, e)}
1058
1451
  {...{ "data-gantt-resize-handle": physicalLeftEdge }}
1059
1452
  />
1060
1453
  <YStack
@@ -1066,7 +1459,7 @@ export function GanttView({
1066
1459
  cursor="ew-resize"
1067
1460
  backgroundColor={showHandles ? "$color12" : "transparent"}
1068
1461
  opacity={showHandles ? 0.28 : 0}
1069
- onPointerDown={(e: any) => handleResizePointerDown(task, physicalRightEdge, e)}
1462
+ onPointerDown={(e: any) => handleResizePointerDown(task, ref, physicalRightEdge, e)}
1070
1463
  {...{ "data-gantt-resize-handle": physicalRightEdge }}
1071
1464
  />
1072
1465
  </>
@@ -1075,7 +1468,7 @@ export function GanttView({
1075
1468
  {/* Native drag surface: responders live on a plain fill view because
1076
1469
  the bar's Tamagui pressability would clobber them (Kanban keeps
1077
1470
  its drag responders off the pressable Card the same way). */}
1078
- {nativeDragEnabled && (
1471
+ {nativeDragEnabled && bounded && (
1079
1472
  <YStack
1080
1473
  position="absolute"
1081
1474
  top={0}
@@ -1083,7 +1476,7 @@ export function GanttView({
1083
1476
  right={0}
1084
1477
  bottom={0}
1085
1478
  zIndex={5}
1086
- {...getBarDragProps(task)}
1479
+ {...getBarDragProps(task, ref)}
1087
1480
  />
1088
1481
  )}
1089
1482
  </YStack>
@@ -1092,7 +1485,7 @@ export function GanttView({
1092
1485
  };
1093
1486
 
1094
1487
  // ── Render ────────────────────────────────────────────────────────────────
1095
- const scrollLocked = drag !== null || resize !== null;
1488
+ const scrollLocked = drag !== null || resize !== null || markerDragging;
1096
1489
  const majorGridColor = "$borderColor";
1097
1490
  const minorGridColor = componentColors.divider;
1098
1491
  // Manual mirroring owns RTL inside the horizontal scrollers: force their
@@ -1267,6 +1660,60 @@ export function GanttView({
1267
1660
  backgroundColor={componentColors.divider}
1268
1661
  pointerEvents="none"
1269
1662
  />
1663
+ {/* The marker is scrubbed on the AXIS, never on the rows — a
1664
+ press inside a row stays a press on that bar. */}
1665
+ {markerX !== null && (
1666
+ <>
1667
+ <YStack
1668
+ position="absolute"
1669
+ {...canvasPos(markerX - 1)}
1670
+ top={0}
1671
+ bottom={0}
1672
+ width={2}
1673
+ backgroundColor="$color12"
1674
+ opacity={0.9}
1675
+ pointerEvents="none"
1676
+ />
1677
+ <YStack
1678
+ position="absolute"
1679
+ {...canvasPos(markerX - 12)}
1680
+ top={0}
1681
+ bottom={0}
1682
+ width={24}
1683
+ alignItems="center"
1684
+ justifyContent="flex-end"
1685
+ zIndex={4}
1686
+ cursor={markerDragEnabled ? "ew-resize" : undefined}
1687
+ role="slider"
1688
+ tabIndex={markerDragEnabled ? 0 : -1}
1689
+ aria-label={markerName}
1690
+ aria-valuetext={markerDate ? formatAbsoluteDate(markerDate) : undefined}
1691
+ {...(markerDragEnabled && hasDom
1692
+ ? { onPointerDown: (e: any) => handleMarkerPointerDown(e) }
1693
+ : null)}
1694
+ {...markerNativeProps}
1695
+ onKeyDown={
1696
+ ((ke: any) => {
1697
+ if (ke.key !== "ArrowLeft" && ke.key !== "ArrowRight") return;
1698
+ ke.preventDefault?.();
1699
+ ke.stopPropagation?.();
1700
+ handleMarkerKeyboardMove(ke.key, ke.shiftKey === true);
1701
+ }) as any
1702
+ }
1703
+ {...{ "data-gantt-marker-handle": "true" }}
1704
+ >
1705
+ <YStack
1706
+ width={11}
1707
+ height={9}
1708
+ marginBottom={2}
1709
+ borderRadius={2}
1710
+ backgroundColor="$color12"
1711
+ opacity={markerDragging ? 1 : 0.9}
1712
+ pointerEvents="none"
1713
+ />
1714
+ </YStack>
1715
+ </>
1716
+ )}
1270
1717
  </XStack>
1271
1718
  </ScrollView>
1272
1719
  </XStack>
@@ -1279,9 +1726,13 @@ export function GanttView({
1279
1726
  {/* Task label column */}
1280
1727
  <YStack width={labelWidth} flexShrink={0} {...hairlineEnd} borderColor="$borderColor">
1281
1728
  {tasks.map((task, index) => {
1282
- const selected = focusedBarId === task.id;
1729
+ const refs = resolveSpans(task);
1730
+ // A row is "selected" when any of ITS spans holds focus — for
1731
+ // a single-span row that is still `focusedBarId === task.id`.
1732
+ const selected = refs.some((ref) => ref.key === focusedBarId);
1283
1733
  const hovered = hoveredId === task.id;
1284
1734
  const last = index === tasks.length - 1;
1735
+ const firstKey = refs[0]?.key ?? task.id;
1285
1736
  return (
1286
1737
  <XStack
1287
1738
  key={task.id}
@@ -1308,12 +1759,12 @@ export function GanttView({
1308
1759
  onItemPress
1309
1760
  ? () => {
1310
1761
  if (suppressClickRef.current) return;
1311
- setFocusedBarId(task.id);
1762
+ setFocusedBarId(firstKey);
1312
1763
  setBarKeyboardRing(false);
1313
1764
  onItemPress(task);
1314
1765
  }
1315
1766
  : () => {
1316
- setFocusedBarId(task.id);
1767
+ setFocusedBarId(firstKey);
1317
1768
  setBarKeyboardRing(false);
1318
1769
  }
1319
1770
  }
@@ -1385,7 +1836,8 @@ export function GanttView({
1385
1836
 
1386
1837
  {/* Task rows */}
1387
1838
  {tasks.map((task, index) => {
1388
- const selected = focusedBarId === task.id;
1839
+ const refs = resolveSpans(task);
1840
+ const selected = refs.some((ref) => ref.key === focusedBarId);
1389
1841
  const hovered = hoveredId === task.id;
1390
1842
  const last = index === tasks.length - 1;
1391
1843
  return (
@@ -1408,11 +1860,27 @@ export function GanttView({
1408
1860
  } as Record<string, unknown>)}
1409
1861
  {...{ "data-gantt-row": task.id }}
1410
1862
  >
1411
- {renderBar(task)}
1863
+ {refs.map((ref) => renderBar(task, ref))}
1412
1864
  </XStack>
1413
1865
  );
1414
1866
  })}
1415
1867
 
1868
+ {/* Marker line (controlled — the caller owns the date) */}
1869
+ {markerX !== null && (
1870
+ <YStack
1871
+ position="absolute"
1872
+ {...canvasPos(markerX - 1)}
1873
+ top={0}
1874
+ bottom={0}
1875
+ width={2}
1876
+ backgroundColor="$color12"
1877
+ opacity={markerDragging ? 0.95 : 0.75}
1878
+ pointerEvents="none"
1879
+ zIndex={2}
1880
+ {...{ "data-gantt-marker": "true" }}
1881
+ />
1882
+ )}
1883
+
1416
1884
  {/* Today line (above bars, non-interactive) */}
1417
1885
  {todayX !== null && (
1418
1886
  <YStack