@jant/core 0.6.13 → 0.6.15

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 (316) hide show
  1. package/bin/commands/import-site.js +81 -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-BxCOR3Uc.js +6 -0
  12. package/dist/{app-HnzmsaqU.js → app-C-oi_t8W.js} +7419 -6350
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-3_OaxhTs.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-Dfbq9aot.js → export-CsFx6F_M.js} +1273 -131
  18. package/dist/{github-sync--PSoE-Ne.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-BGTLx8Mf.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
  47. package/src/client/components/__tests__/jant-compose-dialog.test.ts +373 -5
  48. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  49. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  50. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  51. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  52. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  53. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  54. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  55. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  56. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  57. package/src/client/components/collection-manager-types.ts +9 -3
  58. package/src/client/components/compose-types.ts +8 -0
  59. package/src/client/components/jant-collection-directory.ts +165 -30
  60. package/src/client/components/jant-command-palette.ts +102 -71
  61. package/src/client/components/jant-compose-dialog.ts +86 -22
  62. package/src/client/components/jant-compose-editor.ts +13 -7
  63. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  64. package/src/client/components/jant-config-editor.ts +722 -0
  65. package/src/client/components/jant-media-lightbox.ts +100 -18
  66. package/src/client/components/jant-nav-manager.ts +1283 -54
  67. package/src/client/components/jant-post-form.ts +3 -3
  68. package/src/client/components/jant-post-menu.ts +86 -74
  69. package/src/client/components/nav-manager-types.ts +51 -2
  70. package/src/client/components/post-form-types.ts +1 -1
  71. package/src/client/compose-bridge.ts +4 -1
  72. package/src/client/feed-video-player.ts +62 -2
  73. package/src/client/footnote-rail.ts +478 -0
  74. package/src/client/hydrate-partial.ts +2 -0
  75. package/src/client/media-lightbox-events.ts +63 -0
  76. package/src/client/site-header-fragment.ts +38 -0
  77. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  78. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  79. package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
  80. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  81. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +872 -2
  82. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  83. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  84. package/src/client/tiptap/bubble-menu.ts +91 -13
  85. package/src/client/tiptap/continuous-lists.ts +102 -0
  86. package/src/client/tiptap/create-editor.ts +3 -0
  87. package/src/client/tiptap/extensions.ts +14 -5
  88. package/src/client/tiptap/footnotes.ts +156 -0
  89. package/src/client/tiptap/markdown-clipboard.ts +614 -12
  90. package/src/client/tiptap/slash-commands.ts +8 -1
  91. package/src/client/tiptap/structural-keymap.ts +211 -0
  92. package/src/client/tiptap/tab-indent.ts +8 -22
  93. package/src/client/tiptap/table-control-labels.ts +30 -0
  94. package/src/client/tiptap/table-controls.ts +537 -0
  95. package/src/client/tiptap/table-size-picker.ts +254 -0
  96. package/src/client/toast.ts +8 -6
  97. package/src/client-auth.ts +1 -0
  98. package/src/client.ts +1 -0
  99. package/src/db/__tests__/d1-query.test.ts +43 -1
  100. package/src/db/__tests__/migration-runner.test.ts +63 -0
  101. package/src/db/__tests__/migrations.test.ts +469 -0
  102. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  103. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  104. package/src/db/migrations/0028_superb_post.sql +2 -0
  105. package/src/db/migrations/0029_boring_magma.sql +1 -0
  106. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  107. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  108. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  109. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  110. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  111. package/src/db/migrations/meta/_journal.json +28 -0
  112. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  113. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  114. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  115. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  116. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  117. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  118. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  119. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  120. package/src/db/migrations/pg/meta/_journal.json +28 -0
  121. package/src/db/pg/schema.ts +28 -20
  122. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  123. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  124. package/src/db/schema.ts +28 -20
  125. package/src/i18n/__tests__/detect.test.ts +8 -2
  126. package/src/i18n/detect.ts +14 -5
  127. package/src/i18n/locales/public/en.po +133 -30
  128. package/src/i18n/locales/public/en.ts +1 -1
  129. package/src/i18n/locales/public/zh-Hans.po +128 -25
  130. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  131. package/src/i18n/locales/public/zh-Hant.po +128 -25
  132. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  133. package/src/i18n/locales/settings/en.po +513 -10
  134. package/src/i18n/locales/settings/en.ts +1 -1
  135. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  136. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  137. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  138. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  139. package/src/lib/__tests__/api-settings.test.ts +299 -0
  140. package/src/lib/__tests__/constants.test.ts +5 -0
  141. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  142. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  143. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  144. package/src/lib/__tests__/markdown-to-tiptap.test.ts +36 -4
  145. package/src/lib/__tests__/markdown.test.ts +8 -6
  146. package/src/lib/__tests__/navigation.test.ts +59 -2
  147. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  148. package/src/lib/__tests__/post-display.test.ts +64 -0
  149. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  150. package/src/lib/__tests__/summary.test.ts +82 -0
  151. package/src/lib/__tests__/timeline.test.ts +109 -70
  152. package/src/lib/__tests__/timezones.test.ts +31 -3
  153. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  154. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  155. package/src/lib/__tests__/view.test.ts +53 -0
  156. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  157. package/src/lib/api-settings.ts +239 -12
  158. package/src/lib/collection-sort.ts +7 -5
  159. package/src/lib/constants.ts +2 -0
  160. package/src/lib/feed-path.ts +25 -0
  161. package/src/lib/feed-policy.ts +64 -0
  162. package/src/lib/footnotes.ts +264 -0
  163. package/src/lib/github-sync-site-config.ts +2 -0
  164. package/src/lib/hugo-markdown.ts +10 -3
  165. package/src/lib/jant-branding.ts +1 -0
  166. package/src/lib/markdown-manager.ts +202 -10
  167. package/src/lib/markdown.ts +10 -3
  168. package/src/lib/navigation.ts +16 -3
  169. package/src/lib/post-body-html.ts +131 -0
  170. package/src/lib/post-display.ts +7 -1
  171. package/src/lib/render.tsx +9 -2
  172. package/src/lib/resolve-config.ts +100 -16
  173. package/src/lib/schemas.ts +133 -1
  174. package/src/lib/settings-paths.ts +5 -0
  175. package/src/lib/site-skill.ts +78 -0
  176. package/src/lib/summary.ts +16 -5
  177. package/src/lib/timeline.ts +98 -122
  178. package/src/lib/timezones.ts +54 -14
  179. package/src/lib/tiptap-render.ts +355 -81
  180. package/src/lib/view.ts +30 -20
  181. package/src/lib/worker-response-cache.ts +11 -18
  182. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  183. package/src/middleware/config.ts +2 -2
  184. package/src/middleware/public-content-access.ts +87 -0
  185. package/src/node/__tests__/cli-export.test.ts +14 -0
  186. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  187. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  188. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  189. package/src/node/index.ts +2 -1
  190. package/src/preset.css +16 -2
  191. package/src/routes/__tests__/compose.test.ts +51 -0
  192. package/src/routes/__tests__/site-skill.test.ts +112 -0
  193. package/src/routes/api/__tests__/collections.test.ts +91 -19
  194. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  195. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  196. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  197. package/src/routes/api/__tests__/settings.test.ts +217 -0
  198. package/src/routes/api/collections.ts +22 -26
  199. package/src/routes/api/export.ts +2 -0
  200. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  201. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  202. package/src/routes/api/internal/post-body-html.ts +43 -0
  203. package/src/routes/api/internal/sites.ts +24 -0
  204. package/src/routes/api/nav-items.ts +24 -1
  205. package/src/routes/api/posts.ts +2 -2
  206. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  207. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  208. package/src/routes/api/public/archive.ts +4 -0
  209. package/src/routes/api/public/posts.ts +41 -24
  210. package/src/routes/api/search.ts +3 -0
  211. package/src/routes/api/settings.ts +33 -0
  212. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  213. package/src/routes/compose.tsx +2 -1
  214. package/src/routes/dash/settings.tsx +45 -2
  215. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  216. package/src/routes/feed/feed.ts +23 -14
  217. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  218. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  219. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  220. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  221. package/src/routes/pages/__tests__/search.test.ts +69 -0
  222. package/src/routes/pages/archive.tsx +27 -7
  223. package/src/routes/pages/collection.tsx +32 -19
  224. package/src/routes/pages/collections.tsx +6 -1
  225. package/src/routes/pages/page.tsx +83 -11
  226. package/src/routes/site-skill.ts +18 -0
  227. package/src/services/__tests__/auth.test.ts +7 -5
  228. package/src/services/__tests__/collection.test.ts +159 -44
  229. package/src/services/__tests__/navigation.test.ts +229 -21
  230. package/src/services/__tests__/post-timeline.test.ts +109 -30
  231. package/src/services/__tests__/post.test.ts +567 -5
  232. package/src/services/__tests__/search.test.ts +48 -1
  233. package/src/services/__tests__/settings.test.ts +8 -0
  234. package/src/services/__tests__/site-admin.test.ts +121 -1
  235. package/src/services/auth.ts +2 -2
  236. package/src/services/bootstrap.ts +3 -2
  237. package/src/services/collection.ts +226 -167
  238. package/src/services/export-theme/assets/client-site.css +1 -1
  239. package/src/services/export-theme/assets/client-site.js +46 -25
  240. package/src/services/export-theme/layouts/_default/list.html +18 -4
  241. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  242. package/src/services/export-theme/layouts/featured/list.html +5 -4
  243. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  244. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  245. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  246. package/src/services/export-theme/styles/main.css +30 -2
  247. package/src/services/export.ts +91 -80
  248. package/src/services/mcp.ts +37 -27
  249. package/src/services/media.ts +1 -1
  250. package/src/services/navigation.ts +185 -46
  251. package/src/services/post.ts +874 -271
  252. package/src/services/search.ts +8 -2
  253. package/src/services/settings.ts +2 -10
  254. package/src/services/site-admin.ts +63 -43
  255. package/src/style-cjk-jp.css +1 -1
  256. package/src/style-cjk-kr.css +1 -1
  257. package/src/style-cjk-tc.css +1 -1
  258. package/src/style-cjk.css +1 -1
  259. package/src/styles/components.css +540 -0
  260. package/src/styles/site-media.css +111 -19
  261. package/src/styles/tokens.css +14 -9
  262. package/src/styles/ui.css +952 -64
  263. package/src/types/bindings.ts +3 -0
  264. package/src/types/config.ts +243 -12
  265. package/src/types/constants.ts +22 -1
  266. package/src/types/entities.ts +9 -8
  267. package/src/types/operations.ts +18 -10
  268. package/src/types/props.ts +8 -2
  269. package/src/types/raw-assets.d.ts +5 -0
  270. package/src/types/views.ts +16 -4
  271. package/src/ui/__tests__/font-themes.test.ts +42 -30
  272. package/src/ui/compose/ComposeDialog.tsx +92 -2
  273. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  274. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  275. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  276. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  277. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  278. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  279. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  280. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  281. package/src/ui/feed/LinkCard.tsx +2 -12
  282. package/src/ui/feed/NoteCard.tsx +10 -20
  283. package/src/ui/feed/QuoteCard.tsx +2 -12
  284. package/src/ui/feed/ThreadPreview.tsx +26 -34
  285. package/src/ui/feed/TimelineFeed.tsx +2 -2
  286. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  287. package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -6
  288. package/src/ui/feed/thread-preview-state.ts +5 -7
  289. package/src/ui/font-themes.ts +74 -26
  290. package/src/ui/layouts/BaseLayout.tsx +23 -9
  291. package/src/ui/layouts/SiteLayout.tsx +19 -13
  292. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  293. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
  294. package/src/ui/pages/CollectionPage.tsx +101 -22
  295. package/src/ui/pages/CollectionsPage.tsx +3 -1
  296. package/src/ui/pages/PostPage.tsx +39 -12
  297. package/src/ui/pages/SearchPage.tsx +6 -14
  298. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  299. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  300. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  301. package/src/ui/shared/CollectionsManager.tsx +17 -7
  302. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  303. package/src/ui/shared/HomePageBranding.tsx +2 -2
  304. package/src/ui/shared/MediaGallery.tsx +1 -0
  305. package/src/ui/shared/PostFooter.tsx +3 -1
  306. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  307. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  308. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  309. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  310. package/src/ui/shared/collection-management-labels.ts +33 -2
  311. package/src/ui/shared/navigation-labels.ts +2 -4
  312. package/src/ui/shared/post-article-attributes.ts +46 -0
  313. package/dist/app-CWJFPjuR.js +0 -6
  314. package/dist/client/_assets/client-DCrcHVJl.css +0 -2
  315. package/dist/client/_assets/client-auth-TsYq90xm.js +0 -4733
  316. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -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) => {
@@ -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
  });
@@ -7,23 +7,29 @@ import {
7
7
  } from "../jant-command-palette.js";
8
8
  import type { JantCommandPalette } from "../jant-command-palette.js";
9
9
 
10
- function mockPaletteApi() {
11
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
12
- new Response(JSON.stringify({ items: [] }), {
13
- headers: { "Content-Type": "application/json" },
14
- }),
10
+ interface MockPaletteItem {
11
+ title: string;
12
+ path: string;
13
+ type: "post" | "collection" | "system";
14
+ }
15
+
16
+ function mockPaletteApi(items: MockPaletteItem[] = []) {
17
+ vi.spyOn(globalThis, "fetch").mockImplementation(
18
+ async () =>
19
+ new Response(JSON.stringify({ items }), {
20
+ headers: { "Content-Type": "application/json" },
21
+ }),
15
22
  );
16
23
  }
17
24
 
18
- async function renderPalette(query: string) {
19
- mockPaletteApi();
25
+ async function renderPalette(query: string, items: MockPaletteItem[] = []) {
26
+ mockPaletteApi(items);
20
27
  const el = document.createElement(
21
28
  "jant-command-palette",
22
29
  ) as JantCommandPalette;
23
- el._open = true;
24
- el._query = query;
25
30
  document.body.appendChild(el);
26
- await Promise.resolve();
31
+ await el.open();
32
+ el._query = query;
27
33
  await el.updateComplete;
28
34
  return el;
29
35
  }
@@ -39,6 +45,7 @@ describe("JantCommandPalette slash path mode", () => {
39
45
  vi.restoreAllMocks();
40
46
  document.body.innerHTML = "";
41
47
  globalThis.localStorage.clear();
48
+ globalThis.history.replaceState({}, "", "/");
42
49
  });
43
50
 
44
51
  it("shows go-to-path first and search second for slash queries", async () => {
@@ -61,3 +68,123 @@ describe("JantCommandPalette slash path mode", () => {
61
68
  expect(resultTitles(el)).toEqual(["Go to /notes", 'Search for "notes"']);
62
69
  });
63
70
  });
71
+
72
+ describe("JantCommandPalette persistent search action", () => {
73
+ beforeEach(() => {
74
+ vi.restoreAllMocks();
75
+ document.body.innerHTML = "";
76
+ globalThis.localStorage.clear();
77
+ globalThis.history.replaceState({}, "", "/");
78
+ });
79
+
80
+ const matchingItems: MockPaletteItem[] = Array.from(
81
+ { length: 12 },
82
+ (_, index) => ({
83
+ title: `Match ${index + 1}`,
84
+ path: `match-${index + 1}`,
85
+ type: "post",
86
+ }),
87
+ );
88
+
89
+ it("keeps search visible below all navigation matches", async () => {
90
+ const el = await renderPalette("match", matchingItems);
91
+
92
+ expect(
93
+ el.querySelectorAll(".command-palette-results > .command-palette-result"),
94
+ ).toHaveLength(12);
95
+ expect(resultTitles(el)).toEqual([
96
+ "Match 1",
97
+ "Match 2",
98
+ "Match 3",
99
+ "Match 4",
100
+ "Match 5",
101
+ "Match 6",
102
+ "Match 7",
103
+ "Match 8",
104
+ "Match 9",
105
+ "Match 10",
106
+ "Match 11",
107
+ "Match 12",
108
+ 'Search all content for "match"',
109
+ ]);
110
+ expect(
111
+ el
112
+ .querySelector(".command-palette-results-container")
113
+ ?.lastElementChild?.classList.contains("command-palette-search-action"),
114
+ ).toBe(true);
115
+ });
116
+
117
+ it("uses plain Enter without a shortcut hint when search is the only result", async () => {
118
+ const el = await renderPalette("nothing", matchingItems);
119
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
120
+
121
+ expect(resultTitles(el)).toEqual(['Search all content for "nothing"']);
122
+ expect(el.querySelector(".command-palette-result-shortcut")).toBeNull();
123
+
124
+ dialog?.dispatchEvent(
125
+ new globalThis.KeyboardEvent("keydown", {
126
+ key: "Enter",
127
+ bubbles: true,
128
+ cancelable: true,
129
+ }),
130
+ );
131
+
132
+ expect(globalThis.location.pathname).toBe("/search");
133
+ expect(globalThis.location.search).toBe("?q=nothing");
134
+ });
135
+
136
+ it("wraps ArrowUp from the best match to the search action", async () => {
137
+ const el = await renderPalette("match", matchingItems);
138
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
139
+
140
+ dialog?.dispatchEvent(
141
+ new globalThis.KeyboardEvent("keydown", {
142
+ key: "ArrowUp",
143
+ bubbles: true,
144
+ cancelable: true,
145
+ }),
146
+ );
147
+ await el.updateComplete;
148
+
149
+ expect(
150
+ el.querySelector(
151
+ ".command-palette-result-selected .command-palette-result-title",
152
+ )?.textContent,
153
+ ).toBe('Search all content for "match"');
154
+ });
155
+
156
+ it.each(["metaKey", "ctrlKey"] as const)(
157
+ "runs full-text search with the %s Enter shortcut",
158
+ async (modifier) => {
159
+ const el = await renderPalette("match", matchingItems);
160
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
161
+
162
+ dialog?.dispatchEvent(
163
+ new globalThis.KeyboardEvent("keydown", {
164
+ key: "Enter",
165
+ [modifier]: true,
166
+ bubbles: true,
167
+ cancelable: true,
168
+ }),
169
+ );
170
+
171
+ expect(globalThis.location.pathname).toBe("/search");
172
+ expect(globalThis.location.search).toBe("?q=match");
173
+ },
174
+ );
175
+
176
+ it("keeps plain Enter on the best navigation match", async () => {
177
+ const el = await renderPalette("match", matchingItems);
178
+ const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
179
+
180
+ dialog?.dispatchEvent(
181
+ new globalThis.KeyboardEvent("keydown", {
182
+ key: "Enter",
183
+ bubbles: true,
184
+ cancelable: true,
185
+ }),
186
+ );
187
+
188
+ expect(globalThis.location.pathname).toBe("/match-1");
189
+ });
190
+ });