@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
@@ -11,6 +11,8 @@ const TEST_COLLECTION_ID = "col_test00000000000000000000001";
11
11
  const TEST_COLLECTION_ID_2 = "col_test00000000000000000000002";
12
12
  const TEST_POST_ID = "pst_test00000000000000000000001";
13
13
  const TEST_POST_ID_2 = "pst_test00000000000000000000002";
14
+ const TEST_POST_ID_3 = "pst_test00000000000000000000003";
15
+ const TEST_POST_ID_4 = "pst_test00000000000000000000004";
14
16
 
15
17
  function insertTestPath(
16
18
  sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
@@ -76,6 +78,7 @@ function insertTestPost(
76
78
  id: string;
77
79
  slug: string;
78
80
  title: string | null;
81
+ format?: "note" | "link" | "quote";
79
82
  status?: "draft" | "published";
80
83
  visibility?: "public" | "latest_hidden" | "private";
81
84
  },
@@ -87,11 +90,12 @@ function insertTestPost(
87
90
  id, site_id, format, status, visibility, title, thread_id,
88
91
  published_at, last_activity_at, created_at, updated_at
89
92
  )
90
- VALUES (?, ?, 'note', ?, ?, ?, ?, ?, ?, ?, ?)`,
93
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
91
94
  )
92
95
  .run(
93
96
  input.id,
94
97
  DEFAULT_TEST_SITE_ID,
98
+ input.format ?? "note",
95
99
  input.status ?? "published",
96
100
  input.visibility ?? "latest_hidden",
97
101
  input.title,
@@ -232,15 +236,71 @@ describe("NavItemService", () => {
232
236
  }),
233
237
  ).rejects.toThrow("Built-in navigation item already exists");
234
238
  });
239
+
240
+ it("creates a page nav item from an eligible titled note", async () => {
241
+ insertTestPost(sqlite, {
242
+ id: TEST_POST_ID,
243
+ slug: "about-me",
244
+ title: "About me",
245
+ });
246
+
247
+ const item = await navItemService.create({
248
+ type: "page",
249
+ postId: TEST_POST_ID,
250
+ });
251
+
252
+ expect(item).toMatchObject({
253
+ type: "page",
254
+ postId: TEST_POST_ID,
255
+ label: "About me",
256
+ url: "/about-me",
257
+ placement: "header",
258
+ });
259
+ });
260
+
261
+ it("rejects duplicate and ineligible page nav items", async () => {
262
+ insertTestPost(sqlite, {
263
+ id: TEST_POST_ID,
264
+ slug: "about",
265
+ title: "About",
266
+ });
267
+ insertTestPost(sqlite, {
268
+ id: TEST_POST_ID_2,
269
+ slug: "private-page",
270
+ title: "Private page",
271
+ visibility: "private",
272
+ });
273
+ await navItemService.create({ type: "page", postId: TEST_POST_ID });
274
+
275
+ await expect(
276
+ navItemService.create({ type: "page", postId: TEST_POST_ID }),
277
+ ).rejects.toThrow("Page already added to navigation");
278
+ await expect(
279
+ navItemService.create({ type: "page", postId: TEST_POST_ID_2 }),
280
+ ).rejects.toThrow("Page must be a published, non-private titled note");
281
+ });
282
+
283
+ it("truncates a page's default navigation label", async () => {
284
+ insertTestPost(sqlite, {
285
+ id: TEST_POST_ID,
286
+ slug: "long-title",
287
+ title: "A".repeat(150),
288
+ });
289
+
290
+ const item = await navItemService.create({
291
+ type: "page",
292
+ postId: TEST_POST_ID,
293
+ });
294
+
295
+ expect(item.label).toBe("A".repeat(100));
296
+ });
235
297
  });
236
298
 
237
- describe("ensureSystemDefaults", () => {
238
- it("creates the six default system items once", async () => {
239
- const created = await navItemService.ensureSystemDefaults();
299
+ describe("materializeDefaultNavigation", () => {
300
+ it("creates the five-item default profile without Latest", async () => {
301
+ const created = await navItemService.materializeDefaultNavigation();
240
302
 
241
- expect(created).toHaveLength(6);
242
303
  expect(created.map((item) => item.systemKey)).toEqual([
243
- "latest",
244
304
  "featured",
245
305
  "collections",
246
306
  "archive",
@@ -251,39 +311,86 @@ describe("NavItemService", () => {
251
311
  "header",
252
312
  "header",
253
313
  "header",
254
- "header",
255
314
  "more",
256
315
  "more",
257
316
  ]);
317
+ expect(created.map((item) => item.systemKey)).not.toContain("latest");
258
318
  });
259
319
 
260
- it("creates only missing default system items", async () => {
261
- await navItemService.create({
320
+ it("preserves an existing optional Latest item", async () => {
321
+ const latest = await navItemService.create({
262
322
  type: "system",
263
- systemKey: "archive",
323
+ systemKey: "latest",
324
+ });
325
+ const customizedLatest = await navItemService.update(latest.id, {
326
+ label: "Recent writing",
327
+ placement: "more",
328
+ position: "a3",
264
329
  });
265
330
 
266
- const created = await navItemService.ensureSystemDefaults();
331
+ const created = await navItemService.materializeDefaultNavigation();
267
332
  const items = await navItemService.list();
268
333
 
269
334
  expect(created.map((item) => item.systemKey)).toEqual([
270
- "latest",
271
335
  "featured",
272
336
  "collections",
337
+ "archive",
273
338
  "rss",
274
339
  "settings",
275
340
  ]);
341
+ expect(items.find((item) => item.id === latest.id)).toEqual(
342
+ customizedLatest,
343
+ );
276
344
  expect(items.filter((item) => item.type === "system")).toHaveLength(6);
277
345
  });
278
346
 
279
- it("is idempotent when defaults already exist", async () => {
280
- await navItemService.ensureSystemDefaults();
347
+ it("only appends missing profile items during recovery", async () => {
348
+ const archive = await navItemService.create({
349
+ type: "system",
350
+ systemKey: "archive",
351
+ });
352
+ const customizedArchive = await navItemService.update(archive.id, {
353
+ label: "All posts",
354
+ placement: "more",
355
+ position: "a2",
356
+ });
357
+ const customLink = await navItemService.create({
358
+ type: "link",
359
+ label: "Elsewhere",
360
+ url: "https://example.com",
361
+ position: "a3",
362
+ });
363
+
364
+ const created = await navItemService.materializeDefaultNavigation();
365
+ const items = await navItemService.list();
366
+
367
+ expect(created.map((item) => item.systemKey)).toEqual([
368
+ "featured",
369
+ "collections",
370
+ "rss",
371
+ "settings",
372
+ ]);
373
+ expect(items.slice(0, 2).map((item) => item.id)).toEqual([
374
+ archive.id,
375
+ customLink.id,
376
+ ]);
377
+ expect(items.find((item) => item.id === archive.id)).toEqual(
378
+ customizedArchive,
379
+ );
380
+ expect(items.find((item) => item.id === customLink.id)).toEqual(
381
+ customLink,
382
+ );
383
+ expect(items.map((item) => item.systemKey)).not.toContain("latest");
384
+ });
385
+
386
+ it("is idempotent when the current profile already exists", async () => {
387
+ await navItemService.materializeDefaultNavigation();
281
388
 
282
- const created = await navItemService.ensureSystemDefaults();
389
+ const created = await navItemService.materializeDefaultNavigation();
283
390
  const items = await navItemService.list();
284
391
 
285
392
  expect(created).toEqual([]);
286
- expect(items.filter((item) => item.type === "system")).toHaveLength(6);
393
+ expect(items.filter((item) => item.type === "system")).toHaveLength(5);
287
394
  });
288
395
  });
289
396
 
@@ -361,8 +468,53 @@ describe("NavItemService", () => {
361
468
  });
362
469
  });
363
470
 
471
+ describe("listPageCandidates", () => {
472
+ it("returns eligible notes by title and excludes added pages", async () => {
473
+ insertTestPost(sqlite, {
474
+ id: TEST_POST_ID,
475
+ slug: "about",
476
+ title: "About this site",
477
+ visibility: "latest_hidden",
478
+ });
479
+ insertTestPost(sqlite, {
480
+ id: TEST_POST_ID_2,
481
+ slug: "another-note",
482
+ title: "Another note",
483
+ visibility: "public",
484
+ });
485
+ insertTestPost(sqlite, {
486
+ id: TEST_POST_ID_3,
487
+ slug: "about-link",
488
+ title: "About link",
489
+ format: "link",
490
+ });
491
+ insertTestPost(sqlite, {
492
+ id: TEST_POST_ID_4,
493
+ slug: "about-draft",
494
+ title: "About draft",
495
+ status: "draft",
496
+ });
497
+
498
+ expect(
499
+ await navItemService.listPageCandidates({ query: "ABOUT" }),
500
+ ).toEqual([
501
+ {
502
+ id: TEST_POST_ID,
503
+ title: "About this site",
504
+ slug: "about",
505
+ updatedAt: expect.any(Number),
506
+ },
507
+ ]);
508
+
509
+ await navItemService.create({ type: "page", postId: TEST_POST_ID });
510
+ expect(
511
+ await navItemService.listPageCandidates({ query: "about" }),
512
+ ).toEqual([]);
513
+ });
514
+ });
515
+
364
516
  describe("listSuggestedLinks", () => {
365
- it("suggests a published /about page as a normal link", async () => {
517
+ it("suggests a published /about page as a page nav item", async () => {
366
518
  insertTestPost(sqlite, {
367
519
  id: TEST_POST_ID,
368
520
  slug: "about",
@@ -377,7 +529,8 @@ describe("NavItemService", () => {
377
529
  label: "About me",
378
530
  url: "/about",
379
531
  targetType: "page",
380
- navItemType: "link",
532
+ navItemType: "page",
533
+ postId: TEST_POST_ID,
381
534
  },
382
535
  ]);
383
536
  });
@@ -786,6 +939,32 @@ describe("NavItemService", () => {
786
939
  });
787
940
  });
788
941
 
942
+ describe("page nav items", () => {
943
+ beforeEach(() => {
944
+ insertTestPost(sqlite, {
945
+ id: TEST_POST_ID,
946
+ slug: "about",
947
+ title: "About",
948
+ });
949
+ });
950
+
951
+ it("allows label updates but keeps the page URL managed", async () => {
952
+ const item = await navItemService.create({
953
+ type: "page",
954
+ postId: TEST_POST_ID,
955
+ });
956
+
957
+ const updated = await navItemService.update(item.id, {
958
+ label: "Start here",
959
+ });
960
+ expect(updated?.label).toBe("Start here");
961
+
962
+ await expect(
963
+ navItemService.update(item.id, { url: "/other" }),
964
+ ).rejects.toThrow("Page navigation URLs are managed automatically");
965
+ });
966
+ });
967
+
789
968
  describe("getCollectionFreshness", () => {
790
969
  beforeEach(() => {
791
970
  insertTestCollection(sqlite, TEST_COLLECTION_ID, "design", "Design");
@@ -810,7 +989,7 @@ describe("NavItemService", () => {
810
989
  // Add it to the collection recently
811
990
  sqlite
812
991
  .prepare(
813
- `INSERT INTO post_collection (site_id, post_id, collection_id, created_at, position)
992
+ `INSERT INTO thread_collection (site_id, thread_id, collection_id, created_at, position)
814
993
  VALUES (?, ?, ?, ?, 0)`,
815
994
  )
816
995
  .run(DEFAULT_TEST_SITE_ID, "pst_test001", TEST_COLLECTION_ID, ts);
@@ -838,7 +1017,7 @@ describe("NavItemService", () => {
838
1017
  );
839
1018
  sqlite
840
1019
  .prepare(
841
- `INSERT INTO post_collection (site_id, post_id, collection_id, created_at, position)
1020
+ `INSERT INTO thread_collection (site_id, thread_id, collection_id, created_at, position)
842
1021
  VALUES (?, ?, ?, ?, 0)`,
843
1022
  )
844
1023
  .run(DEFAULT_TEST_SITE_ID, "pst_test002", TEST_COLLECTION_ID, oldTs);
@@ -849,6 +1028,35 @@ describe("NavItemService", () => {
849
1028
  expect(result.has(TEST_COLLECTION_ID)).toBe(false);
850
1029
  });
851
1030
 
1031
+ it("detects freshness from a recent root edit even when Thread activity is old", async () => {
1032
+ const oldTs = now() - 60 * 60 * 72;
1033
+ const recentTs = now();
1034
+ sqlite
1035
+ .prepare(
1036
+ `INSERT INTO post (id, site_id, thread_id, format, status, visibility, created_at, updated_at, last_activity_at)
1037
+ VALUES (?, ?, ?, 'note', 'published', 'public', ?, ?, ?)`,
1038
+ )
1039
+ .run(
1040
+ "pst_edited",
1041
+ DEFAULT_TEST_SITE_ID,
1042
+ "pst_edited",
1043
+ oldTs,
1044
+ recentTs,
1045
+ oldTs,
1046
+ );
1047
+ sqlite
1048
+ .prepare(
1049
+ `INSERT INTO thread_collection (site_id, thread_id, collection_id, created_at, position)
1050
+ VALUES (?, ?, ?, ?, 0)`,
1051
+ )
1052
+ .run(DEFAULT_TEST_SITE_ID, "pst_edited", TEST_COLLECTION_ID, oldTs);
1053
+
1054
+ const result = await navItemService.getCollectionFreshness([
1055
+ TEST_COLLECTION_ID,
1056
+ ]);
1057
+ expect(result.get(TEST_COLLECTION_ID)).toBe(recentTs);
1058
+ });
1059
+
852
1060
  it("detects freshness from recent thread replies", async () => {
853
1061
  const oldTs = now() - 60 * 60 * 72; // 72 hours ago
854
1062
  const recentTs = now();
@@ -871,7 +1079,7 @@ describe("NavItemService", () => {
871
1079
  // Add the root to the collection (old)
872
1080
  sqlite
873
1081
  .prepare(
874
- `INSERT INTO post_collection (site_id, post_id, collection_id, created_at, position)
1082
+ `INSERT INTO thread_collection (site_id, thread_id, collection_id, created_at, position)
875
1083
  VALUES (?, ?, ?, ?, 0)`,
876
1084
  )
877
1085
  .run(DEFAULT_TEST_SITE_ID, "pst_root", TEST_COLLECTION_ID, oldTs);