@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -1,352 +0,0 @@
1
- import { html, nothing } from "lit";
2
- import { unsafeSVG } from "lit/directives/unsafe-svg.js";
3
- import type { JantPostForm } from "./jant-post-form.js";
4
- import type { PostMediaItem } from "./post-form-types.js";
5
- import { getMediaCategory } from "../../lib/upload.js";
6
-
7
- function renderFileIcon(mimeType: string) {
8
- const doc = `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>`;
9
-
10
- let inner: string;
11
- if (mimeType === "application/pdf") {
12
- inner = `<text x="12" y="16.5" text-anchor="middle" fill="currentColor" stroke="none" font-size="6" font-weight="700" font-family="system-ui, sans-serif">PDF</text>`;
13
- } else if (mimeType === "text/markdown") {
14
- inner = `<text x="12" y="16.5" text-anchor="middle" fill="currentColor" stroke="none" font-size="10" font-weight="700" font-family="system-ui, sans-serif">#</text>`;
15
- } else if (mimeType === "text/csv") {
16
- inner = `<line x1="8" y1="12" x2="16" y2="12"/><line x1="8" y1="15" x2="16" y2="15"/><line x1="8" y1="18" x2="16" y2="18"/><line x1="10.7" y1="12" x2="10.7" y2="18"/><line x1="13.3" y1="12" x2="13.3" y2="18"/>`;
17
- } else if (getMediaCategory(mimeType) === "archive") {
18
- inner = `<line x1="12" y1="10" x2="12" y2="11.5"/><line x1="12" y1="13" x2="12" y2="14.5"/><line x1="12" y1="16" x2="12" y2="17.5"/>`;
19
- } else if (mimeType.startsWith("audio/")) {
20
- return html`<svg
21
- width="24"
22
- height="24"
23
- viewBox="0 0 24 24"
24
- fill="none"
25
- stroke="currentColor"
26
- stroke-width="1.5"
27
- stroke-linecap="round"
28
- stroke-linejoin="round"
29
- >
30
- ${unsafeSVG(
31
- `<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>`,
32
- )}
33
- </svg>`;
34
- } else if (mimeType.startsWith("video/")) {
35
- return html`<svg
36
- width="24"
37
- height="24"
38
- viewBox="0 0 24 24"
39
- fill="none"
40
- stroke="currentColor"
41
- stroke-width="1.5"
42
- stroke-linecap="round"
43
- stroke-linejoin="round"
44
- >
45
- ${unsafeSVG(
46
- `<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>`,
47
- )}
48
- </svg>`;
49
- } else {
50
- inner = `<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>`;
51
- }
52
-
53
- return html`<svg
54
- width="24"
55
- height="24"
56
- viewBox="0 0 24 24"
57
- fill="none"
58
- stroke="currentColor"
59
- stroke-width="1.5"
60
- stroke-linecap="round"
61
- stroke-linejoin="round"
62
- >
63
- ${unsafeSVG(doc + inner)}
64
- </svg>`;
65
- }
66
-
67
- function renderMediaThumb(item: PostMediaItem) {
68
- const category = getMediaCategory(item.mimeType);
69
-
70
- if (category === "image") {
71
- return html`<img
72
- src=${item.thumbUrl}
73
- alt=${item.alt}
74
- class="w-full h-full object-cover rounded-lg border"
75
- loading="lazy"
76
- />`;
77
- }
78
-
79
- return html`<div
80
- class="w-full h-full rounded-lg border bg-muted flex flex-col items-center justify-center gap-1 p-1 text-muted-foreground"
81
- >
82
- ${renderFileIcon(item.mimeType)}
83
- <span class="text-[10px] leading-tight text-center truncate w-full px-1"
84
- >${item.originalName}</span
85
- >
86
- </div>`;
87
- }
88
-
89
- function renderMediaList(component: JantPostForm) {
90
- const { media, labels, _mediaIds } = component;
91
- if (_mediaIds.length === 0) {
92
- return html`<p class="text-sm text-muted-foreground">
93
- ${labels.mediaEmptyLabel}
94
- </p>`;
95
- }
96
-
97
- const mediaMap = new Map(media.map((item) => [item.id, item]));
98
-
99
- return html`<div class="grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2">
100
- ${_mediaIds.map((id) => {
101
- const item = mediaMap.get(id);
102
- if (!item) {
103
- return html`<div
104
- class="relative group aspect-square rounded-lg border bg-muted flex items-center justify-center text-xs text-muted-foreground"
105
- >
106
- ${id}
107
- <button
108
- type="button"
109
- class="absolute top-1 right-1 w-5 h-5 flex items-center justify-center bg-black/60 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
110
- @click=${() => component.removeMedia(id)}
111
- aria-label=${labels.mediaRemoveButton}
112
- >
113
- &times;
114
- </button>
115
- </div>`;
116
- }
117
-
118
- return html`<div class="relative group aspect-square" data-media-id=${id}>
119
- ${renderMediaThumb(item)}
120
- <button
121
- type="button"
122
- class="absolute top-1 right-1 w-5 h-5 flex items-center justify-center bg-black/60 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
123
- @click=${() => component.removeMedia(id)}
124
- aria-label=${labels.mediaRemoveButton}
125
- >
126
- &times;
127
- </button>
128
- </div>`;
129
- })}
130
- </div>`;
131
- }
132
-
133
- function renderCollections(component: JantPostForm) {
134
- if (!component.collections.length) return nothing;
135
-
136
- return html`<div class="field">
137
- <label class="label">${component.labels.collectionsLabel}</label>
138
- <div class="flex flex-col gap-1">
139
- ${component.collections.map((col) => {
140
- return html`<label class="flex items-center gap-2 text-sm">
141
- <input
142
- type="checkbox"
143
- class="checkbox"
144
- .checked=${component._collectionIds.includes(col.id)}
145
- @change=${() => component.toggleCollection(col.id)}
146
- />
147
- <span>${col.title}</span>
148
- </label>`;
149
- })}
150
- </div>
151
- </div>`;
152
- }
153
-
154
- export function renderPostForm(component: JantPostForm) {
155
- return html`<form
156
- class="flex flex-col gap-4 max-w-2xl"
157
- @submit=${(e: Event) => component.handleSubmit(e)}
158
- >
159
- <div class="field">
160
- <label class="label">${component.labels.formatLabel}</label>
161
- <select
162
- class="select"
163
- .value=${component._format}
164
- @change=${(e: Event) => {
165
- const target = e.target as HTMLSelectElement;
166
- component._format =
167
- (target.value as typeof component._format) ?? "note";
168
- }}
169
- >
170
- <option value="note">${component.labels.noteOption}</option>
171
- <option value="link">${component.labels.linkOption}</option>
172
- <option value="quote">${component.labels.quoteOption}</option>
173
- </select>
174
- </div>
175
-
176
- <div class="field">
177
- <label class="label">${component.labels.titleLabel}</label>
178
- <input
179
- type="text"
180
- class="input"
181
- placeholder=${component.labels.titlePlaceholder}
182
- .value=${component._title}
183
- @input=${(e: Event) => component.handleTitleInput(e)}
184
- />
185
- </div>
186
-
187
- <div class="field">
188
- <label class="label">${component.labels.slugLabel}</label>
189
- <input
190
- type="text"
191
- class="input"
192
- placeholder=${component.labels.slugPlaceholder}
193
- .value=${component._slug}
194
- @input=${(e: Event) => component.handleSlugInput(e)}
195
- />
196
- ${component._slug
197
- ? html`<p class="text-xs text-muted-foreground mt-1">
198
- ${component.siteUrl}/${component._slug}
199
- </p>`
200
- : html`<p class="text-xs text-muted-foreground mt-1">
201
- ${component.labels.slugHelp}
202
- </p>`}
203
- </div>
204
-
205
- <div class="field">
206
- <label class="label">${component.labels.bodyLabel}</label>
207
- <div
208
- class="post-form-tiptap-body compose-tiptap-body border rounded-lg p-3 min-h-32"
209
- ></div>
210
- </div>
211
-
212
- <div class="field">
213
- <label class="label">${component.labels.urlLabel}</label>
214
- <input
215
- type="url"
216
- class="input"
217
- placeholder=${component.labels.urlPlaceholder}
218
- .value=${component._url}
219
- @input=${(e: Event) => component.handleInput("_url", e)}
220
- />
221
- </div>
222
-
223
- ${component._format === "quote"
224
- ? html`<div class="field">
225
- <label class="label">${component.labels.quoteTextLabel}</label>
226
- <textarea
227
- class="textarea"
228
- rows="3"
229
- placeholder=${component.labels.quoteTextPlaceholder}
230
- .value=${component._quoteText}
231
- @input=${(e: Event) => component.handleInput("_quoteText", e)}
232
- ></textarea>
233
- </div>`
234
- : nothing}
235
-
236
- <div class="field">
237
- <label class="label">${component.labels.mediaLabel}</label>
238
- ${renderMediaList(component)}
239
- <button
240
- type="button"
241
- class="btn-outline text-sm"
242
- @click=${() => component.openMediaPicker()}
243
- >
244
- ${component.labels.mediaAddButton}
245
- </button>
246
- </div>
247
-
248
- <div class="field">
249
- <label class="label">${component.labels.statusLabel}</label>
250
- <select
251
- class="select"
252
- .value=${component._status}
253
- @change=${(e: Event) => {
254
- const target = e.target as HTMLSelectElement;
255
- component._status =
256
- (target.value as typeof component._status) ?? "published";
257
- }}
258
- >
259
- <option value="published">${component.labels.statusPublished}</option>
260
- <option value="draft">${component.labels.statusDraft}</option>
261
- </select>
262
- </div>
263
-
264
- <div class="field">
265
- <label class="label">${component.labels.visibilityLabel}</label>
266
- <select
267
- class="select"
268
- .value=${component._visibility}
269
- @change=${(e: Event) => {
270
- const target = e.target as HTMLSelectElement;
271
- component._visibility =
272
- (target.value as typeof component._visibility) ?? "public";
273
- }}
274
- >
275
- <option value="public">${component.labels.visibilityPublic}</option>
276
- <option value="latest_hidden">
277
- ${component.labels.visibilityHiddenFromLatest}
278
- </option>
279
- </select>
280
- </div>
281
-
282
- <label class="flex items-center gap-2 text-sm">
283
- <input
284
- type="checkbox"
285
- class="checkbox"
286
- .checked=${component._pinned}
287
- @change=${(e: Event) => {
288
- const target = e.target as HTMLInputElement;
289
- component._pinned = target.checked;
290
- }}
291
- />
292
- ${component.labels.pinnedLabel}
293
- </label>
294
-
295
- ${renderCollections(component)}
296
-
297
- <div class="flex gap-2">
298
- <button type="submit" class="btn" ?disabled=${component._loading}>
299
- ${component._loading
300
- ? html`<svg
301
- class="animate-spin size-4"
302
- xmlns="http://www.w3.org/2000/svg"
303
- viewBox="0 0 24 24"
304
- fill="none"
305
- stroke="currentColor"
306
- stroke-width="2"
307
- stroke-linecap="round"
308
- stroke-linejoin="round"
309
- role="status"
310
- >
311
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
312
- </svg>`
313
- : nothing}
314
- ${component.labels.submitLabel}
315
- </button>
316
- <a href=${component.cancelHref} class="btn-outline"
317
- >${component.labels.cancelLabel}</a
318
- >
319
- </div>
320
- </form>
321
-
322
- <dialog
323
- id="post-media-picker"
324
- class="p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50"
325
- @click=${(event: Event) => {
326
- if (event.target === event.currentTarget) {
327
- component.closeMediaPicker();
328
- }
329
- }}
330
- >
331
- <div class="flex items-center justify-between mb-4">
332
- <h2 class="text-lg font-semibold">
333
- ${component.labels.mediaDialogTitle}
334
- </h2>
335
- <button
336
- type="button"
337
- class="btn-outline text-sm"
338
- @click=${() => component.closeMediaPicker()}
339
- >
340
- ${component.labels.mediaDialogDone}
341
- </button>
342
- </div>
343
- <div
344
- id="post-media-grid"
345
- class="grid grid-cols-4 gap-2 max-h-96 overflow-y-auto"
346
- >
347
- <p class="text-muted-foreground text-sm col-span-4">
348
- ${component.labels.mediaDialogLoading}
349
- </p>
350
- </div>
351
- </dialog>`;
352
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * Shared type definitions for the post form Lit component.
3
- */
4
-
5
- export type PostFormat = "note" | "link" | "quote";
6
- export type PostStatus = "published" | "draft";
7
- export type PostVisibility = "public" | "latest_hidden";
8
-
9
- export interface PostFormLabels {
10
- formatLabel: string;
11
- noteOption: string;
12
- linkOption: string;
13
- quoteOption: string;
14
- titleLabel: string;
15
- titlePlaceholder: string;
16
- slugLabel: string;
17
- slugPlaceholder: string;
18
- slugHelp: string;
19
- bodyLabel: string;
20
- bodyPlaceholder: string;
21
- urlLabel: string;
22
- urlPlaceholder: string;
23
- quoteTextLabel: string;
24
- quoteTextPlaceholder: string;
25
- mediaLabel: string;
26
- mediaAddButton: string;
27
- mediaRemoveButton: string;
28
- mediaEmptyLabel: string;
29
- statusLabel: string;
30
- statusPublished: string;
31
- statusDraft: string;
32
- visibilityLabel: string;
33
- visibilityPublic: string;
34
- visibilityHiddenFromLatest: string;
35
- pinnedLabel: string;
36
- collectionsLabel: string;
37
- submitLabel: string;
38
- cancelLabel: string;
39
- mediaDialogTitle: string;
40
- mediaDialogDone: string;
41
- mediaDialogLoading: string;
42
- submitSuccessMessage: string;
43
- submitErrorMessage: string;
44
- draftFallbackMessage: string;
45
- }
46
-
47
- export interface PostFormInitial {
48
- format: PostFormat;
49
- title: string;
50
- slug: string;
51
- body: string;
52
- url: string;
53
- quoteText: string;
54
- status: PostStatus;
55
- visibility: PostVisibility;
56
- pinned: boolean;
57
- rating: number;
58
- collectionIds: number[];
59
- mediaIds: string[];
60
- }
61
-
62
- export interface ThreadCollectionOption {
63
- id: number;
64
- title: string;
65
- }
66
-
67
- export interface PostMediaItem {
68
- id: string;
69
- thumbUrl: string;
70
- alt: string;
71
- mimeType: string;
72
- originalName: string;
73
- }
74
-
75
- export interface PostSubmitDetail {
76
- endpoint: string;
77
- isEdit: boolean;
78
- data: {
79
- format: PostFormat;
80
- title?: string;
81
- slug?: string;
82
- body?: string;
83
- status: PostStatus;
84
- visibility: PostVisibility;
85
- pinned: boolean;
86
- url?: string;
87
- quoteText?: string;
88
- rating?: number;
89
- collectionIds: number[];
90
- mediaIds: string[];
91
- };
92
- messages: {
93
- success: string;
94
- error: string;
95
- };
96
- }
@@ -1,247 +0,0 @@
1
- /**
2
- * Client-side Media Metadata Extraction
3
- *
4
- * Extracts dimensions and blurhash from image/video files using
5
- * Canvas API and the blurhash library.
6
- */
7
-
8
- import { encode } from "blurhash";
9
-
10
- export interface MediaMetadata {
11
- width?: number;
12
- height?: number;
13
- blurhash?: string;
14
- waveform?: string;
15
- poster?: Blob;
16
- }
17
-
18
- /**
19
- * Extract metadata (width, height, blurhash) from an image file.
20
- * Uses a small canvas (max 32px) for blurhash computation.
21
- */
22
- export async function extractImageMetadata(
23
- file: File,
24
- ): Promise<{ width: number; height: number; blurhash: string }> {
25
- const img = await loadImage(file);
26
- const { width, height } = img;
27
-
28
- // Scale down for blurhash — max 32px on the longest side
29
- const scale = Math.min(32 / width, 32 / height, 1);
30
- const bw = Math.max(Math.round(width * scale), 1);
31
- const bh = Math.max(Math.round(height * scale), 1);
32
-
33
- const canvas = document.createElement("canvas");
34
- canvas.width = bw;
35
- canvas.height = bh;
36
- const ctx = canvas.getContext("2d");
37
- if (!ctx) throw new Error("Failed to get canvas context");
38
- ctx.drawImage(img, 0, 0, bw, bh);
39
-
40
- const imageData = ctx.getImageData(0, 0, bw, bh);
41
- const blurhash = encode(imageData.data, bw, bh, 4, 3);
42
-
43
- return { width, height, blurhash };
44
- }
45
-
46
- /**
47
- * Extract metadata from a video file.
48
- * Loads the video to get dimensions, then seeks to `min(duration * 0.1, 1)` and
49
- * captures a frame for blurhash (32px canvas) and a poster image (640px WebP).
50
- * Uses an 8s timeout — returns only dimensions if capture times out.
51
- */
52
- export async function extractVideoMetadata(file: File): Promise<{
53
- width: number;
54
- height: number;
55
- blurhash?: string;
56
- poster?: Blob;
57
- }> {
58
- const url = URL.createObjectURL(file);
59
- try {
60
- const video = document.createElement("video");
61
- video.muted = true;
62
- video.preload = "auto";
63
-
64
- // Wait for metadata to load (includes duration)
65
- const { width, height, duration } = await new Promise<{
66
- width: number;
67
- height: number;
68
- duration: number;
69
- }>((resolve, reject) => {
70
- video.onloadedmetadata = () =>
71
- resolve({
72
- width: video.videoWidth,
73
- height: video.videoHeight,
74
- duration: video.duration,
75
- });
76
- video.onerror = () => reject(new Error("Failed to load video metadata"));
77
- video.src = url;
78
- });
79
-
80
- // Try to capture frame for blurhash + poster (8s timeout)
81
- let blurhash: string | undefined;
82
- let poster: Blob | undefined;
83
- try {
84
- const seekTime = Math.min(duration * 0.1, 1);
85
- const result = await Promise.race([
86
- captureVideoFrameAndPoster(video, width, height, seekTime),
87
- timeout(8000),
88
- ]);
89
- blurhash = result.blurhash;
90
- poster = result.poster;
91
- } catch {
92
- // Timeout or capture failed — return dimensions only
93
- }
94
-
95
- return { width, height, blurhash, poster };
96
- } finally {
97
- URL.revokeObjectURL(url);
98
- }
99
- }
100
-
101
- /**
102
- * Extract waveform peak amplitudes from an audio file.
103
- * Decodes via Web Audio API and returns a JSON string of ~100 normalized peak values (0–1).
104
- *
105
- * @param file - Audio file to extract peaks from
106
- * @returns JSON string of peak values, e.g. "[0.2,0.8,0.5,...]"
107
- */
108
- export async function extractAudioWaveform(file: File): Promise<string> {
109
- const buffer = await file.arrayBuffer();
110
- const audioCtx = new AudioContext();
111
-
112
- try {
113
- const decoded = await audioCtx.decodeAudioData(buffer);
114
- const raw = decoded.getChannelData(0);
115
- const count = 100;
116
- const step = Math.max(1, Math.floor(raw.length / count));
117
- const peaks: number[] = new Array(count);
118
-
119
- for (let i = 0; i < count; i++) {
120
- let max = 0;
121
- const start = i * step;
122
- const end = Math.min(start + step, raw.length);
123
- for (let j = start; j < end; j++) {
124
- const v = Math.abs(raw[j]);
125
- if (v > max) max = v;
126
- }
127
- peaks[i] = max;
128
- }
129
-
130
- let maxPeak = 0;
131
- for (const p of peaks) if (p > maxPeak) maxPeak = p;
132
- if (maxPeak > 0) {
133
- for (let i = 0; i < count; i++)
134
- peaks[i] = Math.round((peaks[i] / maxPeak) * 100) / 100;
135
- }
136
-
137
- return JSON.stringify(peaks);
138
- } finally {
139
- await audioCtx.close();
140
- }
141
- }
142
-
143
- /**
144
- * Extract metadata from any media file based on MIME type.
145
- */
146
- export async function extractMediaMetadata(file: File): Promise<MediaMetadata> {
147
- try {
148
- if (file.type.startsWith("image/")) {
149
- return await extractImageMetadata(file);
150
- }
151
- if (file.type.startsWith("video/")) {
152
- const result = await extractVideoMetadata(file);
153
- return {
154
- width: result.width,
155
- height: result.height,
156
- blurhash: result.blurhash,
157
- poster: result.poster,
158
- };
159
- }
160
- if (file.type.startsWith("audio/")) {
161
- const waveform = await extractAudioWaveform(file);
162
- return { waveform };
163
- }
164
- } catch {
165
- // Extraction failed — return empty metadata
166
- }
167
- return {};
168
- }
169
-
170
- // --- Helpers ---
171
-
172
- function loadImage(file: File): Promise<HTMLImageElement> {
173
- return new Promise((resolve, reject) => {
174
- const img = new Image();
175
- const url = URL.createObjectURL(file);
176
- img.onload = () => {
177
- URL.revokeObjectURL(url);
178
- resolve(img);
179
- };
180
- img.onerror = () => {
181
- URL.revokeObjectURL(url);
182
- reject(new Error("Failed to load image"));
183
- };
184
- img.src = url;
185
- });
186
- }
187
-
188
- function captureVideoFrameAndPoster(
189
- video: HTMLVideoElement,
190
- width: number,
191
- height: number,
192
- seekTime: number,
193
- ): Promise<{ blurhash: string; poster?: Blob }> {
194
- return new Promise((resolve, reject) => {
195
- video.currentTime = seekTime;
196
- video.onseeked = () => {
197
- try {
198
- // Blurhash: small 32px canvas
199
- const scale = Math.min(32 / width, 32 / height, 1);
200
- const bw = Math.max(Math.round(width * scale), 1);
201
- const bh = Math.max(Math.round(height * scale), 1);
202
-
203
- const bhCanvas = document.createElement("canvas");
204
- bhCanvas.width = bw;
205
- bhCanvas.height = bh;
206
- const bhCtx = bhCanvas.getContext("2d");
207
- if (!bhCtx) throw new Error("Failed to get canvas context");
208
- bhCtx.drawImage(video, 0, 0, bw, bh);
209
-
210
- const imageData = bhCtx.getImageData(0, 0, bw, bh);
211
- const blurhash = encode(imageData.data, bw, bh, 4, 3);
212
-
213
- // Poster: 640px wide WebP
214
- const posterScale = Math.min(640 / width, 1);
215
- const pw = Math.round(width * posterScale);
216
- const ph = Math.round(height * posterScale);
217
-
218
- const posterCanvas = document.createElement("canvas");
219
- posterCanvas.width = pw;
220
- posterCanvas.height = ph;
221
- const pCtx = posterCanvas.getContext("2d");
222
- if (!pCtx) {
223
- resolve({ blurhash });
224
- return;
225
- }
226
- pCtx.drawImage(video, 0, 0, pw, ph);
227
-
228
- posterCanvas.toBlob(
229
- (blob) => {
230
- resolve({ blurhash, poster: blob ?? undefined });
231
- },
232
- "image/webp",
233
- 0.8,
234
- );
235
- } catch (err) {
236
- reject(err);
237
- }
238
- };
239
- video.onerror = () => reject(new Error("Video seek failed"));
240
- });
241
- }
242
-
243
- function timeout(ms: number): Promise<never> {
244
- return new Promise((_, reject) =>
245
- setTimeout(() => reject(new Error("Timeout")), ms),
246
- );
247
- }