@jant/core 0.6.14 → 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 (311) 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-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  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-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.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-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -5,7 +5,7 @@
5
5
  * with fractional indexing for efficient reordering.
6
6
  */
7
7
 
8
- import { and, eq, asc, sql, inArray } from "drizzle-orm";
8
+ import { and, eq, asc, desc, isNull, sql, inArray } from "drizzle-orm";
9
9
  import { generateKeyBetween } from "fractional-indexing";
10
10
  import type { Database } from "../db/index.js";
11
11
  import {
@@ -21,7 +21,10 @@ import {
21
21
  stripSitePathPrefix,
22
22
  toSameSitePath,
23
23
  } from "../lib/url.js";
24
- import { COLLECTION_FRESHNESS_WINDOW_SECONDS } from "../types.js";
24
+ import {
25
+ COLLECTION_FRESHNESS_WINDOW_SECONDS,
26
+ DEFAULT_NAVIGATION_PROFILE,
27
+ } from "../types.js";
25
28
  import type {
26
29
  NavItem,
27
30
  NavItemType,
@@ -30,6 +33,7 @@ import type {
30
33
  UpdateNavItem,
31
34
  SystemNavKey,
32
35
  SuggestedNavLink,
36
+ NavigationPageCandidate,
33
37
  } from "../types.js";
34
38
  import { SYSTEM_NAV_KEYS } from "../types.js";
35
39
 
@@ -49,11 +53,18 @@ import { isUniqueConstraintError } from "../db/dialect.js";
49
53
 
50
54
  export interface NavItemService {
51
55
  list(): Promise<NavItem[]>;
56
+ listPageCandidates(options?: {
57
+ query?: string;
58
+ limit?: number;
59
+ }): Promise<NavigationPageCandidate[]>;
52
60
  getById(id: string): Promise<NavItem | null>;
53
61
  create(data: CreateNavItem): Promise<NavItem>;
54
- ensureSystemDefaults(
55
- systemKeys?: readonly SystemNavKey[],
56
- ): Promise<NavItem[]>;
62
+ /**
63
+ * Add missing items from the current default profile without changing
64
+ * existing navigation state. Call only during provisioning or incomplete
65
+ * onboarding recovery.
66
+ */
67
+ materializeDefaultNavigation(): Promise<NavItem[]>;
57
68
  update(id: string, data: UpdateNavItem): Promise<NavItem | null>;
58
69
  delete(id: string): Promise<boolean>;
59
70
  move(
@@ -72,18 +83,9 @@ export function createNavItemService(
72
83
  siteId: string,
73
84
  databaseSchema: DatabaseSchema = sqliteSchemaBundle,
74
85
  ): NavItemService {
75
- const { navItems, postCollections, posts, pathRegistry, collections } =
86
+ const { navItems, threadCollections, posts, pathRegistry, collections } =
76
87
  databaseSchema;
77
88
 
78
- const defaultSystemOrder = [
79
- "latest",
80
- "featured",
81
- "collections",
82
- "archive",
83
- "rss",
84
- "settings",
85
- ] as const satisfies readonly SystemNavKey[];
86
-
87
89
  function toNavItem(row: typeof navItems.$inferSelect): NavItem {
88
90
  return {
89
91
  id: row.id,
@@ -91,6 +93,7 @@ export function createNavItemService(
91
93
  type: row.type as NavItemType,
92
94
  systemKey: (row.systemKey as SystemNavKey | null) ?? undefined,
93
95
  collectionId: row.collectionId ?? undefined,
96
+ postId: row.postId ?? undefined,
94
97
  label: row.label,
95
98
  url: row.url,
96
99
  placement: (row.placement ?? "header") as NavItemPlacement,
@@ -100,7 +103,7 @@ export function createNavItemService(
100
103
  };
101
104
  }
102
105
 
103
- function normalizeCreateData(data: CreateNavItem) {
106
+ async function normalizeCreateData(data: CreateNavItem) {
104
107
  if (data.type === "system") {
105
108
  const config = SYSTEM_NAV_KEYS[data.systemKey];
106
109
  if (!config) {
@@ -111,6 +114,7 @@ export function createNavItemService(
111
114
  type: data.type,
112
115
  systemKey: data.systemKey,
113
116
  collectionId: null,
117
+ postId: null,
114
118
  label: "",
115
119
  url: config.url,
116
120
  placement: data.placement ?? config.defaultPlacement,
@@ -123,6 +127,7 @@ export function createNavItemService(
123
127
  type: data.type,
124
128
  systemKey: null,
125
129
  collectionId: data.collectionId,
130
+ postId: null,
126
131
  label: data.label,
127
132
  url: data.url,
128
133
  placement: data.placement ?? "header",
@@ -130,10 +135,57 @@ export function createNavItemService(
130
135
  };
131
136
  }
132
137
 
138
+ if (data.type === "page") {
139
+ const rows = await db
140
+ .select({
141
+ title: posts.title,
142
+ slug: pathRegistry.path,
143
+ })
144
+ .from(posts)
145
+ .innerJoin(
146
+ pathRegistry,
147
+ and(
148
+ eq(pathRegistry.siteId, siteId),
149
+ eq(pathRegistry.postId, posts.id),
150
+ eq(pathRegistry.kind, "slug"),
151
+ ),
152
+ )
153
+ .where(
154
+ and(
155
+ eq(posts.siteId, siteId),
156
+ eq(posts.id, data.postId),
157
+ eq(posts.format, "note"),
158
+ eq(posts.status, "published"),
159
+ sql`${posts.visibility} != 'private'`,
160
+ isNull(posts.replyToId),
161
+ sql`trim(${posts.title}) != ''`,
162
+ ),
163
+ )
164
+ .limit(1);
165
+ const page = rows[0];
166
+ if (!page?.title) {
167
+ throw new ValidationError(
168
+ "Page must be a published, non-private titled note",
169
+ );
170
+ }
171
+
172
+ return {
173
+ type: data.type,
174
+ systemKey: null,
175
+ collectionId: null,
176
+ postId: data.postId,
177
+ label: (data.label?.trim() || page.title.trim()).slice(0, 100),
178
+ url: withLeadingSlash(page.slug),
179
+ placement: data.placement ?? "header",
180
+ position: data.position,
181
+ };
182
+ }
183
+
133
184
  return {
134
185
  type: data.type,
135
186
  systemKey: null,
136
187
  collectionId: null,
188
+ postId: null,
137
189
  label: data.label,
138
190
  url: data.url,
139
191
  placement: data.placement ?? "header",
@@ -256,6 +308,63 @@ export function createNavItemService(
256
308
  return rows.map(toNavItem);
257
309
  },
258
310
 
311
+ async listPageCandidates(options = {}) {
312
+ const query = options.query?.trim() ?? "";
313
+ const limit = Math.min(Math.max(Math.trunc(options.limit ?? 20), 1), 50);
314
+ const escapedQuery = query.replace(/[\\%_]/g, "\\$&");
315
+ const conditions = [
316
+ eq(posts.siteId, siteId),
317
+ eq(posts.format, "note"),
318
+ eq(posts.status, "published"),
319
+ sql`${posts.visibility} != 'private'`,
320
+ isNull(posts.replyToId),
321
+ sql`trim(${posts.title}) != ''`,
322
+ isNull(navItems.id),
323
+ ];
324
+ if (query) {
325
+ conditions.push(
326
+ sql`lower(${posts.title}) LIKE lower(${`%${escapedQuery}%`}) ESCAPE '\\'`,
327
+ );
328
+ }
329
+
330
+ const rows = await db
331
+ .select({
332
+ id: posts.id,
333
+ title: posts.title,
334
+ slug: pathRegistry.path,
335
+ updatedAt: posts.updatedAt,
336
+ })
337
+ .from(posts)
338
+ .innerJoin(
339
+ pathRegistry,
340
+ and(
341
+ eq(pathRegistry.siteId, siteId),
342
+ eq(pathRegistry.postId, posts.id),
343
+ eq(pathRegistry.kind, "slug"),
344
+ ),
345
+ )
346
+ .leftJoin(
347
+ navItems,
348
+ and(eq(navItems.siteId, siteId), eq(navItems.postId, posts.id)),
349
+ )
350
+ .where(and(...conditions))
351
+ .orderBy(desc(posts.updatedAt), desc(posts.id))
352
+ .limit(limit);
353
+
354
+ return rows.flatMap((row) =>
355
+ row.title
356
+ ? [
357
+ {
358
+ id: row.id,
359
+ title: row.title,
360
+ slug: row.slug,
361
+ updatedAt: row.updatedAt,
362
+ },
363
+ ]
364
+ : [],
365
+ );
366
+ },
367
+
259
368
  async getById(id) {
260
369
  const result = await db
261
370
  .select()
@@ -268,7 +377,7 @@ export function createNavItemService(
268
377
  async create(data) {
269
378
  const id = createEntityId("navItem");
270
379
  const timestamp = now();
271
- const normalized = normalizeCreateData(data);
380
+ const normalized = await normalizeCreateData(data);
272
381
 
273
382
  if (normalized.systemKey) {
274
383
  const existingSystemItem = await db
@@ -304,6 +413,23 @@ export function createNavItemService(
304
413
  }
305
414
  }
306
415
 
416
+ if (normalized.postId) {
417
+ const existingPageItem = await db
418
+ .select({ id: navItems.id })
419
+ .from(navItems)
420
+ .where(
421
+ and(
422
+ eq(navItems.siteId, siteId),
423
+ eq(navItems.postId, normalized.postId),
424
+ ),
425
+ )
426
+ .limit(1);
427
+
428
+ if (existingPageItem[0]) {
429
+ throw new ValidationError("Page already added to navigation");
430
+ }
431
+ }
432
+
307
433
  if (normalized.position !== undefined) {
308
434
  const result = await db
309
435
  .insert(navItems)
@@ -313,6 +439,7 @@ export function createNavItemService(
313
439
  type: normalized.type,
314
440
  systemKey: normalized.systemKey,
315
441
  collectionId: normalized.collectionId,
442
+ postId: normalized.postId,
316
443
  label: normalized.label,
317
444
  url: normalized.url,
318
445
  placement: normalized.placement,
@@ -336,6 +463,7 @@ export function createNavItemService(
336
463
  type: normalized.type,
337
464
  systemKey: normalized.systemKey,
338
465
  collectionId: normalized.collectionId,
466
+ postId: normalized.postId,
339
467
  label: normalized.label,
340
468
  url: normalized.url,
341
469
  placement: normalized.placement,
@@ -360,7 +488,7 @@ export function createNavItemService(
360
488
  throw new Error("Failed to assign a unique nav item position");
361
489
  },
362
490
 
363
- async ensureSystemDefaults(systemKeys = defaultSystemOrder) {
491
+ async materializeDefaultNavigation() {
364
492
  const existingRows = await db
365
493
  .select({ systemKey: navItems.systemKey })
366
494
  .from(navItems)
@@ -377,7 +505,7 @@ export function createNavItemService(
377
505
  );
378
506
 
379
507
  const created: NavItem[] = [];
380
- for (const systemKey of systemKeys) {
508
+ for (const systemKey of DEFAULT_NAVIGATION_PROFILE.systemKeys) {
381
509
  if (existing.has(systemKey)) continue;
382
510
  try {
383
511
  created.push(
@@ -408,12 +536,18 @@ export function createNavItemService(
408
536
  .limit(1);
409
537
  if (!existing[0]) return null;
410
538
 
411
- if (existing[0].type === "system" || existing[0].type === "collection") {
539
+ if (
540
+ existing[0].type === "system" ||
541
+ existing[0].type === "collection" ||
542
+ existing[0].type === "page"
543
+ ) {
412
544
  if (data.url !== undefined) {
413
545
  throw new ValidationError(
414
546
  existing[0].type === "system"
415
547
  ? "Built-in navigation URLs are managed automatically"
416
- : "Collection navigation URLs are managed automatically",
548
+ : existing[0].type === "collection"
549
+ ? "Collection navigation URLs are managed automatically"
550
+ : "Page navigation URLs are managed automatically",
417
551
  );
418
552
  }
419
553
  }
@@ -528,9 +662,12 @@ export function createNavItemService(
528
662
  if (record.postId) {
529
663
  const postRows = await db
530
664
  .select({
665
+ id: posts.id,
666
+ format: posts.format,
531
667
  title: posts.title,
532
668
  status: posts.status,
533
669
  visibility: posts.visibility,
670
+ replyToId: posts.replyToId,
534
671
  })
535
672
  .from(posts)
536
673
  .where(and(eq(posts.siteId, siteId), eq(posts.id, record.postId)))
@@ -538,8 +675,11 @@ export function createNavItemService(
538
675
  const post = postRows[0];
539
676
  if (
540
677
  !post ||
678
+ post.format !== "note" ||
541
679
  post.status !== "published" ||
542
- post.visibility === "private"
680
+ post.visibility === "private" ||
681
+ post.replyToId !== null ||
682
+ !post.title?.trim()
543
683
  ) {
544
684
  continue;
545
685
  }
@@ -549,7 +689,8 @@ export function createNavItemService(
549
689
  label: normalizeSuggestedLabel(post.title, candidate.label),
550
690
  url: path,
551
691
  targetType: "page",
552
- navItemType: "link",
692
+ navItemType: "page",
693
+ postId: post.id,
553
694
  });
554
695
  continue;
555
696
  }
@@ -603,47 +744,45 @@ export function createNavItemService(
603
744
  if (collectionIds.length === 0) return new Map<string, number>();
604
745
 
605
746
  const threshold = now() - COLLECTION_FRESHNESS_WINDOW_SECONDS;
606
-
607
- // Find collections with recent activity and the latest update timestamp.
608
- // 1. A post was added to the collection within the freshness window
609
- // 2. A post in the collection was edited within the freshness window
610
- // 3. A thread reply was published where the thread root is in the collection
747
+ const threadActivityAt = sql<number>`CASE
748
+ WHEN ${posts.updatedAt} > ${posts.createdAt}
749
+ AND ${posts.updatedAt} > COALESCE(${posts.lastActivityAt}, -1)
750
+ THEN ${posts.updatedAt}
751
+ ELSE COALESCE(${posts.lastActivityAt}, ${posts.updatedAt})
752
+ END`;
753
+
754
+ // Find collections with recent Thread activity and its latest timestamp.
755
+ // `lastActivityAt` is maintained on the root when a non-quiet reply is
756
+ // published. A later `updatedAt` keeps actual root edits visible without
757
+ // treating a freshly imported historical root as newly active.
611
758
  const rows = await db
612
759
  .select({
613
- collectionId: postCollections.collectionId,
760
+ collectionId: threadCollections.collectionId,
614
761
  latestAt:
615
- sql<number>`MAX(CASE WHEN ${postCollections.createdAt} > ${posts.updatedAt} THEN ${postCollections.createdAt} ELSE ${posts.updatedAt} END)`.as(
762
+ sql<number>`MAX(CASE WHEN ${threadCollections.createdAt} > ${threadActivityAt} THEN ${threadCollections.createdAt} ELSE ${threadActivityAt} END)`.as(
616
763
  "latest_at",
617
764
  ),
618
765
  })
619
- .from(postCollections)
766
+ .from(threadCollections)
620
767
  .innerJoin(
621
768
  posts,
622
769
  and(
623
- eq(posts.id, postCollections.postId),
624
- eq(posts.siteId, postCollections.siteId),
770
+ eq(posts.id, threadCollections.threadId),
771
+ eq(posts.siteId, threadCollections.siteId),
625
772
  ),
626
773
  )
627
774
  .where(
628
775
  and(
629
- eq(postCollections.siteId, siteId),
630
- inArray(postCollections.collectionId, collectionIds),
776
+ eq(threadCollections.siteId, siteId),
777
+ inArray(threadCollections.collectionId, collectionIds),
631
778
  sql`(
632
- ${postCollections.createdAt} > ${threshold}
633
- OR (${posts.updatedAt} > ${threshold}
779
+ ${threadCollections.createdAt} > ${threshold}
780
+ OR (${threadActivityAt} > ${threshold}
634
781
  AND ${posts.status} = 'published')
635
- OR EXISTS (
636
- SELECT 1 FROM ${posts} reply
637
- WHERE reply.site_id = ${postCollections.siteId}
638
- AND reply.thread_id = ${postCollections.postId}
639
- AND reply.reply_to_id IS NOT NULL
640
- AND reply.status = 'published'
641
- AND reply.created_at > ${threshold}
642
- )
643
782
  )`,
644
783
  ),
645
784
  )
646
- .groupBy(postCollections.collectionId);
785
+ .groupBy(threadCollections.collectionId);
647
786
 
648
787
  return new Map(rows.map((r) => [r.collectionId, r.latestAt]));
649
788
  },