@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -97,7 +97,16 @@
97
97
  {{- $selfUrl := .Permalink -}}
98
98
  {{- $now := now.UTC.Format "2006-01-02T15:04:05Z" -}}
99
99
  <?xml version="1.0" encoding="UTF-8"?>
100
- <feed xmlns="http://www.w3.org/2005/Atom">
100
+ {{- /* Namespaces ride along only when something in them is emitted, the same
101
+ rule `lib/feed.ts` follows. */ -}}
102
+ {{- $anyMedia := false -}}
103
+ {{- range $posts -}}
104
+ {{- if .Params.media -}}{{- $anyMedia = true -}}{{- end -}}
105
+ {{- range .Pages -}}{{- if .Params.media -}}{{- $anyMedia = true -}}{{- end -}}{{- end -}}
106
+ {{- end -}}
107
+ {{- $jantNs := cond (gt (len $posts) 0) ` xmlns:jant="https://jant.me/ns"` "" -}}
108
+ {{- $mediaNs := cond $anyMedia ` xmlns:media="http://search.yahoo.com/mrss/"` "" -}}
109
+ <feed xmlns="http://www.w3.org/2005/Atom"{{ $jantNs | safeHTMLAttr }}{{ $mediaNs | safeHTMLAttr }}>
101
110
  <title>{{ $feedTitle | transform.XMLEscape }}</title>
102
111
  <subtitle>{{ $siteDescription | transform.XMLEscape }}</subtitle>
103
112
  <link href="{{ $siteBase | transform.XMLEscape }}" rel="alternate"/>
@@ -129,15 +138,82 @@
129
138
  {{- $summaryText = or (string (.Params.summary_text | default "")) (.Title | default "") (string (.Params.link_url | default "")) -}}
130
139
  {{- end -}}
131
140
  {{- if eq $summaryText "" -}}{{- $summaryText = printf "Post %s" (string (.Params.id | default "")) -}}{{- end -}}
141
+
142
+ {{- /* Whether the timeline cut the text of any post the entry's card shows.
143
+ Per post it is written into front matter by the exporter, which shares
144
+ the boundary with the served feed; Hugo's own summary rule would cut
145
+ somewhere else. Per entry the rule is `isEntryTruncated` in
146
+ `lib/feed.ts`: the root plus every reply the fold keeps — the first
147
+ THREAD_LEADING_REPLIES (2) and the last THREAD_TRAILING_REPLIES (3),
148
+ newest included — as `lib/thread-fold.ts` defines it. A thread of five
149
+ replies or fewer is shown whole; `first` and `last` clamp to the slice,
150
+ so the two windows simply overlap there. */ -}}
151
+ {{- $replies := (where $post.Pages "Params.visibility" "public").ByDate -}}
152
+ {{- $truncated := $post.Params.truncated | default false -}}
153
+ {{- range first 2 $replies -}}
154
+ {{- if .Params.truncated -}}{{- $truncated = true -}}{{- end -}}
155
+ {{- end -}}
156
+ {{- range last 3 $replies -}}
157
+ {{- if .Params.truncated -}}{{- $truncated = true -}}{{- end -}}
158
+ {{- end -}}
159
+
160
+ {{- /* Every attachment in the thread, each paired with the post that carries
161
+ it — a text attachment's page lives under its own post. */ -}}
162
+ {{- $entryMedia := slice -}}
163
+ {{- range ($post.Params.media | default slice) -}}
164
+ {{- $entryMedia = $entryMedia | append (dict "m" . "page" $permalink) -}}
165
+ {{- end -}}
166
+ {{- range $replies -}}
167
+ {{- $replyPermalink := .Permalink -}}
168
+ {{- range (.Params.media | default slice) -}}
169
+ {{- $entryMedia = $entryMedia | append (dict "m" . "page" $replyPermalink) -}}
170
+ {{- end -}}
171
+ {{- end -}}
132
172
  <entry>
133
173
  <title>{{ $entryTitle | transform.XMLEscape }}</title>
134
174
  <link href="{{ $alternate | transform.XMLEscape }}" rel="alternate"/>
135
175
  {{- if $isLink }}
136
176
  <link href="{{ $permalink | transform.XMLEscape }}" rel="related"/>
137
177
  {{- end }}
178
+ {{- /* Images carry no enclosure: the content already shows them full size
179
+ inside a link to the original, so one would only ask an attachment
180
+ shelf to list the picture again. */ -}}
181
+ {{- range $entryMedia }}
182
+ {{- $m := .m }}
183
+ {{- if ne (string ($m.kind | default "document")) "image" }}
184
+ <link rel="enclosure" type="{{ (string ($m.mime_type | default "application/octet-stream")) | transform.XMLEscape }}" href="{{ (string $m.src) | absURL | transform.XMLEscape }}"{{ with $m.size }} length="{{ . }}"{{ end }}{{ with $m.original_name }} title="{{ . | transform.XMLEscape }}"{{ end }}/>
185
+ {{- end }}
186
+ {{- end }}
138
187
  <id>{{ $permalink | transform.XMLEscape }}</id>
139
188
  <published>{{ $published }}</published>
140
189
  <updated>{{ $updated }}</updated>
190
+ <jant:format>{{ $format | transform.XMLEscape }}</jant:format>
191
+ {{- if $truncated }}
192
+ <jant:truncated/>
193
+ {{- end }}
194
+ {{- /* A collection is a label the author chose, which is what <category>
195
+ is for. `jant:page` carries the URL because a single collection lives
196
+ in the root URL namespace. */ -}}
197
+ {{- range ($post.Params.collections | default slice) }}
198
+ <category term="{{ (string .slug) | transform.XMLEscape }}" label="{{ (string (.title | default .slug)) | transform.XMLEscape }}" jant:page="{{ (printf "/%s/" (string .slug)) | absURL | transform.XMLEscape }}"/>
199
+ {{- end }}
200
+ {{- range $entryMedia }}
201
+ {{- $m := .m }}
202
+ {{- $kind := string ($m.kind | default "document") }}
203
+ {{- $medium := cond (or (eq $kind "image") (or (eq $kind "video") (eq $kind "audio"))) $kind "document" }}
204
+ {{- $fileUrl := (string $m.src) | absURL }}
205
+ {{- /* Where a click should land. Only emitted where it is not the file:
206
+ a text attachment's markdown would download or dump unstyled. */ -}}
207
+ {{- $pageUrl := $fileUrl }}
208
+ {{- if eq $kind "text" }}{{ $pageUrl = printf "%s/text/%s" (strings.TrimSuffix "/" .page) (string $m.id) }}{{ end }}
209
+ {{- $desc := string ($m.alt | default ($m.summary | default "")) }}
210
+ {{- $poster := string ($m.poster | default "") }}
211
+ <media:content url="{{ $fileUrl | transform.XMLEscape }}" type="{{ (string ($m.mime_type | default "application/octet-stream")) | transform.XMLEscape }}" medium="{{ $medium }}"{{ with $m.size }} fileSize="{{ . }}"{{ end }}{{ with $m.width }} width="{{ . }}"{{ end }}{{ with $m.height }} height="{{ . }}"{{ end }}{{ with $m.duration_seconds }} duration="{{ math.Round . }}"{{ end }}{{ if ne $pageUrl $fileUrl }} jant:page="{{ $pageUrl | transform.XMLEscape }}"{{ end }}>
212
+ {{- with $m.original_name }}<media:title type="plain">{{ . | transform.XMLEscape }}</media:title>{{ end }}
213
+ {{- if ne $desc "" }}<media:description type="plain">{{ $desc | transform.XMLEscape }}</media:description>{{ end }}
214
+ {{- if ne $poster "" }}<media:thumbnail url="{{ $poster | absURL | transform.XMLEscape }}"/>{{ end -}}
215
+ </media:content>
216
+ {{- end }}
141
217
  <summary type="text">{{ $summaryText | transform.XMLEscape }}</summary>
142
218
  <content type="html"><![CDATA[
143
219
  {{- partial "feed-post-content.xml" (dict "post" $post "permalink" $permalink "includeReplies" true) -}}
@@ -53,8 +53,23 @@
53
53
  {{- $sourceUrl := string ($post.Params.source_url | default "") -}}
54
54
  {{- $cite := "" -}}
55
55
  {{- if ne $sourceUrl "" -}}{{- $cite = printf ` cite=%q` $sourceUrl -}}{{- end -}}
56
- {{- $block := printf "<blockquote%s><p>%s</p></blockquote>" $cite (transform.XMLEscape $quoteText) -}}
57
- {{- $parts = $parts | append $block -}}
56
+ {{- /* Line breaks the author typed must survive as markup: the site
57
+ keeps them with `white-space: pre-line`, but feed readers strip
58
+ CSS. Blank lines split <p> blocks, single newlines become <br/>
59
+ (mirrors `renderPlainTextHtml` in `lib/feed.ts`). */ -}}
60
+ {{- $quoteBlocks := slice -}}
61
+ {{- range split (replaceRE `\r\n?` "\n" $quoteText) "\n\n" -}}
62
+ {{- $para := trim . " \t\n" -}}
63
+ {{- if ne $para "" -}}
64
+ {{- $lines := slice -}}
65
+ {{- range split $para "\n" -}}{{- $lines = $lines | append (transform.XMLEscape .) -}}{{- end -}}
66
+ {{- $quoteBlocks = $quoteBlocks | append (printf "<p>%s</p>" (delimit $lines "<br/>")) -}}
67
+ {{- end -}}
68
+ {{- end -}}
69
+ {{- if gt (len $quoteBlocks) 0 -}}
70
+ {{- $block := printf "<blockquote%s>%s</blockquote>" $cite (delimit $quoteBlocks "\n") -}}
71
+ {{- $parts = $parts | append $block -}}
72
+ {{- end -}}
58
73
  {{- /* Attribution line — prefer name, fall back to URL host or the
59
74
  raw URL itself (mirrors main site's extractDisplayDomain fallback
60
75
  chain in `lib/url.ts`). */ -}}
@@ -88,6 +103,94 @@
88
103
  {{- end -}}
89
104
  {{- end -}}
90
105
 
106
+ {{- /* --- Attachments --- */ -}}
107
+ {{- /* One container per post, marked `data-post-media` — the attribute the
108
+ site puts on its gallery strip, so a consumer styles the strip instead of
109
+ reassembling it from the Media RSS elements. In a thread this is also the
110
+ only surface that says which post an attachment belongs to. Mirrors
111
+ `renderMediaItem` in `packages/core/src/lib/feed.ts`. */ -}}
112
+ {{- $mediaList := $post.Params.media | default slice -}}
113
+ {{- if gt (len $mediaList) 0 -}}
114
+ {{- $items := slice -}}
115
+ {{- range $mediaList -}}
116
+ {{- $kind := string (.kind | default "document") -}}
117
+ {{- $mime := string (.mime_type | default "application/octet-stream") -}}
118
+ {{- /* `trim` wraps the index rather than piping into it: a Go template
119
+ pipe binds to the last argument, so `index … 0 | trim " "` would trim
120
+ the index instead of the result. */ -}}
121
+ {{- $mimeLabel := trim (index (split $mime ";") 0) " " -}}
122
+ {{- $src := (string .src) | absURL -}}
123
+ {{- $name := string (.original_name | default "") -}}
124
+ {{- $alt := string (.alt | default "") -}}
125
+ {{- /* Duration and size, in the shapes `formatFeedDuration` and
126
+ `formatFeedBytes` produce. */ -}}
127
+ {{- $metaParts := slice -}}
128
+ {{- with .duration_seconds -}}
129
+ {{- $total := int (math.Round .) -}}
130
+ {{- if gt $total 0 -}}
131
+ {{- $metaParts = $metaParts | append (printf "%d:%02d" (div $total 60) (mod $total 60)) -}}
132
+ {{- end -}}
133
+ {{- end -}}
134
+ {{- with .size -}}
135
+ {{- $b := int . -}}
136
+ {{- if gt $b 0 -}}
137
+ {{- if lt $b 1024 -}}
138
+ {{- $metaParts = $metaParts | append (printf "%d B" $b) -}}
139
+ {{- else if lt $b 1048576 -}}
140
+ {{- $metaParts = $metaParts | append (printf "%d KB" (int (math.Round (div (float $b) 1024.0)))) -}}
141
+ {{- else -}}
142
+ {{- $metaParts = $metaParts | append (printf "%.1f MB" (div (float $b) 1048576.0)) -}}
143
+ {{- end -}}
144
+ {{- end -}}
145
+ {{- end -}}
146
+ {{- $meta := delimit $metaParts " · " -}}
147
+ {{- $metaSuffix := "" -}}
148
+ {{- if ne $meta "" -}}{{- $metaSuffix = printf " (%s)" (transform.XMLEscape $meta) -}}{{- end -}}
149
+ {{- $dims := "" -}}
150
+ {{- if and .width .height -}}{{- $dims = printf ` width="%d" height="%d"` (int .width) (int .height) -}}{{- end -}}
151
+
152
+ {{- if eq $kind "image" -}}
153
+ {{- $caption := "" -}}
154
+ {{- if ne $alt "" -}}{{- $caption = printf "<figcaption>%s</figcaption>" (transform.XMLEscape $alt) -}}{{- end -}}
155
+ {{- /* `%q` is Go string quoting, not attribute escaping: a `"` or `\`
156
+ in the alt would leave the attribute malformed. URLs go through
157
+ `%q` because they never carry either; author text goes through
158
+ `transform.XMLEscape`, the same as the caption beside it. */ -}}
159
+ {{- $items = $items | append (printf `<figure><a href=%q><img src=%q alt="%s"%s/></a>%s</figure>` $src $src (transform.XMLEscape $alt) $dims $caption) -}}
160
+ {{- else if eq $kind "video" -}}
161
+ {{- /* A poster is written only when there is a real still; the pipeline
162
+ leaves nothing usable otherwise, and `<img src="clip.mp4">` was the
163
+ bug that taught us. The link sits outside the <video> so it survives
164
+ a sanitizer that drops the player with its children, and
165
+ `preload="none"` keeps a reader from pulling the whole file. */ -}}
166
+ {{- $posterAttr := "" -}}
167
+ {{- with .poster -}}{{- $posterAttr = printf ` poster=%q` ((string .) | absURL) -}}{{- end -}}
168
+ {{- $altSuffix := "" -}}
169
+ {{- if ne $alt "" -}}{{- $altSuffix = printf ": %s" (transform.XMLEscape $alt) -}}{{- end -}}
170
+ {{- $items = $items | append (printf `<figure><video controls preload="none"%s%s><source src=%q type=%q/></video><figcaption><a href=%q>▶ Watch video</a>%s%s</figcaption></figure>` $posterAttr $dims $src $mimeLabel $src $metaSuffix $altSuffix) -}}
171
+ {{- else if eq $kind "text" -}}
172
+ {{- /* Markdown a browser would download or dump unstyled, so the link
173
+ goes to the page that renders it. */ -}}
174
+ {{- $href := printf "%s/text/%s" (strings.TrimSuffix "/" $permalink) (string .id) -}}
175
+ {{- $label := printf "📎 [%s] %s" (transform.XMLEscape $mimeLabel) (transform.XMLEscape (or $name "Attached text")) -}}
176
+ {{- $textMeta := $metaSuffix -}}
177
+ {{- with .chars -}}
178
+ {{- if gt (int .) 0 -}}{{- $textMeta = printf " (%d chars)" (int .) -}}{{- end -}}
179
+ {{- end -}}
180
+ {{- $summarySuffix := "" -}}
181
+ {{- with .summary -}}
182
+ {{- if ne (string .) "" -}}{{- $summarySuffix = printf ": %s" (transform.XMLEscape (string .)) -}}{{- end -}}
183
+ {{- end -}}
184
+ {{- $items = $items | append (printf `<p><a href=%q>%s</a>%s%s</p>` $href $label $textMeta $summarySuffix) -}}
185
+ {{- else -}}
186
+ {{- $fallbackName := cond (eq $kind "audio") "Audio" "Attachment" -}}
187
+ {{- $label := printf "📎 [%s] %s" (transform.XMLEscape $mimeLabel) (transform.XMLEscape (or $name $fallbackName)) -}}
188
+ {{- $items = $items | append (printf `<p><a href=%q>%s</a>%s</p>` $src $label $metaSuffix) -}}
189
+ {{- end -}}
190
+ {{- end -}}
191
+ {{- $parts = $parts | append (printf "<div data-post-media>\n%s\n</div>" (delimit $items "\n")) -}}
192
+ {{- end -}}
193
+
91
194
  {{- /* --- Star rating --- */ -}}
92
195
  {{- with $post.Params.rating -}}
93
196
  {{- $r := int . -}}
@@ -8,10 +8,14 @@
8
8
  `<jant-media-lightbox>` delegates clicks on those anchors.
9
9
 
10
10
  Layout: mirrors MediaGallery.tsx — one horizontal row with a computed
11
- `rowHeight` (driven by the first visual's aspect ratio) so mixed sizes
12
- align into a clean strip. Single-visual posts expand to their natural
13
- aspect ratio (capped at 24rem / 85% width). Non-visual attachments
14
- (audio/doc/text) use a 3:4 portrait card sized to the row height.
11
+ `rowHeight` so mixed sizes align into a clean strip. The opening visual
12
+ leads (320px / its ratio), the widest visual is held to 520px so a landscape
13
+ video never swallows the column, and the result is clamped to 240–384px —
14
+ 384 being the same 24rem cap a lone visual gets, so a second attachment
15
+ never makes the first picture grow. Non-visual attachments (audio/doc/text)
16
+ do not enter that math; their 3:4 portrait cards are sized from its result.
17
+ Single-visual posts skip the strip and expand to their natural aspect ratio
18
+ (capped at 24rem / 85% width).
15
19
 
16
20
  Inline styles (not Tailwind utilities) because the exported theme ships
17
21
  no utility CSS — only tokens.css + main.css + theme.css + site-media.css.
@@ -27,8 +31,7 @@
27
31
  {{- $items := slice -}}
28
32
  {{- $lightbox := slice -}}
29
33
  {{- $lbIdx := 0 -}}
30
- {{- $hasNonVisual := false -}}
31
- {{- $firstRatio := 0.0 -}}
34
+ {{- $visualRatios := slice -}}
32
35
  {{- range $media -}}
33
36
  {{- $kind := .kind | default "file" -}}
34
37
  {{- $item := . -}}
@@ -46,13 +49,15 @@
46
49
  {{- $lightbox = $lightbox | append $lbEntry -}}
47
50
  {{- $items = $items | append (merge $item (dict "_kind" $kind "_lbIdx" $lbIdx)) -}}
48
51
  {{- $lbIdx = add $lbIdx 1 -}}
49
- {{- /* Capture first visual's aspect ratio */ -}}
50
- {{- if and (eq $firstRatio 0.0) $item.width $item.height (gt (float $item.width) 0.0) (gt (float $item.height) 0.0) -}}
51
- {{- $firstRatio = div (float $item.width) (float $item.height) -}}
52
+ {{- /* Every visual's aspect ratio, in display order — 4:3 when unknown,
53
+ matching getMediaAspectRatio() in MediaGallery.tsx. */ -}}
54
+ {{- $ratio := 1.3333 -}}
55
+ {{- if and $item.width $item.height (gt (float $item.width) 0.0) (gt (float $item.height) 0.0) -}}
56
+ {{- $ratio = div (float $item.width) (float $item.height) -}}
52
57
  {{- end -}}
58
+ {{- $visualRatios = $visualRatios | append $ratio -}}
53
59
  {{- else -}}
54
60
  {{- $items = $items | append (merge $item (dict "_kind" $kind)) -}}
55
- {{- $hasNonVisual = true -}}
56
61
  {{- end -}}
57
62
  {{- end -}}
58
63
 
@@ -62,14 +67,19 @@
62
67
  {{- $firstKind := $first._kind -}}
63
68
  {{- $singleVisual := and $single (or (eq $firstKind "image") (eq $firstKind "video")) -}}
64
69
 
65
- {{- /* rowHeight — mirrors MediaGallery.tsx. 4/3 fallback when no visuals. */ -}}
66
- {{- $rowMin := cond $hasNonVisual 220 280 -}}
67
- {{- $rowMax := cond $hasNonVisual 300 440 -}}
68
- {{- $rowHeight := cond $hasNonVisual 220 360 -}}
69
- {{- if and (not $singleVisual) (gt $count 1) (gt $firstRatio 0.0) -}}
70
- {{- $ratio := cond (lt $firstRatio 0.5) 0.5 $firstRatio -}}
71
- {{- $computed := math.Round (div 320.0 $ratio) -}}
72
- {{- $rowHeight = cond (gt $computed $rowMax) $rowMax (cond (lt $computed $rowMin) $rowMin (int $computed)) -}}
70
+ {{- /* rowHeight — mirrors getGalleryRowHeight() in MediaGallery.tsx. */ -}}
71
+ {{- $rowMin := 240 -}}
72
+ {{- $rowMax := 384 -}}
73
+ {{- $rowHeight := $rowMin -}}
74
+ {{- if $visualRatios -}}
75
+ {{- $widest := 0.0 -}}
76
+ {{- range $visualRatios -}}
77
+ {{- if gt . $widest }}{{ $widest = . }}{{ end -}}
78
+ {{- end -}}
79
+ {{- $byLead := div 320.0 (index $visualRatios 0) -}}
80
+ {{- $byWidestItem := div 520.0 $widest -}}
81
+ {{- $computed := math.Round (cond (lt $byWidestItem $byLead) $byWidestItem $byLead) -}}
82
+ {{- $rowHeight = cond (gt $computed (float $rowMax)) $rowMax (cond (lt $computed (float $rowMin)) $rowMin (int $computed)) -}}
73
83
  {{- end -}}
74
84
  {{- $docCardWidth := int (math.Round (mul (float $rowHeight) 0.75)) -}}
75
85
 
@@ -80,7 +90,7 @@
80
90
  <div data-post-media
81
91
  {{- if $lightbox }} data-lightbox-group="{{ $lightbox | jsonify }}"{{- end -}}
82
92
  {{- if not $singleVisual }} tabindex="0" role="group" aria-label="Media gallery"{{- end -}}
83
- style="display: flex; gap: 0.5rem;
93
+ style="display: flex; gap: 0.75rem;
84
94
  {{- if not $singleVisual }} overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none;{{- end }}">
85
95
  {{- range $items -}}
86
96
  {{- $k := ._kind -}}
@@ -130,10 +140,14 @@
130
140
  {{- else -}}
131
141
  {{- $videoWrapStyle = printf "%s display: block; flex-shrink: 0; width: %dpx; max-width: 85%%;" $videoWrapStyle $itemWidth -}}
132
142
  {{- end -}}
143
+ {{- /* Fit travels with the size, as in MediaGallery.tsx, and
144
+ site-media.css sets none: a clip in a row is cropped to the
145
+ row's height like the pictures beside it; a lone clip is
146
+ letterboxed in case its frames disagree with the stored size. */ -}}
133
147
  {{- $videoStyle := cond $singleVisual
134
148
  (cond (and (gt (int (default 0 .width)) 0) (gt (int (default 0 .height)) 0))
135
- (printf "display: block; width: 100%%; height: auto; aspect-ratio: %d/%d;" (int .width) (int .height))
136
- "display: block; width: 100%; height: auto;")
149
+ (printf "display: block; width: 100%%; height: auto; aspect-ratio: %d/%d; object-fit: contain;" (int .width) (int .height))
150
+ "display: block; width: 100%; height: auto; object-fit: contain;")
137
151
  (printf "display: block; width: 100%%; height: %dpx; object-fit: cover;" $rowHeight) -}}
138
152
  {{- if $isShort -}}
139
153
  <div class="media-video-wrap media-video-wrap-short" style="{{ $videoWrapStyle | safeCSS }}">
@@ -2,26 +2,27 @@
2
2
  {{- $total := $paginator.TotalPages -}}
3
3
  {{- $current := $paginator.PageNumber -}}
4
4
  {{- if gt $total 1 -}}
5
- {{- /* Mirror main site's getPageNumbers: always include 1, last, current,
6
- and current±1. Insert 0 as an ellipsis marker for gaps. */ -}}
5
+ {{- /* Mirror main site's getPageNumbers: past seven pages, always seven
6
+ slots — 1, last, and a three-page window around the current page.
7
+ The second and second-to-last slots hold a page or 0 (an ellipsis
8
+ marker), so an ellipsis never stands in for a single page. */ -}}
7
9
  {{- $pages := slice -}}
8
10
  {{- if le $total 7 -}}
9
11
  {{- range seq 1 $total -}}
10
12
  {{- $pages = $pages | append . -}}
11
13
  {{- end -}}
12
14
  {{- else -}}
13
- {{- $set := slice 1 $total $current -}}
14
- {{- if gt $current 1 -}}{{- $set = $set | append (sub $current 1) -}}{{- end -}}
15
- {{- if lt $current $total -}}{{- $set = $set | append (add $current 1) -}}{{- end -}}
16
- {{- $sorted := sort (uniq $set) -}}
17
- {{- $prev := 0 -}}
18
- {{- range $i, $p := $sorted -}}
19
- {{- if and (gt $i 0) (gt (sub $p $prev) 1) -}}
20
- {{- $pages = $pages | append 0 -}}
21
- {{- end -}}
22
- {{- $pages = $pages | append $p -}}
23
- {{- $prev = $p -}}
15
+ {{- $start := sub $current 1 -}}
16
+ {{- if lt $start 3 -}}{{- $start = 3 -}}{{- end -}}
17
+ {{- if gt $start (sub $total 4) -}}{{- $start = sub $total 4 -}}{{- end -}}
18
+ {{- $end := add $start 2 -}}
19
+ {{- $pages = $pages | append 1 -}}
20
+ {{- $pages = $pages | append (cond (eq $start 3) 2 0) -}}
21
+ {{- range seq $start $end -}}
22
+ {{- $pages = $pages | append . -}}
24
23
  {{- end -}}
24
+ {{- $pages = $pages | append (cond (eq $end (sub $total 2)) (sub $total 1) 0) -}}
25
+ {{- $pages = $pages | append $total -}}
25
26
  {{- end -}}
26
27
  <nav class="pagination" aria-label="Pagination">
27
28
  {{- if $paginator.HasPrev -}}
@@ -5,6 +5,10 @@
5
5
  {{- $collections := $post.Params.collections | default slice -}}
6
6
  {{- $slug := $post.Params.slug | default $post.Slug -}}
7
7
  {{- $linkUrl := $post.Params.link_url -}}
8
+ {{- /* A reply is not rendered as a page (`build: render: never`), so its
9
+ `.RelPermalink` is empty. The exporter registers every reply slug as an
10
+ alias on the root, and that alias is the address a reply has. */ -}}
11
+ {{- $permalink := $post.RelPermalink | default (printf "/%s/" $slug | relURL) -}}
8
12
  <article class="post-card post-card-{{ $format }}" data-format="{{ $format }}" data-slug="{{ $slug }}"
9
13
  {{- if $post.Params.pinned_at }} data-post-pinned{{ end -}}
10
14
  {{- if $post.Params.featured_at }} data-post-featured{{ end -}}
@@ -27,7 +31,7 @@
27
31
  <a href="{{ $linkUrl }}" rel="noopener noreferrer" target="_blank">{{ $post.Title }}</a>
28
32
  </h2>
29
33
  {{- else if $post.Title -}}
30
- <h2 class="post-card-title post-card-link-title"><a href="{{ $post.RelPermalink }}">{{ $post.Title }}</a></h2>
34
+ <h2 class="post-card-title post-card-link-title"><a href="{{ $permalink }}">{{ $post.Title }}</a></h2>
31
35
  {{- end -}}
32
36
  {{- /* Prefer Hugo's rendered HTML body — preserves paragraphs, links,
33
37
  emphasis, lists. Falls back to the plain-text `summary_text` stored
@@ -73,7 +77,7 @@
73
77
  {{- end -}}
74
78
  {{- else -}}
75
79
  {{- with $post.Title -}}
76
- <h2 class="post-card-title"><a href="{{ $post.RelPermalink }}">{{ . }}</a></h2>
80
+ <h2 class="post-card-title"><a href="{{ $permalink }}">{{ . }}</a></h2>
77
81
  {{- end -}}
78
82
  {{- /* Prefer Hugo's rendered HTML summary — preserves paragraphs, links,
79
83
  emphasis, lists. Falls back to the plain-text `summary_text` stored
@@ -85,7 +89,7 @@
85
89
  {{- end -}}
86
90
  {{- end -}}
87
91
 
88
- {{ partial "media-gallery.html" (dict "media" $media "permalink" $post.RelPermalink) }}
92
+ {{ partial "media-gallery.html" (dict "media" $media "permalink" $permalink) }}
89
93
 
90
94
  <footer class="post-card-footer post-menu-footer" data-post-meta>
91
95
  <div class="post-footer-meta">
@@ -96,7 +100,7 @@
96
100
  </svg>
97
101
  </span>
98
102
  {{- end -}}
99
- <a class="post-footer-link post-card-permalink" href="{{ $post.RelPermalink }}">
103
+ <a class="post-footer-link post-card-permalink" href="{{ $permalink }}">
100
104
  <time datetime="{{ $post.Date.Format "2006-01-02T15:04:05Z07:00" }}">
101
105
  {{ $post.Date.Format "Jan 2, 2006" }}
102
106
  </time>
@@ -69,7 +69,8 @@
69
69
  </div>
70
70
  {{- end -}}
71
71
 
72
- {{ partial "media-gallery.html" (dict "media" $media "permalink" $reply.RelPermalink) }}
72
+ {{- /* Replies are not rendered as pages, so the address is the reply's alias on the root. */ -}}
73
+ {{ partial "media-gallery.html" (dict "media" $media "permalink" ($reply.RelPermalink | default (printf "/%s/" $slug | relURL))) }}
73
74
 
74
75
  <footer class="reply-footer post-menu-footer" data-post-meta>
75
76
  <div class="post-footer-meta">
@@ -1,12 +1,17 @@
1
1
  {{- /*
2
2
  Thread preview partial.
3
3
 
4
- Matches the main site's `ThreadPreview` component layout:
5
- root post (full card, as "context")
6
- second reply (full card, as "context") ← if distinct
7
- "N more posts" gap pill ← if there are hidden middle replies
8
- penultimate reply (full card, as "context") ← if distinct
9
- latest reply (full card, as "hero")
4
+ Matches the main site's `ThreadPreview` component, and the fold both it and
5
+ the feed take from `lib/thread-fold.ts` (`foldThreadReplies`):
6
+ root post (full card, as "context")
7
+ the first THREAD_LEADING_REPLIES (2) (full cards, as "context")
8
+ "N more posts" gap pill ← only when the fold hides any
9
+ the two before the newest reply (full cards, as "context")
10
+ newest reply (full card, as "hero")
11
+
12
+ The trailing window is THREAD_TRAILING_REPLIES (3) with the hero inside it.
13
+ On a short thread the windows overlap, so each list drops whatever an
14
+ earlier one already shows, hero first — the same order the site dedupes in.
10
15
 
11
16
  Each entry is wrapped in a `.thread-item` so the rail dot markers
12
17
  line up the same way the main site does via `.thread-item::before`.
@@ -20,29 +25,34 @@
20
25
  {{- if eq $replyCount 0 -}}
21
26
  {{ partial "post-card.html" $root }}
22
27
  {{- else -}}
23
- {{- /* First-in / last-in replies, with overlap handled below. */ -}}
24
28
  {{- $latest := index $replies (sub $replyCount 1) -}}
25
- {{- $second := index $replies 0 -}}
26
- {{- $penultimate := cond (gt $replyCount 2) (index $replies (sub $replyCount 2)) false -}}
27
-
28
- {{- $renderSecond := ne $second.File.UniqueID $latest.File.UniqueID -}}
29
- {{- $renderPenultimate := and $penultimate (ne $penultimate.File.UniqueID $latest.File.UniqueID) -}}
30
- {{- if $penultimate -}}
31
- {{- if eq $penultimate.File.UniqueID $second.File.UniqueID -}}
32
- {{- $renderPenultimate = false -}}
29
+ {{- $leading := slice -}}
30
+ {{- range first 2 $replies -}}
31
+ {{- if ne .File.UniqueID $latest.File.UniqueID -}}
32
+ {{- $leading = $leading | append . -}}
33
+ {{- end -}}
34
+ {{- end -}}
35
+ {{- $trailing := slice -}}
36
+ {{- range last 3 $replies -}}
37
+ {{- $reply := . -}}
38
+ {{- $shown := eq $reply.File.UniqueID $latest.File.UniqueID -}}
39
+ {{- range $leading -}}
40
+ {{- if eq .File.UniqueID $reply.File.UniqueID -}}{{- $shown = true -}}{{- end -}}
33
41
  {{- end -}}
42
+ {{- if not $shown -}}{{- $trailing = $trailing | append $reply -}}{{- end -}}
34
43
  {{- end -}}
35
44
 
36
- {{- /* Count of context posts actually rendered above the hero (second
37
- + penultimate when distinct from latest and each other). The
38
- remainder of replies is the "hidden middle" count. Mirrors
39
- `getThreadPreviewState` in thread-preview-state.ts. */ -}}
40
- {{- $visibleContextReplies := 0 -}}
41
- {{- if $renderSecond -}}{{- $visibleContextReplies = add $visibleContextReplies 1 -}}{{- end -}}
42
- {{- if $renderPenultimate -}}{{- $visibleContextReplies = add $visibleContextReplies 1 -}}{{- end -}}
43
- {{- /* Total replies rendered = visibleContextReplies + 1 (hero). */ -}}
44
- {{- $hiddenCount := sub $replyCount (add $visibleContextReplies 1) -}}
45
- {{- if lt $hiddenCount 0 -}}{{- $hiddenCount = 0 -}}{{- end -}}
45
+ {{- /* Whatever neither window shows is the hidden middle. It is never
46
+ negative: the three lists are disjoint and all drawn from $replies. */ -}}
47
+ {{- $hiddenCount := sub $replyCount (add (add (len $leading) (len $trailing)) 1) -}}
48
+ {{- /* The gap leads to the first hidden reply, as the site's does. A fold
49
+ only hides anything once both windows are full, so that reply sits
50
+ right after the leading window. Replies are not rendered as pages
51
+ (`build: render: never`), so their `.RelPermalink` is empty; the
52
+ exporter registers every reply slug as an alias on the root, and
53
+ that is the address a reply has. */ -}}
54
+ {{- $gapHref := $root.RelPermalink -}}
55
+ {{- if gt $hiddenCount 0 -}}{{- $gapHref = printf "/%s/" (index $replies 2).Slug | relURL -}}{{- end -}}
46
56
 
47
57
  {{- $slug := $root.Params.slug | default $root.Slug -}}
48
58
  <article class="thread-preview" data-slug="{{ $slug }}" data-reply-count="{{ $replyCount }}">
@@ -51,23 +61,23 @@
51
61
  {{ partial "post-card.html" $root }}
52
62
  </div>
53
63
 
54
- {{- if $renderSecond -}}
64
+ {{- range $leading -}}
55
65
  <div class="thread-item thread-item-context">
56
- {{ partial "post-card.html" $second }}
66
+ {{ partial "post-card.html" . }}
57
67
  </div>
58
68
  {{- end -}}
59
69
 
60
70
  {{- if gt $hiddenCount 0 -}}
61
71
  <div class="thread-item thread-item-gap">
62
- <a class="thread-preview-gap" href="{{ $root.RelPermalink }}">
72
+ <a class="thread-preview-gap" href="{{ $gapHref }}">
63
73
  {{ $hiddenCount }} more {{ if eq $hiddenCount 1 }}post{{ else }}posts{{ end }}
64
74
  </a>
65
75
  </div>
66
76
  {{- end -}}
67
77
 
68
- {{- if $renderPenultimate -}}
78
+ {{- range $trailing -}}
69
79
  <div class="thread-item thread-item-context">
70
- {{ partial "post-card.html" $penultimate }}
80
+ {{ partial "post-card.html" . }}
71
81
  </div>
72
82
  {{- end -}}
73
83
  </div>
@@ -36,6 +36,13 @@ body {
36
36
  background-color: var(--site-page-bg);
37
37
  -webkit-font-smoothing: antialiased;
38
38
  -moz-osx-font-smoothing: grayscale;
39
+ /* Every text node in an exported page is authored content, and authored text
40
+ can contain a run with no break opportunity — a long URL, an ID, a word
41
+ typed without spaces. Such a run does not wrap on its own and spills past
42
+ the content column across the page. `break-word` splits it only when it
43
+ cannot fit a line by itself, so ordinary text keeps its natural line breaks
44
+ and intrinsic sizing is unaffected. */
45
+ overflow-wrap: break-word;
39
46
  }
40
47
 
41
48
  img,
@@ -1067,6 +1074,11 @@ hr.feed-divider {
1067
1074
  align-items: center;
1068
1075
  gap: 0.45rem;
1069
1076
  flex-wrap: wrap;
1077
+ /* The name and source are flex items, which refuse to shrink below their
1078
+ min-content width. The inherited `break-word` leaves min-content at the
1079
+ longest word and the item still overflows; `anywhere` also shrinks
1080
+ min-content, which is what lets the row wrap. */
1081
+ overflow-wrap: anywhere;
1070
1082
  margin-top: 0.95rem;
1071
1083
  color: var(--site-text-secondary);
1072
1084
  font-family: var(--font-ui);
@@ -1998,7 +2010,7 @@ hr.feed-divider {
1998
2010
  .pagination {
1999
2011
  display: flex;
2000
2012
  align-items: center;
2001
- justify-content: center;
2013
+ justify-content: flex-start;
2002
2014
  flex-wrap: wrap;
2003
2015
  gap: 1rem;
2004
2016
  padding: 1.5rem 0;