@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,44 @@
1
+ /**
2
+ * Shared client-side helpers for adding Collections to site navigation.
3
+ */
4
+
5
+ import { getJsonString, readJsonObject } from "./json.js";
6
+ import { applySiteHeaderHtml } from "./site-header-fragment.js";
7
+
8
+ const SITE_HEADER_REQUEST_HEADER = "X-Jant-Site-Header";
9
+ const INCLUDE_SITE_HEADER_RESPONSE = "include";
10
+
11
+ /**
12
+ * Add a Collection to the end of the primary navigation and refresh the
13
+ * rendered site header when the server returns a fragment.
14
+ *
15
+ * @param collectionId - TypeID of the Collection to add
16
+ * @returns The created navigation item ID when present
17
+ * @example
18
+ * const navItemId = await addCollectionToNavigation("col_01abc");
19
+ */
20
+ export async function addCollectionToNavigation(
21
+ collectionId: string,
22
+ ): Promise<string | undefined> {
23
+ const response = await fetch("/api/nav-items", {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ Accept: "application/json",
28
+ [SITE_HEADER_REQUEST_HEADER]: INCLUDE_SITE_HEADER_RESPONSE,
29
+ },
30
+ body: JSON.stringify({
31
+ type: "collection",
32
+ collectionId,
33
+ placement: "header",
34
+ }),
35
+ });
36
+ const body = await readJsonObject(response);
37
+
38
+ if (!response.ok) {
39
+ throw new Error(getJsonString(body, "error") ?? `HTTP ${response.status}`);
40
+ }
41
+
42
+ applySiteHeaderHtml(getJsonString(body, "headerHtml"));
43
+ return getJsonString(body, "id");
44
+ }
@@ -1,9 +1,16 @@
1
1
  import { getCollectionsDirectoryPath } from "../lib/collection-paths.js";
2
+ import { NAVIGATION_SETTINGS_PATH } from "../lib/settings-paths.js";
3
+ import { addCollectionToNavigation } from "./collection-navigation.js";
2
4
  import { showConfirmDialog } from "./confirm.js";
3
- import { showToast } from "./toast.js";
5
+ import { showToast, showToastWithAction } from "./toast.js";
4
6
 
5
7
  interface CollectionPageActionLabels {
6
8
  edit: string;
9
+ addToNavigation: string;
10
+ addingToNavigation: string;
11
+ addedToNavigation: string;
12
+ editNavigation: string;
13
+ addToNavigationFailed: string;
7
14
  moreActions: string;
8
15
  deleteCollection: string;
9
16
  confirmDelete: string;
@@ -16,6 +23,11 @@ const parseLabels = (value: string | undefined): CollectionPageActionLabels => {
16
23
  if (!value) {
17
24
  return {
18
25
  edit: "",
26
+ addToNavigation: "",
27
+ addingToNavigation: "",
28
+ addedToNavigation: "",
29
+ editNavigation: "",
30
+ addToNavigationFailed: "",
19
31
  moreActions: "",
20
32
  deleteCollection: "",
21
33
  confirmDelete: "",
@@ -30,6 +42,11 @@ const parseLabels = (value: string | undefined): CollectionPageActionLabels => {
30
42
  } catch {
31
43
  return {
32
44
  edit: "",
45
+ addToNavigation: "",
46
+ addingToNavigation: "",
47
+ addedToNavigation: "",
48
+ editNavigation: "",
49
+ addToNavigationFailed: "",
33
50
  moreActions: "",
34
51
  deleteCollection: "",
35
52
  confirmDelete: "",
@@ -53,6 +70,12 @@ document
53
70
  "[data-collection-page-action='toggle-menu']",
54
71
  );
55
72
  const menu = root.querySelector<HTMLElement>("[data-collection-page-menu]");
73
+ const addToNavigationButton = root.querySelector<HTMLButtonElement>(
74
+ "[data-collection-page-action='add-to-navigation']",
75
+ );
76
+ const editNavigationLink = root.querySelector<HTMLAnchorElement>(
77
+ "[data-collection-page-edit-navigation]",
78
+ );
56
79
 
57
80
  if (!collectionId || !trigger || !menu) return;
58
81
 
@@ -96,6 +119,35 @@ document
96
119
  }
97
120
  };
98
121
 
122
+ const handleAddToNavigation = async () => {
123
+ if (!addToNavigationButton || addToNavigationButton.disabled) return;
124
+
125
+ closeMenu(false);
126
+ const label = addToNavigationButton.querySelector<HTMLElement>(
127
+ ".collections-page-menu-item-label",
128
+ );
129
+ addToNavigationButton.disabled = true;
130
+ if (label) label.textContent = labels.addingToNavigation;
131
+
132
+ try {
133
+ await addCollectionToNavigation(collectionId);
134
+ addToNavigationButton.hidden = true;
135
+ if (editNavigationLink) editNavigationLink.hidden = false;
136
+ root.dataset.collectionInNavigation = "true";
137
+ showToastWithAction(labels.addedToNavigation, {
138
+ label: labels.editNavigation,
139
+ href:
140
+ editNavigationLink?.getAttribute("href") ??
141
+ NAVIGATION_SETTINGS_PATH,
142
+ });
143
+ } catch {
144
+ showToast(labels.addToNavigationFailed, "error");
145
+ } finally {
146
+ addToNavigationButton.disabled = false;
147
+ if (label) label.textContent = labels.addToNavigation;
148
+ }
149
+ };
150
+
99
151
  trigger.addEventListener("click", (event) => {
100
152
  event.preventDefault();
101
153
  event.stopPropagation();
@@ -149,6 +201,10 @@ document
149
201
  event.preventDefault();
150
202
  void handleDelete();
151
203
  }
204
+ if (action === "add-to-navigation") {
205
+ event.preventDefault();
206
+ void handleAddToNavigation();
207
+ }
152
208
  });
153
209
 
154
210
  document.addEventListener("click", (event) => {
@@ -55,7 +55,6 @@ describe("convertComposeFormat", () => {
55
55
  "quote",
56
56
  fields({
57
57
  title: "My thoughts",
58
- showTitle: true,
59
58
  bodyJson: doc(paragraph("body")),
60
59
  }),
61
60
  );
@@ -216,7 +215,6 @@ describe("convertComposeFormat", () => {
216
215
  "link",
217
216
  fields({
218
217
  title: "Keep me",
219
- showTitle: true,
220
218
  bodyJson: doc(paragraph("body")),
221
219
  }),
222
220
  );
@@ -236,7 +234,6 @@ describe("convertComposeFormat", () => {
236
234
  }),
237
235
  );
238
236
  expect(result.title).toBe("Keep me");
239
- expect(result.showTitle).toBe(true);
240
237
  expect(result.url).toBe("");
241
238
  expect(firstBlock(result)).toEqual({
242
239
  type: "paragraph",
@@ -317,22 +314,6 @@ describe("convertComposeFormat", () => {
317
314
  expect(result.bodyJson).toBeNull();
318
315
  });
319
316
 
320
- it("sets showTitle only for a note that ends up with a title", () => {
321
- const toNote = convertComposeFormat(
322
- "link",
323
- "note",
324
- fields({ title: "T", url: "https://example.com" }),
325
- );
326
- expect(toNote.showTitle).toBe(true);
327
-
328
- const toQuote = convertComposeFormat(
329
- "note",
330
- "quote",
331
- fields({ title: "T" }),
332
- );
333
- expect(toQuote.showTitle).toBe(false);
334
- });
335
-
336
317
  it("treats a non-attribution last line as part of the quote text", () => {
337
318
  const result = convertComposeFormat(
338
319
  "note",
@@ -15,10 +15,16 @@ vi.mock("sortablejs", () => ({
15
15
  },
16
16
  }));
17
17
 
18
+ vi.mock("../../toast.js", () => ({
19
+ showToast: vi.fn(),
20
+ showToastWithAction: vi.fn(),
21
+ }));
22
+
18
23
  import type {
19
24
  CollectionManagerItem,
20
25
  CollectionManagerLabels,
21
26
  } from "../collection-manager-types.js";
27
+ import { queueCollectionCreatedNotice } from "../../collection-created-notice.js";
22
28
  import "../jant-collection-directory.js";
23
29
  import type { JantCollectionsManager } from "../jant-collection-directory.js";
24
30
 
@@ -35,6 +41,12 @@ const labels: CollectionManagerLabels = {
35
41
  dividerLabelPlaceholder: "Section",
36
42
  newCollection: "New collection",
37
43
  edit: "Edit",
44
+ addToNavigation: "Add to Navigation",
45
+ addingToNavigation: "Adding…",
46
+ addedToNavigation: "Collection added to navigation.",
47
+ editNavigation: "Edit Navigation",
48
+ addToNavigationFailed: "Couldn't add this collection to navigation.",
49
+ notNow: "Not now",
38
50
  label: "Label",
39
51
  url: "URL",
40
52
  linkLabelPlaceholder: "Quotes",
@@ -49,8 +61,8 @@ const labels: CollectionManagerLabels = {
49
61
  deleteLink: "Remove link",
50
62
  confirmDeleteLink: "Remove this link from Collections?",
51
63
  cancel: "Cancel",
52
- entrySingular: "entry",
53
- entryPlural: "entries",
64
+ threadSingular: "thread",
65
+ threadPlural: "threads",
54
66
  emptyState: "Create a collection to get started.",
55
67
  orderSaved: "Collection order updated.",
56
68
  saved: "Collection saved.",
@@ -94,7 +106,7 @@ const items: CollectionManagerItem[] = [
94
106
  title: "Reading",
95
107
  description: "Notes from books",
96
108
  sortOrder: "newest",
97
- postCount: 4,
109
+ threadCount: 4,
98
110
  recentActivityAt: 1_763_619_200,
99
111
  },
100
112
  },
@@ -109,7 +121,7 @@ const items: CollectionManagerItem[] = [
109
121
  title: "Tools",
110
122
  description: "Tools I keep around",
111
123
  sortOrder: "newest",
112
- postCount: 2,
124
+ threadCount: 2,
113
125
  recentActivityAt: 1_763_619_260,
114
126
  },
115
127
  },
@@ -140,7 +152,7 @@ const groupedItems: CollectionManagerItem[] = [
140
152
  title: "Solo",
141
153
  description: null,
142
154
  sortOrder: "newest",
143
- postCount: 1,
155
+ threadCount: 1,
144
156
  recentActivityAt: 1_763_619_300,
145
157
  },
146
158
  },
@@ -162,6 +174,7 @@ async function createElement(): Promise<JantCollectionsManager> {
162
174
  ) as JantCollectionsManager;
163
175
  el.labels = labels;
164
176
  el.items = items;
177
+ el.navigationCollectionIds = [];
165
178
  document.body.appendChild(el);
166
179
  await el.updateComplete;
167
180
  return el;
@@ -175,6 +188,7 @@ async function createElementWithItems(
175
188
  ) as JantCollectionsManager;
176
189
  el.labels = labels;
177
190
  el.items = customItems;
191
+ el.navigationCollectionIds = [];
178
192
  document.body.appendChild(el);
179
193
  await el.updateComplete;
180
194
  return el;
@@ -199,15 +213,24 @@ async function createElementWithManagerRoot(): Promise<JantCollectionsManager> {
199
213
  ) as JantCollectionsManager;
200
214
  el.labels = labels;
201
215
  el.items = items;
216
+ el.navigationCollectionIds = [];
202
217
  root.appendChild(el);
203
218
  document.body.appendChild(root);
204
219
  await el.updateComplete;
205
220
  return el;
206
221
  }
207
222
 
223
+ async function flushAsyncWork() {
224
+ await new Promise((resolve) => setTimeout(resolve, 0));
225
+ await new Promise((resolve) => setTimeout(resolve, 0));
226
+ }
227
+
208
228
  describe("JantCollectionsManager", () => {
209
229
  beforeEach(() => {
210
230
  document.body.innerHTML = "";
231
+ globalThis.sessionStorage.clear();
232
+ vi.restoreAllMocks();
233
+ vi.clearAllMocks();
211
234
  sortableCreateMock.mockClear();
212
235
  sortableDestroyMock.mockClear();
213
236
  });
@@ -305,7 +328,15 @@ describe("JantCollectionsManager", () => {
305
328
  );
306
329
  expect(firstRow?.textContent).toContain("Reading");
307
330
  expect(firstRow?.textContent).toContain("Notes from books");
308
- expect(firstRow?.textContent).toContain("4 entries");
331
+ expect(firstRow?.textContent).toContain("4 threads");
332
+ });
333
+
334
+ it("keeps every Collection action trigger in the keyboard tab order", async () => {
335
+ const el = await createElement();
336
+
337
+ expect(
338
+ el.querySelectorAll(".collection-directory-item-menu > button"),
339
+ ).toHaveLength(2);
309
340
  });
310
341
 
311
342
  it("keeps focus on the URL field while typing in the new link form", async () => {
@@ -342,4 +373,122 @@ describe("JantCollectionsManager", () => {
342
373
  "/archive?format=quote&visibility=public&view=list",
343
374
  );
344
375
  });
376
+
377
+ it("shows a one-time post-create notice in the Collections content", async () => {
378
+ queueCollectionCreatedNotice("collection-1");
379
+ const el = await createElement();
380
+
381
+ const notice = el.querySelector<HTMLElement>(".collection-created-notice");
382
+ expect(notice?.textContent).toContain("Collection created.");
383
+ expect(notice?.textContent).not.toContain("Reading");
384
+ expect(notice?.textContent).toContain("Add to Navigation");
385
+
386
+ const dismissButton = notice?.querySelector<HTMLButtonElement>(
387
+ 'button[aria-label="Not now"]',
388
+ );
389
+ dismissButton?.click();
390
+ await el.updateComplete;
391
+
392
+ expect(el.querySelector(".collection-created-notice")).toBeNull();
393
+ });
394
+
395
+ it("adds a Collection to navigation from its item menu", async () => {
396
+ const el = await createElement();
397
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
398
+ new Response(JSON.stringify({ id: "nav-1" }), {
399
+ status: 201,
400
+ headers: { "Content-Type": "application/json" },
401
+ }),
402
+ );
403
+ const { showToastWithAction } = await import("../../toast.js");
404
+
405
+ el._showItemMenuId = items[0]?.id ?? null;
406
+ await el.updateComplete;
407
+
408
+ const addButton = Array.from(
409
+ el.querySelectorAll<HTMLButtonElement>(".collections-page-menu-item"),
410
+ ).find((button) => button.textContent?.includes("Add to Navigation"));
411
+ expect(addButton).toBeDefined();
412
+
413
+ addButton?.click();
414
+ await flushAsyncWork();
415
+ await el.updateComplete;
416
+
417
+ expect(fetchMock).toHaveBeenCalledWith("/api/nav-items", {
418
+ method: "POST",
419
+ headers: {
420
+ "Content-Type": "application/json",
421
+ Accept: "application/json",
422
+ "X-Jant-Site-Header": "include",
423
+ },
424
+ body: JSON.stringify({
425
+ type: "collection",
426
+ collectionId: "collection-1",
427
+ placement: "header",
428
+ }),
429
+ });
430
+ expect(el.navigationCollectionIds).toContain("collection-1");
431
+ expect(showToastWithAction).toHaveBeenCalledWith(
432
+ "Collection added to navigation.",
433
+ {
434
+ label: "Edit Navigation",
435
+ href: "/settings/navigation",
436
+ },
437
+ );
438
+
439
+ el._showItemMenuId = items[0]?.id ?? null;
440
+ await el.updateComplete;
441
+ const editNavigationLink = Array.from(
442
+ el.querySelectorAll<HTMLAnchorElement>(".collections-page-menu-item"),
443
+ ).find((link) => link.textContent?.includes("Edit Navigation"));
444
+ expect(editNavigationLink?.getAttribute("href")).toBe(
445
+ "/settings/navigation",
446
+ );
447
+ });
448
+
449
+ it("adds the newly created Collection from the inline notice", async () => {
450
+ queueCollectionCreatedNotice("collection-1");
451
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
452
+ new Response(JSON.stringify({ id: "nav-1" }), {
453
+ status: 201,
454
+ headers: { "Content-Type": "application/json" },
455
+ }),
456
+ );
457
+ const { showToastWithAction } = await import("../../toast.js");
458
+ const el = await createElement();
459
+ const notice = el.querySelector<HTMLElement>(".collection-created-notice");
460
+ const addButton = Array.from(
461
+ notice?.querySelectorAll<HTMLButtonElement>("button") ?? [],
462
+ ).find((button) => button.textContent?.includes("Add to Navigation"));
463
+
464
+ expect(addButton).toBeDefined();
465
+ addButton?.click();
466
+ await flushAsyncWork();
467
+ await el.updateComplete;
468
+
469
+ expect(fetchMock).toHaveBeenCalledWith(
470
+ "/api/nav-items",
471
+ expect.objectContaining({
472
+ method: "POST",
473
+ body: JSON.stringify({
474
+ type: "collection",
475
+ collectionId: "collection-1",
476
+ placement: "header",
477
+ }),
478
+ }),
479
+ );
480
+ expect(el.navigationCollectionIds).toContain("collection-1");
481
+ const updatedNotice = el.querySelector<HTMLElement>(
482
+ ".collection-created-notice",
483
+ );
484
+ expect(updatedNotice?.textContent).toContain(
485
+ "Collection added to navigation.",
486
+ );
487
+ expect(
488
+ updatedNotice
489
+ ?.querySelector<HTMLAnchorElement>("a")
490
+ ?.getAttribute("href"),
491
+ ).toBe("/settings/navigation");
492
+ expect(showToastWithAction).not.toHaveBeenCalled();
493
+ });
345
494
  });
@@ -11,6 +11,7 @@ interface MockPaletteItem {
11
11
  title: string;
12
12
  path: string;
13
13
  type: "post" | "collection" | "system";
14
+ status?: "draft" | "published";
14
15
  }
15
16
 
16
17
  function mockPaletteApi(items: MockPaletteItem[] = []) {
@@ -44,6 +45,7 @@ describe("JantCommandPalette slash path mode", () => {
44
45
  beforeEach(() => {
45
46
  vi.restoreAllMocks();
46
47
  document.body.innerHTML = "";
48
+ delete document.documentElement.dataset.sitePathPrefix;
47
49
  globalThis.localStorage.clear();
48
50
  globalThis.history.replaceState({}, "", "/");
49
51
  });
@@ -73,6 +75,7 @@ describe("JantCommandPalette persistent search action", () => {
73
75
  beforeEach(() => {
74
76
  vi.restoreAllMocks();
75
77
  document.body.innerHTML = "";
78
+ delete document.documentElement.dataset.sitePathPrefix;
76
79
  globalThis.localStorage.clear();
77
80
  globalThis.history.replaceState({}, "", "/");
78
81
  });
@@ -187,4 +190,52 @@ describe("JantCommandPalette persistent search action", () => {
187
190
 
188
191
  expect(globalThis.location.pathname).toBe("/match-1");
189
192
  });
193
+
194
+ it("opens draft post matches through the authenticated preview route", async () => {
195
+ const el = await renderPalette("draft", [
196
+ {
197
+ title: "Draft post",
198
+ path: "draft-post",
199
+ type: "post",
200
+ status: "draft",
201
+ },
202
+ ]);
203
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
204
+ expect(dialog?.textContent).toContain("Draft · draft-post");
205
+
206
+ dialog?.dispatchEvent(
207
+ new globalThis.KeyboardEvent("keydown", {
208
+ key: "Enter",
209
+ bubbles: true,
210
+ cancelable: true,
211
+ }),
212
+ );
213
+
214
+ expect(globalThis.location.pathname).toBe("/preview/draft-post");
215
+ expect(globalThis.location.search).toBe("?edit=1");
216
+ });
217
+
218
+ it("keeps the site path prefix when opening a draft preview", async () => {
219
+ document.documentElement.dataset.sitePathPrefix = "/blog";
220
+ const el = await renderPalette("draft", [
221
+ {
222
+ title: "Draft post",
223
+ path: "draft-post",
224
+ type: "post",
225
+ status: "draft",
226
+ },
227
+ ]);
228
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
229
+
230
+ dialog?.dispatchEvent(
231
+ new globalThis.KeyboardEvent("keydown", {
232
+ key: "Enter",
233
+ bubbles: true,
234
+ cancelable: true,
235
+ }),
236
+ );
237
+
238
+ expect(globalThis.location.pathname).toBe("/blog/preview/draft-post");
239
+ expect(globalThis.location.search).toBe("?edit=1");
240
+ });
190
241
  });