@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,4 +1,5 @@
1
1
  import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
2
+ import { ensureCompose } from "./lazy-entries.js";
2
3
  import { showToast } from "./toast.js";
3
4
 
4
5
  interface ReplyToData {
@@ -17,12 +18,64 @@ export interface ComposeOpenOptions {
17
18
  initialFormat?: "note" | "link" | "quote";
18
19
  }
19
20
 
21
+ /**
22
+ * The composer element the server rendered, upgraded or not.
23
+ *
24
+ * Use it to ask whether the page has a composer; to call it, go through
25
+ * {@link ensureComposeDialog}.
26
+ *
27
+ * @returns The element, or null when the page has no composer
28
+ */
20
29
  export function getComposeDialog(): JantComposeDialog | null {
21
30
  return document.querySelector(
22
31
  "jant-compose-dialog",
23
32
  ) as JantComposeDialog | null;
24
33
  }
25
34
 
35
+ /**
36
+ * The composer, ready to take calls.
37
+ *
38
+ * The composer ships in its own bundle, loaded on first use, so the element
39
+ * the server rendered may still be an unupgraded tag when a button is
40
+ * clicked. This loads the bundle — which defines the element and upgrades it
41
+ * in place — before handing the element back.
42
+ *
43
+ * @returns The upgraded composer, or null when the page has no composer
44
+ * @example
45
+ * ```ts
46
+ * const dialog = await ensureComposeDialog();
47
+ * await dialog?.openTranslation(threadId, "zh-Hans");
48
+ * ```
49
+ */
50
+ export async function ensureComposeDialog(): Promise<JantComposeDialog | null> {
51
+ const dialog = getComposeDialog();
52
+ if (!dialog) return null;
53
+ await ensureCompose();
54
+ return dialog;
55
+ }
56
+
57
+ /**
58
+ * The composer's translated strings, readable before its bundle has loaded.
59
+ *
60
+ * The post menu borrows labels the server rendered onto the composer for its
61
+ * inline collection form. Until the element upgrades, Lit has not parsed the
62
+ * `labels` attribute into a property, so read the attribute directly then.
63
+ *
64
+ * @returns The labels, or null when the page has no composer or no labels
65
+ */
66
+ export function readComposeDialogLabels(): JantComposeDialog["labels"] | null {
67
+ const dialog = getComposeDialog();
68
+ if (!dialog) return null;
69
+ if (dialog.labels) return dialog.labels;
70
+ const raw = dialog.getAttribute("labels");
71
+ if (!raw) return null;
72
+ try {
73
+ return JSON.parse(raw) as JantComposeDialog["labels"];
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
26
79
  export function getActiveCollectionId(): string | undefined {
27
80
  return (
28
81
  document.querySelector<HTMLElement>(
@@ -133,14 +186,35 @@ function getReplyData(article: HTMLElement): ReplyToData {
133
186
  export async function openNewCompose(
134
187
  options?: ComposeOpenOptions,
135
188
  ): Promise<void> {
136
- await getComposeDialog()?.openNew(options);
189
+ const dialog = await ensureComposeDialog();
190
+ await dialog?.openNew(options);
191
+ }
192
+
193
+ /**
194
+ * Open a post in the composer for editing.
195
+ *
196
+ * @param postId - The post to edit
197
+ */
198
+ export async function openEditForPost(postId: string): Promise<void> {
199
+ const dialog = await ensureComposeDialog();
200
+ await dialog?.openEdit(postId);
201
+ }
202
+
203
+ /**
204
+ * Open a draft in the composer, from the draft preview page.
205
+ *
206
+ * @param postId - The draft to continue
207
+ */
208
+ export async function openDraftForPost(postId: string): Promise<void> {
209
+ const dialog = await ensureComposeDialog();
210
+ await dialog?.openDraft(postId);
137
211
  }
138
212
 
139
213
  export async function openReplyForArticle(article: HTMLElement): Promise<void> {
140
214
  const postId = article.dataset.postId;
141
215
  if (!postId) return;
142
216
 
143
- const dialog = getComposeDialog();
217
+ const dialog = await ensureComposeDialog();
144
218
  if (!dialog) return;
145
219
 
146
220
  // Only surfaces that hide the trailing draft redirect to it — the feed hands
@@ -3,6 +3,8 @@ import {
3
3
  getComposeDialog,
4
4
  getCurrentDetailPostArticle,
5
5
  getReplyTargetArticle,
6
+ openDraftForPost,
7
+ openEditForPost,
6
8
  openNewCompose,
7
9
  openReplyForArticle,
8
10
  } from "./compose-launch.js";
@@ -64,14 +66,13 @@ function openEditFromQueryParam() {
64
66
 
65
67
  const article = getCurrentDetailPostArticle();
66
68
  const postId = article?.dataset.postId;
67
- const composeEl = getComposeDialog();
68
- if (!postId || !composeEl) return;
69
+ if (!postId || !getComposeDialog()) return;
69
70
 
70
71
  consumeEditQueryParam();
71
72
  if (document.querySelector("[data-preview-status]")) {
72
- void composeEl.openDraft(postId);
73
+ void openDraftForPost(postId);
73
74
  } else {
74
- void composeEl.openEdit(postId);
75
+ void openEditForPost(postId);
75
76
  }
76
77
  }
77
78
 
@@ -141,10 +142,7 @@ document.addEventListener("keydown", (event: globalThis.KeyboardEvent) => {
141
142
  const postId = article.dataset.postId;
142
143
  if (!postId) return;
143
144
  event.preventDefault();
144
- const composeEl = getComposeDialog();
145
- if (composeEl) {
146
- void composeEl.openEdit(postId);
147
- }
145
+ void openEditForPost(postId);
148
146
  return;
149
147
  }
150
148
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Server-rendered buttons that open the composer.
3
+ *
4
+ * The composer ships in its own bundle, loaded on first use, so markup the
5
+ * server renders must not call the element directly: an unupgraded
6
+ * `<jant-compose-dialog>` has no methods yet. Buttons carry a data attribute
7
+ * instead, and this module — always on a signed-in page — turns the click
8
+ * into a call through `compose-launch.ts`, which loads the bundle first.
9
+ *
10
+ * - `[data-compose-open]`, with an optional `data-compose-collection-id`:
11
+ * start a new post
12
+ * - `[data-reply-trigger]` inside `article[data-post]`: reply to that post
13
+ * - `[data-draft-continue]`: reopen an inline draft in the editor
14
+ */
15
+
16
+ import {
17
+ openEditForPost,
18
+ openNewCompose,
19
+ openReplyForArticle,
20
+ } from "./compose-launch.js";
21
+
22
+ document.addEventListener("click", (event: MouseEvent) => {
23
+ const origin =
24
+ event.target instanceof globalThis.Element ? event.target : null;
25
+ if (!origin) return;
26
+
27
+ const composeOpen = origin.closest<HTMLElement>("[data-compose-open]");
28
+ if (composeOpen) {
29
+ const collectionId = composeOpen.dataset.composeCollectionId;
30
+ void openNewCompose(collectionId ? { collectionId } : undefined);
31
+ return;
32
+ }
33
+
34
+ const replyTrigger = origin.closest<HTMLElement>("[data-reply-trigger]");
35
+ if (replyTrigger) {
36
+ const article = replyTrigger.closest<HTMLElement>("article[data-post]");
37
+ if (article) void openReplyForArticle(article);
38
+ return;
39
+ }
40
+
41
+ // The badge on an inline draft doubles as its most likely next action.
42
+ // Publish and Delete live in the post menu with every other post action.
43
+ const draftContinue = origin.closest<HTMLElement>("[data-draft-continue]");
44
+ if (draftContinue) {
45
+ const postId =
46
+ draftContinue.dataset.postId ??
47
+ draftContinue.closest<HTMLElement>("article[data-post]")?.dataset.postId;
48
+ if (postId) void openEditForPost(postId);
49
+ }
50
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Copy field behavior.
3
+ *
4
+ * Enhances the read-only address fields rendered by `ui/shared/CopyField.tsx`
5
+ * and by the feed URL rows in General settings. Delegated from the document, so
6
+ * fields that appear later — the settings page renders its rows from Lit after
7
+ * load — are covered without re-running anything.
8
+ *
9
+ * Two behaviors carry the weight here and must both stay:
10
+ *
11
+ * - The button writes the address to the clipboard and reports either way.
12
+ * - Clicking or focusing the input selects the whole address. The Clipboard API
13
+ * is absent on insecure origins and can be denied by permission, so manual
14
+ * selection is what keeps the address obtainable when the button cannot work.
15
+ *
16
+ * The server renders the button `hidden` — without this script it would be an
17
+ * affordance that does nothing — and it is revealed here. The input alone is
18
+ * the no-JavaScript state, and it is a usable one.
19
+ */
20
+
21
+ import {
22
+ COPY_FIELD_BUTTON_ATTR,
23
+ COPY_FIELD_FAILED_ATTR,
24
+ COPY_FIELD_VALUE_ATTR,
25
+ } from "../lib/copy-field.js";
26
+ import { showToast } from "./toast.js";
27
+
28
+ const BUTTON_SELECTOR = `[${COPY_FIELD_BUTTON_ATTR}]`;
29
+ const VALUE_SELECTOR = `[${COPY_FIELD_VALUE_ATTR}]`;
30
+
31
+ /**
32
+ * Find the address a copy button belongs to.
33
+ *
34
+ * @param button - The copy button that was activated
35
+ * @returns The input holding the address, or null when the field is malformed
36
+ */
37
+ function findValueInput(button: HTMLElement): HTMLInputElement | null {
38
+ return (
39
+ button.parentElement?.querySelector<HTMLInputElement>(VALUE_SELECTOR) ??
40
+ button
41
+ .closest("[data-copy-field-root]")
42
+ ?.querySelector<HTMLInputElement>(VALUE_SELECTOR) ??
43
+ null
44
+ );
45
+ }
46
+
47
+ async function copy(button: HTMLElement): Promise<void> {
48
+ const input = findValueInput(button);
49
+ if (!input) return;
50
+
51
+ const failedMessage = button.getAttribute(COPY_FIELD_FAILED_ATTR) ?? "";
52
+
53
+ try {
54
+ if (!globalThis.navigator.clipboard?.writeText) {
55
+ throw new Error("Clipboard unavailable");
56
+ }
57
+
58
+ await globalThis.navigator.clipboard.writeText(input.value);
59
+ showToast(button.getAttribute(COPY_FIELD_BUTTON_ATTR) || "");
60
+ } catch {
61
+ showToast(failedMessage, "error");
62
+ }
63
+ }
64
+
65
+ /** Reveal buttons the server rendered hidden. */
66
+ function revealButtons(root: globalThis.ParentNode = document): void {
67
+ for (const button of Array.from(
68
+ root.querySelectorAll<HTMLElement>(`${BUTTON_SELECTOR}[hidden]`),
69
+ )) {
70
+ button.hidden = false;
71
+ }
72
+ }
73
+
74
+ document.addEventListener("click", (event: globalThis.MouseEvent) => {
75
+ const target = event.target;
76
+ if (!(target instanceof globalThis.Element)) return;
77
+
78
+ const button = target.closest<HTMLElement>(BUTTON_SELECTOR);
79
+ if (button) {
80
+ event.preventDefault();
81
+ void copy(button);
82
+ return;
83
+ }
84
+
85
+ const input = target.closest<HTMLInputElement>(VALUE_SELECTOR);
86
+ if (input) input.select();
87
+ });
88
+
89
+ // Focus does not bubble; its capturing counterpart does.
90
+ document.addEventListener(
91
+ "focus",
92
+ (event: globalThis.FocusEvent) => {
93
+ const target = event.target;
94
+ if (!(target instanceof globalThis.Element)) return;
95
+
96
+ const input = target.closest<HTMLInputElement>(VALUE_SELECTOR);
97
+ if (input) input.select();
98
+ },
99
+ true,
100
+ );
101
+
102
+ // Module scripts are deferred, so by the time this runs the server's fields are
103
+ // already parsed — but the listener is registered unconditionally rather than
104
+ // branching on readyState, so a field that arrives late is revealed too.
105
+ revealButtons();
106
+ document.addEventListener("DOMContentLoaded", () => revealButtons(), {
107
+ once: true,
108
+ });
@@ -1,10 +1,23 @@
1
1
  /**
2
2
  * Custom URL action menus on the settings page.
3
3
  *
4
- * Keeps row menus mutually exclusive and dismisses them on outside click
5
- * and Escape.
4
+ * Keeps row menus mutually exclusive and dismisses them on any click
5
+ * outside the trigger and the menu — including the rest of their own row —
6
+ * and on Escape.
7
+ *
8
+ * The row list rounds its corners with `overflow: hidden`, and no stacking
9
+ * order escapes an ancestor's clip. So an open menu is promoted to the top
10
+ * layer, which takes it out of the clip entirely and leaves it positioned
11
+ * against the viewport — hence the anchoring done here in script. Browsers
12
+ * without the popover API fall back to the absolutely positioned menu, which
13
+ * is clipped but still usable.
6
14
  */
7
15
 
16
+ /** Space between the trigger and the menu, in pixels. */
17
+ const MENU_GAP = 6;
18
+ /** Closest the menu may sit to a viewport edge, in pixels. */
19
+ const VIEWPORT_MARGIN = 8;
20
+
8
21
  /**
9
22
  * Initialize custom URL action menus within a root.
10
23
  *
@@ -30,8 +43,39 @@ export function initCustomUrlMenus(
30
43
 
31
44
  if (!(trigger instanceof HTMLButtonElement) || !menu) return;
32
45
 
46
+ const topLayer =
47
+ typeof menu.showPopover === "function" && menu.hasAttribute("popover");
48
+
49
+ /** Anchor the top-layer menu under the trigger, flipping above it when
50
+ * the viewport bottom is closer than the menu is tall. */
51
+ const anchorToTrigger = () => {
52
+ const anchor = trigger.getBoundingClientRect();
53
+ const { width, height } = menu.getBoundingClientRect();
54
+ const below = anchor.bottom + MENU_GAP;
55
+ const above = anchor.top - MENU_GAP - height;
56
+ const top =
57
+ below + height <= window.innerHeight - VIEWPORT_MARGIN ||
58
+ above < VIEWPORT_MARGIN
59
+ ? below
60
+ : above;
61
+
62
+ menu.style.top = `${top}px`;
63
+ menu.style.left = `${Math.max(
64
+ VIEWPORT_MARGIN,
65
+ Math.min(
66
+ anchor.right - width,
67
+ window.innerWidth - VIEWPORT_MARGIN - width,
68
+ ),
69
+ )}px`;
70
+ };
71
+
33
72
  const close = (focusTrigger = false) => {
34
73
  if (menu.hidden) return;
74
+ if (topLayer) {
75
+ window.removeEventListener("resize", anchorToTrigger);
76
+ window.removeEventListener("scroll", anchorToTrigger, true);
77
+ if (menu.matches(":popover-open")) menu.hidePopover();
78
+ }
35
79
  menu.hidden = true;
36
80
  trigger.setAttribute("aria-expanded", "false");
37
81
  if (focusTrigger) trigger.focus();
@@ -44,6 +88,14 @@ export function initCustomUrlMenus(
44
88
  }),
45
89
  );
46
90
  menu.hidden = false;
91
+ if (topLayer) {
92
+ if (!menu.matches(":popover-open")) menu.showPopover();
93
+ anchorToTrigger();
94
+ window.addEventListener("resize", anchorToTrigger);
95
+ // Capture phase: the menu follows a scroll of any ancestor, not
96
+ // only the document.
97
+ window.addEventListener("scroll", anchorToTrigger, true);
98
+ }
47
99
  trigger.setAttribute("aria-expanded", "true");
48
100
  if (focusFirstItem) {
49
101
  const firstItem =
@@ -105,11 +157,15 @@ export function initCustomUrlMenus(
105
157
  }
106
158
  });
107
159
 
160
+ // The row is the action root, but it is not the menu's dismiss region:
161
+ // the rest of the row is blank space like any other, so only the
162
+ // trigger and the menu itself count as a click inside.
108
163
  document.addEventListener("click", (event) => {
109
164
  if (!(event.target instanceof Node)) return;
110
- if (!menuRoot.contains(event.target)) {
111
- close(false);
165
+ if (trigger.contains(event.target) || menu.contains(event.target)) {
166
+ return;
112
167
  }
168
+ close(false);
113
169
  });
114
170
 
115
171
  document.addEventListener("jant:custom-url-menu", (event) => {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Inline SVG for the few Lucide icons client components render themselves.
3
+ *
4
+ * Server-rendered UI goes through `lib/icons.ts`, which indexes the whole
5
+ * `lucide-static` set (1,700+ icons, ≈1 MB minified). Client code must not
6
+ * import that module — one icon lookup would ship the entire set to the
7
+ * browser — so the handful of icons a component needs at runtime live here as
8
+ * literal markup, copied from `lucide-static` with the project's `icon-fine`
9
+ * stroke class appended.
10
+ */
11
+
12
+ /** Lucide `funnel`, the marker for a smart collection. */
13
+ export const FUNNEL_ICON_SVG =
14
+ '<svg class="lucide lucide-funnel icon-fine" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"/></svg>';
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Image processing plan and worker protocol.
3
+ *
4
+ * The pure decisions shared by the `ImageProcessor` facade on the main thread
5
+ * and the `image-worker` that decodes and encodes off it. Nothing here touches
6
+ * the DOM, so both sides — and the tests — import it without a canvas.
7
+ */
8
+
9
+ /** Cap for the shorter image side — the side that determines text sharpness. */
10
+ export const MAX_SHORT_SIDE = 1920;
11
+
12
+ /**
13
+ * Largest long side we can still redraw on a canvas. A canvas bounded by
14
+ * MAX_SHORT_SIDE × MAX_LONG_SIDE (1920 × 8192 ≈ 15.7M px) stays under the
15
+ * ~16.7M-pixel area limit older mobile Safari enforces. Anything longer
16
+ * can't be re-encoded, so it uploads as-is.
17
+ */
18
+ export const MAX_LONG_SIDE = 8192;
19
+
20
+ /** Longest side of the tiny canvas a blurhash is sampled from. */
21
+ export const BLURHASH_MAX_SIDE = 32;
22
+
23
+ export interface ImageProcessOptions {
24
+ maxShortSide: number;
25
+ maxLongSide: number;
26
+ quality: number;
27
+ mimeType: "image/webp" | "image/jpeg" | "image/png";
28
+ }
29
+
30
+ export const DEFAULT_IMAGE_PROCESS_OPTIONS: ImageProcessOptions = {
31
+ maxShortSide: MAX_SHORT_SIDE,
32
+ maxLongSide: MAX_LONG_SIDE,
33
+ quality: 0.85,
34
+ mimeType: "image/webp",
35
+ };
36
+
37
+ export interface ImageProcessPlan {
38
+ /** When true, upload the original file untouched (too large to re-encode). */
39
+ passthrough: boolean;
40
+ /** Target dimensions — equal to the source dimensions when `passthrough`. */
41
+ width: number;
42
+ height: number;
43
+ }
44
+
45
+ /**
46
+ * Decide how to handle an image given its source dimensions.
47
+ *
48
+ * - Long side over `maxLongSide` → `passthrough` (canvas can't redraw it).
49
+ * - Short side within `maxShortSide` → keep dimensions, just re-encode.
50
+ * - Otherwise → scale down so the short side hits `maxShortSide`.
51
+ *
52
+ * @param sourceWidth - Natural image width in pixels
53
+ * @param sourceHeight - Natural image height in pixels
54
+ * @param options - `maxShortSide` and `maxLongSide` caps
55
+ * @returns The processing plan
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * planImageProcessing(1080, 6000, { maxShortSide: 1920, maxLongSide: 8192 });
60
+ * // { passthrough: false, width: 1080, height: 6000 }
61
+ * ```
62
+ */
63
+ export function planImageProcessing(
64
+ sourceWidth: number,
65
+ sourceHeight: number,
66
+ options: { maxShortSide: number; maxLongSide: number },
67
+ ): ImageProcessPlan {
68
+ const longSide = Math.max(sourceWidth, sourceHeight);
69
+ if (longSide > options.maxLongSide) {
70
+ return { passthrough: true, width: sourceWidth, height: sourceHeight };
71
+ }
72
+
73
+ const shortSide = Math.min(sourceWidth, sourceHeight);
74
+ if (shortSide <= options.maxShortSide) {
75
+ return { passthrough: false, width: sourceWidth, height: sourceHeight };
76
+ }
77
+
78
+ const scale = options.maxShortSide / shortSide;
79
+ return {
80
+ passthrough: false,
81
+ width: Math.round(sourceWidth * scale),
82
+ height: Math.round(sourceHeight * scale),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Settle the encoding format after a probe encode.
88
+ *
89
+ * A browser that cannot encode the requested format does not fail — it
90
+ * silently hands back a PNG (Safari has no WebP encoder). PNG ignores the
91
+ * quality parameter and produces oversized files, so anything other than the
92
+ * format asked for means "encode as JPEG", which every engine can do lossily.
93
+ * Probing a 1×1 canvas once beats encoding every photo twice.
94
+ *
95
+ * @param requested - The MIME type asked of the encoder
96
+ * @param produced - The MIME type of the blob it returned
97
+ * @returns The MIME type to encode real images with
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * resolveEncodedMimeType("image/webp", "image/png"); // "image/jpeg"
102
+ * resolveEncodedMimeType("image/webp", "image/webp"); // "image/webp"
103
+ * ```
104
+ */
105
+ export function resolveEncodedMimeType(
106
+ requested: string,
107
+ produced: string,
108
+ ): string {
109
+ return produced === requested ? requested : "image/jpeg";
110
+ }
111
+
112
+ /**
113
+ * Size of the canvas a blurhash is sampled from: the image scaled so its
114
+ * longest side is at most {@link BLURHASH_MAX_SIDE}, never below 1px.
115
+ *
116
+ * @param width - Source width in pixels
117
+ * @param height - Source height in pixels
118
+ * @returns The sample canvas dimensions
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * blurhashDimensions(4032, 3024); // { width: 32, height: 24 }
123
+ * ```
124
+ */
125
+ export function blurhashDimensions(
126
+ width: number,
127
+ height: number,
128
+ ): { width: number; height: number } {
129
+ const scale = Math.min(
130
+ BLURHASH_MAX_SIDE / width,
131
+ BLURHASH_MAX_SIDE / height,
132
+ 1,
133
+ );
134
+ return {
135
+ width: Math.max(Math.round(width * scale), 1),
136
+ height: Math.max(Math.round(height * scale), 1),
137
+ };
138
+ }
139
+
140
+ // ── Worker protocol ──────────────────────────────────────────────────────
141
+
142
+ export interface ImageWorkerRequest {
143
+ id: number;
144
+ file: File;
145
+ options: ImageProcessOptions;
146
+ }
147
+
148
+ export interface ImageWorkerSuccess {
149
+ id: number;
150
+ ok: true;
151
+ blob: Blob;
152
+ width: number;
153
+ height: number;
154
+ /** False when `blob` is the untouched original (too large to re-encode). */
155
+ processed: boolean;
156
+ blurhash?: string;
157
+ }
158
+
159
+ export interface ImageWorkerFailure {
160
+ id: number;
161
+ ok: false;
162
+ error: string;
163
+ }
164
+
165
+ export type ImageWorkerResponse = ImageWorkerSuccess | ImageWorkerFailure;