@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Bubble Menu Extension
3
+ *
4
+ * Floating toolbar that appears on text selection with inline
5
+ * formatting actions: Bold, Italic, H1, H2, Blockquote, Link.
6
+ * Vanilla DOM — positioned via ProseMirror plugin, dialog-aware.
7
+ */
8
+
9
+ import { Extension, type Editor } from "@tiptap/core";
10
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
11
+ import type { EditorView } from "@tiptap/pm/view";
12
+ import { isLinkToolbarInputActive } from "./link-toolbar.js";
13
+
14
+ const bubbleMenuKey = new PluginKey("bubbleMenu");
15
+
16
+ // SVG icons (16×16, stroke-based)
17
+ const ICONS = {
18
+ bold: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 0 1 0 8H6V4h8a4 4 0 0 1 0 8"/></svg>`,
19
+ italic: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>`,
20
+ h1: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M17 12l3-2v10"/></svg>`,
21
+ h2: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/></svg>`,
22
+ blockquote: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>`,
23
+ link: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
24
+ } as const;
25
+
26
+ interface BubbleBtn {
27
+ key: string;
28
+ icon: string;
29
+ title: string;
30
+ action: (view: EditorView) => void;
31
+ isActive: (view: EditorView) => boolean;
32
+ }
33
+
34
+ function getButtons(editor: Editor): BubbleBtn[] {
35
+ return [
36
+ {
37
+ key: "bold",
38
+ icon: ICONS.bold,
39
+ title: "Bold",
40
+ action: () => editor.chain().focus().toggleBold().run(),
41
+ isActive: () => editor.isActive("bold"),
42
+ },
43
+ {
44
+ key: "italic",
45
+ icon: ICONS.italic,
46
+ title: "Italic",
47
+ action: () => editor.chain().focus().toggleItalic().run(),
48
+ isActive: () => editor.isActive("italic"),
49
+ },
50
+ {
51
+ key: "h1",
52
+ icon: ICONS.h1,
53
+ title: "Heading 1",
54
+ action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
55
+ isActive: () => editor.isActive("heading", { level: 1 }),
56
+ },
57
+ {
58
+ key: "h2",
59
+ icon: ICONS.h2,
60
+ title: "Heading 2",
61
+ action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
62
+ isActive: () => editor.isActive("heading", { level: 2 }),
63
+ },
64
+ {
65
+ key: "sep",
66
+ icon: "",
67
+ title: "",
68
+ action: () => {},
69
+ isActive: () => false,
70
+ },
71
+ {
72
+ key: "blockquote",
73
+ icon: ICONS.blockquote,
74
+ title: "Quote",
75
+ action: () => editor.chain().focus().toggleBlockquote().run(),
76
+ isActive: () => editor.isActive("blockquote"),
77
+ },
78
+ {
79
+ key: "link",
80
+ icon: ICONS.link,
81
+ title: "Link",
82
+ action: (view: EditorView) => {
83
+ if (editor.isActive("link")) {
84
+ editor.chain().focus().unsetLink().run();
85
+ } else {
86
+ view.dom.dispatchEvent(new CustomEvent("tiptap:open-link-input"));
87
+ }
88
+ },
89
+ isActive: () => editor.isActive("link"),
90
+ },
91
+ ];
92
+ }
93
+
94
+ export const BubbleMenu = Extension.create({
95
+ name: "bubbleMenu",
96
+
97
+ addProseMirrorPlugins() {
98
+ const editor = this.editor;
99
+ let el: HTMLElement | null = null;
100
+ let buttons: BubbleBtn[] = [];
101
+ const btnEls: Map<string, HTMLButtonElement> = new Map();
102
+
103
+ function create() {
104
+ el = document.createElement("div");
105
+ el.className = "tiptap-bubble-menu";
106
+ el.style.position = "fixed";
107
+ el.style.display = "none";
108
+
109
+ buttons = getButtons(editor);
110
+ for (const btn of buttons) {
111
+ if (btn.key === "sep") {
112
+ const sep = document.createElement("span");
113
+ sep.className = "tiptap-bubble-sep";
114
+ el.appendChild(sep);
115
+ continue;
116
+ }
117
+ const b = document.createElement("button");
118
+ b.type = "button";
119
+ b.innerHTML = btn.icon;
120
+ b.title = btn.title;
121
+ b.className = "tiptap-bubble-btn";
122
+ b.addEventListener("mousedown", (e) => {
123
+ e.preventDefault();
124
+ btn.action(editor.view);
125
+ });
126
+ el.appendChild(b);
127
+ btnEls.set(btn.key, b);
128
+ }
129
+ }
130
+
131
+ function show(view: EditorView) {
132
+ if (!el) return;
133
+
134
+ // Position above selection center
135
+ const { from, to } = view.state.selection;
136
+ const start = view.coordsAtPos(from);
137
+ const end = view.coordsAtPos(to);
138
+ const cx = (start.left + end.right) / 2;
139
+ const top = start.top;
140
+
141
+ // Dialog offset (same pattern as slash commands)
142
+ const dialog = view.dom.closest("dialog");
143
+ const offsetX = dialog?.getBoundingClientRect().left ?? 0;
144
+ const offsetY = dialog?.getBoundingClientRect().top ?? 0;
145
+
146
+ el.style.display = "flex";
147
+ // Measure width after display
148
+ const rect = el.getBoundingClientRect();
149
+ el.style.left = `${cx - rect.width / 2 - offsetX}px`;
150
+ el.style.top = `${top - rect.height - 8 - offsetY}px`;
151
+
152
+ syncActive();
153
+ }
154
+
155
+ function hide() {
156
+ if (!el) return;
157
+ el.style.display = "none";
158
+ }
159
+
160
+ function syncActive() {
161
+ for (const btn of buttons) {
162
+ if (btn.key === "sep") continue;
163
+ const b = btnEls.get(btn.key);
164
+ if (b) b.classList.toggle("is-active", btn.isActive(editor.view));
165
+ }
166
+ }
167
+
168
+ function shouldShow(view: EditorView): boolean {
169
+ const { state } = view;
170
+ const { selection } = state;
171
+ const { empty } = selection;
172
+ // Only show for non-empty text selections (not node selections)
173
+ if (empty) return false;
174
+ if (!selection.$from.parent.isTextblock) return false;
175
+ // Hide when link input popup is open
176
+ if (isLinkToolbarInputActive()) return false;
177
+ return true;
178
+ }
179
+
180
+ return [
181
+ new Plugin({
182
+ key: bubbleMenuKey,
183
+ view(editorView) {
184
+ create();
185
+ const dialog = editorView.dom.closest("dialog");
186
+ if (el) (dialog ?? document.body).appendChild(el);
187
+
188
+ return {
189
+ update(view) {
190
+ if (shouldShow(view)) {
191
+ show(view);
192
+ } else {
193
+ hide();
194
+ }
195
+ },
196
+ destroy() {
197
+ el?.remove();
198
+ el = null;
199
+ },
200
+ };
201
+ },
202
+ }),
203
+ ];
204
+ },
205
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tiptap Editor Factory
3
+ *
4
+ * Creates configured Tiptap editor instances for use in Lit components.
5
+ */
6
+
7
+ import { Editor, type JSONContent } from "@tiptap/core";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import { Markdown } from "tiptap-markdown";
10
+ import {
11
+ Table,
12
+ TableRow,
13
+ TableCell,
14
+ TableHeader,
15
+ } from "@tiptap/extension-table";
16
+ import { createEditorExtensions } from "./extensions.js";
17
+ import { ImageNode } from "./image-node.js";
18
+ import { MoreBreak } from "./more-break.js";
19
+
20
+ export interface CreateEditorOptions {
21
+ element: HTMLElement;
22
+ placeholder?: string;
23
+ content?: JSONContent | null;
24
+ onUpdate?: (json: JSONContent) => void;
25
+ onFocus?: () => void;
26
+ }
27
+
28
+ /**
29
+ * Creates a Tiptap editor instance with the standard extension set.
30
+ *
31
+ * @param options - Editor configuration
32
+ * @returns Tiptap Editor instance
33
+ */
34
+ export function createTiptapEditor(options: CreateEditorOptions): Editor {
35
+ const editor = new Editor({
36
+ element: options.element,
37
+ extensions: createEditorExtensions({
38
+ placeholder: options.placeholder,
39
+ }),
40
+ content: options.content ?? undefined,
41
+ editorProps: {
42
+ scrollMargin: { top: 5, right: 5, bottom: 80, left: 5 },
43
+ scrollThreshold: { top: 5, right: 5, bottom: 80, left: 5 },
44
+ },
45
+ onUpdate: ({ editor }) => {
46
+ options.onUpdate?.(editor.getJSON());
47
+ },
48
+ onFocus: () => {
49
+ options.onFocus?.();
50
+ },
51
+ });
52
+
53
+ return editor;
54
+ }
55
+
56
+ /**
57
+ * Converts TipTap JSON content to Markdown using a headless editor.
58
+ *
59
+ * @param json - TipTap JSONContent
60
+ * @returns Markdown string
61
+ *
62
+ * @example
63
+ * const md = jsonToMarkdown({ type: "doc", content: [...] });
64
+ */
65
+ export function jsonToMarkdown(json: JSONContent): string {
66
+ const editor = new Editor({
67
+ extensions: [
68
+ StarterKit.configure({
69
+ heading: { levels: [1, 2, 3] },
70
+ link: { openOnClick: false, autolink: false },
71
+ }),
72
+ Markdown,
73
+ Table.configure({ resizable: false }),
74
+ TableRow,
75
+ TableCell,
76
+ TableHeader,
77
+ ImageNode,
78
+ MoreBreak,
79
+ ],
80
+ content: json,
81
+ });
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ const md = ((editor as any).storage.markdown.getMarkdown as () => string)();
84
+ editor.destroy();
85
+ return md;
86
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Exitable Marks Extension
3
+ *
4
+ * Two behaviors for escaping inline marks (bold, italic, strike, code, underline):
5
+ *
6
+ * 1. ArrowRight at end of block — clears stored marks so next typed char is plain.
7
+ * 2. Enter on an empty block — clears stored marks on the new paragraph.
8
+ * (Typing bold → Enter → empty line → Enter = formatting resets.)
9
+ */
10
+
11
+ import { Extension } from "@tiptap/core";
12
+
13
+ const EXITABLE_MARKS = new Set([
14
+ "bold",
15
+ "italic",
16
+ "strike",
17
+ "code",
18
+ "underline",
19
+ ]);
20
+
21
+ function getExitableMarks(marks: readonly import("@tiptap/pm/model").Mark[]) {
22
+ return marks.filter((m) => EXITABLE_MARKS.has(m.type.name));
23
+ }
24
+
25
+ export const ExitableMarks = Extension.create({
26
+ name: "exitableMarks",
27
+
28
+ addKeyboardShortcuts() {
29
+ return {
30
+ ArrowRight: ({ editor }) => {
31
+ const { selection } = editor.state;
32
+ const { $from } = selection;
33
+
34
+ if (!selection.empty) return false;
35
+ if ($from.pos !== $from.end()) return false;
36
+
37
+ const exitables = getExitableMarks($from.marks());
38
+ if (!exitables.length) return false;
39
+
40
+ // Clear stored marks; return true to consume event (don't jump to next block)
41
+ const { tr } = editor.state;
42
+ for (const mark of exitables) {
43
+ tr.removeStoredMark(mark);
44
+ }
45
+ editor.view.dispatch(tr);
46
+ return true;
47
+ },
48
+
49
+ Enter: ({ editor }) => {
50
+ const { selection } = editor.state;
51
+ const { $from } = selection;
52
+
53
+ if (!selection.empty) return false;
54
+
55
+ // Only act on empty blocks (e.g. a blank bold line)
56
+ if ($from.parent.textContent.length > 0) return false;
57
+
58
+ const storedMarks = editor.state.storedMarks ?? $from.marks();
59
+ const exitables = getExitableMarks(storedMarks);
60
+ if (!exitables.length) return false;
61
+
62
+ // Let ProseMirror create the new paragraph first, then clear marks
63
+ requestAnimationFrame(() => {
64
+ const { tr } = editor.state;
65
+ tr.setStoredMarks([]);
66
+ editor.view.dispatch(tr);
67
+ });
68
+
69
+ return false;
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tiptap Extension Configuration
3
+ *
4
+ * Shared extension set for all Tiptap editor instances (compose + post form).
5
+ */
6
+
7
+ import type { Extensions } from "@tiptap/core";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import Placeholder from "@tiptap/extension-placeholder";
10
+ import { Markdown } from "tiptap-markdown";
11
+ import {
12
+ Table,
13
+ TableRow,
14
+ TableCell,
15
+ TableHeader,
16
+ } from "@tiptap/extension-table";
17
+ import { ImageNode } from "./image-node.js";
18
+ import { MoreBreak } from "./more-break.js";
19
+ import { SlashCommands } from "./slash-commands.js";
20
+ import { PasteImage } from "./paste-image.js";
21
+ import { BubbleMenu } from "./bubble-menu.js";
22
+ import { LinkToolbar } from "./link-toolbar.js";
23
+ import { ExitableMarks } from "./exitable-marks.js";
24
+
25
+ export interface EditorExtensionOptions {
26
+ placeholder?: string;
27
+ }
28
+
29
+ /**
30
+ * Creates the standard Tiptap extension array.
31
+ *
32
+ * @param options - Configuration for extensions
33
+ * @returns Configured extension array
34
+ */
35
+ export function createEditorExtensions(
36
+ options: EditorExtensionOptions = {},
37
+ ): Extensions {
38
+ return [
39
+ StarterKit.configure({
40
+ heading: { levels: [1, 2, 3] },
41
+ link: { openOnClick: false, autolink: false },
42
+ }),
43
+ Placeholder.configure({
44
+ placeholder: options.placeholder ?? "Write something…",
45
+ }),
46
+ Markdown.configure({
47
+ transformPastedText: true,
48
+ transformCopiedText: false,
49
+ }),
50
+ Table.configure({
51
+ resizable: false,
52
+ HTMLAttributes: { class: "tiptap-table" },
53
+ }),
54
+ TableRow,
55
+ TableCell,
56
+ TableHeader,
57
+ ImageNode,
58
+ MoreBreak,
59
+ SlashCommands,
60
+ PasteImage,
61
+ BubbleMenu,
62
+ LinkToolbar,
63
+ ExitableMarks,
64
+ ];
65
+ }