@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
@@ -106,4 +106,11 @@ describe("ArchivePage", () => {
106
106
  expect(html).toContain("archive-chip-active");
107
107
  expect(html).toContain("Single posts");
108
108
  });
109
+
110
+ it("renders the feed button only when a feed href is provided", () => {
111
+ expect(renderArchivePage()).not.toContain('class="feed-link"');
112
+ expect(renderArchivePage({ feedHref: "/archive/feed" })).toContain(
113
+ 'class="feed-link"',
114
+ );
115
+ });
109
116
  });
@@ -0,0 +1,88 @@
1
+ import type { Context } from "hono";
2
+ import { renderToString } from "hono/jsx/dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+ import { I18nProvider } from "../../../i18n/context.js";
5
+ import { createI18n } from "../../../i18n/i18n.js";
6
+ import type { PostView } from "../../../types.js";
7
+ import { PostPage } from "../PostPage.js";
8
+
9
+ function createPostView(overrides: Partial<PostView> = {}): PostView {
10
+ return {
11
+ id: "pst_root",
12
+ permalink: "/root",
13
+ slug: "root",
14
+ bodyHtml: "<p>Post body</p>",
15
+ format: "note",
16
+ status: "published",
17
+ visibility: "public",
18
+ pinned: false,
19
+ featured: false,
20
+ publishedAt: "2026-07-18T00:00:00.000Z",
21
+ publishedAtFormatted: "Jul 18, 2026",
22
+ publishedAtTime: "08:00",
23
+ publishedAtRelative: "now",
24
+ updatedAt: "2026-07-18T00:00:00.000Z",
25
+ media: [],
26
+ collections: [],
27
+ isLastInThread: false,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function renderPostPage(post: PostView, threadPosts: PostView[]): string {
33
+ const i18n = createI18n("en");
34
+ const c = {
35
+ get(key: string) {
36
+ if (key === "i18n") return i18n;
37
+ return undefined;
38
+ },
39
+ } as unknown as Context;
40
+
41
+ I18nProvider({ c, children: "" });
42
+ return renderToString(PostPage({ post, threadPosts }));
43
+ }
44
+
45
+ function getArticleTag(html: string, postId: string): string {
46
+ const article = (html.match(/<article\b[^>]*>/g) ?? []).find((tag) =>
47
+ tag.includes(`data-post-id="${postId}"`),
48
+ );
49
+ if (!article) throw new Error(`Missing article for ${postId}`);
50
+ return article;
51
+ }
52
+
53
+ describe("PostPage", () => {
54
+ it("preserves Root-versus-Child action context in detail mode", () => {
55
+ const root = createPostView();
56
+ const child = createPostView({
57
+ id: "pst_child",
58
+ permalink: "/child",
59
+ slug: "child",
60
+ format: "link",
61
+ title: "Child link",
62
+ url: "https://example.com/child",
63
+ replyToId: root.id,
64
+ threadRootId: root.id,
65
+ });
66
+ const grandchild = createPostView({
67
+ id: "pst_grandchild",
68
+ permalink: "/grandchild",
69
+ slug: "grandchild",
70
+ format: "quote",
71
+ quoteText: "Child quote",
72
+ replyToId: child.id,
73
+ threadRootId: root.id,
74
+ isLastInThread: true,
75
+ });
76
+
77
+ const html = renderPostPage(child, [root, child, grandchild]);
78
+ const rootArticle = getArticleTag(html, root.id);
79
+ const childArticle = getArticleTag(html, child.id);
80
+ const grandchildArticle = getArticleTag(html, grandchild.id);
81
+
82
+ expect(rootArticle).not.toContain("data-post-reply");
83
+ expect(childArticle).toContain('data-post-reply=""');
84
+ expect(childArticle).toContain(`data-thread-root-id="${root.id}"`);
85
+ expect(grandchildArticle).toContain('data-post-reply=""');
86
+ expect(grandchildArticle).toContain(`data-thread-root-id="${root.id}"`);
87
+ });
88
+ });
@@ -124,7 +124,7 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
124
124
  i18n._(
125
125
  msg({
126
126
  message:
127
- "No collections yet. Start one to organize posts by topic.",
127
+ "No collections yet. Start one to organize threads by topic.",
128
128
  comment: "@context: Empty state message on collections page",
129
129
  }),
130
130
  )}
@@ -231,7 +231,9 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
231
231
  <div
232
232
  class="collection-directory-description prose"
233
233
  dangerouslySetInnerHTML={{
234
- __html: renderMarkdown(item.description),
234
+ __html: renderMarkdown(item.description, {
235
+ namespace: item.id,
236
+ }),
235
237
  }}
236
238
  />
237
239
  ) : (
@@ -279,24 +281,26 @@ export const CollectionDirectory: FC<CollectionDirectoryProps> = ({
279
281
  <div
280
282
  class="collection-directory-description prose"
281
283
  dangerouslySetInnerHTML={{
282
- __html: renderMarkdown(collection.description),
284
+ __html: renderMarkdown(collection.description, {
285
+ namespace: collection.id,
286
+ }),
283
287
  }}
284
288
  />
285
289
  )}
286
290
  <p class="collection-directory-summary">
287
291
  <span class="collection-directory-meta">
288
- {collection.postCount}{" "}
289
- {collection.postCount === 1
292
+ {collection.threadCount}{" "}
293
+ {collection.threadCount === 1
290
294
  ? i18n._(
291
295
  msg({
292
- message: "entry",
293
- comment: "@context: Singular entry count label",
296
+ message: "thread",
297
+ comment: "@context: Singular thread count label",
294
298
  }),
295
299
  )
296
300
  : i18n._(
297
301
  msg({
298
- message: "entries",
299
- comment: "@context: Plural entry count label",
302
+ message: "threads",
303
+ comment: "@context: Plural thread count label",
300
304
  }),
301
305
  )}
302
306
  </span>
@@ -15,12 +15,14 @@ const escapeJson = (data: unknown) =>
15
15
 
16
16
  export interface CollectionsManagerProps {
17
17
  items: CollectionDirectoryItem[];
18
+ navigationCollectionIds?: string[];
18
19
  sitePathPrefix?: string;
19
20
  siteOrigin?: string;
20
21
  }
21
22
 
22
23
  export const CollectionsManager: FC<CollectionsManagerProps> = ({
23
24
  items,
25
+ navigationCollectionIds = [],
24
26
  sitePathPrefix = "",
25
27
  siteOrigin = "",
26
28
  }) => {
@@ -102,21 +104,21 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
102
104
  comment: "@context: Tooltip for divider delete button",
103
105
  }),
104
106
  ),
105
- entrySingular: i18n._(
107
+ threadSingular: i18n._(
106
108
  msg({
107
- message: "entry",
108
- comment: "@context: Singular entry count label",
109
+ message: "thread",
110
+ comment: "@context: Singular thread count label",
109
111
  }),
110
112
  ),
111
- entryPlural: i18n._(
113
+ threadPlural: i18n._(
112
114
  msg({
113
- message: "entries",
114
- comment: "@context: Plural entry count label",
115
+ message: "threads",
116
+ comment: "@context: Plural thread count label",
115
117
  }),
116
118
  ),
117
119
  emptyState: i18n._(
118
120
  msg({
119
- message: "No collections yet. Start one to organize posts by topic.",
121
+ message: "No collections yet. Start one to organize threads by topic.",
120
122
  comment: "@context: Empty state message on collections page",
121
123
  }),
122
124
  ),
@@ -126,6 +128,13 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
126
128
  comment: "@context: Toast after reordering collections",
127
129
  }),
128
130
  ),
131
+ notNow: i18n._(
132
+ msg({
133
+ message: "Not now",
134
+ comment:
135
+ "@context: Action to dismiss the post-create Collection navigation prompt",
136
+ }),
137
+ ),
129
138
  ...mutationLabels,
130
139
  };
131
140
 
@@ -312,6 +321,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
312
321
  <jant-collections-manager
313
322
  items={escapeJson(items)}
314
323
  labels={escapeJson(labels)}
324
+ navigation-collection-ids={escapeJson(navigationCollectionIds)}
315
325
  >
316
326
  <CollectionDirectory
317
327
  items={items}
@@ -0,0 +1,33 @@
1
+ import { msg } from "@lingui/core/macro";
2
+ import type { FC } from "hono/jsx";
3
+ import { useLingui } from "../../i18n/context.js";
4
+
5
+ /** Persistent page chrome that distinguishes an authenticated draft preview. */
6
+ export const DraftPreviewBar: FC = () => {
7
+ const { i18n } = useLingui();
8
+ const label = i18n._(
9
+ msg({
10
+ message: "Draft preview",
11
+ comment: "@context: Status label above a draft preview page",
12
+ }),
13
+ );
14
+
15
+ return (
16
+ <aside class="draft-preview-bar" aria-label={label} data-preview-status>
17
+ <div class="draft-preview-bar-inner">
18
+ <span class="draft-preview-bar-label">
19
+ <span class="draft-preview-bar-dot" aria-hidden="true" />
20
+ {label}
21
+ </span>
22
+ <span class="draft-preview-bar-description">
23
+ {i18n._(
24
+ msg({
25
+ message: "This post isn’t published.",
26
+ comment: "@context: Explanation in the draft preview status bar",
27
+ }),
28
+ )}
29
+ </span>
30
+ </div>
31
+ </aside>
32
+ );
33
+ };
@@ -2,7 +2,7 @@ import type { FC } from "hono/jsx";
2
2
  import {
3
3
  HOME_BRANDING_LINK_LABEL,
4
4
  HOME_BRANDING_PREFIX,
5
- JANT_REPO_URL,
5
+ JANT_HOME_URL,
6
6
  } from "../../lib/jant-branding.js";
7
7
 
8
8
  export const HomePageBranding: FC = () => {
@@ -10,7 +10,7 @@ export const HomePageBranding: FC = () => {
10
10
  <footer class="home-branding-credit">
11
11
  {HOME_BRANDING_PREFIX}{" "}
12
12
  <a
13
- href={JANT_REPO_URL}
13
+ href={JANT_HOME_URL}
14
14
  target="_blank"
15
15
  rel="noopener noreferrer"
16
16
  class="home-branding-link"
@@ -208,6 +208,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
208
208
  return cat === "image" || cat === "video";
209
209
  })
210
210
  .map((a) => ({
211
+ id: a.id,
211
212
  url: a.url,
212
213
  alt: a.altText || "",
213
214
  width: a.width,
@@ -186,6 +186,8 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
186
186
  const showReply = !hideReply && post.isLastInThread;
187
187
  const showCollectionSeparator =
188
188
  showTimestamp || !!safeExternalUrl || post.featured;
189
+ const visibleCollections =
190
+ post.replyToId || post.threadRootId ? [] : post.collections;
189
191
 
190
192
  return (
191
193
  <footer
@@ -224,7 +226,7 @@ export const PostFooter: FC<PostFooterProps> = ({ post, detail, display }) => {
224
226
  </a>
225
227
  )}
226
228
  <CompactCollectionTags
227
- collections={post.collections}
229
+ collections={visibleCollections}
228
230
  showSeparator={showCollectionSeparator}
229
231
  showIcon={detail}
230
232
  />
@@ -7,7 +7,7 @@ describe("HomePageBranding", () => {
7
7
  const html = renderToString(HomePageBranding({}));
8
8
 
9
9
  expect(html).toContain("Build with");
10
- expect(html).toContain("https://github.com/jant-me/jant");
10
+ expect(html).toContain('href="https://jant.me"');
11
11
  expect(html).toContain(">Jant</span>");
12
12
  expect(html).not.toContain("<svg");
13
13
  });
@@ -104,6 +104,7 @@ describe("MediaGallery", () => {
104
104
 
105
105
  expect(html).toContain("data-feed-short-video");
106
106
  expect(html).toContain('data-video-src="/media/clip.mp4"');
107
+ expect(html).toContain("&quot;id&quot;:&quot;media-3&quot;");
107
108
  expect(html).toContain("data-feed-video-mute-toggle");
108
109
  expect(html).not.toContain("media-video-play-overlay");
109
110
  expect(html).not.toContain("media-short-video-progress");
@@ -139,10 +139,10 @@ describe("system nav labels", () => {
139
139
 
140
140
  it("translates system nav descriptions", () => {
141
141
  expect(getSystemNavDescription("latest", i18n)).toBe(
142
- "translated:Link to your latest posts. If it comes before Featured, the homepage opens here.",
142
+ "translated:Link to your latest posts. Your homepage shows this feed.",
143
143
  );
144
144
  expect(getSystemNavDescription("featured", i18n)).toBe(
145
- "translated:Link to your featured posts. If it comes before Latest, the homepage opens here.",
145
+ "translated:Link to posts you've marked as featured.",
146
146
  );
147
147
  expect(getSystemNavDescription("archive", i18n)).toBe(
148
148
  "translated:Link to the post archive",
@@ -106,6 +106,21 @@ describe("PostFooter", () => {
106
106
  expect(html.match(/href="\/studio"/g)).toHaveLength(1);
107
107
  });
108
108
 
109
+ it("shows collection tags only on the thread root", () => {
110
+ const html = renderPostFooter(
111
+ createPostView({
112
+ id: "post-child",
113
+ replyToId: "post-root",
114
+ }),
115
+ );
116
+
117
+ expect(html).not.toContain('class="post-collection-tags"');
118
+ expect(html).not.toContain('href="/notes"');
119
+ expect(html).not.toContain("data-collection-popover-trigger");
120
+ expect(html).toContain('aria-label="More actions"');
121
+ expect(html).toContain("data-post-menu-trigger");
122
+ });
123
+
109
124
  it("renders the featured icon before the timestamp for client-side toggles", () => {
110
125
  const html = renderPostFooter(
111
126
  createPostView({
@@ -53,7 +53,7 @@ const collectionFormMessages = {
53
53
  }),
54
54
  createdLabel: msg({
55
55
  message: "Collection created.",
56
- comment: "@context: Toast shown after creating a collection",
56
+ comment: "@context: Confirmation shown after creating a collection",
57
57
  }),
58
58
  descriptionLabel: msg({
59
59
  message: "Description (optional)",
@@ -94,6 +94,30 @@ const collectionMutationMessages = {
94
94
  message: "Edit",
95
95
  comment: "@context: Per-collection edit action",
96
96
  }),
97
+ addToNavigation: msg({
98
+ message: "Add to Navigation",
99
+ comment: "@context: Action that adds a Collection to site navigation",
100
+ }),
101
+ addingToNavigation: msg({
102
+ message: "Adding…",
103
+ comment:
104
+ "@context: Loading label while adding a Collection to site navigation",
105
+ }),
106
+ addedToNavigation: msg({
107
+ message: "Collection added to navigation.",
108
+ comment:
109
+ "@context: Confirmation after adding a Collection to site navigation",
110
+ }),
111
+ editNavigation: msg({
112
+ message: "Edit Navigation",
113
+ comment:
114
+ "@context: Action that opens Navigation settings for a Collection already in navigation",
115
+ }),
116
+ addToNavigationFailed: msg({
117
+ message: "Couldn't add this collection to navigation. Try again.",
118
+ comment:
119
+ "@context: Error after a Collection could not be added to site navigation",
120
+ }),
97
121
  moreActions: msg({
98
122
  message: "More actions",
99
123
  comment: "@context: Aria-label for collections page more button",
@@ -112,7 +136,7 @@ const collectionMutationMessages = {
112
136
  }),
113
137
  confirmDelete: msg({
114
138
  message:
115
- "Delete this collection permanently? Posts inside won't be removed.",
139
+ "Delete this collection permanently? Threads inside won't be removed.",
116
140
  comment: "@context: Confirm dialog for deleting a collection",
117
141
  }),
118
142
  cancel: msg({
@@ -210,6 +234,13 @@ export const getCollectionFormLabels = (i18n: Translator) => ({
210
234
 
211
235
  export const getCollectionMutationLabels = (i18n: Translator) => ({
212
236
  edit: i18n._(collectionMutationMessages.edit),
237
+ addToNavigation: i18n._(collectionMutationMessages.addToNavigation),
238
+ addingToNavigation: i18n._(collectionMutationMessages.addingToNavigation),
239
+ addedToNavigation: i18n._(collectionMutationMessages.addedToNavigation),
240
+ editNavigation: i18n._(collectionMutationMessages.editNavigation),
241
+ addToNavigationFailed: i18n._(
242
+ collectionMutationMessages.addToNavigationFailed,
243
+ ),
213
244
  moreActions: i18n._(collectionMutationMessages.moreActions),
214
245
  label: i18n._(collectionMutationMessages.label),
215
246
  url: i18n._(collectionMutationMessages.url),
@@ -81,13 +81,11 @@ const SYSTEM_NAV_TITLES: Partial<Record<SystemNavKey, MessageDescriptor>> = {
81
81
 
82
82
  const SYSTEM_NAV_DESCRIPTIONS: Record<SystemNavKey, MessageDescriptor> = {
83
83
  latest: msg({
84
- message:
85
- "Link to your latest posts. If it comes before Featured, the homepage opens here.",
84
+ message: "Link to your latest posts. Your homepage shows this feed.",
86
85
  comment: "@context: Description for the latest system navigation toggle",
87
86
  }),
88
87
  featured: msg({
89
- message:
90
- "Link to your featured posts. If it comes before Latest, the homepage opens here.",
88
+ message: "Link to posts you've marked as featured.",
91
89
  comment: "@context: Description for the featured system navigation toggle",
92
90
  }),
93
91
  rss: msg({
@@ -0,0 +1,46 @@
1
+ import type { PostView } from "../../types.js";
2
+
3
+ type PostArticleAttributeSource = Pick<
4
+ PostView,
5
+ | "id"
6
+ | "slug"
7
+ | "format"
8
+ | "visibility"
9
+ | "replyToId"
10
+ | "threadRootId"
11
+ | "pinned"
12
+ | "pinnedInCollection"
13
+ | "featured"
14
+ >;
15
+
16
+ /**
17
+ * Builds the shared data-attribute contract for interactive Post articles.
18
+ * Child identity is content-derived and therefore stays stable across feed,
19
+ * detail, search, and partial-render modes.
20
+ *
21
+ * @param post - Render-ready Post identity and action state
22
+ * @returns Data attributes consumed by menus, shortcuts, themes, and scripts
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <article {...getPostArticleAttributes(post)}>...</article>
27
+ * ```
28
+ */
29
+ export function getPostArticleAttributes(post: PostArticleAttributeSource) {
30
+ const isChildPost = Boolean(post.replyToId || post.threadRootId);
31
+
32
+ return {
33
+ "data-post": "",
34
+ "data-format": post.format,
35
+ "data-post-id": post.id,
36
+ "data-post-slug": post.slug,
37
+ "data-thread-root-id": post.threadRootId ?? post.id,
38
+ ...(post.pinned ? { "data-post-pinned": "" } : {}),
39
+ ...(post.pinnedInCollection
40
+ ? { "data-post-pinned-in-collection": "" }
41
+ : {}),
42
+ ...(post.featured ? { "data-post-featured": "" } : {}),
43
+ "data-post-visibility": post.visibility,
44
+ ...(isChildPost ? { "data-post-reply": "" } : {}),
45
+ };
46
+ }
@@ -1,6 +0,0 @@
1
- import "./url-BMYO-Zlt.js";
2
- import { t as createApp } from "./app-2i7-WQHg.js";
3
- import "./export-DWn149b7.js";
4
- import "./env-OHRKGcMj.js";
5
- import "./github-sync-Ckk0sJxK.js";
6
- export { createApp };