@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -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
- }