@heyramzi/ui 2.1.0 → 2.2.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 (312) hide show
  1. package/package.json +7 -8
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/card/card-frame.svelte +43 -39
  31. package/src/canvas/card/resize-handles.svelte +89 -80
  32. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  33. package/src/canvas/chrome/lock-badge.svelte +8 -8
  34. package/src/canvas/chrome/side-handles.svelte +24 -19
  35. package/src/canvas/chrome/size-chip.svelte +30 -30
  36. package/src/canvas/chrome/snap-guides.svelte +20 -20
  37. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  38. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  39. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  40. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  41. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  42. package/src/canvas/edges/bend-handle.svelte +46 -46
  43. package/src/canvas/edges/edge-gradient.svelte +10 -10
  44. package/src/canvas/edges/gradient-edge.svelte +45 -29
  45. package/src/canvas/editor/lock.test.ts +6 -1
  46. package/src/canvas/editor/style.ts +7 -1
  47. package/src/canvas/headless/svg-figure.test.ts +5 -1
  48. package/src/canvas/lod/lod-switch.svelte +111 -106
  49. package/src/canvas/pen/PenField.svelte +87 -81
  50. package/src/canvas/pen/PenOverlay.svelte +127 -127
  51. package/src/canvas/pen/ShapeNode.svelte +31 -32
  52. package/src/canvas/pen/TextNode.svelte +54 -53
  53. package/src/canvas/pen/host-field.test.svelte +7 -7
  54. package/src/canvas/pen/shapes.ts +7 -1
  55. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  56. package/src/canvas/screen/PartView.svelte +138 -72
  57. package/src/canvas/screen/ScreenBody.svelte +81 -60
  58. package/src/canvas/screen/ScreenCard.svelte +20 -20
  59. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  60. package/src/canvas/ui/FrameLayer.svelte +59 -59
  61. package/src/canvas/ui/PenLayer.svelte +39 -30
  62. package/src/canvas/ui/StylePanel.svelte +119 -102
  63. package/src/canvas/ui/canvas-grid.svelte +137 -129
  64. package/src/charts/animated-value.svelte +1 -1
  65. package/src/charts/area-chart.css +2 -5
  66. package/src/charts/area-chart.svelte +1 -1
  67. package/src/charts/bar-chart.css +2 -3
  68. package/src/charts/bar-chart.svelte +1 -1
  69. package/src/charts/charts.css +5 -17
  70. package/src/charts/crosshair.svelte +1 -1
  71. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  72. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  73. package/src/charts/dashboard/data-panel.svelte +50 -50
  74. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  75. package/src/charts/dashboard/data-table.svelte +138 -138
  76. package/src/charts/dashboard/panel-grid.svelte +30 -30
  77. package/src/charts/dashboard/period-filter.svelte +25 -25
  78. package/src/charts/dashboard/rating-chip.svelte +19 -19
  79. package/src/charts/dashboard/stat-band.svelte +32 -32
  80. package/src/charts/dashboard/stat-tile.svelte +106 -99
  81. package/src/charts/donut-chart.svelte +1 -1
  82. package/src/charts/edge-fade.svelte +1 -1
  83. package/src/charts/figure.svelte +1 -1
  84. package/src/charts/gauge-chart.css +4 -7
  85. package/src/charts/gauge-chart.svelte +1 -1
  86. package/src/charts/hover-focus.svelte +1 -1
  87. package/src/charts/legend.css +1 -3
  88. package/src/charts/legend.svelte +1 -1
  89. package/src/charts/line-chart.svelte +1 -1
  90. package/src/charts/number-chart.css +6 -9
  91. package/src/charts/number-chart.svelte +1 -1
  92. package/src/charts/reference-line.svelte +19 -19
  93. package/src/charts/reveal.svelte +1 -1
  94. package/src/charts/ring-chart.css +2 -4
  95. package/src/charts/ring-chart.svelte +1 -1
  96. package/src/charts/sparkline.svelte +5 -2
  97. package/src/charts/stat-card.svelte +1 -1
  98. package/src/charts/time-series-chart.css +2 -5
  99. package/src/charts/trend-pill.css +2 -4
  100. package/src/charts/trend-pill.svelte +1 -1
  101. package/src/components/copy/copy-button.svelte +93 -98
  102. package/src/components/copy/copy-button.test.ts +10 -2
  103. package/src/components/data-table/infinite-scroll.svelte +42 -42
  104. package/src/components/data-table/pagination.svelte +34 -22
  105. package/src/components/data-table/row-arrow.svelte +19 -16
  106. package/src/components/data-table/row-context-menu.svelte +52 -52
  107. package/src/components/data-table/sortable-head.svelte +25 -25
  108. package/src/components/data-table/table-skeleton.svelte +41 -36
  109. package/src/components/docs/doc-page.svelte +18 -9
  110. package/src/components/docs/on-this-page.svelte +16 -3
  111. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  112. package/src/components/field-row/field-row.svelte +10 -8
  113. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  114. package/src/components/icon/icon.svelte +14 -14
  115. package/src/components/language-flag/language-flag.svelte +3 -1
  116. package/src/components/list-page/detail-empty.svelte +34 -34
  117. package/src/components/list-page/detail-header.svelte +55 -47
  118. package/src/components/list-page/detail-panel.svelte +51 -59
  119. package/src/components/list-page/detail-row.svelte +19 -19
  120. package/src/components/list-page/detail-stack.svelte +21 -21
  121. package/src/components/list-page/empty-state.svelte +30 -30
  122. package/src/components/list-page/filter-item.svelte +20 -20
  123. package/src/components/list-page/filter-popover.svelte +60 -55
  124. package/src/components/list-page/grouped-list.svelte +4 -2
  125. package/src/components/list-page/index.ts +5 -1
  126. package/src/components/list-page/list-page-shell.svelte +2 -5
  127. package/src/components/list-page/master-detail.svelte +87 -88
  128. package/src/components/list-page/notice-bar.svelte +38 -38
  129. package/src/components/list-page/prev-next.svelte +63 -63
  130. package/src/components/list-page/rail-body.svelte +18 -18
  131. package/src/components/list-page/rail-empty.svelte +23 -30
  132. package/src/components/list-page/rail-head.svelte +24 -24
  133. package/src/components/list-page/rail-row.svelte +77 -73
  134. package/src/components/list-page/section-host.ts +2 -3
  135. package/src/components/locale-switch/locale-switch.svelte +41 -41
  136. package/src/components/mark/mark.svelte +34 -38
  137. package/src/components/open-in/index.ts +1 -5
  138. package/src/components/open-in/open-in.svelte +40 -40
  139. package/src/components/picker/mark-picker.svelte +49 -49
  140. package/src/components/picker/picker.svelte +5 -3
  141. package/src/components/section-header/section-header.test.ts +7 -1
  142. package/src/components/section-header/section-label.svelte +20 -20
  143. package/src/components/segmented/animated-tabs.svelte +31 -2
  144. package/src/components/segmented/collapse.test.ts +2 -1
  145. package/src/components/segmented/index.ts +2 -0
  146. package/src/components/segmented/segment.svelte +52 -49
  147. package/src/components/segmented/segmented-filter.svelte +79 -80
  148. package/src/components/segmented/segmented-nav.svelte +34 -40
  149. package/src/components/segmented/segmented-overflow.svelte +53 -50
  150. package/src/components/segmented/segmented-track.svelte +62 -46
  151. package/src/components/segmented/segmented.ts +2 -3
  152. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  153. package/src/components/svg/svg.svelte +4 -4
  154. package/src/components/table-view/table-view.svelte +17 -2
  155. package/src/components/table-view/table-view.test.ts +7 -1
  156. package/src/components/table-view/types.ts +11 -11
  157. package/src/components/tags-input/index.ts +13 -0
  158. package/src/components/tags-input/tags-input.svelte +305 -0
  159. package/src/components/tags-input/tags.test.ts +64 -0
  160. package/src/components/tags-input/tags.ts +142 -0
  161. package/src/components/theme/index.ts +2 -4
  162. package/src/components/theme/theme-provider.svelte +1 -3
  163. package/src/components/toolbar/toolbar-search.svelte +25 -25
  164. package/src/components/toolbar/toolbar.svelte +11 -7
  165. package/src/components/toolbar/toolbar.test.ts +10 -1
  166. package/src/design/index.css +21 -78
  167. package/src/design/polish.css +26 -35
  168. package/src/editor/editor-toolbar.svelte +104 -99
  169. package/src/editor/editor.svelte +296 -266
  170. package/src/editor/image-toolbar.svelte +20 -22
  171. package/src/editor/index.ts +1 -4
  172. package/src/editor/inline-editor.svelte +280 -238
  173. package/src/editor/markdown.ts +15 -15
  174. package/src/editor/renderer.svelte +71 -71
  175. package/src/editor/slash-menu.svelte +24 -27
  176. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  177. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  178. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  179. package/src/effects/magic-card/MagicCard.css +14 -7
  180. package/src/effects/marquee/Marquee.svelte +3 -5
  181. package/src/effects/reveal/observe.ts +0 -3
  182. package/src/effects/spotlight/Spotlight.svelte +1 -1
  183. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  184. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  185. package/src/gallery.test.ts +14 -4
  186. package/src/inbox/README.md +1 -3
  187. package/src/inbox/inbox.test.ts +181 -138
  188. package/src/inbox/index.ts +20 -20
  189. package/src/inbox/logic.ts +114 -111
  190. package/src/inbox/thread-inbox.svelte +227 -151
  191. package/src/inbox/types.ts +130 -108
  192. package/src/lib/theme-transition.ts +2 -4
  193. package/src/lib/tone.ts +2 -4
  194. package/src/lib/utils.ts +2 -2
  195. package/src/modules.test.ts +79 -69
  196. package/src/onboarding/define-tours.test.ts +55 -40
  197. package/src/onboarding/define-tours.ts +23 -23
  198. package/src/onboarding/demo.svelte +24 -5
  199. package/src/onboarding/index.ts +1 -8
  200. package/src/onboarding/onboarding-provider.svelte +19 -7
  201. package/src/onboarding/onboarding.svelte.ts +16 -16
  202. package/src/onboarding/pick.test.ts +61 -52
  203. package/src/onboarding/pick.ts +16 -13
  204. package/src/onboarding/stores.test.ts +80 -80
  205. package/src/onboarding/stores.ts +41 -39
  206. package/src/onboarding/tour-card.svelte +30 -5
  207. package/src/onboarding/tour-runner.svelte +12 -3
  208. package/src/onboarding/types.ts +20 -12
  209. package/src/onboarding/wait.ts +22 -22
  210. package/src/panel/side-panel.svelte +94 -93
  211. package/src/record/demo-data.ts +291 -225
  212. package/src/record/field-section.svelte +38 -34
  213. package/src/record/fields.ts +148 -96
  214. package/src/record/format.ts +63 -56
  215. package/src/record/index.ts +49 -35
  216. package/src/record/info-list.svelte +145 -114
  217. package/src/record/kinds.ts +31 -31
  218. package/src/record/logic.ts +216 -214
  219. package/src/record/next-up.svelte +45 -34
  220. package/src/record/project-list.svelte +56 -50
  221. package/src/record/record-demo.svelte +130 -110
  222. package/src/record/record-header.svelte +37 -28
  223. package/src/record/record-shell.svelte +54 -54
  224. package/src/record/record-stats.svelte +25 -25
  225. package/src/record/record-timeline.svelte +134 -114
  226. package/src/record/record.test.ts +430 -309
  227. package/src/record/steps-rail.svelte +225 -216
  228. package/src/record/types.ts +234 -215
  229. package/src/record/where-we-are.svelte +62 -58
  230. package/src/shadcn/button/button.svelte +1 -1
  231. package/src/shadcn/card/card-title.svelte +1 -5
  232. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  233. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  234. package/src/shadcn/table/table-cell.svelte +2 -3
  235. package/src/shadcn/utilities.css +3 -8
  236. package/src/slides/Controls.svelte +150 -124
  237. package/src/slides/Deck.svelte +243 -238
  238. package/src/slides/Fragment.svelte +64 -57
  239. package/src/slides/Overview.svelte +106 -101
  240. package/src/slides/Presenter.svelte +300 -266
  241. package/src/slides/Slide.svelte +185 -170
  242. package/src/slides/SlideList.svelte +16 -12
  243. package/src/slides/ambient.d.ts +1 -1
  244. package/src/slides/bridges.ts +42 -23
  245. package/src/slides/deck-controller.svelte.ts +47 -35
  246. package/src/slides/demo-slides.svelte +330 -306
  247. package/src/slides/demo.svelte +164 -160
  248. package/src/slides/export/capture.ts +20 -16
  249. package/src/slides/export/ffmpeg.ts +43 -38
  250. package/src/slides/export/index.ts +5 -5
  251. package/src/slides/export/launch.ts +3 -1
  252. package/src/slides/export/pdf.ts +30 -27
  253. package/src/slides/export/types.ts +10 -8
  254. package/src/slides/index.ts +11 -11
  255. package/src/slides/labels.ts +22 -19
  256. package/src/slides/layout.ts +9 -2
  257. package/src/slides/model.ts +4 -4
  258. package/src/slides/navigation.ts +43 -31
  259. package/src/slides/theme.css +5 -7
  260. package/src/slides/types.ts +9 -10
  261. package/src/status/define-statuses.ts +17 -24
  262. package/src/status/index.ts +5 -5
  263. package/src/status/status-badge.svelte +3 -1
  264. package/src/status/status-dot.svelte +1 -3
  265. package/src/status/status-icon.svelte +1 -1
  266. package/src/status/status-types.ts +21 -21
  267. package/src/tasks/board-order.ts +2 -4
  268. package/src/tasks/demo.svelte +189 -31
  269. package/src/tasks/detail/detail-demo.svelte +73 -13
  270. package/src/tasks/detail/detail-types.ts +45 -45
  271. package/src/tasks/detail/task-detail.svelte +40 -10
  272. package/src/tasks/detail/task-property.svelte +8 -2
  273. package/src/tasks/detail/task-thread.svelte +53 -12
  274. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  275. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  276. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  277. package/src/tasks/gantt/gantt-header.svelte +9 -3
  278. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  279. package/src/tasks/gantt/gantt.css +2 -1
  280. package/src/tasks/gantt/gantt.test.ts +3 -1
  281. package/src/tasks/gantt/logic.test.ts +110 -20
  282. package/src/tasks/gantt/logic.ts +42 -6
  283. package/src/tasks/inbox/inbox-card.svelte +13 -5
  284. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  285. package/src/tasks/inbox/inbox-logic.ts +6 -40
  286. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  287. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  288. package/src/tasks/inbox/inbox-types.ts +54 -58
  289. package/src/tasks/inbox/index.ts +22 -21
  290. package/src/tasks/index.ts +49 -52
  291. package/src/tasks/kanban-board.svelte +16 -12
  292. package/src/tasks/kanban-types.ts +2 -3
  293. package/src/tasks/presets/sales.ts +6 -6
  294. package/src/tasks/task-gantt.svelte +28 -8
  295. package/src/tasks/task-list.svelte +41 -17
  296. package/src/tasks/time/duration.ts +40 -40
  297. package/src/tasks/time/time-chip.svelte +9 -2
  298. package/src/tasks/time/time-dot.svelte +1 -1
  299. package/src/tasks/time/time-ring.svelte +11 -3
  300. package/src/tasks/time/time-tracker.svelte +57 -15
  301. package/src/tasks/time/time-types.ts +57 -57
  302. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  303. package/src/thread/index.ts +7 -7
  304. package/src/thread/logic.ts +36 -33
  305. package/src/thread/thread.svelte +82 -16
  306. package/src/thread/thread.test.ts +58 -47
  307. package/src/thread/types.ts +68 -68
  308. package/src/tokens/craft.css +17 -53
  309. package/src/tokens/index.css +3 -13
  310. package/src/tokens/palette.css +13 -11
  311. package/src/tokens/primitives.css +10 -23
  312. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -1,290 +1,318 @@
1
1
  <script lang="ts">
2
- // =========================
3
- // IMPORTS
4
- // =========================
5
- import { Editor } from "@tiptap/core";
6
- import { untrack } from "svelte";
7
- import "./content.css";
8
- import { buildExtensions } from "./extensions.js";
9
- import { buildSlashItems, type SlashItem } from "./slash-items.js";
10
- import { parseYoutubeUrl } from "./youtube-url.js";
11
- import EditorToolbar from "./editor-toolbar.svelte";
12
- import SlashMenu from "./slash-menu.svelte";
13
- import ImageToolbar from "./image-toolbar.svelte";
14
-
15
- // =========================
16
- // TYPES
17
- // =========================
18
- interface Props {
19
- content?: Record<string, unknown> | null;
20
- onchange?: (json: Record<string, unknown>) => void;
21
- placeholder?: string;
22
- minHeight?: string;
23
- preset?: "full" | "notes";
24
- blockIds?: boolean;
25
- }
26
-
27
- // =========================
28
- // PROPS
29
- // =========================
30
- let { content = null, onchange, placeholder = "Start writing...", minHeight = "28rem", preset = "full", blockIds = false }: Props = $props();
31
-
32
- // =========================
33
- // STATE
34
- // =========================
35
- let editorElement: HTMLDivElement | undefined = $state();
36
- let editorInstance = $state<Editor | null>(null);
37
- let tick = $state(0);
38
-
39
- // Slash commands
40
- let showSlashMenu = $state(false);
41
- let slashQuery = $state("");
42
- let slashMenuPos = $state({ top: 0, left: 0 });
43
- let slashSelectedIndex = $state(0);
44
- let slashMenuEl: HTMLDivElement | undefined = $state();
45
-
46
- // Image toolbar
47
- let showImageToolbar = $state(false);
48
- let imageToolbarPos = $state({ top: 0, left: 0 });
49
- let activeImageElement: HTMLImageElement | null = $state(null);
50
-
51
- // =========================
52
- // DERIVED
53
- // =========================
54
- let editor: Editor | null = $derived(tick >= 0 ? editorInstance : null);
55
-
56
- let slashItems = $derived.by((): SlashItem[] =>
57
- editor ? buildSlashItems(editor, slashQuery, { preset, insertImage: insertImageFromSlash, insertYoutube: insertYoutubeVideo }) : [],
58
- );
59
-
60
- // =========================
61
- // EFFECTS
62
- // =========================
63
- $effect(() => {
64
- if (showSlashMenu) {
65
- slashSelectedIndex = 0;
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { Editor } from "@tiptap/core";
6
+ import { untrack } from "svelte";
7
+ import "./content.css";
8
+ import { buildExtensions } from "./extensions.js";
9
+ import { buildSlashItems, type SlashItem } from "./slash-items.js";
10
+ import { parseYoutubeUrl } from "./youtube-url.js";
11
+ import EditorToolbar from "./editor-toolbar.svelte";
12
+ import SlashMenu from "./slash-menu.svelte";
13
+ import ImageToolbar from "./image-toolbar.svelte";
14
+
15
+ // =========================
16
+ // TYPES
17
+ // =========================
18
+ interface Props {
19
+ content?: Record<string, unknown> | null;
20
+ onchange?: (json: Record<string, unknown>) => void;
21
+ placeholder?: string;
22
+ minHeight?: string;
23
+ preset?: "full" | "notes";
24
+ blockIds?: boolean;
66
25
  }
67
- });
68
-
69
- // Close menus on outside click
70
- $effect(() => {
71
- if (!showSlashMenu && !showImageToolbar) return;
72
- function handleClick(e: MouseEvent) {
73
- const target = e.target as Node;
74
- if (slashMenuEl && !slashMenuEl.contains(target)) {
75
- closeSlashMenu();
26
+
27
+ // =========================
28
+ // PROPS
29
+ // =========================
30
+ let {
31
+ content = null,
32
+ onchange,
33
+ placeholder = "Start writing...",
34
+ minHeight = "28rem",
35
+ preset = "full",
36
+ blockIds = false,
37
+ }: Props = $props();
38
+
39
+ // =========================
40
+ // STATE
41
+ // =========================
42
+ let editorElement: HTMLDivElement | undefined = $state();
43
+ let editorInstance = $state<Editor | null>(null);
44
+ let tick = $state(0);
45
+
46
+ // Slash commands
47
+ let showSlashMenu = $state(false);
48
+ let slashQuery = $state("");
49
+ let slashMenuPos = $state({ top: 0, left: 0 });
50
+ let slashSelectedIndex = $state(0);
51
+ let slashMenuEl: HTMLDivElement | undefined = $state();
52
+
53
+ // Image toolbar
54
+ let showImageToolbar = $state(false);
55
+ let imageToolbarPos = $state({ top: 0, left: 0 });
56
+ let activeImageElement: HTMLImageElement | null = $state(null);
57
+
58
+ // =========================
59
+ // DERIVED
60
+ // =========================
61
+ let editor: Editor | null = $derived(tick >= 0 ? editorInstance : null);
62
+
63
+ let slashItems = $derived.by((): SlashItem[] =>
64
+ editor
65
+ ? buildSlashItems(editor, slashQuery, {
66
+ preset,
67
+ insertImage: insertImageFromSlash,
68
+ insertYoutube: insertYoutubeVideo,
69
+ })
70
+ : [],
71
+ );
72
+
73
+ // =========================
74
+ // EFFECTS
75
+ // =========================
76
+ $effect(() => {
77
+ if (showSlashMenu) {
78
+ slashSelectedIndex = 0;
76
79
  }
77
- // Close image toolbar when clicking outside editor or on non-image
78
- if (showImageToolbar && editorElement && !editorElement.contains(target)) {
79
- showImageToolbar = false;
80
- activeImageElement = null;
80
+ });
81
+
82
+ // Close menus on outside click
83
+ $effect(() => {
84
+ if (!showSlashMenu && !showImageToolbar) return;
85
+ function handleClick(e: MouseEvent) {
86
+ const target = e.target as Node;
87
+ if (slashMenuEl && !slashMenuEl.contains(target)) {
88
+ closeSlashMenu();
89
+ }
90
+ // Close image toolbar when clicking outside editor or on non-image
91
+ if (showImageToolbar && editorElement && !editorElement.contains(target)) {
92
+ showImageToolbar = false;
93
+ activeImageElement = null;
94
+ }
95
+ }
96
+ document.addEventListener("mousedown", handleClick);
97
+ return () => document.removeEventListener("mousedown", handleClick);
98
+ });
99
+
100
+ // =========================
101
+ // FUNCTIONS
102
+ // =========================
103
+ function insertImageFromSlash() {
104
+ const url = prompt("Image URL:");
105
+ if (url?.trim()) {
106
+ editor?.chain().focus().setImage({ src: url.trim() }).run();
107
+ }
108
+ }
109
+
110
+ function insertYoutubeVideo() {
111
+ const url = prompt("YouTube URL:");
112
+ if (!url?.trim()) return;
113
+ const parsed = parseYoutubeUrl(url.trim());
114
+ if (!parsed) {
115
+ alert("That is not a recognized YouTube URL.");
116
+ return;
81
117
  }
118
+ const title = prompt("Video title:")?.trim() || "";
119
+ editor
120
+ ?.chain()
121
+ .focus()
122
+ .insertContent({
123
+ type: "youtube",
124
+ attrs: { videoId: parsed.videoId, title, start: parsed.start },
125
+ })
126
+ .run();
82
127
  }
83
- document.addEventListener("mousedown", handleClick);
84
- return () => document.removeEventListener("mousedown", handleClick);
85
- });
86
-
87
- // =========================
88
- // FUNCTIONS
89
- // =========================
90
- function insertImageFromSlash() {
91
- const url = prompt("Image URL:");
92
- if (url?.trim()) {
93
- editor?.chain().focus().setImage({ src: url.trim() }).run();
128
+
129
+ function closeSlashMenu() {
130
+ showSlashMenu = false;
131
+ slashQuery = "";
94
132
  }
95
- }
96
-
97
- function insertYoutubeVideo() {
98
- const url = prompt("YouTube URL:");
99
- if (!url?.trim()) return;
100
- const parsed = parseYoutubeUrl(url.trim());
101
- if (!parsed) {
102
- alert("That is not a recognized YouTube URL.");
103
- return;
133
+
134
+ function executeSlashCommand(item: SlashItem) {
135
+ if (!editor) return;
136
+ const { from } = editor.state.selection;
137
+ const slashStart = from - slashQuery.length - 1;
138
+ editor.chain().focus().deleteRange({ from: slashStart, to: from }).run();
139
+ item.command();
140
+ closeSlashMenu();
104
141
  }
105
- const title = prompt("Video title:")?.trim() || "";
106
- editor
107
- ?.chain()
108
- .focus()
109
- .insertContent({ type: "youtube", attrs: { videoId: parsed.videoId, title, start: parsed.start } })
110
- .run();
111
- }
112
-
113
- function closeSlashMenu() {
114
- showSlashMenu = false;
115
- slashQuery = "";
116
- }
117
-
118
- function executeSlashCommand(item: SlashItem) {
119
- if (!editor) return;
120
- const { from } = editor.state.selection;
121
- const slashStart = from - slashQuery.length - 1;
122
- editor.chain().focus().deleteRange({ from: slashStart, to: from }).run();
123
- item.command();
124
- closeSlashMenu();
125
- }
126
-
127
- function getCaretCoords(): { top: number; left: number } {
128
- const sel = window.getSelection();
129
- if (!sel || sel.rangeCount === 0) return { top: 0, left: 0 };
130
- const range = sel.getRangeAt(0);
131
- const rect = range.getBoundingClientRect();
132
- const wrapperRect = editorElement?.getBoundingClientRect();
133
- if (!wrapperRect) return { top: 0, left: 0 };
134
- return {
135
- top: rect.bottom - wrapperRect.top + 4,
136
- left: rect.left - wrapperRect.left,
137
- };
138
- }
139
-
140
- function setImageAlignment(align: "left" | "center" | "right") {
141
- if (!editor || !activeImageElement) return;
142
- const { state } = editor;
143
- const { selection } = state;
144
- const { from, to } = selection;
145
-
146
- editor.chain().focus().command(({ tr }) => {
147
- tr.doc.nodesBetween(from, to, (node, pos) => {
148
- if (node.type.name === "image") {
149
- tr.setNodeMarkup(pos, undefined, { ...node.attrs, align });
150
- }
151
- return true;
152
- });
153
- return true;
154
- }).run();
155
-
156
- showImageToolbar = false;
157
- activeImageElement = null;
158
- }
159
-
160
- function deleteActiveImage() {
161
- if (!editor || !activeImageElement) return;
162
- const { state } = editor;
163
- const { selection } = state;
164
- const { from, to } = selection;
165
-
166
- editor.chain().focus().command(({ tr }) => {
167
- tr.doc.nodesBetween(from, to, (node, pos) => {
168
- if (node.type.name === "image") {
169
- tr.delete(pos, pos + node.nodeSize);
170
- }
171
- return false;
172
- });
173
- return true;
174
- }).run();
175
-
176
- showImageToolbar = false;
177
- activeImageElement = null;
178
- }
179
-
180
- function mount() {
181
- if (!editorElement || editorInstance) return;
182
-
183
- const extensions = buildExtensions({ preset, placeholder, blockIds });
184
-
185
- editorInstance = new Editor({
186
- element: editorElement,
187
- extensions,
188
- content: content ?? { type: "doc", content: [{ type: "paragraph" }] },
189
- editorProps: {
190
- handleKeyDown: (_view, event) => {
191
- if (showSlashMenu) {
192
- if (event.key === "ArrowDown") {
193
- event.preventDefault();
194
- slashSelectedIndex = (slashSelectedIndex + 1) % slashItems.length;
195
- return true;
142
+
143
+ function getCaretCoords(): { top: number; left: number } {
144
+ const sel = window.getSelection();
145
+ if (!sel || sel.rangeCount === 0) return { top: 0, left: 0 };
146
+ const range = sel.getRangeAt(0);
147
+ const rect = range.getBoundingClientRect();
148
+ const wrapperRect = editorElement?.getBoundingClientRect();
149
+ if (!wrapperRect) return { top: 0, left: 0 };
150
+ return {
151
+ top: rect.bottom - wrapperRect.top + 4,
152
+ left: rect.left - wrapperRect.left,
153
+ };
154
+ }
155
+
156
+ function setImageAlignment(align: "left" | "center" | "right") {
157
+ if (!editor || !activeImageElement) return;
158
+ const { state } = editor;
159
+ const { selection } = state;
160
+ const { from, to } = selection;
161
+
162
+ editor
163
+ .chain()
164
+ .focus()
165
+ .command(({ tr }) => {
166
+ tr.doc.nodesBetween(from, to, (node, pos) => {
167
+ if (node.type.name === "image") {
168
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, align });
196
169
  }
197
- if (event.key === "ArrowUp") {
198
- event.preventDefault();
199
- slashSelectedIndex = (slashSelectedIndex - 1 + slashItems.length) % slashItems.length;
200
- return true;
170
+ return true;
171
+ });
172
+ return true;
173
+ })
174
+ .run();
175
+
176
+ showImageToolbar = false;
177
+ activeImageElement = null;
178
+ }
179
+
180
+ function deleteActiveImage() {
181
+ if (!editor || !activeImageElement) return;
182
+ const { state } = editor;
183
+ const { selection } = state;
184
+ const { from, to } = selection;
185
+
186
+ editor
187
+ .chain()
188
+ .focus()
189
+ .command(({ tr }) => {
190
+ tr.doc.nodesBetween(from, to, (node, pos) => {
191
+ if (node.type.name === "image") {
192
+ tr.delete(pos, pos + node.nodeSize);
201
193
  }
202
- if (event.key === "Enter") {
203
- event.preventDefault();
204
- if (slashItems[slashSelectedIndex]) {
205
- executeSlashCommand(slashItems[slashSelectedIndex]);
194
+ return false;
195
+ });
196
+ return true;
197
+ })
198
+ .run();
199
+
200
+ showImageToolbar = false;
201
+ activeImageElement = null;
202
+ }
203
+
204
+ function mount() {
205
+ if (!editorElement || editorInstance) return;
206
+
207
+ const extensions = buildExtensions({ preset, placeholder, blockIds });
208
+
209
+ editorInstance = new Editor({
210
+ element: editorElement,
211
+ extensions,
212
+ content: content ?? { type: "doc", content: [{ type: "paragraph" }] },
213
+ editorProps: {
214
+ handleKeyDown: (_view, event) => {
215
+ if (showSlashMenu) {
216
+ if (event.key === "ArrowDown") {
217
+ event.preventDefault();
218
+ slashSelectedIndex = (slashSelectedIndex + 1) % slashItems.length;
219
+ return true;
220
+ }
221
+ if (event.key === "ArrowUp") {
222
+ event.preventDefault();
223
+ slashSelectedIndex = (slashSelectedIndex - 1 + slashItems.length) % slashItems.length;
224
+ return true;
225
+ }
226
+ if (event.key === "Enter") {
227
+ event.preventDefault();
228
+ if (slashItems[slashSelectedIndex]) {
229
+ executeSlashCommand(slashItems[slashSelectedIndex]);
230
+ }
231
+ return true;
232
+ }
233
+ if (event.key === "Escape") {
234
+ event.preventDefault();
235
+ closeSlashMenu();
236
+ return true;
237
+ }
238
+ if (event.key === "Backspace" && slashQuery.length === 0) {
239
+ closeSlashMenu();
240
+ return false;
206
241
  }
207
- return true;
208
242
  }
209
- if (event.key === "Escape") {
210
- event.preventDefault();
211
- closeSlashMenu();
243
+ return false;
244
+ },
245
+ // handleClick, not handleClickOn: ProseMirror tries both in the same spot, and
246
+ // this one needs only the event.
247
+ handleClick(_view, _pos, event) {
248
+ const target = event.target as HTMLElement;
249
+ if (preset === "full" && target?.tagName === "IMG") {
250
+ activeImageElement = target as HTMLImageElement;
251
+ const rect = target.getBoundingClientRect();
252
+ const wrapperRect = editorElement?.getBoundingClientRect();
253
+ if (wrapperRect) {
254
+ imageToolbarPos = {
255
+ top: rect.top - wrapperRect.top - 40,
256
+ left: rect.left - wrapperRect.left + rect.width / 2 - 60,
257
+ };
258
+ showImageToolbar = true;
259
+ }
212
260
  return true;
213
261
  }
214
- if (event.key === "Backspace" && slashQuery.length === 0) {
215
- closeSlashMenu();
216
- return false;
217
- }
218
- }
219
- return false;
262
+ showImageToolbar = false;
263
+ activeImageElement = null;
264
+ return false;
265
+ },
220
266
  },
221
- // handleClick, not handleClickOn: ProseMirror tries both in the same spot, and
222
- // this one needs only the event.
223
- handleClick(_view, _pos, event) {
224
- const target = event.target as HTMLElement;
225
- if (preset === "full" && target?.tagName === "IMG") {
226
- activeImageElement = target as HTMLImageElement;
227
- const rect = target.getBoundingClientRect();
228
- const wrapperRect = editorElement?.getBoundingClientRect();
229
- if (wrapperRect) {
230
- imageToolbarPos = {
231
- top: rect.top - wrapperRect.top - 40,
232
- left: rect.left - wrapperRect.left + rect.width / 2 - 60,
233
- };
234
- showImageToolbar = true;
235
- }
236
- return true;
267
+ onUpdate: ({ editor: e }) => {
268
+ onchange?.(e.getJSON());
269
+
270
+ const { from } = e.state.selection;
271
+ const textBefore = e.state.doc.textBetween(Math.max(0, from - 20), from, "\n");
272
+ const slashMatch = textBefore.match(/\/([a-zA-Z0-9 ]*)$/);
273
+
274
+ if (slashMatch) {
275
+ slashQuery = slashMatch[1];
276
+ slashMenuPos = getCaretCoords();
277
+ showSlashMenu = true;
278
+ } else if (showSlashMenu) {
279
+ closeSlashMenu();
237
280
  }
238
- showImageToolbar = false;
239
- activeImageElement = null;
240
- return false;
241
281
  },
242
- },
243
- onUpdate: ({ editor: e }) => {
244
- onchange?.(e.getJSON());
245
-
246
- const { from } = e.state.selection;
247
- const textBefore = e.state.doc.textBetween(Math.max(0, from - 20), from, "\n");
248
- const slashMatch = textBefore.match(/\/([a-zA-Z0-9 ]*)$/);
249
-
250
- if (slashMatch) {
251
- slashQuery = slashMatch[1];
252
- slashMenuPos = getCaretCoords();
253
- showSlashMenu = true;
254
- } else if (showSlashMenu) {
255
- closeSlashMenu();
256
- }
257
- },
258
- onTransaction: () => {
259
- tick++;
260
- },
261
- onFocus: () => {
262
- tick++;
263
- },
264
- onBlur: () => {
265
- tick++;
266
- },
282
+ onTransaction: () => {
283
+ tick++;
284
+ },
285
+ onFocus: () => {
286
+ tick++;
287
+ },
288
+ onBlur: () => {
289
+ tick++;
290
+ },
291
+ });
292
+ tick++;
293
+
294
+ return () => {
295
+ editorInstance?.destroy();
296
+ editorInstance = null;
297
+ };
298
+ }
299
+
300
+ // Only the element mounts the editor. Read tracked, `content` (fed back by onchange) and the
301
+ // `tick++` each transaction makes would destroy and rebuild it until Svelte stops the loop.
302
+ $effect(() => {
303
+ if (editorElement) return untrack(mount);
267
304
  });
268
- tick++;
269
-
270
- return () => {
271
- editorInstance?.destroy();
272
- editorInstance = null;
273
- };
274
- }
275
-
276
- // Only the element mounts the editor. Read tracked, `content` (fed back by onchange) and the
277
- // `tick++` each transaction makes would destroy and rebuild it until Svelte stops the loop.
278
- $effect(() => {
279
- if (editorElement) return untrack(mount);
280
- });
281
305
  </script>
282
306
 
283
307
  <!-- =========================== -->
284
308
  <!-- MARKUP -->
285
309
  <!-- =========================== -->
286
- <div class="editor-wrapper" class:focused={editor?.isFocused} style="--editor-min-height: {minHeight};">
287
- <EditorToolbar editor={editor} {tick} {preset} onInsertYoutube={insertYoutubeVideo} />
310
+ <div
311
+ class="editor-wrapper"
312
+ class:focused={editor?.isFocused}
313
+ style="--editor-min-height: {minHeight};"
314
+ >
315
+ <EditorToolbar {editor} {tick} {preset} onInsertYoutube={insertYoutubeVideo} />
288
316
 
289
317
  <!-- Editor surface -->
290
318
  <div bind:this={editorElement} class="editor-surface tiptap-content"></div>
@@ -323,7 +351,9 @@ $effect(() => {
323
351
  border-radius: var(--radius-xl);
324
352
  background: var(--color-background);
325
353
  border: 1px solid var(--color-border);
326
- transition: border-color var(--duration-quick) var(--ease-smooth-out), box-shadow var(--duration-quick) var(--ease-smooth-out);
354
+ transition:
355
+ border-color var(--duration-quick) var(--ease-smooth-out),
356
+ box-shadow var(--duration-quick) var(--ease-smooth-out);
327
357
  }
328
358
 
329
359
  .editor-wrapper.focused {
@@ -1,32 +1,28 @@
1
1
  <script lang="ts">
2
- // =========================
3
- // IMPORTS
4
- // =========================
5
- import { Trash, TextAlignLeft, TextAlignCenter, TextAlignRight } from "phosphor-svelte";
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { Trash, TextAlignLeft, TextAlignCenter, TextAlignRight } from "phosphor-svelte";
6
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
- }
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
15
 
16
- // =========================
17
- // PROPS
18
- // =========================
19
- let { pos, onalign, ondelete }: Props = $props();
16
+ // =========================
17
+ // PROPS
18
+ // =========================
19
+ let { pos, onalign, ondelete }: Props = $props();
20
20
  </script>
21
21
 
22
22
  <!-- =========================== -->
23
23
  <!-- MARKUP -->
24
24
  <!-- =========================== -->
25
- <div
26
- class="image-toolbar"
27
- style="top: {pos.top}px; left: {pos.left}px;"
28
- role="toolbar"
29
- >
25
+ <div class="image-toolbar" style="top: {pos.top}px; left: {pos.left}px;" role="toolbar">
30
26
  <button
31
27
  type="button"
32
28
  class="image-toolbar-btn"
@@ -92,7 +88,9 @@ let { pos, onalign, ondelete }: Props = $props();
92
88
  background: transparent;
93
89
  color: var(--color-muted-foreground);
94
90
  cursor: pointer;
95
- transition: opacity var(--duration-quick) var(--ease-smooth-out), transform var(--duration-quick) var(--ease-smooth-out);
91
+ transition:
92
+ opacity var(--duration-quick) var(--ease-smooth-out),
93
+ transform var(--duration-quick) var(--ease-smooth-out);
96
94
  }
97
95
 
98
96
  @media (hover: hover) and (pointer: fine) {
@@ -5,10 +5,7 @@ export {
5
5
  extractHeadings,
6
6
  extractYoutubeVideos,
7
7
  } from "./serializer.js";
8
- export type {
9
- ExtractedHeading,
10
- ExtractedYoutubeVideo,
11
- } from "./serializer.js";
8
+ export type { ExtractedHeading, ExtractedYoutubeVideo } from "./serializer.js";
12
9
  export { BlockIdExtension, generateBlockId } from "./block-id.js";
13
10
 
14
11
  // WHY: the markdown bridge and block-ops pull in markdown-it, so they aren't