@heyramzi/ui 2.8.0 → 2.10.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 (426) hide show
  1. package/COMPONENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/agents/helpers.js +52 -0
  4. package/dist/agents/index.js +3 -0
  5. package/dist/agents/page-state.js +65 -0
  6. package/dist/agents/types.js +1 -0
  7. package/dist/agents/vendor/color.js +149 -0
  8. package/dist/agents/vendor/draw.js +548 -0
  9. package/dist/agents/vendor/engine.js +632 -0
  10. package/dist/agents/vendor/mesh.js +727 -0
  11. package/dist/agents/vendor/plastic.js +3273 -0
  12. package/dist/agents/vendor/presets.js +171 -0
  13. package/dist/agents/vendor/shapes.js +24 -0
  14. package/dist/agents/vendor/ticker.js +56 -0
  15. package/dist/agents/vendor/types.js +0 -0
  16. package/dist/agents/vendor/wear.js +1525 -0
  17. package/dist/ai/ai-button/index.js +2 -0
  18. package/dist/ai/confirmation/confirmation-context.svelte.js +12 -0
  19. package/dist/ai/confirmation/index.js +9 -0
  20. package/dist/ai/generation-progress/index.js +2 -0
  21. package/dist/ai/index.js +11 -0
  22. package/dist/ai/loading-state/index.js +3 -0
  23. package/dist/ai/prompt-input/index.js +6 -0
  24. package/dist/ai/reasoning/index.js +4 -0
  25. package/dist/ai/reasoning/reasoning-context.svelte.js +12 -0
  26. package/dist/ai/response/index.js +3 -0
  27. package/dist/ai/suggestion/index.js +3 -0
  28. package/dist/ai/thinking-dots/index.js +3 -0
  29. package/dist/ai/typing-dots/index.js +2 -0
  30. package/dist/ai/voice-waveform/index.js +3 -0
  31. package/dist/animations/text-swap.js +30 -0
  32. package/dist/brand/index.js +65 -0
  33. package/dist/canvas/card/resize.js +10 -0
  34. package/dist/canvas/edges/edge-path.js +284 -0
  35. package/dist/canvas/editor/context-menu.js +42 -0
  36. package/dist/canvas/editor/dispatch.svelte.js +157 -0
  37. package/dist/canvas/editor/editor-context.js +8 -0
  38. package/dist/canvas/editor/editor-mode.js +0 -0
  39. package/dist/canvas/editor/frame.js +87 -0
  40. package/dist/canvas/editor/hand-context.js +8 -0
  41. package/dist/canvas/editor/history-core.js +221 -0
  42. package/dist/canvas/editor/keymap.js +193 -0
  43. package/dist/canvas/editor/layout-actions.js +170 -0
  44. package/dist/canvas/editor/lock.js +65 -0
  45. package/dist/canvas/editor/rebase.js +279 -0
  46. package/dist/canvas/editor/style.js +174 -0
  47. package/dist/canvas/editor/text-field-context.js +8 -0
  48. package/dist/canvas/editor/z-order.js +82 -0
  49. package/dist/canvas/headless/alignment.js +20 -0
  50. package/dist/canvas/headless/break-apart-find.js +324 -0
  51. package/dist/canvas/headless/break-apart-pieces.js +74 -0
  52. package/dist/canvas/headless/break-apart-plan.js +378 -0
  53. package/dist/canvas/headless/break-apart-style.js +93 -0
  54. package/dist/canvas/headless/break-apart.js +206 -0
  55. package/dist/canvas/headless/canvas-cleanup.js +70 -0
  56. package/dist/canvas/headless/edge-anchors.js +134 -0
  57. package/dist/canvas/headless/edge-memo.js +28 -0
  58. package/dist/canvas/headless/edge-serialization.js +31 -0
  59. package/dist/canvas/headless/figure-geometry.js +331 -0
  60. package/dist/canvas/headless/fit-viewport.js +82 -0
  61. package/dist/canvas/headless/grid.js +96 -0
  62. package/dist/canvas/headless/icon-node.js +45 -0
  63. package/dist/canvas/headless/index.js +1 -0
  64. package/dist/canvas/headless/linkify.js +25 -0
  65. package/dist/canvas/headless/pen-marks.js +115 -0
  66. package/dist/canvas/headless/sketch.js +250 -0
  67. package/dist/canvas/headless/snap-guides.js +100 -0
  68. package/dist/canvas/headless/svg-figure.js +137 -0
  69. package/dist/canvas/headless/table-layout.js +161 -0
  70. package/dist/canvas/index.js +25 -0
  71. package/dist/canvas/lod/lod-mode.svelte.js +22 -0
  72. package/dist/canvas/lod/render-all.svelte.js +45 -0
  73. package/dist/canvas/ops/arrange.js +83 -0
  74. package/dist/canvas/ops/ops.js +318 -0
  75. package/dist/canvas/pen/colors.js +41 -0
  76. package/dist/canvas/pen/eraser.js +35 -0
  77. package/dist/canvas/pen/geometry.js +143 -0
  78. package/dist/canvas/pen/index.js +9 -0
  79. package/dist/canvas/pen/ops.js +15 -0
  80. package/dist/canvas/pen/pen-session.js +171 -0
  81. package/dist/canvas/pen/plugin.js +129 -0
  82. package/dist/canvas/pen/shapes.js +80 -0
  83. package/dist/canvas/pen/stroke-path.js +37 -0
  84. package/dist/canvas/pen/types.js +30 -0
  85. package/dist/canvas/pen/ui.js +20 -0
  86. package/dist/canvas/plugin.js +51 -0
  87. package/dist/canvas/screen/glyphs.js +24 -0
  88. package/dist/canvas/screen/labels.js +24 -0
  89. package/dist/canvas/screen/parts.js +140 -0
  90. package/dist/canvas/screen/plugin.js +71 -0
  91. package/dist/canvas/screen/types.js +44 -0
  92. package/dist/canvas/screen/ui.js +13 -0
  93. package/dist/canvas/test-utils.js +18 -0
  94. package/dist/canvas/types.js +0 -0
  95. package/dist/canvas/ui/forward-wheel.js +27 -0
  96. package/dist/canvas/ui/icons.js +133 -0
  97. package/dist/canvas/ui/place-panel.js +57 -0
  98. package/dist/canvas/ui/shortcut-tip.js +18 -0
  99. package/dist/canvas/ui/tools.js +411 -0
  100. package/dist/charts/dashboard/chart.js +1 -0
  101. package/dist/charts/dashboard/data-table.js +7 -0
  102. package/dist/charts/dashboard/index.js +13 -0
  103. package/dist/charts/dashboard/windows.js +5 -0
  104. package/dist/charts/edge-fade.js +7 -0
  105. package/dist/charts/hover-band.js +13 -0
  106. package/dist/charts/index.js +19 -0
  107. package/dist/charts/motion.js +15 -0
  108. package/dist/charts/primitives/chart-utils.js +22 -0
  109. package/dist/charts/primitives/index.js +4 -0
  110. package/dist/components/copy/clipboard.js +38 -0
  111. package/dist/components/copy/copied.svelte.js +26 -0
  112. package/dist/components/copy/index.js +3 -0
  113. package/dist/components/data-table/data-table.js +55 -0
  114. package/dist/components/data-table/index.js +7 -0
  115. package/dist/components/docs/index.js +3 -0
  116. package/dist/components/docs/scroll-spy.svelte.js +71 -0
  117. package/dist/components/field-row/index.js +1 -0
  118. package/dist/components/hold-to-confirm/index.js +1 -0
  119. package/dist/components/icon/index.js +1 -0
  120. package/dist/components/language-flag/index.js +1 -0
  121. package/dist/components/list-page/filter-section.js +4 -0
  122. package/dist/components/list-page/index.js +25 -0
  123. package/dist/components/list-page/list-table.js +5 -0
  124. package/dist/components/list-page/queue-rail.js +3 -0
  125. package/dist/components/list-page/section-host.js +8 -0
  126. package/dist/components/list-page/step.js +5 -0
  127. package/dist/components/list-page/types.js +0 -0
  128. package/dist/components/locale-switch/index.js +1 -0
  129. package/dist/components/mark/index.js +1 -0
  130. package/dist/components/open-in/index.js +1 -0
  131. package/dist/components/picker/index.js +3 -0
  132. package/dist/components/picker/trigger.js +33 -0
  133. package/dist/components/picker/types.js +0 -0
  134. package/dist/components/section-card/index.js +1 -0
  135. package/dist/components/section-header/index.js +2 -0
  136. package/dist/components/segmented/collapse.js +32 -0
  137. package/dist/components/segmented/index.js +8 -0
  138. package/dist/components/segmented/segmented.js +50 -0
  139. package/dist/components/star-rating-input/index.js +2 -0
  140. package/dist/components/svg/index.js +1 -0
  141. package/dist/components/table-view/index.js +1 -0
  142. package/dist/components/table-view/types.js +0 -0
  143. package/dist/components/tags-input/index.js +2 -0
  144. package/dist/components/tags-input/tags.js +90 -0
  145. package/dist/components/theme/index.js +3 -0
  146. package/dist/components/toolbar/index.js +3 -0
  147. package/dist/components/toolbar/toolbar-trigger.js +4 -0
  148. package/dist/editor/anchors.js +17 -0
  149. package/dist/editor/block-id.js +62 -0
  150. package/dist/editor/block-ops.js +105 -0
  151. package/dist/editor/doc-blocks.js +181 -0
  152. package/dist/editor/extensions.js +200 -0
  153. package/dist/editor/index.js +4 -0
  154. package/dist/editor/markdown-test-helpers.js +6 -0
  155. package/dist/editor/markdown.js +486 -0
  156. package/dist/editor/serializer.js +268 -0
  157. package/dist/editor/slash-items.js +104 -0
  158. package/dist/editor/youtube-url.js +27 -0
  159. package/dist/effects/animated-beam/index.js +1 -0
  160. package/dist/effects/animated-beam/path-calculator.svelte.js +42 -0
  161. package/dist/effects/animated-beam/types.js +0 -0
  162. package/dist/effects/border-beam/css.js +420 -0
  163. package/dist/effects/border-beam/presets.js +1792 -0
  164. package/dist/effects/border-beam/register-properties.js +39 -0
  165. package/dist/effects/border-beam/types.js +0 -0
  166. package/dist/effects/image-comparison/compare.js +4 -0
  167. package/dist/effects/image-reveal/index.js +1 -0
  168. package/dist/effects/image-reveal/types.js +0 -0
  169. package/dist/effects/magnetic/magnetic.js +54 -0
  170. package/dist/effects/reveal/index.js +4 -0
  171. package/dist/effects/reveal/observe.js +43 -0
  172. package/dist/effects/reveal/types.js +0 -0
  173. package/dist/effects/shader-gradient/color.js +34 -0
  174. package/dist/effects/shader-gradient/index.js +1 -0
  175. package/dist/effects/shader-gradient/renderer.js +320 -0
  176. package/dist/effects/shader-gradient/shaders.js +280 -0
  177. package/dist/effects/shader-gradient/types.js +0 -0
  178. package/dist/icons/apple-symbol-exports.js +126 -0
  179. package/dist/icons/sf-symbols.js +465 -0
  180. package/dist/icons/trades.js +34 -0
  181. package/dist/inbox/index.js +4 -0
  182. package/dist/inbox/logic.js +156 -0
  183. package/dist/inbox/types.js +46 -0
  184. package/dist/index.js +18 -0
  185. package/dist/lib/arrive.js +13 -0
  186. package/dist/lib/icon-types.js +0 -0
  187. package/dist/lib/initials.js +17 -0
  188. package/dist/lib/layout.js +1 -0
  189. package/dist/lib/panel.js +3 -0
  190. package/dist/lib/result.js +26 -0
  191. package/dist/lib/sliding-pill.js +45 -0
  192. package/dist/lib/theme-transition.js +53 -0
  193. package/dist/lib/tone.js +32 -0
  194. package/dist/lib/utils.js +5 -0
  195. package/dist/onboarding/define-tours.js +28 -0
  196. package/dist/onboarding/index.js +4 -0
  197. package/dist/onboarding/onboarding.svelte.js +15 -0
  198. package/dist/onboarding/pick.js +16 -0
  199. package/dist/onboarding/stores.js +50 -0
  200. package/dist/onboarding/types.js +1 -0
  201. package/dist/onboarding/wait.js +25 -0
  202. package/dist/panel/index.js +1 -0
  203. package/dist/panel/side-panel-types.js +0 -0
  204. package/dist/record/demo-data.js +512 -0
  205. package/dist/record/fields.js +148 -0
  206. package/dist/record/format.js +97 -0
  207. package/dist/record/gutter.js +1 -0
  208. package/dist/record/index.js +15 -0
  209. package/dist/record/kinds.js +110 -0
  210. package/dist/record/logic.js +255 -0
  211. package/dist/record/types.js +52 -0
  212. package/dist/shadcn/accordion/index.js +5 -0
  213. package/dist/shadcn/alert/index.js +5 -0
  214. package/dist/shadcn/alert-dialog/index.js +12 -0
  215. package/dist/shadcn/attachment/index.js +10 -0
  216. package/dist/shadcn/avatar/index.js +7 -0
  217. package/dist/shadcn/badge/index.js +2 -0
  218. package/dist/shadcn/breadcrumb/index.js +8 -0
  219. package/dist/shadcn/bubble/index.js +5 -0
  220. package/dist/shadcn/button/index.js +2 -0
  221. package/dist/shadcn/calendar/index.js +19 -0
  222. package/dist/shadcn/card/index.js +8 -0
  223. package/dist/shadcn/checkbox/index.js +2 -0
  224. package/dist/shadcn/code-block/index.js +9 -0
  225. package/dist/shadcn/collapsible/index.js +4 -0
  226. package/dist/shadcn/command/index.js +13 -0
  227. package/dist/shadcn/context-menu/index.js +17 -0
  228. package/dist/shadcn/dialog/index.js +11 -0
  229. package/dist/shadcn/dropdown-menu/index.js +19 -0
  230. package/dist/shadcn/dropdown-menu/menu-item.js +23 -0
  231. package/dist/shadcn/empty/index.js +7 -0
  232. package/dist/shadcn/field/index.js +12 -0
  233. package/dist/shadcn/hooks/highlighter.js +92 -0
  234. package/dist/shadcn/hooks/is-mobile.svelte.js +7 -0
  235. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +65 -0
  236. package/dist/shadcn/hover-card/index.js +5 -0
  237. package/dist/shadcn/input/index.js +3 -0
  238. package/dist/shadcn/input-otp/index.js +5 -0
  239. package/dist/shadcn/kbd/index.js +3 -0
  240. package/dist/shadcn/label/index.js +2 -0
  241. package/dist/shadcn/marker/index.js +4 -0
  242. package/dist/shadcn/message/index.js +7 -0
  243. package/dist/shadcn/message-scroller/index.js +8 -0
  244. package/dist/shadcn/popover/index.js +9 -0
  245. package/dist/shadcn/progress/index.js +2 -0
  246. package/dist/shadcn/progress/tone.js +11 -0
  247. package/dist/shadcn/radio-group/index.js +3 -0
  248. package/dist/shadcn/scroll-area/index.js +3 -0
  249. package/dist/shadcn/select/index.js +12 -0
  250. package/dist/shadcn/separator/index.js +2 -0
  251. package/dist/shadcn/sheet/index.js +11 -0
  252. package/dist/shadcn/sidebar/constants.js +6 -0
  253. package/dist/shadcn/sidebar/context.svelte.js +38 -0
  254. package/dist/shadcn/sidebar/index.js +25 -0
  255. package/dist/shadcn/skeleton/index.js +2 -0
  256. package/dist/shadcn/slider/index.js +2 -0
  257. package/dist/shadcn/sonner/index.js +1 -0
  258. package/dist/shadcn/spinner/index.js +1 -0
  259. package/dist/shadcn/switch/index.js +2 -0
  260. package/dist/shadcn/table/index.js +9 -0
  261. package/dist/shadcn/tabs/index.js +5 -0
  262. package/dist/shadcn/textarea/index.js +2 -0
  263. package/dist/shadcn/toggle/index.js +3 -0
  264. package/dist/shadcn/toggle-group/index.js +3 -0
  265. package/dist/shadcn/tooltip/context.js +8 -0
  266. package/dist/shadcn/tooltip/index.js +6 -0
  267. package/dist/shadcn/trace/index.js +9 -0
  268. package/dist/slides/bridges.js +66 -0
  269. package/dist/slides/deck-controller.svelte.js +221 -0
  270. package/dist/slides/export/capture.js +81 -0
  271. package/dist/slides/export/ffmpeg.js +85 -0
  272. package/dist/slides/export/index.js +4 -0
  273. package/dist/slides/export/launch.js +8 -0
  274. package/dist/slides/export/pdf.js +78 -0
  275. package/dist/slides/export/types.js +0 -0
  276. package/dist/slides/index.js +7 -0
  277. package/dist/slides/labels.js +26 -0
  278. package/dist/slides/layout.js +47 -0
  279. package/dist/slides/model.js +9 -0
  280. package/dist/slides/navigation.js +89 -0
  281. package/dist/slides/types.js +6 -0
  282. package/dist/status/define-statuses.js +16 -0
  283. package/dist/status/index.js +4 -0
  284. package/dist/status/status-types.js +0 -0
  285. package/dist/test-setup.js +26 -0
  286. package/dist/thread/index.js +3 -0
  287. package/dist/thread/logic.js +94 -0
  288. package/dist/thread/types.js +23 -0
  289. package/dist/work/board-order.js +109 -0
  290. package/dist/work/dates.js +19 -0
  291. package/dist/work/detail/detail-types.js +20 -0
  292. package/dist/work/docs/edited.js +39 -0
  293. package/dist/work/docs/index.js +7 -0
  294. package/dist/work/docs/labels.js +38 -0
  295. package/dist/work/docs/menu.js +121 -0
  296. package/dist/work/docs/outline.js +39 -0
  297. package/dist/work/gantt/logic.js +260 -0
  298. package/dist/work/inbox/inbox-logic.js +94 -0
  299. package/dist/work/inbox/inbox-statuses.js +86 -0
  300. package/dist/work/inbox/inbox-types.js +0 -0
  301. package/dist/work/inbox/index.js +1 -0
  302. package/dist/work/index.js +21 -0
  303. package/dist/work/kanban-drag.js +25 -0
  304. package/dist/work/kanban-types.js +0 -0
  305. package/dist/work/presets/sales.js +35 -0
  306. package/dist/work/reorder.js +10 -0
  307. package/dist/work/task-types.js +0 -0
  308. package/dist/work/time/duration.js +60 -0
  309. package/dist/work/time/time-types.js +25 -0
  310. package/dist/work/use-kanban-dnd.svelte.js +128 -0
  311. package/package.json +102 -76
  312. package/src/agents/vendor/draw.ts +4 -4
  313. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  314. package/src/canvas/ops/ops.ts +1 -1
  315. package/src/charts/area-chart.svelte +11 -1
  316. package/src/charts/bar-chart.svelte +18 -3
  317. package/src/charts/line-chart.svelte +11 -1
  318. package/src/components/data-table/data-table.ts +4 -5
  319. package/src/components/data-table/pagination.svelte +5 -3
  320. package/src/components/docs/on-this-page.svelte +82 -5
  321. package/src/components/docs/scroll-spy.svelte.ts +6 -1
  322. package/src/components/list-page/detail-header.svelte +7 -3
  323. package/src/components/list-page/empty-state.svelte +3 -1
  324. package/src/components/list-page/filter-popover.svelte +2 -2
  325. package/src/components/list-page/rail-row.svelte +5 -2
  326. package/src/components/mark/mark.svelte +2 -1
  327. package/src/components/section-card/section-card.svelte +3 -1
  328. package/src/components/section-header/section-header.svelte +5 -5
  329. package/src/components/segmented/segment.svelte +1 -1
  330. package/src/components/table-view/table-view.svelte +6 -1
  331. package/src/components/tags-input/tags-input.svelte +1 -1
  332. package/src/components/toolbar/toolbar-trigger.ts +8 -2
  333. package/src/components/toolbar/toolbar.svelte +1 -1
  334. package/src/editor/anchors.ts +28 -0
  335. package/src/editor/doc-blocks.ts +168 -0
  336. package/src/editor/doc-page.css +389 -0
  337. package/src/editor/editor.svelte +47 -5
  338. package/src/editor/extensions.ts +26 -2
  339. package/src/editor/markdown.ts +27 -2
  340. package/src/editor/slash-items.ts +18 -0
  341. package/src/icons/trades.ts +1 -1
  342. package/src/inbox/README.md +6 -4
  343. package/src/inbox/inbox.test.ts +1 -1
  344. package/src/inbox/index.ts +2 -2
  345. package/src/inbox/logic.ts +29 -5
  346. package/src/inbox/thread-inbox.svelte +219 -92
  347. package/src/inbox/types.ts +27 -1
  348. package/src/lib/initials.test.ts +25 -0
  349. package/src/lib/initials.ts +29 -0
  350. package/src/modules.test.ts +4 -64
  351. package/src/panel/README.md +1 -1
  352. package/src/record/README.md +1 -1
  353. package/src/record/demo-data.ts +4 -4
  354. package/src/record/info-list.svelte +5 -4
  355. package/src/record/next-up.svelte +1 -1
  356. package/src/record/project-list.svelte +1 -1
  357. package/src/record/record-demo.svelte +2 -2
  358. package/src/record/steps-rail.svelte +3 -3
  359. package/src/shadcn/badge/badge.svelte +1 -1
  360. package/src/shadcn/card/card-title.svelte +1 -5
  361. package/src/shadcn/empty/empty-description.svelte +1 -1
  362. package/src/shadcn/progress/progress.svelte +8 -1
  363. package/src/shadcn/table/table-cell.svelte +10 -2
  364. package/src/status/README.md +1 -1
  365. package/src/thread/README.md +2 -0
  366. package/src/thread/index.ts +9 -1
  367. package/src/thread/logic.ts +33 -3
  368. package/src/thread/thread-body.svelte +37 -0
  369. package/src/thread/thread-events.svelte +30 -0
  370. package/src/thread/thread.svelte +74 -59
  371. package/src/thread/types.ts +21 -0
  372. package/src/{tasks → work}/README.md +11 -8
  373. package/src/{tasks → work}/detail/task-thread.svelte +8 -15
  374. package/src/work/docs/doc-editor-page.svelte +194 -0
  375. package/src/work/docs/doc-menu.svelte +333 -0
  376. package/src/work/docs/docs-demo.svelte +227 -0
  377. package/src/work/docs/edited-chip.svelte +125 -0
  378. package/src/work/docs/edited.ts +54 -0
  379. package/src/work/docs/index.ts +38 -0
  380. package/src/work/docs/labels.ts +70 -0
  381. package/src/work/docs/menu.ts +127 -0
  382. package/src/work/docs/outline.ts +60 -0
  383. package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
  384. package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
  385. package/src/work/inbox/inbox-logic.ts +98 -0
  386. package/src/work/inbox/inbox-trace.svelte +58 -0
  387. package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
  388. package/src/{tasks → work}/index.ts +0 -10
  389. package/src/{tasks → work}/kanban-board.svelte +1 -1
  390. package/src/{tasks → work}/task-list.svelte +1 -1
  391. package/src/{tasks → work}/time/time-tracker.svelte +2 -5
  392. package/src/tasks/inbox/inbox-logic.ts +0 -21
  393. package/src/tasks/inbox/inbox-trace.svelte +0 -51
  394. /package/src/{tasks → work}/ambient.d.ts +0 -0
  395. /package/src/{tasks → work}/board-order.ts +0 -0
  396. /package/src/{tasks → work}/dates.ts +0 -0
  397. /package/src/{tasks → work}/demo.svelte +0 -0
  398. /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
  399. /package/src/{tasks → work}/detail/detail-types.ts +0 -0
  400. /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
  401. /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
  402. /package/src/{tasks → work}/detail/task-property.svelte +0 -0
  403. /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
  404. /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
  405. /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
  406. /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
  407. /package/src/{tasks → work}/gantt/gantt.css +0 -0
  408. /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
  409. /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
  410. /package/src/{tasks → work}/gantt/logic.ts +0 -0
  411. /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
  412. /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
  413. /package/src/{tasks → work}/inbox/index.ts +0 -0
  414. /package/src/{tasks → work}/kanban-column.svelte +0 -0
  415. /package/src/{tasks → work}/kanban-drag.ts +0 -0
  416. /package/src/{tasks → work}/kanban-types.ts +0 -0
  417. /package/src/{tasks → work}/presets/sales.ts +0 -0
  418. /package/src/{tasks → work}/reorder.ts +0 -0
  419. /package/src/{tasks → work}/task-gantt.svelte +0 -0
  420. /package/src/{tasks → work}/task-types.ts +0 -0
  421. /package/src/{tasks → work}/time/duration.ts +0 -0
  422. /package/src/{tasks → work}/time/time-chip.svelte +0 -0
  423. /package/src/{tasks → work}/time/time-dot.svelte +0 -0
  424. /package/src/{tasks → work}/time/time-ring.svelte +0 -0
  425. /package/src/{tasks → work}/time/time-types.ts +0 -0
  426. /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
@@ -117,6 +117,16 @@
117
117
  const fadeMask = $derived(fade > 0 ? `url(#${uid}-fade)` : undefined);
118
118
  const fallback = $derived(data.length > 0 ? data[data.length - 1] : null);
119
119
  const display = $derived(active ?? fallback);
120
+
121
+ // A long tick label ("1,000,000") falls back to "1M" so the left gutter holds it.
122
+ const compact = new Intl.NumberFormat(undefined, {
123
+ notation: "compact",
124
+ maximumFractionDigits: 1,
125
+ });
126
+ function valueTick(v: number): string {
127
+ const full = formatValue(v);
128
+ return full.length > 6 && Number.isFinite(v) ? compact.format(v) : full;
129
+ }
120
130
  // WHY: a dashed tail is two splines sharing the boundary point so the solid
121
131
  // and dashed segments meet without a gap.
122
132
  const hasTail = $derived(
@@ -189,7 +199,7 @@
189
199
  motion={DOMAIN_MOTION}
190
200
  >
191
201
  <Svg>
192
- <Axis placement="left" format={(v) => formatValue(Number(v))} ticks={4} />
202
+ <Axis placement="left" format={(v) => valueTick(Number(v))} ticks={4} />
193
203
  <Axis placement="bottom" format={(v) => formatX(v)} />
194
204
  <g class="hr-line-grid">
195
205
  <Axis placement="left" grid rule={false} ticks={4} format={() => ""} />
@@ -69,9 +69,8 @@ export function totalPages(total: number, pageSize: number): number {
69
69
 
70
70
  export function formatCount(visible: number, total: number, label: string): string {
71
71
  const singular = label.endsWith("s") ? label.slice(0, -1) : label;
72
- if (visible === total) {
73
- const displayLabel = visible === 1 ? singular : label;
74
- return `${total} ${displayLabel}`;
75
- }
76
- return `${visible} of ${total} ${label}`;
72
+ // The noun agrees with the total: "0 of 1 item", not "0 of 1 items".
73
+ const noun = total === 1 ? singular : label;
74
+ if (visible === total) return `${total.toLocaleString()} ${noun}`;
75
+ return `${visible.toLocaleString()} of ${total.toLocaleString()} ${noun}`;
77
76
  }
@@ -26,7 +26,9 @@
26
26
 
27
27
  {#if pages > 1}
28
28
  <div class="flex items-center justify-between border-t px-4 py-3 text-sm text-muted-foreground">
29
- <span>{start}-{end} of {total}</span>
29
+ <span class="tabular-nums"
30
+ >{start.toLocaleString()}-{end.toLocaleString()} of {total.toLocaleString()}</span
31
+ >
30
32
  <div class="flex items-center gap-1">
31
33
  {#if buildHref}
32
34
  {#if hasPrev}
@@ -38,7 +40,7 @@
38
40
  <CaretLeft class="size-4" />
39
41
  </Button>
40
42
  {/if}
41
- <span class="mx-2 tabular-nums">{page} / {pages}</span>
43
+ <span class="mx-2 tabular-nums">{page.toLocaleString()} / {pages.toLocaleString()}</span>
42
44
  {#if hasNext}
43
45
  <Button variant="outline" size="sm" icon href={buildHref(page + 1)}>
44
46
  <CaretRight class="size-4" />
@@ -58,7 +60,7 @@
58
60
  >
59
61
  <CaretLeft class="size-4" />
60
62
  </Button>
61
- <span class="mx-2 tabular-nums">{page} / {pages}</span>
63
+ <span class="mx-2 tabular-nums">{page.toLocaleString()} / {pages.toLocaleString()}</span>
62
64
  <Button
63
65
  variant="outline"
64
66
  size="sm"
@@ -17,10 +17,10 @@
17
17
  label: string;
18
18
  onselect: (id: string) => void;
19
19
  /**
20
- * `rail` is the column beside the document. `bar` is the strip that takes
21
- * its place on a narrow pane: it names where you are and opens the list.
20
+ * `rail` sits beside the document, `bar` replaces it on a narrow pane, and `ticks` marks
21
+ * each heading in the gutter with a card of headings on hover or focus.
22
22
  */
23
- variant?: "rail" | "bar";
23
+ variant?: "rail" | "bar" | "ticks";
24
24
  }
25
25
 
26
26
  // =========================
@@ -28,6 +28,10 @@
28
28
  // =========================
29
29
  let { items, active, label, onselect, variant = "rail" }: Props = $props();
30
30
 
31
+ let hovering = $state(false);
32
+ let focusing = $state(false);
33
+ let showCard = $derived(hovering || focusing);
34
+
31
35
  // =========================
32
36
  // STATE
33
37
  // =========================
@@ -101,7 +105,63 @@
101
105
  <!-- =========================== -->
102
106
  <!-- MARKUP -->
103
107
  <!-- =========================== -->
104
- {#if variant === "rail"}
108
+ {#if variant === "ticks"}
109
+ <nav
110
+ aria-label={label}
111
+ class="otp-ticks relative"
112
+ onpointerenter={() => (hovering = true)}
113
+ onpointerleave={() => (hovering = false)}
114
+ onfocusin={() => (focusing = true)}
115
+ onfocusout={(event) => {
116
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) focusing = false;
117
+ }}
118
+ >
119
+ <ul class="flex flex-col py-2">
120
+ {#each items as item (item.id)}
121
+ {@const current = item.id === active}
122
+ <li>
123
+ <button
124
+ type="button"
125
+ aria-label={item.label}
126
+ aria-current={current ? "location" : undefined}
127
+ onclick={() => pick(item.id)}
128
+ class="otp-tick group/tick flex h-3.5 w-7 items-center pl-2 any-pointer-coarse:h-6"
129
+ >
130
+ <span
131
+ class="block h-0.5 rounded-full transition-[width,background-color] duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] {current
132
+ ? 'w-3 bg-foreground'
133
+ : 'w-2 bg-foreground/25 group-hover/tick:bg-foreground/60'}"
134
+ ></span>
135
+ </button>
136
+ </li>
137
+ {/each}
138
+ </ul>
139
+ <!-- The card starts inside the nav's own box, so moving from a tick onto it never leaves
140
+ the hover. Rows are for the pointer; the ticks are the keyboard path. -->
141
+ <div
142
+ inert={!showCard}
143
+ data-open={showCard}
144
+ class="otp-card thin-scrollbar absolute left-full top-1/2 z-30 max-h-[min(70svh,28rem)] w-max min-w-44 max-w-64 overflow-y-auto rounded-xl bg-popover p-1.5 shadow-raised ring-1 ring-foreground/10"
145
+ >
146
+ {#each items as item (item.id)}
147
+ {@const current = item.id === active}
148
+ <button
149
+ type="button"
150
+ tabindex="-1"
151
+ onclick={() => pick(item.id)}
152
+ class="block w-full truncate rounded-md py-1 pr-3 text-left text-[13px] leading-5 transition-colors duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] hover:bg-foreground/6 {item.level ===
153
+ 3
154
+ ? 'pl-5'
155
+ : 'pl-2'} {current
156
+ ? 'font-medium text-foreground'
157
+ : 'text-muted-foreground hover:text-foreground'}"
158
+ >
159
+ {item.label}
160
+ </button>
161
+ {/each}
162
+ </div>
163
+ </nav>
164
+ {:else if variant === "rail"}
105
165
  <nav aria-label={label} class="flex max-h-[calc(100svh-8rem)] flex-col gap-3">
106
166
  <!-- Set like the map's own headings on the left, so the two rails read as one. -->
107
167
  <p class="font-heading text-xs font-semibold uppercase tracking-wider text-muted-foreground">
@@ -221,9 +281,26 @@
221
281
  );
222
282
  }
223
283
 
284
+ /* Opacity and a short slide, no scale: the card is anchored to a line of ticks. */
285
+ .otp-card {
286
+ opacity: 0;
287
+ translate: -4px -50%;
288
+ pointer-events: none;
289
+ transition:
290
+ opacity var(--duration-fast) var(--ease-smooth-out),
291
+ translate var(--duration-fast) var(--ease-smooth-out);
292
+ }
293
+
294
+ .otp-card[data-open="true"] {
295
+ opacity: 1;
296
+ translate: 0 -50%;
297
+ pointer-events: auto;
298
+ }
299
+
224
300
  @media (prefers-reduced-motion: reduce) {
225
301
  .otp-marker.is-ready,
226
- .otp-progress {
302
+ .otp-progress,
303
+ .otp-card {
227
304
  transition: none;
228
305
  }
229
306
  }
@@ -3,7 +3,12 @@
3
3
  * side rail and the compact bar, so the two can never disagree.
4
4
  */
5
5
 
6
- export type DocHeading = { id: string; label: string };
6
+ export type DocHeading = {
7
+ id: string;
8
+ label: string;
9
+ /** 2 or 3. A list with no levels is flat; the edge ticks indent a 3 under its 2. */
10
+ level?: 2 | 3;
11
+ };
7
12
 
8
13
  /**
9
14
  * A heading's offset in scroller coordinates. A framed document must supply its own, since
@@ -77,14 +77,18 @@
77
77
  flex sibling it wrapped to its own row on every two-line title. -->
78
78
  <!-- No text-balance: it evens the lines out, so a two-line title stopped
79
79
  short of the corner instead of running the full width. -->
80
- <h2 class="min-w-0 font-heading text-lg font-semibold leading-snug tracking-tight">
80
+ <h2
81
+ class="min-w-0 font-heading text-lg [overflow-wrap:anywhere] font-semibold leading-snug tracking-tight"
82
+ >
81
83
  {title}{#if badge}<span class="ms-2 inline-flex align-middle">{@render badge()}</span>{/if}
82
84
  </h2>
83
85
  {#if facts.length > 0}
84
86
  <!-- Each dot hangs off the fact it leads, so a wrapped line's dot stays
85
87
  in the gutter. A standalone flex-item dot dangled on the line above. -->
86
88
  <div class="mt-1 overflow-hidden">
87
- <p class="-ms-3 flex flex-wrap items-center text-sm text-muted-foreground">
89
+ <p
90
+ class="-ms-3 flex flex-wrap items-center text-sm text-muted-foreground [overflow-wrap:anywhere]"
91
+ >
88
92
  {#each facts as fact, i (i)}
89
93
  <span
90
94
  class="relative ps-3 before:absolute before:start-1 before:opacity-40 before:content-['·']"
@@ -95,7 +99,7 @@
95
99
  </div>
96
100
  {/if}
97
101
  {#if note}
98
- <p class="mt-1 text-sm text-muted-foreground">{note}</p>
102
+ <p class="mt-1 text-sm text-muted-foreground [overflow-wrap:anywhere]">{note}</p>
99
103
  {/if}
100
104
  <!-- With a picture or a corner, actions sit in this column. Pinned right
101
105
  they wrapped under the picture and read like its caption. -->
@@ -45,7 +45,9 @@
45
45
  <Icon weight={iconWeight} class="size-8" />
46
46
  </Empty.Media>
47
47
  {/if}
48
- <Empty.Title class="font-heading text-lg font-semibold">{title}</Empty.Title>
48
+ <Empty.Title class="font-heading text-lg font-semibold [overflow-wrap:anywhere]"
49
+ >{title}</Empty.Title
50
+ >
49
51
  {#if description}
50
52
  <Empty.Description>{description}</Empty.Description>
51
53
  {/if}
@@ -71,9 +71,9 @@
71
71
  {/if}
72
72
  {#if hasCount}
73
73
  <span
74
- class="ml-0.5 inline-flex size-4 items-center justify-center rounded-full bg-brand/15 text-[10px] font-semibold text-brand dark:text-brand-light"
74
+ class="ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-brand/15 px-1 text-[10px] tabular-nums font-semibold text-brand dark:text-brand-light"
75
75
  >
76
- {count}
76
+ {count.toLocaleString()}
77
77
  </span>
78
78
  {/if}
79
79
  </Popover.Trigger>
@@ -122,6 +122,7 @@
122
122
  titleLines === 2 ? "line-clamp-2" : "truncate",
123
123
  selected && "text-accent-foreground",
124
124
  )}
125
+ {title}
125
126
  >
126
127
  {title}
127
128
  </span>
@@ -130,7 +131,7 @@
130
131
  {#if preview || trailing}
131
132
  <span class="flex min-w-0 items-center text-xs text-muted-foreground">
132
133
  {#if preview}
133
- <span class="truncate">{preview}</span>
134
+ <span class="truncate" title={preview}>{preview}</span>
134
135
  {/if}
135
136
  {#if trailing}
136
137
  <span class="ml-auto flex shrink-0 items-center gap-1.5 pl-2">{@render trailing()}</span
@@ -146,7 +147,9 @@
146
147
  {/if}
147
148
  {@const flexible =
148
149
  facts.length > 1 && i === (metaTruncate === "last" ? facts.length - 1 : 0)}
149
- <span class={flexible ? "truncate" : "shrink-0"}>{fact}</span>
150
+ <span class={flexible ? "truncate" : "shrink-0"} title={flexible ? fact : undefined}
151
+ >{fact}</span
152
+ >
150
153
  {/each}
151
154
  {#if metaTrail}
152
155
  <span class="ml-1.5 flex shrink-0 items-center gap-1">{@render metaTrail()}</span>
@@ -3,6 +3,7 @@
3
3
  // IMPORTS
4
4
  // =========================
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { initial } from "../../lib/initials.js";
6
7
 
7
8
  // =========================
8
9
  // TYPES
@@ -61,6 +62,6 @@
61
62
  className,
62
63
  )}
63
64
  >
64
- {label.charAt(0)}
65
+ {initial(label, "")}
65
66
  </span>
66
67
  {/if}
@@ -32,7 +32,9 @@
32
32
  <!-- =========================== -->
33
33
  <Card.Root class={cn("gap-0 py-0", className)}>
34
34
  <header class="flex min-h-14 items-center justify-between gap-3 border-b px-4 py-3 sm:px-6">
35
- <Card.Title as="h3" class="text-base font-medium">{title}</Card.Title>
35
+ <Card.Title as="h3" class="min-w-0 text-base font-medium [overflow-wrap:anywhere]"
36
+ >{title}</Card.Title
37
+ >
36
38
  {#if actions}
37
39
  <div class="flex shrink-0 items-center gap-2">{@render actions()}</div>
38
40
  {/if}
@@ -34,18 +34,18 @@
34
34
  <header class="flex items-end justify-between gap-3">
35
35
  <div class="min-w-0 space-y-1">
36
36
  <svelte:element this={`h${level}`} class="flex items-center gap-2">
37
- <SectionLabel>{title}</SectionLabel>
37
+ <SectionLabel class="min-w-0 [overflow-wrap:anywhere]">{title}</SectionLabel>
38
38
  {#if count !== undefined}
39
39
  <span
40
- class="rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
41
- aria-label={`${count} ${countLabel}`}
40
+ class="shrink-0 rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
41
+ aria-label={`${count.toLocaleString()} ${countLabel}`}
42
42
  >
43
- {count}
43
+ {count.toLocaleString()}
44
44
  </span>
45
45
  {/if}
46
46
  </svelte:element>
47
47
  {#if description}
48
- <p class="text-sm text-muted-foreground">{description}</p>
48
+ <p class="text-sm text-muted-foreground [overflow-wrap:anywhere]">{description}</p>
49
49
  {/if}
50
50
  </div>
51
51
  {#if actions}
@@ -95,7 +95,7 @@
95
95
  "group-data-[state=active]/segment:bg-brand/14 group-data-[state=active]/segment:text-brand",
96
96
  )}
97
97
  >
98
- {count}
98
+ {count.toLocaleString()}
99
99
  </span>
100
100
  {:else if done}
101
101
  <span
@@ -154,6 +154,11 @@
154
154
  function textOf(column: TableColumn<Row>, row: Row): string {
155
155
  const value = (row as Record<string, unknown>)[key(column)];
156
156
  if (column.format) return column.format(value, row);
157
+ // WHY: a raw 1284 or 0.30000000000000004 in an amount column. Only `numeric`
158
+ // columns: an unformatted year or id must print as it is.
159
+ if (column.numeric && typeof value === "number") {
160
+ return Number.isFinite(value) ? value.toLocaleString() : "";
161
+ }
157
162
  return value === null || value === undefined ? "" : String(value);
158
163
  }
159
164
 
@@ -227,7 +232,7 @@
227
232
  <Table.Cell
228
233
  class={cn(
229
234
  "align-top",
230
- column.numeric ? "text-end tabular-nums" : "whitespace-normal",
235
+ column.numeric ? "text-end tabular-nums" : "whitespace-normal [overflow-wrap:anywhere]",
231
236
  column.muted && "text-muted-foreground",
232
237
  column.interactive && "relative z-10",
233
238
  column.hideBelow && HIDE[column.hideBelow],
@@ -198,7 +198,7 @@
198
198
  removable ? "rounded-(--radius-xs) pr-0.5" : "rounded-(--radius-sm)",
199
199
  )}
200
200
  >
201
- <span class="truncate">{tag}</span>
201
+ <span class="truncate" title={tag}>{tag}</span>
202
202
  {#if removable}
203
203
  <button
204
204
  type="button"
@@ -15,9 +15,15 @@ export function toolbarTriggerClass({
15
15
  className,
16
16
  }: ToolbarTriggerOpts = {}): string {
17
17
  return cn(
18
- "inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border font-medium transition-colors active:translate-y-px",
18
+ // The outline button's edge: a real border plus the resting shadow. A `ring-1` hairline
19
+ // shares box-shadow with `shadow-btn` and drew a broken edge beside Display (26 Oct 2026).
20
+ "t-press inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border font-medium shadow-btn transition-[background-color,border-color,color] duration-quick ease-smooth-out hover:border-foreground/15 motion-reduce:transition-none",
19
21
  "h-7 px-2.5 text-xs any-pointer-coarse:h-11 any-pointer-coarse:px-3",
20
- active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground",
22
+ // Active is a selected state, so it may tint; the tint reads off the foreground and
23
+ // shows on the canvas, where bg-muted vanished.
24
+ active
25
+ ? "bg-foreground/6 text-foreground"
26
+ : "bg-card text-muted-foreground hover:bg-foreground/3 hover:text-foreground",
21
27
  className,
22
28
  );
23
29
  }
@@ -115,7 +115,7 @@
115
115
  {item.label}
116
116
  {#if item.count != null}
117
117
  <span class="ms-auto pe-2 text-xs text-muted-foreground tabular-nums"
118
- >{item.count}</span
118
+ >{item.count.toLocaleString()}</span
119
119
  >
120
120
  {/if}
121
121
  </DropdownMenu.RadioItem>
@@ -0,0 +1,28 @@
1
+ /** Heading anchors: one slug per heading, unique within a document. Pure. */
2
+
3
+ /** Lowercase, words joined by `-`, accents dropped. An empty result is `section`. */
4
+ export function slugify(text: string): string {
5
+ const slug = text
6
+ .normalize("NFKD")
7
+ .replace(/[̀-ͯ]/g, "")
8
+ .toLowerCase()
9
+ .replace(/[^\p{L}\p{N}]+/gu, "-")
10
+ .replace(/^-+|-+$/g, "");
11
+ return slug || "section";
12
+ }
13
+
14
+ /** One unique anchor per label, in order: a repeat becomes `name-2`, `name-3`. */
15
+ export function anchorIds(labels: string[]): string[] {
16
+ const seen = new Map<string, number>();
17
+ const taken = new Set<string>();
18
+ return labels.map((label) => {
19
+ const base = slugify(label);
20
+ let count = (seen.get(base) ?? 0) + 1;
21
+ let id = count === 1 ? base : `${base}-${count}`;
22
+ // A heading literally called "Setup 2" must not steal the second "Setup".
23
+ while (taken.has(id)) id = `${base}-${++count}`;
24
+ seen.set(base, count);
25
+ taken.add(id);
26
+ return id;
27
+ });
28
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Heading anchors with a hover "Copy link", and code blocks with line numbers and a language
3
+ * label. Both are decorations, so the document stays plain Tiptap JSON.
4
+ */
5
+ import { Extension } from "@tiptap/core";
6
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
7
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
+ import { anchorIds } from "./anchors.js";
9
+
10
+ const LINK_ICON = `<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M240 88.23a54.43 54.43 0 0 1-16 37L189.25 160a54.27 54.27 0 0 1-38.63 16h-.05A54.63 54.63 0 0 1 96 119.84a8 8 0 0 1 16 .45A38.62 38.62 0 0 0 150.58 160a38.4 38.4 0 0 0 27.31-11.31l34.75-34.75a38.63 38.63 0 0 0-54.63-54.63l-11 11A8 8 0 0 1 135.7 59l11-11a54.65 54.65 0 0 1 77.3 0A54.86 54.86 0 0 1 240 88.23ZM109 185.66l-11 11A38.41 38.41 0 0 1 70.6 208a38.63 38.63 0 0 1-27.29-65.94L78 107.31a38.63 38.63 0 0 1 54.63 0 8 8 0 0 0 11.31-11.31 54.65 54.65 0 0 0-77.3 0L32 130.75A54.62 54.62 0 0 0 70.56 224a54.28 54.28 0 0 0 38.64-16l11-11a8 8 0 0 0-11.2-11.34Z"/></svg>`;
11
+
12
+ export type HeadingAnchorsOptions = {
13
+ /** The word on the hover button. */
14
+ label: string;
15
+ /** Runs after the link is copied; a host shows a toast here. */
16
+ oncopy?: (url: string, id: string) => void;
17
+ };
18
+
19
+ const anchorsKey = new PluginKey("headingAnchors");
20
+
21
+ /** Gives every heading an `id` and a hover "Copy link" that copies a URL to that section. */
22
+ export const HeadingAnchors = Extension.create<HeadingAnchorsOptions>({
23
+ name: "headingAnchors",
24
+
25
+ addOptions() {
26
+ return { label: "Copy link", oncopy: undefined };
27
+ },
28
+
29
+ addProseMirrorPlugins() {
30
+ const options = this.options;
31
+ return [
32
+ new Plugin({
33
+ key: anchorsKey,
34
+ props: {
35
+ decorations(state) {
36
+ const found: { pos: number; size: number; label: string }[] = [];
37
+ state.doc.descendants((node, pos) => {
38
+ if (node.type.name !== "heading") return;
39
+ found.push({ pos, size: node.nodeSize, label: node.textContent.trim() });
40
+ });
41
+ if (found.length === 0) return DecorationSet.empty;
42
+ const ids = anchorIds(found.map((heading) => heading.label));
43
+ const decorations = found.flatMap((heading, i) => {
44
+ const id = ids[i];
45
+ return [
46
+ Decoration.node(heading.pos, heading.pos + heading.size, {
47
+ id,
48
+ class: "has-anchor",
49
+ }),
50
+ Decoration.widget(
51
+ heading.pos + heading.size - 1,
52
+ () => {
53
+ const button = document.createElement("button");
54
+ button.type = "button";
55
+ button.className = "heading-anchor";
56
+ button.contentEditable = "false";
57
+ button.innerHTML = `${LINK_ICON}<span>${options.label}</span>`;
58
+ button.addEventListener("mousedown", (event) => event.preventDefault());
59
+ button.addEventListener("click", () => {
60
+ const { origin, pathname, search } = window.location;
61
+ const url = `${origin}${pathname}${search}#${id}`;
62
+ void navigator.clipboard?.writeText(url).catch(() => {});
63
+ button.dataset.copied = "true";
64
+ setTimeout(() => delete button.dataset.copied, 1400);
65
+ options.oncopy?.(url, id);
66
+ });
67
+ return button;
68
+ },
69
+ { side: 1, key: `anchor-${id}`, ignoreSelection: true },
70
+ ),
71
+ ];
72
+ });
73
+ return DecorationSet.create(state.doc, decorations);
74
+ },
75
+ },
76
+ }),
77
+ ];
78
+ },
79
+ });
80
+
81
+ export type CodeLanguage = { value: string; label: string };
82
+
83
+ export const CODE_LANGUAGES: CodeLanguage[] = [
84
+ { value: "", label: "Plain text" },
85
+ { value: "typescript", label: "TypeScript" },
86
+ { value: "javascript", label: "JavaScript" },
87
+ { value: "svelte", label: "Svelte" },
88
+ { value: "html", label: "HTML" },
89
+ { value: "css", label: "CSS" },
90
+ { value: "json", label: "JSON" },
91
+ { value: "bash", label: "Bash" },
92
+ { value: "sql", label: "SQL" },
93
+ { value: "python", label: "Python" },
94
+ { value: "markdown", label: "Markdown" },
95
+ ];
96
+
97
+ /** "1\n2\n3" for a block of three lines: the gutter, which CSS prints beside the code. */
98
+ export function lineNumbers(code: string): string {
99
+ const count = code.split("\n").length;
100
+ return Array.from({ length: count }, (_, i) => i + 1).join("\n");
101
+ }
102
+
103
+ const codeKey = new PluginKey("codeBlockChrome");
104
+
105
+ /** Line numbers in a gutter and a language picker in the corner of every code block. */
106
+ export const CodeBlockChrome = Extension.create({
107
+ name: "codeBlockChrome",
108
+
109
+ addProseMirrorPlugins() {
110
+ return [
111
+ new Plugin({
112
+ key: codeKey,
113
+ props: {
114
+ decorations(state) {
115
+ const decorations: Decoration[] = [];
116
+ state.doc.descendants((node, pos) => {
117
+ if (node.type.name !== "codeBlock") return;
118
+ const language = typeof node.attrs.language === "string" ? node.attrs.language : "";
119
+ decorations.push(
120
+ Decoration.node(pos, pos + node.nodeSize, {
121
+ "data-lines": lineNumbers(node.textContent),
122
+ }),
123
+ Decoration.widget(
124
+ pos + 1,
125
+ (view, getPos) => {
126
+ const select = document.createElement("select");
127
+ select.className = "code-language";
128
+ select.contentEditable = "false";
129
+ select.setAttribute("aria-label", "Language");
130
+ const known = CODE_LANGUAGES.some((item) => item.value === language);
131
+ const options = known
132
+ ? CODE_LANGUAGES
133
+ : [...CODE_LANGUAGES, { value: language, label: language }];
134
+ for (const item of options) {
135
+ const option = document.createElement("option");
136
+ option.value = item.value;
137
+ option.textContent = item.label;
138
+ option.selected = item.value === language;
139
+ select.append(option);
140
+ }
141
+ select.addEventListener("mousedown", (event) => event.stopPropagation());
142
+ select.addEventListener("change", () => {
143
+ const at = (getPos() ?? 1) - 1;
144
+ const target = view.state.doc.nodeAt(at);
145
+ if (!target) return;
146
+ view.dispatch(
147
+ view.state.tr.setNodeMarkup(at, undefined, {
148
+ ...target.attrs,
149
+ language: select.value || null,
150
+ }),
151
+ );
152
+ view.focus();
153
+ });
154
+ return select;
155
+ },
156
+ { side: -1, key: `lang-${pos}-${language}`, stopEvent: () => true },
157
+ ),
158
+ );
159
+ });
160
+ return decorations.length
161
+ ? DecorationSet.create(state.doc, decorations)
162
+ : DecorationSet.empty;
163
+ },
164
+ },
165
+ }),
166
+ ];
167
+ },
168
+ });