@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -0,0 +1,254 @@
1
+ /** Accessible table-dimension picker opened by the compose slash command. */
2
+
3
+ import type { Editor, Range } from "@tiptap/core";
4
+ import {
5
+ getFixedFloatingContainerRect,
6
+ getFloatingPosition,
7
+ } from "./floating-position.js";
8
+ import type { TableControlLabels } from "./table-control-labels.js";
9
+
10
+ const MAX_ROWS = 8;
11
+ const MAX_COLUMNS = 8;
12
+ const DEFAULT_ROWS = 3;
13
+ const DEFAULT_COLUMNS = 3;
14
+
15
+ interface ActivePicker {
16
+ close: (restoreFocus?: boolean) => void;
17
+ }
18
+
19
+ let activePicker: ActivePicker | null = null;
20
+
21
+ function formatSizeLabel(
22
+ template: string,
23
+ rows: number,
24
+ columns: number,
25
+ ): string {
26
+ return template
27
+ .replaceAll("%rows%", String(rows))
28
+ .replaceAll("%cols%", String(columns));
29
+ }
30
+
31
+ /**
32
+ * Opens a bounded 8×8 table-size picker at the current slash-command range.
33
+ *
34
+ * @param editor - Editor receiving the selected table
35
+ * @param range - Slash query range to remove before opening the picker
36
+ * @param labels - Localized table UI labels
37
+ * @returns Nothing
38
+ */
39
+ export function openTableSizePicker(
40
+ editor: Editor,
41
+ range: Range,
42
+ labels: TableControlLabels,
43
+ ): void {
44
+ activePicker?.close(false);
45
+
46
+ editor.chain().focus().deleteRange(range).run();
47
+ const insertPosition = editor.state.selection.from;
48
+ const dialog = editor.view.dom.closest("dialog");
49
+ const container = dialog ?? document.body;
50
+
51
+ const picker = document.createElement("div");
52
+ picker.className = "tiptap-table-size-picker";
53
+ picker.dataset.editorFloatingUi = "true";
54
+ picker.style.position = "fixed";
55
+ picker.setAttribute("role", "dialog");
56
+ picker.setAttribute("aria-label", labels.sizePickerLabel);
57
+
58
+ const header = document.createElement("div");
59
+ header.className = "tiptap-table-size-picker-header";
60
+ const title = document.createElement("span");
61
+ title.textContent = labels.sizePickerLabel;
62
+ const dimensions = document.createElement("output");
63
+ dimensions.className = "tiptap-table-size-picker-dimensions";
64
+ dimensions.setAttribute("aria-live", "polite");
65
+ header.appendChild(title);
66
+ header.appendChild(dimensions);
67
+ picker.appendChild(header);
68
+
69
+ const grid = document.createElement("div");
70
+ grid.className = "tiptap-table-size-grid";
71
+ grid.setAttribute("role", "grid");
72
+ grid.setAttribute("aria-label", labels.sizePickerLabel);
73
+ picker.appendChild(grid);
74
+
75
+ let selectedRows = DEFAULT_ROWS;
76
+ let selectedColumns = DEFAULT_COLUMNS;
77
+ let closed = false;
78
+ const cells: HTMLButtonElement[][] = [];
79
+
80
+ function labelFor(rows: number, columns: number): string {
81
+ return formatSizeLabel(labels.insertTableSize, rows, columns);
82
+ }
83
+
84
+ function updateSelection(rows: number, columns: number): void {
85
+ selectedRows = Math.min(Math.max(rows, 1), MAX_ROWS);
86
+ selectedColumns = Math.min(Math.max(columns, 1), MAX_COLUMNS);
87
+ dimensions.textContent = `${selectedRows} × ${selectedColumns}`;
88
+ picker.setAttribute(
89
+ "aria-label",
90
+ `${labels.sizePickerLabel}: ${labelFor(selectedRows, selectedColumns)}`,
91
+ );
92
+
93
+ for (let row = 1; row <= MAX_ROWS; row += 1) {
94
+ for (let column = 1; column <= MAX_COLUMNS; column += 1) {
95
+ const cell = cells[row - 1]?.[column - 1];
96
+ if (!cell) continue;
97
+ const isCurrent = row === selectedRows && column === selectedColumns;
98
+ cell.classList.toggle(
99
+ "is-selected",
100
+ row <= selectedRows && column <= selectedColumns,
101
+ );
102
+ cell.classList.toggle("is-current", isCurrent);
103
+ cell.tabIndex = isCurrent ? 0 : -1;
104
+ cell.setAttribute("aria-selected", String(isCurrent));
105
+ }
106
+ }
107
+ }
108
+
109
+ function focusSelection(): void {
110
+ cells[selectedRows - 1]?.[selectedColumns - 1]?.focus();
111
+ }
112
+
113
+ function insertTable(): void {
114
+ if (closed) return;
115
+ close(false);
116
+ editor
117
+ .chain()
118
+ .focus()
119
+ .setTextSelection(insertPosition)
120
+ .insertTable({
121
+ rows: selectedRows,
122
+ cols: selectedColumns,
123
+ withHeaderRow: true,
124
+ })
125
+ .run();
126
+ }
127
+
128
+ function onKeydown(event: KeyboardEvent): void {
129
+ let nextRows = selectedRows;
130
+ let nextColumns = selectedColumns;
131
+ if (event.key === "ArrowUp") nextRows -= 1;
132
+ else if (event.key === "ArrowDown") nextRows += 1;
133
+ else if (event.key === "ArrowLeft") nextColumns -= 1;
134
+ else if (event.key === "ArrowRight") nextColumns += 1;
135
+ else if (event.key === "Enter" || event.key === " ") {
136
+ event.preventDefault();
137
+ event.stopPropagation();
138
+ insertTable();
139
+ return;
140
+ } else if (event.key === "Escape") {
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ close(true);
144
+ return;
145
+ } else {
146
+ return;
147
+ }
148
+
149
+ event.preventDefault();
150
+ event.stopPropagation();
151
+ updateSelection(nextRows, nextColumns);
152
+ focusSelection();
153
+ }
154
+
155
+ for (let row = 1; row <= MAX_ROWS; row += 1) {
156
+ const rowElement = document.createElement("div");
157
+ rowElement.className = "tiptap-table-size-grid-row";
158
+ rowElement.setAttribute("role", "row");
159
+ const rowCells: HTMLButtonElement[] = [];
160
+ for (let column = 1; column <= MAX_COLUMNS; column += 1) {
161
+ const cell = document.createElement("button");
162
+ cell.type = "button";
163
+ cell.className = "tiptap-table-size-cell";
164
+ cell.dataset.row = String(row);
165
+ cell.dataset.column = String(column);
166
+ cell.setAttribute("role", "gridcell");
167
+ cell.setAttribute("aria-label", labelFor(row, column));
168
+ cell.addEventListener("pointerenter", () => {
169
+ updateSelection(row, column);
170
+ });
171
+ cell.addEventListener("focus", () => {
172
+ updateSelection(row, column);
173
+ });
174
+ cell.addEventListener("keydown", onKeydown);
175
+ cell.addEventListener("click", insertTable);
176
+ rowElement.appendChild(cell);
177
+ rowCells.push(cell);
178
+ }
179
+ cells.push(rowCells);
180
+ grid.appendChild(rowElement);
181
+ }
182
+
183
+ function positionPicker(): void {
184
+ if (closed) return;
185
+ const coords = editor.view.coordsAtPos(insertPosition);
186
+ const rect = picker.getBoundingClientRect();
187
+ const layout = getFloatingPosition({
188
+ anchorRect: {
189
+ left: coords.left,
190
+ right: coords.right,
191
+ top: coords.top,
192
+ bottom: coords.bottom,
193
+ },
194
+ containerRect: getFixedFloatingContainerRect(dialog),
195
+ floatingWidth: rect.width,
196
+ floatingHeight: rect.height,
197
+ preferredPlacement: "bottom",
198
+ fallbackPlacement: "top",
199
+ align: "start",
200
+ });
201
+ picker.style.left = `${layout.left}px`;
202
+ picker.style.top = `${layout.top}px`;
203
+ picker.dataset.placement = layout.placement;
204
+ }
205
+
206
+ function onOutsidePointer(event: MouseEvent): void {
207
+ const target = event.target;
208
+ if (target instanceof Node && !picker.contains(target)) {
209
+ close(false);
210
+ }
211
+ }
212
+
213
+ function onOutsideFocus(event: globalThis.FocusEvent): void {
214
+ const target = event.target;
215
+ if (
216
+ target instanceof Node &&
217
+ !picker.contains(target) &&
218
+ !editor.view.dom.contains(target)
219
+ ) {
220
+ close(false);
221
+ }
222
+ }
223
+
224
+ function onEditorDestroy(): void {
225
+ close(false);
226
+ }
227
+
228
+ function close(restoreFocus = false): void {
229
+ if (closed) return;
230
+ closed = true;
231
+ document.removeEventListener("mousedown", onOutsidePointer, true);
232
+ document.removeEventListener("focusin", onOutsideFocus, true);
233
+ window.removeEventListener("resize", positionPicker);
234
+ window.removeEventListener("scroll", positionPicker, true);
235
+ editor.off("destroy", onEditorDestroy);
236
+ picker.remove();
237
+ if (activePicker?.close === close) activePicker = null;
238
+ if (restoreFocus && !editor.isDestroyed) editor.view.focus();
239
+ }
240
+
241
+ activePicker = { close };
242
+ container.appendChild(picker);
243
+ updateSelection(DEFAULT_ROWS, DEFAULT_COLUMNS);
244
+ positionPicker();
245
+ window.addEventListener("resize", positionPicker);
246
+ window.addEventListener("scroll", positionPicker, true);
247
+ editor.on("destroy", onEditorDestroy);
248
+ setTimeout(() => {
249
+ if (closed) return;
250
+ document.addEventListener("mousedown", onOutsidePointer, true);
251
+ document.addEventListener("focusin", onOutsideFocus, true);
252
+ focusSelection();
253
+ }, 0);
254
+ }
@@ -20,8 +20,40 @@ interface QueuedToast {
20
20
 
21
21
  export const QUEUED_TOAST_STORAGE_KEY = "jant.pendingToast";
22
22
 
23
- /** Ensure the toast container is in the top layer (above <dialog> etc.) */
23
+ function openModalDialogs(): HTMLDialogElement[] {
24
+ const open = [
25
+ ...document.querySelectorAll<HTMLDialogElement>("dialog[open]"),
26
+ ];
27
+ try {
28
+ const modal = open.filter((dialog) => dialog.matches(":modal"));
29
+ if (modal.length > 0) return modal;
30
+ } catch {
31
+ // ":modal" unsupported — treat every open dialog as potentially modal.
32
+ }
33
+ // Engines without working ":modal" (happy-dom) match nothing above; Jant
34
+ // only opens dialogs via showModal(), so open dialogs are the right proxy.
35
+ return open;
36
+ }
37
+
38
+ /**
39
+ * A modal <dialog> makes everything outside its subtree inert: the popover
40
+ * promotion keeps toasts *painted* above the dialog, but clicks and text
41
+ * selection would still fall through to the dialog. Mounting the container
42
+ * inside the topmost open modal dialog escapes the inert tree; the close
43
+ * watcher moves it back out when that dialog closes.
44
+ */
45
+ function mountOutsideInertTree(container: HTMLElement): void {
46
+ const dialogs = openModalDialogs();
47
+ // Top-layer order isn't queryable; document order matches Jant's dialog
48
+ // nesting (confirm dialogs are descendants of the dialog that opened them).
49
+ const host = dialogs.at(-1) ?? document.body;
50
+ if (container.parentElement !== host) host.appendChild(container);
51
+ }
52
+
53
+ /** Ensure the toast container is interactive and in the top layer (above <dialog> etc.) */
24
54
  function ensureTopLayer(container: HTMLElement): void {
55
+ mountOutsideInertTree(container);
56
+
25
57
  if (typeof container.showPopover !== "function") return;
26
58
 
27
59
  // Re-promote above any modal dialog that was opened after the toast container.
@@ -38,7 +70,21 @@ function ensureTopLayer(container: HTMLElement): void {
38
70
  }
39
71
 
40
72
  function getToastContainer(): HTMLElement | null {
41
- return document.getElementById("toast-container");
73
+ const existing = document.getElementById("toast-container");
74
+ if (existing) return existing;
75
+ if (!document.body) return null;
76
+
77
+ // The container can vanish with a modal dialog it was mounted into if that
78
+ // dialog is removed from the DOM while open. Recreate it so toasts keep
79
+ // working for the rest of the page's lifetime.
80
+ const container = document.createElement("div");
81
+ container.id = "toast-container";
82
+ container.className = "toast-container";
83
+ container.setAttribute("popover", "manual");
84
+ container.setAttribute("aria-live", "polite");
85
+ container.setAttribute("aria-relevant", "additions text");
86
+ document.body.appendChild(container);
87
+ return container;
42
88
  }
43
89
 
44
90
  function canUseSessionStorage(): boolean {
@@ -92,10 +138,9 @@ const TOAST_ICONS = {
92
138
  '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>',
93
139
  };
94
140
 
95
- const COPY_ICON =
96
- '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
97
- const CHECK_ICON =
98
- '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>';
141
+ const TOAST_DURATION_MS = 3000;
142
+ const ACTION_TOAST_DURATION_MS = 5000;
143
+ const MIN_RESUME_MS = 1000;
99
144
 
100
145
  /** Build toast inner content using safe DOM APIs (icon is trusted, text uses textContent). */
101
146
  function setToastContent(
@@ -115,21 +160,62 @@ function setToastContent(
115
160
  a.textContent = action.label;
116
161
  toast.appendChild(a);
117
162
  }
118
- if (type === "error" && navigator.clipboard) {
119
- const btn = document.createElement("button");
120
- btn.className = "toast-copy";
121
- btn.setAttribute("aria-label", "Copy error message");
122
- btn.innerHTML = COPY_ICON;
123
- btn.addEventListener("click", () => {
124
- navigator.clipboard.writeText(message).then(() => {
125
- btn.innerHTML = CHECK_ICON;
126
- setTimeout(() => {
127
- btn.innerHTML = COPY_ICON;
128
- }, 1500);
129
- });
130
- });
131
- toast.appendChild(btn);
132
- }
163
+ }
164
+
165
+ /**
166
+ * Auto-dismiss a toast after `duration`, pausing while the pointer hovers it
167
+ * or focus is inside it — so the message can be read, selected, and copied.
168
+ * On leave, the remaining time resumes (at least MIN_RESUME_MS).
169
+ */
170
+ function scheduleAutoDismiss(toast: HTMLElement, duration: number): void {
171
+ let remaining = duration;
172
+ let startedAt = 0;
173
+ let timer: ReturnType<typeof setTimeout> | null = null;
174
+ let hovered = false;
175
+ let focused = false;
176
+
177
+ const pause = () => {
178
+ if (timer === null) return;
179
+ clearTimeout(timer);
180
+ timer = null;
181
+ remaining = Math.max(remaining - (Date.now() - startedAt), MIN_RESUME_MS);
182
+ };
183
+
184
+ const resume = () => {
185
+ if (timer !== null) return;
186
+ startedAt = Date.now();
187
+ timer = setTimeout(() => {
188
+ toast.classList.add("toast-out");
189
+ toast.addEventListener("animationend", () => toast.remove());
190
+ }, remaining);
191
+ };
192
+
193
+ const sync = () => {
194
+ if (hovered || focused) {
195
+ pause();
196
+ } else {
197
+ resume();
198
+ }
199
+ };
200
+
201
+ toast.addEventListener("pointerenter", () => {
202
+ hovered = true;
203
+ sync();
204
+ });
205
+ toast.addEventListener("pointerleave", () => {
206
+ hovered = false;
207
+ sync();
208
+ });
209
+ toast.addEventListener("focusin", () => {
210
+ focused = true;
211
+ sync();
212
+ });
213
+ toast.addEventListener("focusout", () => {
214
+ focused = false;
215
+ sync();
216
+ });
217
+
218
+ resume();
133
219
  }
134
220
 
135
221
  /**
@@ -155,17 +241,14 @@ export function showToast(message: string, type: ToastType = "success"): void {
155
241
  setToastContent(toast, type, message);
156
242
  container.appendChild(toast);
157
243
 
158
- setTimeout(() => {
159
- toast.classList.add("toast-out");
160
- toast.addEventListener("animationend", () => toast.remove());
161
- }, 3000);
244
+ scheduleAutoDismiss(toast, TOAST_DURATION_MS);
162
245
  }
163
246
 
164
247
  /**
165
248
  * Show a toast with an action link.
166
249
  *
167
250
  * @param message - Text to display
168
- * @param action - Action link with label and href
251
+ * @param action - Action link rendered beside the message
169
252
  * @param type - Visual style: "success" (default) or "error"
170
253
  *
171
254
  * @example
@@ -188,10 +271,7 @@ export function showToastWithAction(
188
271
  setToastContent(toast, type, message, action);
189
272
  container.appendChild(toast);
190
273
 
191
- setTimeout(() => {
192
- toast.classList.add("toast-out");
193
- toast.addEventListener("animationend", () => toast.remove());
194
- }, 3000);
274
+ scheduleAutoDismiss(toast, ACTION_TOAST_DURATION_MS);
195
275
  }
196
276
 
197
277
  /**
@@ -282,10 +362,7 @@ export function replaceWithAutoClose(
282
362
  toast.replaceChildren();
283
363
  setToastContent(toast, type, message);
284
364
 
285
- setTimeout(() => {
286
- toast.classList.add("toast-out");
287
- toast.addEventListener("animationend", () => toast.remove());
288
- }, 3000);
365
+ scheduleAutoDismiss(toast, TOAST_DURATION_MS);
289
366
  }
290
367
 
291
368
  /**
@@ -293,7 +370,7 @@ export function replaceWithAutoClose(
293
370
  *
294
371
  * @param id - The toast identifier
295
372
  * @param message - New message text
296
- * @param action - Action link with label and href
373
+ * @param action - Action link rendered beside the message
297
374
  * @param type - Visual style: "success" (default) or "error"
298
375
  *
299
376
  * @example
@@ -315,10 +392,7 @@ export function replaceWithAutoCloseAction(
315
392
  toast.replaceChildren();
316
393
  setToastContent(toast, type, message, action);
317
394
 
318
- setTimeout(() => {
319
- toast.classList.add("toast-out");
320
- toast.addEventListener("animationend", () => toast.remove());
321
- }, 3000);
395
+ scheduleAutoDismiss(toast, ACTION_TOAST_DURATION_MS);
322
396
  }
323
397
 
324
398
  /**
@@ -361,6 +435,25 @@ export function consumeQueuedToast(): boolean {
361
435
  return true;
362
436
  }
363
437
 
438
+ function initDialogCloseWatcher(): void {
439
+ // "close" doesn't bubble; capture reaches it from the document anyway.
440
+ document.addEventListener(
441
+ "close",
442
+ (event) => {
443
+ const dialog = event.target;
444
+ if (!(dialog instanceof HTMLDialogElement)) return;
445
+
446
+ const container = document.getElementById("toast-container");
447
+ if (!container || !dialog.contains(container)) return;
448
+
449
+ // The closed dialog is display:none — move the container back out
450
+ // (into the next open modal dialog, if any) so live toasts stay visible.
451
+ ensureTopLayer(container);
452
+ },
453
+ true,
454
+ );
455
+ }
456
+
364
457
  function initQueuedToastConsumer(): void {
365
458
  const showQueuedToast = () => {
366
459
  consumeQueuedToast();
@@ -376,4 +469,5 @@ function initQueuedToastConsumer(): void {
376
469
  showQueuedToast();
377
470
  }
378
471
 
472
+ initDialogCloseWatcher();
379
473
  initQueuedToastConsumer();
@@ -18,6 +18,7 @@ import "./client/slash-discovery-bridge.js";
18
18
  import "./client/compose-shortcuts.js";
19
19
  import "./client/components/jant-settings-general.js";
20
20
  import "./client/components/jant-settings-avatar.js";
21
+ import "./client/components/jant-config-editor.js";
21
22
  import "./client/settings-bridge.js";
22
23
  import "./client/components/jant-repo-picker.js";
23
24
  import "./client/components/jant-collection-form.js";
package/src/client.ts CHANGED
@@ -19,6 +19,7 @@ import "./client/note-expand.js";
19
19
  import "./client/archive-nav.js";
20
20
  import "./client/site-header-nav.js";
21
21
  import "./client/collection-sort-menu.js";
22
+ import "./client/footnote-rail.js";
22
23
 
23
24
  declare const __JANT_VERSION__: string;
24
25