@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
@@ -92,7 +92,7 @@ export interface PostView {
92
92
  // Media -- URLs pre-computed
93
93
  media: MediaView[];
94
94
 
95
- // Collections this post belongs to
95
+ // Thread-level Collections projected onto this post
96
96
  collections: CollectionTagView[];
97
97
 
98
98
  // Thread context
@@ -157,6 +157,7 @@ export interface NavItemView {
157
157
  type: NavItemType;
158
158
  systemKey?: SystemNavKey;
159
159
  collectionId?: string;
160
+ postId?: string;
160
161
  label: string;
161
162
  url: string;
162
163
  /** "header" = visible in nav bar, "more" = under More dropdown */
@@ -173,7 +174,7 @@ export interface NavItemView {
173
174
 
174
175
  export type SuggestedNavLinkTargetType = "page" | "collection" | "archive";
175
176
 
176
- export type SuggestedNavLinkNavItemType = "link" | "collection";
177
+ export type SuggestedNavLinkNavItemType = "link" | "collection" | "page";
177
178
 
178
179
  export interface SuggestedNavLink {
179
180
  key: string;
@@ -182,6 +183,15 @@ export interface SuggestedNavLink {
182
183
  targetType: SuggestedNavLinkTargetType;
183
184
  navItemType: SuggestedNavLinkNavItemType;
184
185
  collectionId?: string;
186
+ postId?: string;
187
+ }
188
+
189
+ /** A published titled note eligible to be added to site navigation. */
190
+ export interface NavigationPageCandidate {
191
+ id: string;
192
+ title: string;
193
+ slug: string;
194
+ updatedAt: number;
185
195
  }
186
196
 
187
197
  /**
@@ -215,13 +225,15 @@ export interface SearchResultView {
215
225
  export interface TimelineItemView {
216
226
  post: PostView;
217
227
  threadPreview?: {
218
- secondReply?: PostView;
219
- penultimateReply?: PostView;
228
+ leadingReplies: PostView[];
229
+ trailingReplies: PostView[];
220
230
  latestReply: PostView;
221
231
  totalReplyCount: number;
222
232
  };
223
233
  curatedThread?: {
224
234
  rootPost: PostView;
235
+ /** Show ratings on non-highlighted context posts (complete Collection Threads). */
236
+ showContextRatings: boolean;
225
237
  segments: {
226
238
  post: PostView;
227
239
  hiddenBeforeCount: number;
@@ -1,9 +1,11 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  BUILTIN_FONT_THEMES,
4
+ DEFAULT_FONT_CJK_SANS_FALLBACK,
4
5
  DEFAULT_FONT_CJK_SERIF_FALLBACK,
5
- getCjkSerifCssVariables,
6
+ getCjkFontCssVariables,
6
7
  getFontThemeCssVariables,
8
+ resolveCjkFontProfile,
7
9
  } from "../font-themes.js";
8
10
 
9
11
  describe("BUILTIN_FONT_THEMES", () => {
@@ -90,52 +92,62 @@ describe("BUILTIN_FONT_THEMES", () => {
90
92
  expect(variables["--type-label-weight"]).toBe("var(--fw-semibold)");
91
93
  });
92
94
 
93
- it("routes zh-Hans to the simplified CJK serif fallback first", () => {
94
- const variables = getCjkSerifCssVariables("zh-Hans");
95
- const stack = variables["--font-cjk-serif-fallback"];
95
+ it("routes zh-Hans content to simplified serif and sans fallbacks", () => {
96
+ const variables = getCjkFontCssVariables("zh-Hans", "off");
97
+ const serif = variables["--font-cjk-serif-fallback"];
98
+ const sans = variables["--font-cjk-sans-fallback"];
96
99
 
97
- expect(stack).toContain('"Songti SC"');
98
- expect(stack).toContain('"Noto Serif SC"');
99
- expect(stack.indexOf('"Songti SC"')).toBeLessThan(
100
- stack.indexOf('"Noto Serif SC"'),
100
+ expect(serif).toContain('"Songti SC"');
101
+ expect(serif).toContain('"Noto Serif SC"');
102
+ expect(sans.indexOf('"PingFang SC"')).toBeLessThan(
103
+ sans.indexOf('"PingFang TC"'),
101
104
  );
102
105
  });
103
106
 
104
- it("routes zh-Hant to the traditional CJK serif fallback first", () => {
105
- const variables = getCjkSerifCssVariables("zh-Hant");
106
- const stack = variables["--font-cjk-serif-fallback"];
107
+ it("routes zh-Hant content to traditional serif and sans fallbacks", () => {
108
+ const variables = getCjkFontCssVariables("zh-Hant", "off");
109
+ const serif = variables["--font-cjk-serif-fallback"];
110
+ const sans = variables["--font-cjk-sans-fallback"];
107
111
 
108
- expect(stack).toContain('"Songti TC"');
109
- expect(stack).toContain('"Noto Serif TC"');
110
- expect(stack.indexOf('"Songti TC"')).toBeLessThan(
111
- stack.indexOf('"Noto Serif TC"'),
112
+ expect(serif).toContain('"Songti TC"');
113
+ expect(serif).toContain('"Noto Serif TC"');
114
+ expect(sans.indexOf('"PingFang TC"')).toBeLessThan(
115
+ sans.indexOf('"PingFang SC"'),
112
116
  );
113
117
  });
114
118
 
115
- it("routes ja to the Japanese CJK serif fallback", () => {
116
- const variables = getCjkSerifCssVariables("ja");
117
- const stack = variables["--font-cjk-serif-fallback"];
119
+ it("routes Japanese content to the Japanese font profile", () => {
120
+ const variables = getCjkFontCssVariables("ja", "off");
118
121
 
119
- expect(stack).toContain('"Noto Serif JP"');
122
+ expect(variables["--font-cjk-serif-fallback"]).toContain('"Noto Serif JP"');
123
+ expect(variables["--font-cjk-sans-fallback"]).toContain('"Noto Sans JP"');
120
124
  });
121
125
 
122
- it("routes ko to the Korean CJK serif fallback", () => {
123
- const variables = getCjkSerifCssVariables("ko");
124
- const stack = variables["--font-cjk-serif-fallback"];
126
+ it("routes Korean content to the Korean font profile", () => {
127
+ const variables = getCjkFontCssVariables("ko", "off");
125
128
 
126
- expect(stack).toContain('"Noto Serif KR"');
129
+ expect(variables["--font-cjk-serif-fallback"]).toContain('"Noto Serif KR"');
130
+ expect(variables["--font-cjk-sans-fallback"]).toContain('"Noto Sans KR"');
127
131
  });
128
132
 
129
- it("does not inject a runtime override when CJK is off", () => {
130
- expect(getCjkSerifCssVariables("off")).toEqual({});
133
+ it("uses the explicit CJK fallback for an unadapted content language", () => {
134
+ expect(resolveCjkFontProfile("en", "zh-Hans")).toBe("zh-Hans");
131
135
  });
132
136
 
133
- it("does not inject a runtime override for unrecognized values", () => {
134
- expect(getCjkSerifCssVariables("en")).toEqual({});
137
+ it("lets an adapted content language override the manual fallback", () => {
138
+ expect(resolveCjkFontProfile("zh-Hant", "zh-Hans")).toBe("zh-Hant");
135
139
  });
136
140
 
137
- it("keeps a default CJK serif fallback for the base tokens", () => {
138
- expect(DEFAULT_FONT_CJK_SERIF_FALLBACK).toContain('"Songti SC"');
139
- expect(DEFAULT_FONT_CJK_SERIF_FALLBACK).toContain('"Noto Serif SC"');
141
+ it("keeps the default profile neutral", () => {
142
+ expect(getCjkFontCssVariables("en", "off")).toEqual({});
143
+ expect(DEFAULT_FONT_CJK_SERIF_FALLBACK).toBe('"Jant Language Fallback"');
144
+ expect(DEFAULT_FONT_CJK_SANS_FALLBACK).toBe('"Jant Language Fallback"');
145
+ });
146
+
147
+ it("puts language slots into every serif and sans theme stack", () => {
148
+ for (const theme of BUILTIN_FONT_THEMES) {
149
+ const combined = `${theme.headingFontFamily} ${theme.bodyFontFamily}`;
150
+ expect(combined).toMatch(/var\(--font-cjk-(?:serif|sans)-fallback\)/);
151
+ }
140
152
  });
141
153
  });
@@ -13,6 +13,7 @@ import type { FC } from "hono/jsx";
13
13
  import { MAX_THREAD_POSTS, type Collection } from "../../types.js";
14
14
  import { useLingui } from "../../i18n/context.js";
15
15
  import { getCollectionFormLabels } from "../shared/collection-management-labels.js";
16
+ import type { ComposeLabels } from "../../client/components/compose-types.js";
16
17
 
17
18
  export interface ComposeDialogProps {
18
19
  collections?: Collection[];
@@ -36,7 +37,7 @@ export const ComposeForm: FC<ComposeFormProps> = ({
36
37
  }) => {
37
38
  const { i18n } = useLingui();
38
39
 
39
- const labels = JSON.stringify({
40
+ const labelsObject = {
40
41
  cancel: i18n._(
41
42
  msg({
42
43
  message: "Cancel",
@@ -476,6 +477,19 @@ export const ComposeForm: FC<ComposeFormProps> = ({
476
477
  comment: "@context: Drafts panel empty state",
477
478
  }),
478
479
  ),
480
+ previewDraft: i18n._(
481
+ msg({
482
+ message: "Preview",
483
+ comment: "@context: Draft item action that opens its rendered preview",
484
+ }),
485
+ ),
486
+ draftActions: i18n._(
487
+ msg({
488
+ message: "Draft actions",
489
+ comment:
490
+ "@context: Accessible label for a draft item's more-actions button",
491
+ }),
492
+ ),
479
493
  deleteDraft: i18n._(
480
494
  msg({
481
495
  message: "Delete Draft",
@@ -807,8 +821,84 @@ export const ComposeForm: FC<ComposeFormProps> = ({
807
821
  "@context: First-use hint shown over the compose editor to surface the slash command menu",
808
822
  }),
809
823
  ),
824
+ tableControls: {
825
+ toolbarLabel: i18n._(
826
+ msg({
827
+ message: "Table controls",
828
+ comment: "@context: Accessible label for the compose table toolbar",
829
+ }),
830
+ ),
831
+ addRowAbove: i18n._(
832
+ msg({
833
+ message: "Add row above",
834
+ comment: "@context: Compose table action",
835
+ }),
836
+ ),
837
+ addRowBelow: i18n._(
838
+ msg({
839
+ message: "Add row below",
840
+ comment: "@context: Compose table action",
841
+ }),
842
+ ),
843
+ addColumnBefore: i18n._(
844
+ msg({
845
+ message: "Add column before",
846
+ comment: "@context: Compose table action",
847
+ }),
848
+ ),
849
+ addColumnAfter: i18n._(
850
+ msg({
851
+ message: "Add column after",
852
+ comment: "@context: Compose table action",
853
+ }),
854
+ ),
855
+ options: i18n._(
856
+ msg({
857
+ message: "Table options",
858
+ comment: "@context: Compose table menu button label",
859
+ }),
860
+ ),
861
+ deleteRow: i18n._(
862
+ msg({
863
+ message: "Delete row",
864
+ comment: "@context: Compose table action",
865
+ }),
866
+ ),
867
+ deleteColumn: i18n._(
868
+ msg({
869
+ message: "Delete column",
870
+ comment: "@context: Compose table action",
871
+ }),
872
+ ),
873
+ toggleHeaderRow: i18n._(
874
+ msg({
875
+ message: "Toggle header row",
876
+ comment: "@context: Compose table action",
877
+ }),
878
+ ),
879
+ deleteTable: i18n._(
880
+ msg({
881
+ message: "Delete table",
882
+ comment: "@context: Destructive compose table action",
883
+ }),
884
+ ),
885
+ sizePickerLabel: i18n._(
886
+ msg({
887
+ message: "Choose table size",
888
+ comment: "@context: Accessible title for the table dimension picker",
889
+ }),
890
+ ),
891
+ insertTableSize: i18n._(
892
+ msg({
893
+ message: "Insert %rows% by %cols% table",
894
+ comment:
895
+ "@context: Accessible label for a table size option. %rows% and %cols% are replaced with dimensions",
896
+ }),
897
+ ),
898
+ },
810
899
  collectionFormLabels: getCollectionFormLabels(i18n),
811
- }).replace(/</g, "\\u003c");
900
+ } satisfies ComposeLabels;
901
+ const labels = JSON.stringify(labelsObject).replace(/</g, "\\u003c");
812
902
 
813
903
  const collectionsJson = JSON.stringify(
814
904
  (collections ?? []).map((c) => ({
@@ -17,7 +17,9 @@ import type {
17
17
  NavManagerSuggestedLink,
18
18
  SystemNavConfig,
19
19
  } from "../../../client/components/nav-manager-types.js";
20
+ import type { CollectionFormLabels } from "../../../client/components/collection-types.js";
20
21
  import { toPublicHref, toPublicPath } from "../../../lib/url.js";
22
+ import { getCollectionFormLabels } from "../../shared/collection-management-labels.js";
21
23
  import {
22
24
  getNavItemDisplayLabel,
23
25
  getSystemNavDescription,
@@ -34,6 +36,7 @@ export function NavigationContent({
34
36
  directoryData,
35
37
  suggestedLinks,
36
38
  mainRssFeed,
39
+ rssFeedsEnabled,
37
40
  siteName,
38
41
  sitePathPrefix = "",
39
42
  }: {
@@ -41,6 +44,7 @@ export function NavigationContent({
41
44
  directoryData: CollectionsDirectoryData;
42
45
  suggestedLinks: SuggestedNavLink[];
43
46
  mainRssFeed: string;
47
+ rssFeedsEnabled: boolean;
44
48
  siteName: string;
45
49
  sitePathPrefix?: string;
46
50
  }) {
@@ -64,6 +68,90 @@ export function NavigationContent({
64
68
  }),
65
69
  );
66
70
  const moreLabel = i18n._(NAV_MORE_LABEL);
71
+ const createCollectionLabel = i18n._(
72
+ msg({
73
+ message: "Create Collection",
74
+ comment:
75
+ "@context: Button and dialog title for quick collection creation in Navigation settings",
76
+ }),
77
+ );
78
+ const collectionFormLabels: CollectionFormLabels = {
79
+ ...getCollectionFormLabels(i18n),
80
+ titleLabel: i18n._(
81
+ msg({
82
+ message: "Title",
83
+ comment: "@context: Collection title field in Navigation settings",
84
+ }),
85
+ ),
86
+ titlePlaceholder: i18n._(
87
+ msg({
88
+ message: "My Collection",
89
+ comment:
90
+ "@context: Collection title placeholder in Navigation settings",
91
+ }),
92
+ ),
93
+ slugLabel: i18n._(
94
+ msg({
95
+ message: "Collection link",
96
+ comment: "@context: Collection slug field in Navigation settings",
97
+ }),
98
+ ),
99
+ slugHelp: i18n._(
100
+ msg({
101
+ message: "This is the last part of the collection link.",
102
+ comment: "@context: Collection slug help text in Navigation settings",
103
+ }),
104
+ ),
105
+ slugInvalidHelp: i18n._(
106
+ msg({
107
+ message: "Use lowercase letters, numbers, and hyphens only.",
108
+ comment:
109
+ "@context: Collection slug validation error in Navigation settings",
110
+ }),
111
+ ),
112
+ slugReservedHelp: i18n._(
113
+ msg({
114
+ message: "This link is reserved. Choose something else.",
115
+ comment:
116
+ "@context: Reserved collection slug error in Navigation settings",
117
+ }),
118
+ ),
119
+ slugTooLongHelp: i18n._(
120
+ msg({
121
+ message: "Keep this link under 200 characters.",
122
+ comment: "@context: Long collection slug error in Navigation settings",
123
+ }),
124
+ ),
125
+ editSlugLabel: i18n._(
126
+ msg({
127
+ message: "Edit link",
128
+ comment:
129
+ "@context: Button to edit the generated collection slug in Navigation settings",
130
+ }),
131
+ ),
132
+ resetSlugLabel: i18n._(
133
+ msg({
134
+ message: "Reset link",
135
+ comment:
136
+ "@context: Button to reset the collection slug in Navigation settings",
137
+ }),
138
+ ),
139
+ quickHint: i18n._(
140
+ msg({
141
+ message: "More options are available after you create it.",
142
+ comment:
143
+ "@context: Helper text in quick collection creation in Navigation settings",
144
+ }),
145
+ ),
146
+ quickSubmitLabel: createCollectionLabel,
147
+ createdLabel: i18n._(
148
+ msg({
149
+ message: "Collection created.",
150
+ comment:
151
+ "@context: Heading after quick collection creation in Navigation settings",
152
+ }),
153
+ ),
154
+ };
67
155
 
68
156
  const suggestedTargetLabels = {
69
157
  page: i18n._(
@@ -98,6 +186,7 @@ export function NavigationContent({
98
186
  type: item.type,
99
187
  systemKey: item.systemKey,
100
188
  collectionId: item.collectionId,
189
+ postId: item.postId,
101
190
  label: item.label,
102
191
  displayLabel: getNavItemDisplayLabel(item, i18n, sitePathPrefix),
103
192
  url,
@@ -185,6 +274,12 @@ export function NavigationContent({
185
274
  comment: "@context: Nav item type badge",
186
275
  }),
187
276
  ),
277
+ page: i18n._(
278
+ msg({
279
+ message: "page",
280
+ comment: "@context: Nav item type badge for page items",
281
+ }),
282
+ ),
188
283
  system: i18n._(
189
284
  msg({
190
285
  message: "system",
@@ -235,6 +330,13 @@ export function NavigationContent({
235
330
  "@context: Confirm dialog for deleting a custom navigation link",
236
331
  }),
237
332
  ),
333
+ confirmDeletePage: i18n._(
334
+ msg({
335
+ message:
336
+ "Remove this page from navigation? The page itself won't be deleted.",
337
+ comment: "@context: Confirm dialog for removing a page from navigation",
338
+ }),
339
+ ),
238
340
  orderSaved: i18n._(
239
341
  msg({
240
342
  message: "Navigation order updated.",
@@ -353,6 +455,182 @@ export function NavigationContent({
353
455
  comment: "@context: Toast after adding a suggested nav link",
354
456
  }),
355
457
  ),
458
+ addPageToNavigation: i18n._(
459
+ msg({
460
+ message: "Add page to navigation",
461
+ comment: "@context: Section heading for adding a page to navigation",
462
+ }),
463
+ ),
464
+ addPageDescription: i18n._(
465
+ msg({
466
+ message:
467
+ "Choose a titled note that isn't already in navigation, or create a new page.",
468
+ comment: "@context: Description for adding a page to navigation",
469
+ }),
470
+ ),
471
+ addPage: i18n._(
472
+ msg({
473
+ message: "Add Page",
474
+ comment: "@context: Button that opens the navigation page picker",
475
+ }),
476
+ ),
477
+ searchPages: i18n._(
478
+ msg({
479
+ message: "Search pages",
480
+ comment: "@context: Placeholder for the navigation page search",
481
+ }),
482
+ ),
483
+ recentPages: i18n._(
484
+ msg({
485
+ message: "Recently updated",
486
+ comment: "@context: Heading above recent pages in the page picker",
487
+ }),
488
+ ),
489
+ searchingPages: i18n._(
490
+ msg({
491
+ message: "Searching pages…",
492
+ comment: "@context: Loading state in the navigation page picker",
493
+ }),
494
+ ),
495
+ noMatchingPages: i18n._(
496
+ msg({
497
+ message:
498
+ "No matching pages available to add. Try another title or create a new page.",
499
+ comment: "@context: Empty search results in the page picker",
500
+ }),
501
+ ),
502
+ noPages: i18n._(
503
+ msg({
504
+ message: "No pages available. Create one to add it to navigation.",
505
+ comment: "@context: Empty state in the navigation page picker",
506
+ }),
507
+ ),
508
+ pageSearchFailed: i18n._(
509
+ msg({
510
+ message: "Couldn't load pages. Try again in a moment.",
511
+ comment: "@context: Error shown when navigation page search fails",
512
+ }),
513
+ ),
514
+ createNewPage: i18n._(
515
+ msg({
516
+ message: "Create new page",
517
+ comment: "@context: Action at the bottom of the navigation page picker",
518
+ }),
519
+ ),
520
+ createPage: i18n._(
521
+ msg({
522
+ message: "Create Page",
523
+ comment: "@context: Button and dialog title for quick page creation",
524
+ }),
525
+ ),
526
+ createPageDescription: i18n._(
527
+ msg({
528
+ message: "Create a public page that won't appear in Latest.",
529
+ comment: "@context: Description in the quick-create page dialog",
530
+ }),
531
+ ),
532
+ pageTitle: i18n._(
533
+ msg({
534
+ message: "Title",
535
+ comment: "@context: Title field label in quick page creation",
536
+ }),
537
+ ),
538
+ pageAddress: i18n._(
539
+ msg({
540
+ message: "Page address",
541
+ comment: "@context: Slug field label in quick page creation",
542
+ }),
543
+ ),
544
+ pageVisibilityHint: i18n._(
545
+ msg({
546
+ message: "The page is public but stays out of Latest.",
547
+ comment: "@context: Visibility note in quick page creation",
548
+ }),
549
+ ),
550
+ titleRequired: i18n._(
551
+ msg({
552
+ message: "Enter a page title.",
553
+ comment: "@context: Validation error for an empty quick page title",
554
+ }),
555
+ ),
556
+ slugInvalid: i18n._(
557
+ msg({
558
+ message: "Use lowercase letters, numbers, and hyphens.",
559
+ comment: "@context: Validation error for an invalid page slug",
560
+ }),
561
+ ),
562
+ slugReserved: i18n._(
563
+ msg({
564
+ message: "That address is reserved. Choose another one.",
565
+ comment: "@context: Validation error for a reserved page slug",
566
+ }),
567
+ ),
568
+ slugTooLong: i18n._(
569
+ msg({
570
+ message: "Keep the page address under 200 characters.",
571
+ comment: "@context: Validation error for a long page slug",
572
+ }),
573
+ ),
574
+ slugUnavailable: i18n._(
575
+ msg({
576
+ message: "That address is already in use. Choose another one.",
577
+ comment: "@context: Validation error for an unavailable page slug",
578
+ }),
579
+ ),
580
+ checkingAddress: i18n._(
581
+ msg({
582
+ message: "Checking address…",
583
+ comment: "@context: Loading text while checking page slug availability",
584
+ }),
585
+ ),
586
+ creatingPage: i18n._(
587
+ msg({
588
+ message: "Creating page…",
589
+ comment: "@context: Loading label while creating a page",
590
+ }),
591
+ ),
592
+ createPageFailed: i18n._(
593
+ msg({
594
+ message: "Couldn't create the page. Check the details and try again.",
595
+ comment: "@context: Error when quick page creation fails",
596
+ }),
597
+ ),
598
+ pageCreated: i18n._(
599
+ msg({
600
+ message: "Page created.",
601
+ comment: "@context: Heading after quick page creation",
602
+ }),
603
+ ),
604
+ pageCreatedDescription: i18n._(
605
+ msg({
606
+ message: "Add it to navigation now or open the editor to add content.",
607
+ comment: "@context: Description after quick page creation",
608
+ }),
609
+ ),
610
+ addToNavigation: i18n._(
611
+ msg({
612
+ message: "Add to Navigation",
613
+ comment: "@context: Button that adds a created page to navigation",
614
+ }),
615
+ ),
616
+ editPage: i18n._(
617
+ msg({
618
+ message: "Edit Page",
619
+ comment: "@context: Button that opens a page for editing",
620
+ }),
621
+ ),
622
+ pageAdded: i18n._(
623
+ msg({
624
+ message: "Page added to navigation.",
625
+ comment: "@context: Confirmation after adding a page to navigation",
626
+ }),
627
+ ),
628
+ back: i18n._(
629
+ msg({
630
+ message: "Back",
631
+ comment: "@context: Button returning from page creation to page search",
632
+ }),
633
+ ),
356
634
  collection: i18n._(
357
635
  msg({
358
636
  message: "collection",
@@ -387,12 +665,56 @@ export function NavigationContent({
387
665
  ),
388
666
  noCollections: i18n._(
389
667
  msg({
390
- message:
391
- "No collections yet. Create one first, then add it to your navigation.",
668
+ message: "No collections yet. Create one here to add it to navigation.",
392
669
  comment:
393
670
  "@context: Empty state when no collections exist for nav picker",
394
671
  }),
395
672
  ),
673
+ createNewCollection: i18n._(
674
+ msg({
675
+ message: "Create new collection",
676
+ comment:
677
+ "@context: Action at the bottom of the Navigation collection picker",
678
+ }),
679
+ ),
680
+ createCollection: createCollectionLabel,
681
+ creatingCollection: i18n._(
682
+ msg({
683
+ message: "Creating collection…",
684
+ comment:
685
+ "@context: Loading label while creating a collection in Navigation settings",
686
+ }),
687
+ ),
688
+ createCollectionFailed: i18n._(
689
+ msg({
690
+ message:
691
+ "Couldn't create the collection. Check the details and try again.",
692
+ comment:
693
+ "@context: Error when quick collection creation fails in Navigation settings",
694
+ }),
695
+ ),
696
+ collectionCreatedDescription: i18n._(
697
+ msg({
698
+ message: "Add it to navigation now or open the editor to add details.",
699
+ comment:
700
+ "@context: Description after quick collection creation in Navigation settings",
701
+ }),
702
+ ),
703
+ editCollection: i18n._(
704
+ msg({
705
+ message: "Edit Collection",
706
+ comment:
707
+ "@context: Button that opens a newly created collection for editing",
708
+ }),
709
+ ),
710
+ collectionAdded: i18n._(
711
+ msg({
712
+ message: "Collection added to navigation.",
713
+ comment:
714
+ "@context: Confirmation after adding a collection to navigation",
715
+ }),
716
+ ),
717
+ collectionFormLabels,
396
718
  confirmDeleteCollection: i18n._(
397
719
  msg({
398
720
  message:
@@ -414,13 +736,19 @@ export function NavigationContent({
414
736
  collections={escapeJson(collectionsData)}
415
737
  suggested-links={escapeJson(suggestedLinksData)}
416
738
  site-name={siteName}
739
+ rss-feeds-enabled={rssFeedsEnabled || undefined}
417
740
  >
418
741
  {/* SSR fallback: static preview until JS hydrates */}
419
742
  {(() => {
420
- const headerNavItems = navItems.filter(
743
+ const previewItems = rssFeedsEnabled
744
+ ? navItems
745
+ : navItems.filter(
746
+ (item) => item.type !== "system" || item.systemKey !== "rss",
747
+ );
748
+ const headerNavItems = previewItems.filter(
421
749
  (item) => item.placement !== "more",
422
750
  );
423
- const moreNavItems = navItems.filter(
751
+ const moreNavItems = previewItems.filter(
424
752
  (item) => item.placement === "more",
425
753
  );
426
754
  return (