@jant/core 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -0,0 +1,98 @@
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 { ConfigEditorFieldState } from "../../../../types.js";
7
+ import { ConfigEditorContent } from "../ConfigEditorContent.js";
8
+
9
+ function renderConfigEditorContent(fields: ConfigEditorFieldState[]): string {
10
+ const i18n = createI18n("en");
11
+ const c = {
12
+ get(key: string) {
13
+ if (key === "i18n") return i18n;
14
+ return undefined;
15
+ },
16
+ } as unknown as Context;
17
+
18
+ I18nProvider({ c, children: "" });
19
+ return renderToString(
20
+ ConfigEditorContent({
21
+ fields,
22
+ endpoint: "/api/settings",
23
+ sitePathPrefix: "/notes",
24
+ }),
25
+ );
26
+ }
27
+
28
+ describe("ConfigEditorContent", () => {
29
+ it("renders the page, endpoint, and a useful server fallback", () => {
30
+ const html = renderConfigEditorContent([
31
+ {
32
+ key: "SITE_NAME",
33
+ mode: "edit",
34
+ type: "string",
35
+ value: "My Jant",
36
+ fallbackValue: "Jant",
37
+ modified: true,
38
+ locked: false,
39
+ maxLength: 120,
40
+ },
41
+ {
42
+ key: "NOINDEX",
43
+ mode: "edit",
44
+ type: "boolean",
45
+ value: "false",
46
+ fallbackValue: "false",
47
+ modified: false,
48
+ locked: false,
49
+ },
50
+ {
51
+ key: "CUSTOM_CSS",
52
+ mode: "link",
53
+ type: "string",
54
+ value: "true",
55
+ fallbackValue: "false",
56
+ modified: true,
57
+ locked: false,
58
+ settingsPath: "/settings/custom-css",
59
+ display: "configured",
60
+ },
61
+ ]);
62
+
63
+ expect(html).toContain("Config Editor");
64
+ expect(html).toContain("Changes apply immediately;");
65
+ expect(html).toContain('endpoint="/api/settings"');
66
+ expect(html).toContain("SITE_NAME");
67
+ expect(html).toContain("My Jant");
68
+ expect(html).toContain("Modified");
69
+ expect(html).toContain("Configured");
70
+ expect(html).toContain('href="/notes/settings/custom-css"');
71
+ expect(html).toContain('class="config-editor-open-control"');
72
+ expect(html).toContain("3 settings shown");
73
+ expect(html).not.toContain("config-editor-guidance");
74
+ expect(html).not.toContain("Save change");
75
+ expect(html).not.toContain('target="_blank"');
76
+ });
77
+
78
+ it("keeps user-controlled values inert in JSON attributes and fallback HTML", () => {
79
+ const html = renderConfigEditorContent([
80
+ {
81
+ key: "SITE_NAME",
82
+ mode: "edit",
83
+ type: "string",
84
+ value: "</script><script>alert(1)</script>",
85
+ fallbackValue: "",
86
+ modified: true,
87
+ locked: false,
88
+ maxLength: 120,
89
+ },
90
+ ]);
91
+
92
+ expect(html).not.toContain("<script>alert(1)</script>");
93
+ expect(html).toContain("\\u003c/script&gt;");
94
+ expect(html).toContain(
95
+ "&lt;/script&gt;&lt;script&gt;alert(1)&lt;/script&gt;",
96
+ );
97
+ });
98
+ });
@@ -21,6 +21,21 @@ function renderSettingsRootContent(
21
21
  }
22
22
 
23
23
  describe("SettingsRootContent", () => {
24
+ it("places Config Editor last in Advanced and preserves the site path prefix", () => {
25
+ const html = renderSettingsRootContent({ sitePathPrefix: "/notes" });
26
+
27
+ const codeInjection = html.indexOf("/notes/settings/code-injection");
28
+ const apiTokens = html.indexOf("/notes/settings/api-tokens");
29
+ const configEditor = html.indexOf("/notes/settings/config");
30
+ const accountSection = html.indexOf(">Account<");
31
+
32
+ expect(codeInjection).toBeGreaterThan(-1);
33
+ expect(apiTokens).toBeGreaterThan(codeInjection);
34
+ expect(configEditor).toBeGreaterThan(apiTokens);
35
+ expect(accountSection).toBeGreaterThan(configEditor);
36
+ expect(html).toContain("Config Editor");
37
+ });
38
+
24
39
  it("omits Manage Hosting when no hosted site settings URL is configured", () => {
25
40
  const html = renderSettingsRootContent({});
26
41
 
@@ -15,6 +15,14 @@ const THREAD_CONTEXT_LAST_DISPLAY = {
15
15
  hideRating: true,
16
16
  } as const;
17
17
 
18
+ const COMPLETE_THREAD_CONTEXT_DISPLAY = {
19
+ footer: {
20
+ hideReply: true,
21
+ },
22
+ } as const;
23
+
24
+ const COMPLETE_THREAD_LAST_DISPLAY = {} as const;
25
+
18
26
  const CURATED_SEGMENT_DISPLAY = {} as const;
19
27
 
20
28
  interface CuratedThreadPreviewProps {
@@ -25,7 +33,7 @@ export const CuratedThreadPreview: FC<CuratedThreadPreviewProps> = ({
25
33
  curatedThread,
26
34
  }) => {
27
35
  const { i18n } = useLingui();
28
- const { segments } = curatedThread;
36
+ const { segments, showContextRatings } = curatedThread;
29
37
 
30
38
  if (segments.length === 0) {
31
39
  return null;
@@ -70,9 +78,13 @@ export const CuratedThreadPreview: FC<CuratedThreadPreviewProps> = ({
70
78
  post={segment.post}
71
79
  mode="feed"
72
80
  display={
73
- index === segments.length - 1
74
- ? THREAD_CONTEXT_LAST_DISPLAY
75
- : THREAD_CONTEXT_DISPLAY
81
+ showContextRatings
82
+ ? index === segments.length - 1
83
+ ? COMPLETE_THREAD_LAST_DISPLAY
84
+ : COMPLETE_THREAD_CONTEXT_DISPLAY
85
+ : index === segments.length - 1
86
+ ? THREAD_CONTEXT_LAST_DISPLAY
87
+ : THREAD_CONTEXT_DISPLAY
76
88
  }
77
89
  />
78
90
  )}
@@ -9,6 +9,7 @@
9
9
  import type { FC } from "hono/jsx";
10
10
  import type { TimelineCardProps } from "../../types.js";
11
11
  import { StarRating } from "../shared/StarRating.js";
12
+ import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
12
13
  import { PostFooter } from "../shared/PostFooter.js";
13
14
  import { PostStatusBadges } from "./PostStatusBadges.js";
14
15
  import { sanitizeUrl, extractDisplayDomain } from "../../lib/url.js";
@@ -101,18 +102,7 @@ export const LinkCard: FC<TimelineCardProps> = ({
101
102
  <article
102
103
  class={articleClass}
103
104
  {...(isDetail ? { "data-page": "post" } : {})}
104
- data-post
105
- data-format="link"
106
- data-post-id={post.id}
107
- data-post-slug={post.slug}
108
- data-thread-root-id={post.threadRootId ?? post.id}
109
- {...(post.pinned ? { "data-post-pinned": "" } : {})}
110
- {...(post.pinnedInCollection
111
- ? { "data-post-pinned-in-collection": "" }
112
- : {})}
113
- {...(post.featured ? { "data-post-featured": "" } : {})}
114
- data-post-visibility={post.visibility}
115
- {...(!isDetail && post.threadRootId ? { "data-post-reply": "" } : {})}
105
+ {...getPostArticleAttributes(post)}
116
106
  >
117
107
  {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
118
108
  {isDetail ? (
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { FC } from "hono/jsx";
9
+ import { getLinkPreviewProviderLabel } from "../../lib/link-preview.js";
9
10
  import { Icon } from "../shared/Icon.js";
10
11
 
11
12
  interface LinkPreviewProps {
@@ -15,12 +16,6 @@ interface LinkPreviewProps {
15
16
  provider?: string;
16
17
  }
17
18
 
18
- const PROVIDER_LABELS: Record<string, string> = {
19
- youtube: "YouTube",
20
- vimeo: "Vimeo",
21
- bilibili: "Bilibili",
22
- };
23
-
24
19
  export const LinkPreview: FC<LinkPreviewProps> = ({
25
20
  imageUrl,
26
21
  linkUrl,
@@ -28,7 +23,7 @@ export const LinkPreview: FC<LinkPreviewProps> = ({
28
23
  provider,
29
24
  }) => {
30
25
  const isVideo = kind === "video";
31
- const providerLabel = provider ? PROVIDER_LABELS[provider] : undefined;
26
+ const providerLabel = getLinkPreviewProviderLabel(provider);
32
27
 
33
28
  return (
34
29
  <a
@@ -10,6 +10,7 @@ import type { FC } from "hono/jsx";
10
10
  import { useLingui } from "../../i18n/context.js";
11
11
  import type { TimelineCardProps } from "../../types.js";
12
12
  import { MediaGallery } from "../shared/MediaGallery.js";
13
+ import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
13
14
  import { StarRating } from "../shared/StarRating.js";
14
15
  import { PostFooter, PostPublishedLink } from "../shared/PostFooter.js";
15
16
  import { PostStatusBadges } from "./PostStatusBadges.js";
@@ -86,18 +87,7 @@ export const NoteCard: FC<TimelineCardProps> = ({
86
87
  <article
87
88
  class={`h-entry post-menu-target${isCompact ? " feed-compact" : isDetail ? " py-6" : ""}`}
88
89
  {...(isDetail ? { "data-page": "post" } : {})}
89
- data-post
90
- data-format="note"
91
- data-post-id={post.id}
92
- data-post-slug={post.slug}
93
- data-thread-root-id={post.threadRootId ?? post.id}
94
- {...(post.pinned ? { "data-post-pinned": "" } : {})}
95
- {...(post.pinnedInCollection
96
- ? { "data-post-pinned-in-collection": "" }
97
- : {})}
98
- {...(post.featured ? { "data-post-featured": "" } : {})}
99
- data-post-visibility={post.visibility}
100
- {...(!isDetail && post.threadRootId ? { "data-post-reply": "" } : {})}
90
+ {...getPostArticleAttributes(post)}
101
91
  >
102
92
  {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
103
93
  {isArticle &&
@@ -132,14 +122,6 @@ export const NoteCard: FC<TimelineCardProps> = ({
132
122
  {(() => {
133
123
  const tail = (
134
124
  <>
135
- {!isCompact && post.media.length > 0 && (
136
- <div class="mt-3" data-post-media>
137
- <MediaGallery
138
- attachments={post.media}
139
- postPermalink={post.permalink}
140
- />
141
- </div>
142
- )}
143
125
  {!isDetail &&
144
126
  !isCompact &&
145
127
  !showFullBody &&
@@ -160,6 +142,14 @@ export const NoteCard: FC<TimelineCardProps> = ({
160
142
  {readMoreLabel}
161
143
  </a>
162
144
  ))}
145
+ {!isCompact && post.media.length > 0 && (
146
+ <div class="mt-3" data-post-media>
147
+ <MediaGallery
148
+ attachments={post.media}
149
+ postPermalink={post.permalink}
150
+ />
151
+ </div>
152
+ )}
163
153
  {!isCompact && !showHeaderRating && !display?.hideRating && (
164
154
  <StarRating rating={post.rating} />
165
155
  )}
@@ -8,24 +8,63 @@
8
8
  * footer meta instead.
9
9
  */
10
10
 
11
+ import { msg } from "@lingui/core/macro";
11
12
  import type { FC } from "hono/jsx";
13
+ import { useLingui } from "../../i18n/context.js";
12
14
  import { Icon } from "../shared/Icon.js";
13
15
 
14
16
  export const PostStatusBadges: FC = () => {
17
+ const { i18n } = useLingui();
18
+
19
+ const pinnedLabel = i18n._(
20
+ msg({
21
+ message: "Pinned",
22
+ comment: "@context: Post status badge for a pinned post",
23
+ }),
24
+ );
25
+
15
26
  return (
16
27
  <div class="post-status-badges">
17
28
  <span class="post-status-badge post-status-pinned">
18
29
  <Icon name="post-status-pin" />
19
- Pinned
30
+ {pinnedLabel}
20
31
  </span>
21
32
  <span class="post-status-badge post-status-pinned-in-collection">
22
33
  <Icon name="post-status-pin" />
23
- Pinned
34
+ {pinnedLabel}
24
35
  </span>
25
36
  <span class="post-status-badge post-status-private">
26
37
  <Icon name="post-status-private" />
27
- Private
38
+ {i18n._(
39
+ msg({
40
+ message: "Private",
41
+ comment: "@context: Post status badge for a private post",
42
+ }),
43
+ )}
28
44
  </span>
45
+ {/* The one badge that is also a control: a draft's most likely next
46
+ action is "finish it", so the label itself opens the editor. */}
47
+ <button
48
+ type="button"
49
+ class="post-status-badge post-status-draft"
50
+ data-draft-continue
51
+ title={i18n._(
52
+ msg({
53
+ message: "Continue writing this draft",
54
+ comment:
55
+ "@context: Tooltip on the draft badge, which opens the editor",
56
+ }),
57
+ )}
58
+ >
59
+ <Icon name="post-status-draft" />
60
+ {i18n._(
61
+ msg({
62
+ message: "Draft",
63
+ comment:
64
+ "@context: Post status badge for an unpublished draft, shown to the author only",
65
+ }),
66
+ )}
67
+ </button>
29
68
  </div>
30
69
  );
31
70
  };
@@ -18,6 +18,7 @@ import { PostStatusBadges } from "./PostStatusBadges.js";
18
18
  import { sanitizeUrl, extractDisplayDomain } from "../../lib/url.js";
19
19
  import { DecorativeQuoteMark } from "../shared/DecorativeQuoteMark.js";
20
20
  import { MediaGallery } from "../shared/MediaGallery.js";
21
+ import { getPostArticleAttributes } from "../shared/post-article-attributes.js";
21
22
 
22
23
  export const QuoteCard: FC<TimelineCardProps> = ({
23
24
  post,
@@ -34,18 +35,7 @@ export const QuoteCard: FC<TimelineCardProps> = ({
34
35
  <article
35
36
  class={articleClass}
36
37
  {...(isDetail ? { "data-page": "post" } : {})}
37
- data-post
38
- data-format="quote"
39
- data-post-id={post.id}
40
- data-post-slug={post.slug}
41
- data-thread-root-id={post.threadRootId ?? post.id}
42
- {...(post.pinned ? { "data-post-pinned": "" } : {})}
43
- {...(post.pinnedInCollection
44
- ? { "data-post-pinned-in-collection": "" }
45
- : {})}
46
- {...(post.featured ? { "data-post-featured": "" } : {})}
47
- data-post-visibility={post.visibility}
48
- {...(!isDetail && post.threadRootId ? { "data-post-reply": "" } : {})}
38
+ {...getPostArticleAttributes(post)}
49
39
  >
50
40
  {!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
51
41
  {post.quoteText && (
@@ -33,15 +33,15 @@ const HERO_DISPLAY = {} as const;
33
33
 
34
34
  export const ThreadPreview: FC<ThreadPreviewProps> = ({
35
35
  rootPost,
36
- secondReply,
37
- penultimateReply,
36
+ leadingReplies,
37
+ trailingReplies,
38
38
  latestReply,
39
39
  totalReplyCount,
40
40
  }) => {
41
41
  const { i18n } = useLingui();
42
42
  const { hiddenCount } = getThreadPreviewState({
43
- secondReply,
44
- penultimateReply,
43
+ leadingReplies,
44
+ trailingReplies,
45
45
  latestReply,
46
46
  totalReplyCount,
47
47
  });
@@ -72,15 +72,16 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
72
72
  "@context: Collapse expanded thread ancestor context in the feed",
73
73
  }),
74
74
  );
75
- const renderedSecondReply =
76
- secondReply && secondReply.id !== latestReply.id ? secondReply : undefined;
77
- const renderedPenultimateReply =
78
- penultimateReply &&
79
- penultimateReply.id !== latestReply.id &&
80
- penultimateReply.id !== secondReply?.id
81
- ? penultimateReply
82
- : undefined;
83
- const gapHref = renderedSecondReply?.permalink ?? latestReply.permalink;
75
+ const visibleReplyIds = new Set([latestReply.id]);
76
+ const dedupeReplies = (replies: typeof leadingReplies) =>
77
+ replies.filter((reply) => {
78
+ if (visibleReplyIds.has(reply.id)) return false;
79
+ visibleReplyIds.add(reply.id);
80
+ return true;
81
+ });
82
+ const renderedLeadingReplies = dedupeReplies(leadingReplies);
83
+ const renderedTrailingReplies = dedupeReplies(trailingReplies);
84
+ const gapHref = renderedLeadingReplies[0]?.permalink ?? latestReply.permalink;
84
85
 
85
86
  // Always render the collapsible shell + toggle: the cap and fade are a
86
87
  // constant "this is context" affordance. The toggle's *initial* visibility
@@ -101,15 +102,16 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
101
102
  </div>
102
103
  );
103
104
 
104
- const secondReplyItem = renderedSecondReply ? (
105
- <div class="thread-item thread-item-context">
106
- <TimelineItemFromPost
107
- post={renderedSecondReply}
108
- mode="feed"
109
- display={CONTEXT_DISPLAY}
110
- />
111
- </div>
112
- ) : null;
105
+ const renderContextReplies = (replies: typeof leadingReplies) =>
106
+ replies.map((reply) => (
107
+ <div class="thread-item thread-item-context">
108
+ <TimelineItemFromPost
109
+ post={reply}
110
+ mode="feed"
111
+ display={CONTEXT_DISPLAY}
112
+ />
113
+ </div>
114
+ ));
113
115
 
114
116
  const gapItem =
115
117
  hiddenCount > 0 ? (
@@ -120,23 +122,13 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
120
122
  </div>
121
123
  ) : null;
122
124
 
123
- const penultimateItem = renderedPenultimateReply ? (
124
- <div class="thread-item thread-item-context">
125
- <TimelineItemFromPost
126
- post={renderedPenultimateReply}
127
- mode="feed"
128
- display={CONTEXT_DISPLAY}
129
- />
130
- </div>
131
- ) : null;
132
-
133
125
  return (
134
126
  <div class="thread-group thread-group-preview">
135
127
  <div class="thread-context-shell" data-thread-context data-collapsed="">
136
128
  {rootItem}
137
- {secondReplyItem}
129
+ {renderContextReplies(renderedLeadingReplies)}
138
130
  {gapItem}
139
- {penultimateItem}
131
+ {renderContextReplies(renderedTrailingReplies)}
140
132
  </div>
141
133
  <button
142
134
  type="button"
@@ -27,8 +27,8 @@ export const TimelineFeedItemContent: FC<TimelineFeedItemContentProps> = ({
27
27
  ) : item.threadPreview ? (
28
28
  <ThreadPreview
29
29
  rootPost={item.post}
30
- secondReply={item.threadPreview.secondReply}
31
- penultimateReply={item.threadPreview.penultimateReply}
30
+ leadingReplies={item.threadPreview.leadingReplies}
31
+ trailingReplies={item.threadPreview.trailingReplies}
32
32
  latestReply={item.threadPreview.latestReply}
33
33
  totalReplyCount={item.threadPreview.totalReplyCount}
34
34
  />