@heyramzi/ui 1.54.2 → 2.0.1

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 (363) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +205 -13
  4. package/scripts/sync-shadcn.mjs +2 -0
  5. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  6. package/src/ambient.d.ts +8 -0
  7. package/src/brand/brand.test.ts +67 -0
  8. package/src/brand/index.ts +137 -0
  9. package/src/canvas/README.md +236 -0
  10. package/src/canvas/card/card-frame.svelte +61 -0
  11. package/src/canvas/card/card.css +39 -0
  12. package/src/canvas/card/resize-handles.svelte +119 -0
  13. package/src/canvas/card/resize.ts +33 -0
  14. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  15. package/src/canvas/chrome/chrome.css +231 -0
  16. package/src/canvas/chrome/lock-badge.svelte +18 -0
  17. package/src/canvas/chrome/side-handles.svelte +30 -0
  18. package/src/canvas/chrome/size-chip.svelte +45 -0
  19. package/src/canvas/chrome/snap-guides.svelte +69 -0
  20. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  21. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  22. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  23. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  24. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  25. package/src/canvas/edges/bend-handle.svelte +76 -0
  26. package/src/canvas/edges/edge-gradient.svelte +30 -0
  27. package/src/canvas/edges/edge-path.test.ts +58 -0
  28. package/src/canvas/edges/edge-path.ts +293 -0
  29. package/src/canvas/edges/gradient-edge.svelte +43 -0
  30. package/src/canvas/editor/context-menu.test.ts +65 -0
  31. package/src/canvas/editor/context-menu.ts +48 -0
  32. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  33. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  34. package/src/canvas/editor/editor-context.ts +62 -0
  35. package/src/canvas/editor/editor-mode.ts +67 -0
  36. package/src/canvas/editor/frame.ts +98 -0
  37. package/src/canvas/editor/hand-context.ts +15 -0
  38. package/src/canvas/editor/history-core.test.ts +239 -0
  39. package/src/canvas/editor/history-core.ts +247 -0
  40. package/src/canvas/editor/keymap.test.ts +161 -0
  41. package/src/canvas/editor/keymap.ts +151 -0
  42. package/src/canvas/editor/layout-actions.test.ts +156 -0
  43. package/src/canvas/editor/layout-actions.ts +219 -0
  44. package/src/canvas/editor/lock.test.ts +68 -0
  45. package/src/canvas/editor/lock.ts +87 -0
  46. package/src/canvas/editor/rebase.test.ts +208 -0
  47. package/src/canvas/editor/rebase.ts +272 -0
  48. package/src/canvas/editor/style.ts +184 -0
  49. package/src/canvas/editor/text-field-context.ts +41 -0
  50. package/src/canvas/editor/z-order.test.ts +75 -0
  51. package/src/canvas/editor/z-order.ts +87 -0
  52. package/src/canvas/headless/alignment.test.ts +59 -0
  53. package/src/canvas/headless/alignment.ts +23 -0
  54. package/src/canvas/headless/break-apart-find.ts +418 -0
  55. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  56. package/src/canvas/headless/break-apart-plan.ts +439 -0
  57. package/src/canvas/headless/break-apart-style.ts +106 -0
  58. package/src/canvas/headless/break-apart.ts +244 -0
  59. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  60. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  61. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  62. package/src/canvas/headless/edge-anchors.ts +143 -0
  63. package/src/canvas/headless/edge-memo.test.ts +58 -0
  64. package/src/canvas/headless/edge-memo.ts +32 -0
  65. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  66. package/src/canvas/headless/edge-serialization.ts +32 -0
  67. package/src/canvas/headless/figure-geometry.ts +293 -0
  68. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  69. package/src/canvas/headless/fit-viewport.ts +137 -0
  70. package/src/canvas/headless/grid.test.ts +34 -0
  71. package/src/canvas/headless/grid.ts +94 -0
  72. package/src/canvas/headless/icon-node.test.ts +23 -0
  73. package/src/canvas/headless/icon-node.ts +66 -0
  74. package/src/canvas/headless/index.ts +9 -0
  75. package/src/canvas/headless/linkify.test.ts +23 -0
  76. package/src/canvas/headless/linkify.ts +25 -0
  77. package/src/canvas/headless/pen-marks.ts +133 -0
  78. package/src/canvas/headless/sketch.test.ts +99 -0
  79. package/src/canvas/headless/sketch.ts +302 -0
  80. package/src/canvas/headless/snap-guides.test.ts +61 -0
  81. package/src/canvas/headless/snap-guides.ts +146 -0
  82. package/src/canvas/headless/svg-figure.test.ts +49 -0
  83. package/src/canvas/headless/svg-figure.ts +176 -0
  84. package/src/canvas/headless/table-layout.test.ts +158 -0
  85. package/src/canvas/headless/table-layout.ts +309 -0
  86. package/src/canvas/index.ts +35 -0
  87. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  88. package/src/canvas/lod/lod-switch.svelte +140 -0
  89. package/src/canvas/lod/render-all.svelte.ts +52 -0
  90. package/src/canvas/ops/arrange.ts +108 -0
  91. package/src/canvas/ops/ops.test.ts +98 -0
  92. package/src/canvas/ops/ops.ts +347 -0
  93. package/src/canvas/pen/PenField.svelte +128 -0
  94. package/src/canvas/pen/PenField.test.ts +87 -0
  95. package/src/canvas/pen/PenOverlay.svelte +172 -0
  96. package/src/canvas/pen/ShapeNode.svelte +64 -0
  97. package/src/canvas/pen/TextNode.svelte +97 -0
  98. package/src/canvas/pen/colors.ts +28 -0
  99. package/src/canvas/pen/eraser.ts +46 -0
  100. package/src/canvas/pen/geometry.ts +148 -0
  101. package/src/canvas/pen/host-field.test.svelte +13 -0
  102. package/src/canvas/pen/index.ts +40 -0
  103. package/src/canvas/pen/ops.ts +16 -0
  104. package/src/canvas/pen/pen-session.ts +177 -0
  105. package/src/canvas/pen/pen.css +149 -0
  106. package/src/canvas/pen/plugin.ts +139 -0
  107. package/src/canvas/pen/shapes.ts +104 -0
  108. package/src/canvas/pen/stroke-path.ts +54 -0
  109. package/src/canvas/pen/types.ts +96 -0
  110. package/src/canvas/pen/ui.ts +26 -0
  111. package/src/canvas/plugin.test.ts +45 -0
  112. package/src/canvas/plugin.ts +159 -0
  113. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  114. package/src/canvas/screen/PartView.svelte +176 -0
  115. package/src/canvas/screen/ScreenBody.svelte +142 -0
  116. package/src/canvas/screen/ScreenCard.svelte +32 -0
  117. package/src/canvas/screen/glyphs/button.svg +1 -0
  118. package/src/canvas/screen/glyphs/close.svg +1 -0
  119. package/src/canvas/screen/glyphs/down.svg +1 -0
  120. package/src/canvas/screen/glyphs/header.svg +1 -0
  121. package/src/canvas/screen/glyphs/hills.svg +1 -0
  122. package/src/canvas/screen/glyphs/image.svg +1 -0
  123. package/src/canvas/screen/glyphs/input.svg +1 -0
  124. package/src/canvas/screen/glyphs/list.svg +1 -0
  125. package/src/canvas/screen/glyphs/modal.svg +1 -0
  126. package/src/canvas/screen/glyphs/plus.svg +1 -0
  127. package/src/canvas/screen/glyphs/up.svg +1 -0
  128. package/src/canvas/screen/glyphs.ts +40 -0
  129. package/src/canvas/screen/labels.ts +41 -0
  130. package/src/canvas/screen/parts.ts +164 -0
  131. package/src/canvas/screen/plugin.ts +67 -0
  132. package/src/canvas/screen/screen.css +593 -0
  133. package/src/canvas/screen/types.ts +47 -0
  134. package/src/canvas/screen/ui.ts +13 -0
  135. package/src/canvas/test-utils.ts +18 -0
  136. package/src/canvas/types.ts +53 -0
  137. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  138. package/src/canvas/ui/FrameLayer.svelte +116 -0
  139. package/src/canvas/ui/PenLayer.svelte +39 -0
  140. package/src/canvas/ui/StylePanel.svelte +268 -0
  141. package/src/canvas/ui/canvas-grid.svelte +156 -0
  142. package/src/canvas/ui/canvas-ui.css +350 -0
  143. package/src/canvas/ui/forward-wheel.ts +29 -0
  144. package/src/canvas/ui/icons.ts +124 -0
  145. package/src/canvas/ui/place-panel.ts +80 -0
  146. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  147. package/src/canvas/ui/shortcut-tip.ts +17 -0
  148. package/src/canvas/ui/tools.ts +146 -0
  149. package/src/charts/README.md +104 -0
  150. package/src/charts/ambient.d.ts +4 -0
  151. package/src/charts/animated-value.css +7 -0
  152. package/src/charts/animated-value.svelte +110 -0
  153. package/src/charts/area-chart.css +29 -0
  154. package/src/charts/area-chart.svelte +204 -0
  155. package/src/charts/bar-chart.css +119 -0
  156. package/src/charts/bar-chart.svelte +275 -0
  157. package/src/charts/charts.css +201 -0
  158. package/src/charts/crosshair.svelte +35 -0
  159. package/src/charts/dashboard/chart.ts +5 -0
  160. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  161. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  162. package/src/charts/dashboard/data-panel.svelte +81 -0
  163. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  164. package/src/charts/dashboard/data-table.svelte +278 -0
  165. package/src/charts/dashboard/data-table.ts +26 -0
  166. package/src/charts/dashboard/index.ts +25 -0
  167. package/src/charts/dashboard/panel-grid.svelte +43 -0
  168. package/src/charts/dashboard/period-filter.svelte +53 -0
  169. package/src/charts/dashboard/rating-chip.svelte +35 -0
  170. package/src/charts/dashboard/stat-band.svelte +47 -0
  171. package/src/charts/dashboard/stat-tile.svelte +176 -0
  172. package/src/charts/dashboard/windows.ts +8 -0
  173. package/src/charts/donut-chart.css +56 -0
  174. package/src/charts/donut-chart.svelte +158 -0
  175. package/src/charts/edge-fade.svelte +43 -0
  176. package/src/charts/edge-fade.ts +16 -0
  177. package/src/charts/figure.css +100 -0
  178. package/src/charts/figure.svelte +122 -0
  179. package/src/charts/gauge-chart.css +94 -0
  180. package/src/charts/gauge-chart.svelte +302 -0
  181. package/src/charts/hover-band.test.ts +30 -0
  182. package/src/charts/hover-band.ts +21 -0
  183. package/src/charts/hover-focus.svelte +85 -0
  184. package/src/charts/index.ts +31 -0
  185. package/src/charts/legend.css +136 -0
  186. package/src/charts/legend.svelte +112 -0
  187. package/src/charts/line-chart.css +26 -0
  188. package/src/charts/line-chart.svelte +256 -0
  189. package/src/charts/motion.ts +19 -0
  190. package/src/charts/number-chart.css +179 -0
  191. package/src/charts/number-chart.svelte +187 -0
  192. package/src/charts/primitives/chart-container.svelte +57 -0
  193. package/src/charts/primitives/chart-style.svelte +36 -0
  194. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  195. package/src/charts/primitives/chart-utils.ts +54 -0
  196. package/src/charts/primitives/index.ts +11 -0
  197. package/src/charts/reference-line.css +18 -0
  198. package/src/charts/reference-line.svelte +31 -0
  199. package/src/charts/reveal.svelte +61 -0
  200. package/src/charts/ring-chart.css +104 -0
  201. package/src/charts/ring-chart.svelte +166 -0
  202. package/src/charts/sparkline.css +45 -0
  203. package/src/charts/sparkline.svelte +141 -0
  204. package/src/charts/stat-card.css +105 -0
  205. package/src/charts/stat-card.svelte +267 -0
  206. package/src/charts/time-series-chart.css +28 -0
  207. package/src/charts/time-series-chart.svelte +179 -0
  208. package/src/charts/trend-pill.css +38 -0
  209. package/src/charts/trend-pill.svelte +48 -0
  210. package/src/components/copy/copy-button.svelte +1 -1
  211. package/src/components/docs/doc-page.svelte +1 -1
  212. package/src/components/docs/on-this-page.svelte +2 -2
  213. package/src/components/list-page/empty-state.svelte +1 -1
  214. package/src/components/list-page/filter-popover.svelte +1 -1
  215. package/src/components/list-page/section-label.svelte +1 -1
  216. package/src/components/segmented/segment.svelte +4 -4
  217. package/src/components/theme/theme-provider.svelte +6 -4
  218. package/src/components/toolbar/index.ts +5 -0
  219. package/src/components/toolbar/toolbar.svelte +168 -0
  220. package/src/components/toolbar/toolbar.test.ts +66 -0
  221. package/src/editor/README.md +58 -0
  222. package/src/editor/ambient.d.ts +1 -0
  223. package/src/editor/block-id.ts +77 -0
  224. package/src/editor/block-ops.test.ts +237 -0
  225. package/src/editor/block-ops.ts +157 -0
  226. package/src/editor/content.css +418 -0
  227. package/src/editor/editor-toolbar.svelte +303 -0
  228. package/src/editor/editor.svelte +469 -0
  229. package/src/editor/extensions.ts +145 -0
  230. package/src/editor/image-toolbar.svelte +123 -0
  231. package/src/editor/index.ts +17 -0
  232. package/src/editor/inline-editor.svelte +261 -0
  233. package/src/editor/markdown-escaping.test.ts +158 -0
  234. package/src/editor/markdown-marks.test.ts +89 -0
  235. package/src/editor/markdown-test-helpers.ts +13 -0
  236. package/src/editor/markdown-youtube.test.ts +93 -0
  237. package/src/editor/markdown.test.ts +250 -0
  238. package/src/editor/markdown.ts +511 -0
  239. package/src/editor/renderer.svelte +92 -0
  240. package/src/editor/serializer.test.ts +133 -0
  241. package/src/editor/serializer.ts +370 -0
  242. package/src/editor/slash-items.ts +115 -0
  243. package/src/editor/slash-menu.svelte +159 -0
  244. package/src/editor/youtube-url.ts +35 -0
  245. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  246. package/src/gallery.test.ts +20 -1
  247. package/src/lib/result.ts +19 -0
  248. package/src/onboarding/README.md +120 -0
  249. package/src/onboarding/define-tours.test.ts +50 -0
  250. package/src/onboarding/define-tours.ts +31 -0
  251. package/src/onboarding/demo.svelte +86 -0
  252. package/src/onboarding/index.ts +12 -0
  253. package/src/onboarding/onboarding-provider.svelte +120 -0
  254. package/src/onboarding/onboarding.svelte.ts +24 -0
  255. package/src/onboarding/pick.test.ts +79 -0
  256. package/src/onboarding/pick.ts +36 -0
  257. package/src/onboarding/stores.test.ts +103 -0
  258. package/src/onboarding/stores.ts +58 -0
  259. package/src/onboarding/tour-card.svelte +70 -0
  260. package/src/onboarding/tour-runner.svelte +197 -0
  261. package/src/onboarding/types.ts +34 -0
  262. package/src/onboarding/wait.ts +29 -0
  263. package/src/shadcn/command/command-input.svelte +1 -1
  264. package/src/slides/Controls.svelte +138 -0
  265. package/src/slides/Deck.svelte +271 -0
  266. package/src/slides/Fragment.svelte +71 -0
  267. package/src/slides/Overview.svelte +113 -0
  268. package/src/slides/Presenter.svelte +292 -0
  269. package/src/slides/README.md +89 -0
  270. package/src/slides/Slide.svelte +179 -0
  271. package/src/slides/SlideList.svelte +19 -0
  272. package/src/slides/ambient.d.ts +1 -0
  273. package/src/slides/bridges.ts +69 -0
  274. package/src/slides/deck-controller.svelte.ts +292 -0
  275. package/src/slides/demo-slides.svelte +358 -0
  276. package/src/slides/demo.svelte +176 -0
  277. package/src/slides/export/capture.ts +113 -0
  278. package/src/slides/export/ffmpeg.ts +120 -0
  279. package/src/slides/export/index.ts +9 -0
  280. package/src/slides/export/launch.ts +10 -0
  281. package/src/slides/export/pdf.ts +152 -0
  282. package/src/slides/export/types.ts +48 -0
  283. package/src/slides/index.ts +19 -0
  284. package/src/slides/labels.ts +48 -0
  285. package/src/slides/layout.ts +51 -0
  286. package/src/slides/model.ts +47 -0
  287. package/src/slides/navigation.ts +100 -0
  288. package/src/slides/theme.css +205 -0
  289. package/src/slides/types.ts +86 -0
  290. package/src/tasks/README.md +312 -0
  291. package/src/tasks/ambient.d.ts +3 -0
  292. package/src/tasks/board-order.ts +137 -0
  293. package/src/tasks/dates.ts +31 -0
  294. package/src/tasks/define-statuses.ts +32 -0
  295. package/src/tasks/demo.svelte +389 -0
  296. package/src/tasks/detail/detail-demo.svelte +186 -0
  297. package/src/tasks/detail/detail-types.ts +54 -0
  298. package/src/tasks/detail/task-chip.svelte +28 -0
  299. package/src/tasks/detail/task-detail.svelte +315 -0
  300. package/src/tasks/detail/task-property.svelte +17 -0
  301. package/src/tasks/detail/task-thread.svelte +258 -0
  302. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  303. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  304. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  305. package/src/tasks/gantt/gantt-header.svelte +71 -0
  306. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  307. package/src/tasks/gantt/gantt.css +22 -0
  308. package/src/tasks/gantt/gantt.test.ts +77 -0
  309. package/src/tasks/gantt/logic.test.ts +163 -0
  310. package/src/tasks/gantt/logic.ts +248 -0
  311. package/src/tasks/inbox/inbox-card.svelte +283 -0
  312. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  313. package/src/tasks/inbox/inbox-logic.ts +55 -0
  314. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  315. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  316. package/src/tasks/inbox/inbox-types.ts +108 -0
  317. package/src/tasks/inbox/inbox.svelte +203 -0
  318. package/src/tasks/inbox/index.ts +23 -0
  319. package/src/tasks/index.ts +85 -0
  320. package/src/tasks/kanban-board.svelte +561 -0
  321. package/src/tasks/kanban-column.svelte +117 -0
  322. package/src/tasks/kanban-drag.ts +38 -0
  323. package/src/tasks/kanban-types.ts +65 -0
  324. package/src/tasks/presets/sales.ts +16 -0
  325. package/src/tasks/record/demo-data.ts +286 -0
  326. package/src/tasks/record/field-section.svelte +53 -0
  327. package/src/tasks/record/fields.ts +142 -0
  328. package/src/tasks/record/format.ts +87 -0
  329. package/src/tasks/record/gutter.ts +2 -0
  330. package/src/tasks/record/index.ts +54 -0
  331. package/src/tasks/record/info-list.svelte +316 -0
  332. package/src/tasks/record/kinds.ts +42 -0
  333. package/src/tasks/record/logic.ts +324 -0
  334. package/src/tasks/record/next-up.svelte +70 -0
  335. package/src/tasks/record/project-list.svelte +133 -0
  336. package/src/tasks/record/record-demo.svelte +324 -0
  337. package/src/tasks/record/record-header.svelte +70 -0
  338. package/src/tasks/record/record-shell.svelte +138 -0
  339. package/src/tasks/record/record-stats.svelte +48 -0
  340. package/src/tasks/record/record-timeline.svelte +231 -0
  341. package/src/tasks/record/record.test.ts +350 -0
  342. package/src/tasks/record/steps-rail.svelte +474 -0
  343. package/src/tasks/record/types.ts +280 -0
  344. package/src/tasks/record/where-we-are.svelte +121 -0
  345. package/src/tasks/reorder.ts +19 -0
  346. package/src/tasks/side-panel-types.ts +16 -0
  347. package/src/tasks/side-panel.svelte +193 -0
  348. package/src/tasks/status-badge.svelte +63 -0
  349. package/src/tasks/status-dot.svelte +228 -0
  350. package/src/tasks/status-icon.svelte +30 -0
  351. package/src/tasks/status-types.ts +40 -0
  352. package/src/tasks/task-gantt.svelte +230 -0
  353. package/src/tasks/task-list.svelte +470 -0
  354. package/src/tasks/task-types.ts +33 -0
  355. package/src/tasks/time/duration.ts +69 -0
  356. package/src/tasks/time/time-chip.svelte +62 -0
  357. package/src/tasks/time/time-dot.svelte +58 -0
  358. package/src/tasks/time/time-ring.svelte +74 -0
  359. package/src/tasks/time/time-tracker.svelte +565 -0
  360. package/src/tasks/time/time-types.ts +74 -0
  361. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  362. package/src/tokens/base.css +5 -5
  363. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,123 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { Trash, TextAlignLeft, TextAlignCenter, TextAlignRight } from "phosphor-svelte";
6
+
7
+ // =========================
8
+ // TYPES
9
+ // =========================
10
+ interface Props {
11
+ pos: { top: number; left: number };
12
+ onalign: (align: "left" | "center" | "right") => void;
13
+ ondelete: () => void;
14
+ }
15
+
16
+ // =========================
17
+ // PROPS
18
+ // =========================
19
+ let { pos, onalign, ondelete }: Props = $props();
20
+ </script>
21
+
22
+ <!-- =========================== -->
23
+ <!-- MARKUP -->
24
+ <!-- =========================== -->
25
+ <div
26
+ class="image-toolbar"
27
+ style="top: {pos.top}px; left: {pos.left}px;"
28
+ role="toolbar"
29
+ >
30
+ <button
31
+ type="button"
32
+ class="image-toolbar-btn"
33
+ title="Align left"
34
+ onclick={() => onalign("left")}
35
+ >
36
+ <TextAlignLeft size={14} />
37
+ </button>
38
+ <button
39
+ type="button"
40
+ class="image-toolbar-btn"
41
+ title="Align center"
42
+ onclick={() => onalign("center")}
43
+ >
44
+ <TextAlignCenter size={14} />
45
+ </button>
46
+ <button
47
+ type="button"
48
+ class="image-toolbar-btn"
49
+ title="Align right"
50
+ onclick={() => onalign("right")}
51
+ >
52
+ <TextAlignRight size={14} />
53
+ </button>
54
+ <div class="image-toolbar-divider"></div>
55
+ <button
56
+ type="button"
57
+ class="image-toolbar-btn destructive"
58
+ title="Delete image"
59
+ onclick={ondelete}
60
+ >
61
+ <Trash size={14} />
62
+ </button>
63
+ </div>
64
+
65
+ <!-- =========================== -->
66
+ <!-- STYLE -->
67
+ <!-- =========================== -->
68
+ <style>
69
+ /* ---- IMAGE TOOLBAR ---- */
70
+ .image-toolbar {
71
+ position: absolute;
72
+ z-index: 40;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 1px;
76
+ padding: 3px;
77
+ background: var(--color-popover);
78
+ border: 1px solid var(--color-border);
79
+ border-radius: var(--radius-lg);
80
+ box-shadow: var(--elev-raised);
81
+ animation: slash-enter var(--duration-fast) var(--ease-out);
82
+ }
83
+
84
+ .image-toolbar-btn {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ width: 1.75rem;
89
+ height: 1.75rem;
90
+ border: none;
91
+ border-radius: var(--radius-sm);
92
+ background: transparent;
93
+ color: var(--color-muted-foreground);
94
+ cursor: pointer;
95
+ transition: opacity var(--duration-quick) var(--ease-smooth-out), transform var(--duration-quick) var(--ease-smooth-out);
96
+ }
97
+
98
+ @media (hover: hover) and (pointer: fine) {
99
+ .image-toolbar-btn:hover {
100
+ background: var(--color-accent);
101
+ color: var(--color-foreground);
102
+ }
103
+
104
+ .image-toolbar-btn.destructive:hover {
105
+ background: oklch(from var(--color-error) l c h / 0.1);
106
+ color: var(--color-error);
107
+ }
108
+ }
109
+
110
+ .image-toolbar-divider {
111
+ width: 1px;
112
+ height: 1rem;
113
+ background: var(--color-border);
114
+ margin: 0 2px;
115
+ opacity: 0.6;
116
+ }
117
+
118
+ @media (prefers-reduced-motion: reduce) {
119
+ .image-toolbar {
120
+ animation: none;
121
+ }
122
+ }
123
+ </style>
@@ -0,0 +1,17 @@
1
+ export { default as RichEditor } from "./editor.svelte";
2
+ export { default as RichRenderer } from "./renderer.svelte";
3
+ export {
4
+ renderTiptapContent,
5
+ extractHeadings,
6
+ extractYoutubeVideos,
7
+ } from "./serializer.js";
8
+ export type {
9
+ ExtractedHeading,
10
+ ExtractedYoutubeVideo,
11
+ } from "./serializer.js";
12
+ export { BlockIdExtension, generateBlockId } from "./block-id.js";
13
+
14
+ // WHY: the markdown bridge and block-ops pull in markdown-it, so they aren't
15
+ // re-exported here. Blog pages would bundle it. Import from their subpaths:
16
+ // @heyramzi/ui/editor/markdown
17
+ // @heyramzi/ui/editor/block-ops
@@ -0,0 +1,261 @@
1
+ <script lang="ts" module>
2
+ // The tiptap side of a small in-place text field (a canvas card). It speaks ProseMirror JSON
3
+ // and hands the host a plain `InlineEditorApi`, so no tiptap type leaks out.
4
+ export type InlineDoc = Record<string, unknown>;
5
+
6
+ export type InlineCommand = "bold" | "italic" | "strike" | "bulletList" | "orderedList" | "taskList";
7
+
8
+ export type InlineEditorApi = {
9
+ toggle: (command: InlineCommand) => void;
10
+ setLink: (href: string | null) => void;
11
+ isActive: (name: string) => boolean;
12
+ linkHref: () => string;
13
+ hasSelection: () => boolean;
14
+ /** Types at the caret, replacing any selection. */
15
+ insertText: (text: string) => void;
16
+ setDoc: (doc: InlineDoc) => void;
17
+ getDoc: () => InlineDoc;
18
+ focus: () => void;
19
+ };
20
+ </script>
21
+
22
+ <script lang="ts">
23
+ // =========================
24
+ // IMPORTS
25
+ // =========================
26
+ import { Editor, Extension, Node as TiptapNode, mergeAttributes, wrappingInputRule } from "@tiptap/core";
27
+ import { StarterKit } from "@tiptap/starter-kit";
28
+ import { Placeholder } from "@tiptap/extension-placeholder";
29
+ import { untrack } from "svelte";
30
+
31
+ // =========================
32
+ // TYPES
33
+ // =========================
34
+ interface Props {
35
+ doc: InlineDoc;
36
+ placeholder?: string;
37
+ class?: string;
38
+ /** Marks only: bold, italic, strike, code and link. No lists, so a table cell stays a line of words. */
39
+ inline?: boolean;
40
+ /** Where the click landed, so the caret opens there. Without it the caret goes to the end. */
41
+ at?: { x: number; y: number };
42
+ /** Opens with all the text selected, so the first key replaces it. */
43
+ selectAll?: boolean;
44
+ /** Opens with the text replaced by these characters and the caret after them. */
45
+ seed?: string;
46
+ onchange: (doc: InlineDoc) => void;
47
+ onexit: () => void;
48
+ onlink?: () => void;
49
+ onstate?: () => void;
50
+ onready?: (api: InlineEditorApi) => void;
51
+ };
52
+
53
+ // =========================
54
+ // PROPS
55
+ // =========================
56
+ let { doc, placeholder = "", class: klass = "", inline = false, at, selectAll = false, seed, onchange, onexit, onlink, onstate, onready }: Props = $props();
57
+
58
+ // =========================
59
+ // STATE
60
+ // =========================
61
+ let element: HTMLDivElement | undefined = $state();
62
+
63
+ // =========================
64
+ // CONSTANTS
65
+ // =========================
66
+ const TaskList = TiptapNode.create({
67
+ name: "taskList",
68
+ group: "block list",
69
+ content: "taskItem+",
70
+ parseHTML: () => [{ tag: 'ul[data-type="taskList"]' }],
71
+ renderHTML: ({ HTMLAttributes }) => ["ul", mergeAttributes(HTMLAttributes, { "data-type": "taskList" }), 0],
72
+ addInputRules() {
73
+ return [wrappingInputRule({ find: /^\s*\[\s?\]\s$/, type: this.type })];
74
+ },
75
+ });
76
+
77
+ // A checklist item. Its box is a real input, so a tick is one transaction and one undo step.
78
+ const TaskItem = TiptapNode.create({
79
+ name: "taskItem",
80
+ content: "paragraph",
81
+ defining: true,
82
+ addAttributes() {
83
+ return { checked: { default: false, keepOnSplit: false } };
84
+ },
85
+ parseHTML: () => [{ tag: 'li[data-type="taskItem"]', priority: 51 }],
86
+ renderHTML: ({ node, HTMLAttributes }) => [
87
+ "li",
88
+ mergeAttributes(HTMLAttributes, { "data-type": "taskItem", "data-checked": String(node.attrs.checked) }),
89
+ 0,
90
+ ],
91
+ addKeyboardShortcuts() {
92
+ return {
93
+ Enter: () => this.editor.commands.splitListItem(this.name),
94
+ "Shift-Tab": () => this.editor.commands.liftListItem(this.name),
95
+ };
96
+ },
97
+ addNodeView() {
98
+ return ({ node, getPos, editor }) => {
99
+ let current = node;
100
+ const li = document.createElement("li");
101
+ li.dataset.type = "taskItem";
102
+ const label = document.createElement("label");
103
+ label.contentEditable = "false";
104
+ const input = document.createElement("input");
105
+ input.type = "checkbox";
106
+ label.append(input);
107
+ const body = document.createElement("div");
108
+ li.append(label, body);
109
+ const paint = () => {
110
+ li.dataset.checked = String(current.attrs.checked);
111
+ input.checked = current.attrs.checked === true;
112
+ };
113
+ paint();
114
+ input.addEventListener("change", () => {
115
+ const pos = getPos();
116
+ if (typeof pos !== "number") return;
117
+ editor
118
+ .chain()
119
+ .command(({ tr }) => {
120
+ tr.setNodeMarkup(pos, undefined, { ...current.attrs, checked: input.checked });
121
+ return true;
122
+ })
123
+ .run();
124
+ });
125
+ return {
126
+ dom: li,
127
+ contentDOM: body,
128
+ update(next) {
129
+ if (next.type !== current.type) return false;
130
+ current = next;
131
+ paint();
132
+ return true;
133
+ },
134
+ };
135
+ };
136
+ },
137
+ });
138
+
139
+ // =========================
140
+ // EFFECTS
141
+ // =========================
142
+ $effect(() => {
143
+ if (!element) return;
144
+ const host = element;
145
+ return untrack(() => build(host));
146
+ });
147
+
148
+ function build(host: HTMLElement) {
149
+
150
+ const keys = Extension.create({
151
+ name: "cardKeys",
152
+ priority: 1000,
153
+ addKeyboardShortcuts() {
154
+ return {
155
+ "Mod-Shift-x": () => this.editor.commands.toggleStrike(),
156
+ "Mod-k": () => {
157
+ onlink?.();
158
+ return true;
159
+ },
160
+ Escape: () => {
161
+ onexit();
162
+ return true;
163
+ },
164
+ // Lists stay flat: the stored text has no nesting, so Tab does nothing.
165
+ Tab: () => true,
166
+ };
167
+ },
168
+ });
169
+
170
+ const marksOnly = untrack(() => inline);
171
+ const editor = new Editor({
172
+ element: host,
173
+ content: doc,
174
+ extensions: [
175
+ StarterKit.configure({
176
+ ...(marksOnly && { bulletList: false, orderedList: false, listItem: false, listKeymap: false }),
177
+ heading: false,
178
+ blockquote: false,
179
+ codeBlock: false,
180
+ horizontalRule: false,
181
+ hardBreak: false,
182
+ underline: false,
183
+ trailingNode: false,
184
+ link: {
185
+ openOnClick: false,
186
+ autolink: false,
187
+ HTMLAttributes: { target: "_blank", rel: "noopener noreferrer" },
188
+ },
189
+ ...(!marksOnly && {
190
+ listKeymap: {
191
+ listTypes: [
192
+ { itemName: "listItem", wrapperNames: ["bulletList", "orderedList"] },
193
+ { itemName: "taskItem", wrapperNames: ["taskList"] },
194
+ ],
195
+ },
196
+ }),
197
+ }),
198
+ ...(marksOnly ? [] : [TaskList, TaskItem]),
199
+ Placeholder.configure({ placeholder }),
200
+ keys,
201
+ ],
202
+ editorProps: {
203
+ attributes: { class: `rich-text ${klass}`, role: "textbox", "aria-multiline": "true" },
204
+ handleKeyDown: (_view, event) => {
205
+ // Every key typed here is the field's. Only save leaves, so no canvas shortcut fires while typing.
206
+ if (!((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s")) event.stopPropagation();
207
+ return false;
208
+ },
209
+ handleDOMEvents: {
210
+ click: (_view, event) => {
211
+ const anchor = (event.target as HTMLElement).closest("a");
212
+ if (anchor && (event.metaKey || event.ctrlKey)) window.open(anchor.getAttribute("href") ?? "", "_blank", "noopener");
213
+ return false;
214
+ },
215
+ },
216
+ },
217
+ onUpdate: ({ editor: ed, transaction }) => {
218
+ if (transaction.docChanged) onchange(ed.getJSON());
219
+ },
220
+ onTransaction: () => onstate?.(),
221
+ });
222
+
223
+ const hit = at ? editor.view.posAtCoords({ left: at.x, top: at.y }) : null;
224
+ if (seed) editor.chain().focus().selectAll().insertContent({ type: "text", text: seed }).run();
225
+ else if (selectAll) editor.chain().focus().selectAll().run();
226
+ else if (hit) editor.chain().setTextSelection(hit.pos).focus().run();
227
+ else editor.commands.focus("end");
228
+
229
+ onready?.({
230
+ toggle: (command) => {
231
+ if (marksOnly && (command === "bulletList" || command === "orderedList" || command === "taskList")) return;
232
+ const chain = editor.chain().focus();
233
+ if (command === "bold") chain.toggleBold().run();
234
+ else if (command === "italic") chain.toggleItalic().run();
235
+ else if (command === "strike") chain.toggleStrike().run();
236
+ else if (command === "bulletList") chain.toggleBulletList().run();
237
+ else if (command === "orderedList") chain.toggleOrderedList().run();
238
+ else chain.toggleList("taskList", "taskItem").run();
239
+ },
240
+ setLink: (href) => {
241
+ const chain = editor.chain().focus().extendMarkRange("link");
242
+ if (href) chain.setLink({ href }).run();
243
+ else chain.unsetLink().run();
244
+ },
245
+ isActive: (name) => editor.isActive(name),
246
+ linkHref: () => String(editor.getAttributes("link").href ?? ""),
247
+ hasSelection: () => !editor.state.selection.empty,
248
+ insertText: (text) => editor.chain().focus().insertContent({ type: "text", text }).run(),
249
+ setDoc: (next) => editor.commands.setContent(next, { emitUpdate: false }),
250
+ getDoc: () => editor.getJSON(),
251
+ focus: () => editor.commands.focus(),
252
+ });
253
+
254
+ return () => editor.destroy();
255
+ }
256
+ </script>
257
+
258
+ <!-- =========================== -->
259
+ <!-- MARKUP -->
260
+ <!-- =========================== -->
261
+ <div bind:this={element} class="inline-editor contents"></div>
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { tiptapToMarkdown, markdownToTiptap } from "./markdown.js";
3
+ import { blocks } from "./markdown-test-helpers.js";
4
+
5
+ // ---- snake_case / underscore correctness ----
6
+ // The reason the hand-rolled parser went: its regex read any single underscore
7
+ // as italic and mangled snake_case identifiers.
8
+ describe("snake_case and underscore safety", () => {
9
+ it("clic_tempo_v2 survives round-trip as plain text with no italic marks", () => {
10
+ const doc = markdownToTiptap("clic_tempo_v2");
11
+ const para = blocks(doc)[0];
12
+ // No node should carry an italic mark.
13
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
14
+ expect(hasItalic).toBe(false);
15
+ // All content should be text nodes.
16
+ const allText = para.content?.every((n) => n.type === "text");
17
+ expect(allText).toBe(true);
18
+ // The serialized markdown should not wrap content in emphasis delimiters.
19
+ const out = tiptapToMarkdown(doc);
20
+ expect(out).not.toContain("*clic");
21
+ expect(out).not.toContain("_clic");
22
+ });
23
+
24
+ it("api_key_prod survives round-trip as plain text with no italic marks", () => {
25
+ const doc = markdownToTiptap("api_key_prod");
26
+ const para = blocks(doc)[0];
27
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
28
+ expect(hasItalic).toBe(false);
29
+ const out = tiptapToMarkdown(doc);
30
+ expect(out).not.toContain("*api");
31
+ expect(out).not.toContain("_api");
32
+ });
33
+
34
+ it("snake_case_naming survives round-trip as plain text with no italic marks", () => {
35
+ const doc = markdownToTiptap("snake_case_naming");
36
+ const para = blocks(doc)[0];
37
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
38
+ expect(hasItalic).toBe(false);
39
+ });
40
+
41
+ it("plain URL with underscores stays literal", () => {
42
+ const doc = markdownToTiptap("https://app.example.com/a_b_c");
43
+ const para = blocks(doc)[0];
44
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
45
+ expect(hasItalic).toBe(false);
46
+ // The URL text must be preserved.
47
+ const fullText = para.content?.map((n) => n.text ?? "").join("") ?? "";
48
+ expect(fullText).toContain("a_b_c");
49
+ });
50
+
51
+ it("link with underscore href parses to a single link mark with the full href", () => {
52
+ const doc = markdownToTiptap("[docs](https://x.com/a_b_c)");
53
+ const para = blocks(doc)[0];
54
+ // Exactly one text node with a link mark.
55
+ const linkNode = para.content?.find((n) => n.marks?.some((m) => m.type === "link"));
56
+ expect(linkNode).toBeDefined();
57
+ const linkMark = linkNode?.marks?.find((m) => m.type === "link");
58
+ expect(linkMark?.attrs?.href).toBe("https://x.com/a_b_c");
59
+ // No italic mark anywhere.
60
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
61
+ expect(hasItalic).toBe(false);
62
+ // Round-trip: serialized form preserves the full href.
63
+ const out = tiptapToMarkdown(doc);
64
+ expect(out).toContain("(https://x.com/a_b_c)");
65
+ });
66
+
67
+ it("inline code protects underscores from italic parsing", () => {
68
+ // Backtick content is never split on underscores.
69
+ const doc = markdownToTiptap("Use `client_id` to auth");
70
+ const para = blocks(doc)[0];
71
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
72
+ expect(hasItalic).toBe(false);
73
+ const codeNode = para.content?.find((n) => n.marks?.some((m) => m.type === "code"));
74
+ expect(codeNode?.text).toBe("client_id");
75
+ // Round-trip preserves the backtick form.
76
+ const out = tiptapToMarkdown(doc);
77
+ expect(out).toContain("`client_id`");
78
+ });
79
+ });
80
+
81
+ // ---- markdown character escaping ----
82
+ describe("markdown character escaping in serializer", () => {
83
+ it("literal * in text is escaped on output and round-trips correctly", () => {
84
+ // Build a doc with plain text that contains an asterisk.
85
+ const doc = {
86
+ type: "doc" as const,
87
+ content: [
88
+ {
89
+ type: "paragraph",
90
+ attrs: { id: "a" },
91
+ content: [{ type: "text", text: "price * 5" }],
92
+ },
93
+ ],
94
+ };
95
+ const out = tiptapToMarkdown(doc);
96
+ // Output should escape the asterisk.
97
+ expect(out).toContain("\\*");
98
+ // Parsing the output must produce plain text with the literal asterisk.
99
+ const doc2 = markdownToTiptap(out);
100
+ const fullText =
101
+ doc2.content[0]?.content?.map((n: { text?: string }) => n.text ?? "").join("") ?? "";
102
+ expect(fullText).toBe("price * 5");
103
+ // No bold/italic marks.
104
+ const hasEmphasis = doc2.content[0]?.content?.some((n: { marks?: { type: string }[] }) =>
105
+ n.marks?.some((m) => m.type === "bold" || m.type === "italic"),
106
+ );
107
+ expect(hasEmphasis).toBeFalsy();
108
+ });
109
+
110
+ it("literal _ in text is escaped on output and round-trips correctly", () => {
111
+ const doc = {
112
+ type: "doc" as const,
113
+ content: [
114
+ {
115
+ type: "paragraph",
116
+ attrs: { id: "b" },
117
+ content: [{ type: "text", text: "a_b" }],
118
+ },
119
+ ],
120
+ };
121
+ const out = tiptapToMarkdown(doc);
122
+ expect(out).toContain("\\_");
123
+ const doc2 = markdownToTiptap(out);
124
+ const fullText =
125
+ doc2.content[0]?.content?.map((n: { text?: string }) => n.text ?? "").join("") ?? "";
126
+ expect(fullText).toBe("a_b");
127
+ });
128
+
129
+ it("literal backtick in text is escaped on output", () => {
130
+ const doc = {
131
+ type: "doc" as const,
132
+ content: [
133
+ {
134
+ type: "paragraph",
135
+ attrs: { id: "c" },
136
+ content: [{ type: "text", text: "use a `tick`" }],
137
+ },
138
+ ],
139
+ };
140
+ const out = tiptapToMarkdown(doc);
141
+ expect(out).toContain("\\`");
142
+ });
143
+
144
+ it("literal [ in text is escaped on output", () => {
145
+ const doc = {
146
+ type: "doc" as const,
147
+ content: [
148
+ {
149
+ type: "paragraph",
150
+ attrs: { id: "d" },
151
+ content: [{ type: "text", text: "not [a link]" }],
152
+ },
153
+ ],
154
+ };
155
+ const out = tiptapToMarkdown(doc);
156
+ expect(out).toContain("\\[");
157
+ });
158
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { tiptapToMarkdown, markdownToTiptap } from "./markdown.js";
3
+ import { blocks } from "./markdown-test-helpers.js";
4
+
5
+ // ---- marks: bold ----
6
+ describe("bold mark", () => {
7
+ it("round-trips bold text", () => {
8
+ const md = "Hello **world** text";
9
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe(md);
10
+ });
11
+
12
+ it("produces bold mark on the right text node", () => {
13
+ const doc = markdownToTiptap("**bold**");
14
+ const para = blocks(doc)[0];
15
+ const boldNode = para.content?.find((n) => n.marks?.some((m) => m.type === "bold"));
16
+ expect(boldNode?.text).toBe("bold");
17
+ });
18
+ });
19
+
20
+ // ---- marks: italic ----
21
+ describe("italic mark", () => {
22
+ it("parses _italic_ and serializes as *italic*", () => {
23
+ // WHY: italic parses from _ or *, but we always write *. Underscore emphasis
24
+ // has word-boundary rules that would corrupt identifiers.
25
+ const md = "Some _italic_ word";
26
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe("Some *italic* word");
27
+ });
28
+
29
+ it("round-trips *italic* (asterisk form)", () => {
30
+ const md = "Some *italic* word";
31
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe(md);
32
+ });
33
+ });
34
+
35
+ // ---- marks: strike ----
36
+ describe("strike mark", () => {
37
+ it("round-trips strikethrough text", () => {
38
+ const md = "Some ~~struck~~ text";
39
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe(md);
40
+ });
41
+ });
42
+
43
+ // ---- marks: inline code ----
44
+ describe("inline code mark", () => {
45
+ it("round-trips inline code", () => {
46
+ const md = "Use `console.log()` to debug";
47
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe(md);
48
+ });
49
+ });
50
+
51
+ // ---- marks: link ----
52
+ describe("link mark", () => {
53
+ it("round-trips a link", () => {
54
+ const md = "Visit [example](https://example.com) now";
55
+ expect(tiptapToMarkdown(markdownToTiptap(md))).toBe(md);
56
+ });
57
+
58
+ it("produces link mark with href attr", () => {
59
+ const doc = markdownToTiptap("[click](https://example.com)");
60
+ const para = blocks(doc)[0];
61
+ const linkNode = para.content?.find((n) => n.marks?.some((m) => m.type === "link"));
62
+ const linkMark = linkNode?.marks?.find((m) => m.type === "link");
63
+ expect(linkMark?.attrs?.href).toBe("https://example.com");
64
+ });
65
+ });
66
+
67
+ // ---- italic parsing: both forms ----
68
+ describe("italic parsing: both * and _ forms", () => {
69
+ it("*italic* parses to italic mark", () => {
70
+ const doc = markdownToTiptap("*italic*");
71
+ const para = blocks(doc)[0];
72
+ const italicNode = para.content?.find((n) => n.marks?.some((m) => m.type === "italic"));
73
+ expect(italicNode?.text).toBe("italic");
74
+ });
75
+
76
+ it("_italic_ (word-boundary) parses to italic mark", () => {
77
+ const doc = markdownToTiptap("_italic_");
78
+ const para = blocks(doc)[0];
79
+ const italicNode = para.content?.find((n) => n.marks?.some((m) => m.type === "italic"));
80
+ expect(italicNode?.text).toBe("italic");
81
+ });
82
+
83
+ it("intraword underscores (clic_tempo) are NOT parsed as italic", () => {
84
+ const doc = markdownToTiptap("clic_tempo");
85
+ const para = blocks(doc)[0];
86
+ const hasItalic = para.content?.some((n) => n.marks?.some((m) => m.type === "italic"));
87
+ expect(hasItalic).toBe(false);
88
+ });
89
+ });
@@ -0,0 +1,13 @@
1
+ import type { markdownToTiptap } from "./markdown.js";
2
+
3
+ type Doc = ReturnType<typeof markdownToTiptap>;
4
+
5
+ /** The top-level block nodes of a doc. */
6
+ export function blocks(doc: Doc) {
7
+ return doc.content;
8
+ }
9
+
10
+ /** True when every top-level block carries a string id. */
11
+ export function allHaveIds(doc: Doc) {
12
+ return doc.content.every((n) => typeof n.attrs?.id === "string" && n.attrs.id.length > 0);
13
+ }