@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Client-side Video Processor
3
+ *
4
+ * Processes videos before upload using mediabunny:
5
+ * - Transcodes to H.264/AAC MP4 (universal playback)
6
+ * - Resizes to max 1920×1080
7
+ * - Extracts poster frame + blurhash during processing
8
+ *
9
+ * Requires WebCodecs API support — check `isSupported()` before use.
10
+ */
11
+
12
+ import {
13
+ Input,
14
+ Output,
15
+ Mp4OutputFormat,
16
+ BufferTarget,
17
+ BlobSource,
18
+ CanvasSink,
19
+ Conversion,
20
+ QUALITY_HIGH,
21
+ ALL_FORMATS,
22
+ } from "mediabunny";
23
+ import { encode } from "blurhash";
24
+
25
+ const MAX_WIDTH = 1920;
26
+ const MAX_HEIGHT = 1080;
27
+ const POSTER_WIDTH = 640;
28
+ const BLURHASH_SIZE = 32;
29
+
30
+ export interface VideoProcessResult {
31
+ file: File;
32
+ width: number;
33
+ height: number;
34
+ poster?: Blob;
35
+ blurhash?: string;
36
+ }
37
+
38
+ /**
39
+ * Check if the browser supports WebCodecs-based video processing.
40
+ *
41
+ * @returns `true` if `VideoEncoder` is available in the current environment
42
+ */
43
+ function isSupported(): boolean {
44
+ return typeof VideoEncoder !== "undefined";
45
+ }
46
+
47
+ /**
48
+ * Extract a poster frame, blurhash, and source dimensions from a video file.
49
+ * Seeks to `min(duration × 0.1, 3s)` and captures the frame.
50
+ * Also returns the original video dimensions so the caller can compute
51
+ * the correct output size without opening a second Input instance.
52
+ *
53
+ * @param file - Source video file
54
+ * @returns Poster blob (640px-wide WebP), blurhash string, and source dimensions
55
+ */
56
+ async function extractPoster(file: File): Promise<{
57
+ poster?: Blob;
58
+ blurhash?: string;
59
+ sourceWidth?: number;
60
+ sourceHeight?: number;
61
+ }> {
62
+ const input = new Input({
63
+ source: new BlobSource(file),
64
+ formats: ALL_FORMATS,
65
+ });
66
+ try {
67
+ const videoTrack = await input.getPrimaryVideoTrack();
68
+ if (!videoTrack) return {};
69
+
70
+ const sourceWidth = videoTrack.displayWidth;
71
+ const sourceHeight = videoTrack.displayHeight;
72
+
73
+ const duration = await input.computeDuration();
74
+ const seekTime = Math.min(duration * 0.1, 3);
75
+
76
+ const sink = new CanvasSink(videoTrack);
77
+ const wrapped = await sink.getCanvas(seekTime);
78
+ if (!wrapped) return { sourceWidth, sourceHeight };
79
+
80
+ const canvas = wrapped.canvas as HTMLCanvasElement;
81
+
82
+ // Poster: 640px wide WebP
83
+ const srcW = canvas.width;
84
+ const srcH = canvas.height;
85
+ const posterScale = Math.min(POSTER_WIDTH / srcW, 1);
86
+ const pw = Math.round(srcW * posterScale);
87
+ const ph = Math.round(srcH * posterScale);
88
+
89
+ const posterCanvas = document.createElement("canvas");
90
+ posterCanvas.width = pw;
91
+ posterCanvas.height = ph;
92
+ const pCtx = posterCanvas.getContext("2d");
93
+ if (!pCtx) return { sourceWidth, sourceHeight };
94
+ pCtx.drawImage(canvas, 0, 0, pw, ph);
95
+
96
+ const poster = await new Promise<Blob | undefined>((resolve) => {
97
+ posterCanvas.toBlob(
98
+ (blob) => resolve(blob ?? undefined),
99
+ "image/webp",
100
+ 0.8,
101
+ );
102
+ });
103
+
104
+ // Blurhash: 32px canvas, 4×3 components
105
+ const bhScale = Math.min(BLURHASH_SIZE / srcW, BLURHASH_SIZE / srcH, 1);
106
+ const bw = Math.max(Math.round(srcW * bhScale), 1);
107
+ const bh = Math.max(Math.round(srcH * bhScale), 1);
108
+
109
+ const bhCanvas = document.createElement("canvas");
110
+ bhCanvas.width = bw;
111
+ bhCanvas.height = bh;
112
+ const bhCtx = bhCanvas.getContext("2d");
113
+ if (!bhCtx) return { poster, sourceWidth, sourceHeight };
114
+ bhCtx.drawImage(canvas, 0, 0, bw, bh);
115
+
116
+ const imageData = bhCtx.getImageData(0, 0, bw, bh);
117
+ const blurhash = encode(imageData.data, bw, bh, 4, 3);
118
+
119
+ return { poster, blurhash, sourceWidth, sourceHeight };
120
+ } catch {
121
+ return {};
122
+ } finally {
123
+ input.dispose();
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Process a video file: transcode to H.264/AAC MP4, resize to fit within
129
+ * 1920×1080, and extract poster frame + blurhash.
130
+ *
131
+ * @param file - Source video file
132
+ * @param onProgress - Optional callback receiving progress from 0 to 1
133
+ * @returns Processed MP4 file with dimensions, poster, and blurhash
134
+ */
135
+ async function processToFile(
136
+ file: File,
137
+ onProgress?: (progress: number) => void,
138
+ ): Promise<VideoProcessResult> {
139
+ // Extract poster + blurhash + source dimensions (separate Input instance,
140
+ // so the transcoding Input below starts with clean demuxer state).
141
+ const { poster, blurhash, sourceWidth, sourceHeight } =
142
+ await extractPoster(file);
143
+
144
+ // Compute output size preserving the original aspect ratio
145
+ let width = MAX_WIDTH;
146
+ let height = MAX_HEIGHT;
147
+ if (sourceWidth && sourceHeight) {
148
+ const scale = Math.min(
149
+ MAX_WIDTH / sourceWidth,
150
+ MAX_HEIGHT / sourceHeight,
151
+ 1,
152
+ );
153
+ width = Math.round(sourceWidth * scale);
154
+ height = Math.round(sourceHeight * scale);
155
+ }
156
+ // H.264 requires even dimensions
157
+ width += width % 2;
158
+ height += height % 2;
159
+
160
+ // Transcode to MP4 H.264/AAC (fresh Input — not shared with extractPoster)
161
+ const input = new Input({
162
+ source: new BlobSource(file),
163
+ formats: ALL_FORMATS,
164
+ });
165
+ const target = new BufferTarget();
166
+ const output = new Output({
167
+ format: new Mp4OutputFormat({ fastStart: "in-memory" }),
168
+ target,
169
+ });
170
+
171
+ try {
172
+ const conversion = await Conversion.init({
173
+ input,
174
+ output,
175
+ video: {
176
+ codec: "avc",
177
+ width,
178
+ height,
179
+ fit: "contain",
180
+ bitrate: QUALITY_HIGH,
181
+ },
182
+ audio: {
183
+ codec: "aac",
184
+ },
185
+ });
186
+
187
+ if (onProgress) {
188
+ conversion.onProgress = onProgress;
189
+ }
190
+
191
+ await conversion.execute();
192
+
193
+ const buffer = target.buffer;
194
+ if (!buffer) throw new Error("Video processing produced no output");
195
+
196
+ const originalName = file.name.replace(/\.[^.]+$/, "");
197
+ const mp4File = new File([buffer], `${originalName}.mp4`, {
198
+ type: "video/mp4",
199
+ });
200
+
201
+ return { file: mp4File, width, height, poster, blurhash };
202
+ } finally {
203
+ input.dispose();
204
+ }
205
+ }
206
+
207
+ export const VideoProcessor = { isSupported, processToFile };
package/src/client.ts CHANGED
@@ -9,21 +9,31 @@
9
9
 
10
10
  import "./vendor/datastar.js";
11
11
  import "basecoat-css/all";
12
- import "./lib/image-processor.js";
13
- import "./lib/media-upload.js";
14
- import "./lib/avatar-upload.js";
15
- import "./lib/collections-reorder.js";
12
+ import "./client/image-processor.js";
13
+ import "./client/avatar-upload.js";
16
14
 
17
- // Lit Web Components
18
- import "./ui/components/jant-compose-dialog.js";
19
- import "./ui/components/jant-compose-editor.js";
20
- import "./lib/compose-bridge.js";
21
- import "./ui/components/jant-settings-general.js";
22
- import "./ui/components/jant-settings-avatar.js";
23
- import "./lib/settings-bridge.js";
24
- import "./ui/components/jant-collection-form.js";
25
- import "./lib/collection-form-bridge.js";
26
- import "./ui/components/jant-post-form.js";
27
- import "./lib/post-form-bridge.js";
28
- import "./ui/components/jant-nav-manager.js";
29
- import "./lib/nav-manager-bridge.js";
15
+ // Lit Web Components (and their bridge modules)
16
+ import "./client/components/jant-compose-dialog.js";
17
+ import "./client/components/jant-compose-editor.js";
18
+ import "./client/components/jant-compose-fullscreen.js";
19
+
20
+ // Mount fullscreen overlay at body level to escape the dialog's containing block
21
+ // (dialog animation creates a containing block that traps position:fixed descendants)
22
+ document.body.appendChild(document.createElement("jant-compose-fullscreen"));
23
+ import "./client/compose-bridge.js";
24
+ import "./client/components/jant-settings-general.js";
25
+ import "./client/components/jant-settings-avatar.js";
26
+ import "./client/settings-bridge.js";
27
+ import "./client/components/jant-collection-form.js";
28
+ import "./client/components/jant-collection-sidebar.js";
29
+ import "./client/collection-form-bridge.js";
30
+ import "./client/components/jant-post-form.js";
31
+ import "./client/post-form-bridge.js";
32
+ import "./client/components/jant-nav-manager.js";
33
+ import "./client/nav-manager-bridge.js";
34
+ import "./client/audio-player.js";
35
+ import "./client/components/jant-media-lightbox.js";
36
+ import "./client/components/jant-text-preview.js";
37
+ import "./client/components/jant-post-menu.js";
38
+ import "./client/thread-context.js";
39
+ import "./client/archive-nav.js";
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Migration Integrity Tests
3
+ *
4
+ * Ensures every migration SQL file is tracked in the Drizzle journal.
5
+ * Hand-written migrations bypass drizzle-kit and won't have journal entries,
6
+ * which breaks `drizzle-kit generate` for future schema changes.
7
+ *
8
+ * Fix: always run `mise run db-generate` instead of writing SQL by hand.
9
+ */
10
+
11
+ import { describe, it, expect } from "vitest";
12
+ import { readdirSync, readFileSync } from "fs";
13
+ import { resolve } from "path";
14
+
15
+ const MIGRATIONS_DIR = resolve(import.meta.dirname, "../migrations");
16
+ const JOURNAL_PATH = resolve(MIGRATIONS_DIR, "meta/_journal.json");
17
+
18
+ interface JournalEntry {
19
+ idx: number;
20
+ version: string;
21
+ when: number;
22
+ tag: string;
23
+ breakpoints: boolean;
24
+ }
25
+
26
+ interface Journal {
27
+ version: string;
28
+ dialect: string;
29
+ entries: JournalEntry[];
30
+ }
31
+
32
+ function readJournal(): Journal {
33
+ return JSON.parse(readFileSync(JOURNAL_PATH, "utf-8"));
34
+ }
35
+
36
+ function listMigrationFiles(): string[] {
37
+ return readdirSync(MIGRATIONS_DIR)
38
+ .filter((f) => f.endsWith(".sql"))
39
+ .sort();
40
+ }
41
+
42
+ describe("migration integrity", () => {
43
+ it("every SQL file has a corresponding journal entry", () => {
44
+ const journal = readJournal();
45
+ const tags = new Set(journal.entries.map((e) => e.tag));
46
+ const sqlFiles = listMigrationFiles();
47
+
48
+ const untracked = sqlFiles
49
+ .map((f) => f.replace(".sql", ""))
50
+ .filter((tag) => !tags.has(tag));
51
+
52
+ expect(
53
+ untracked,
54
+ [
55
+ "These migration files are not tracked in meta/_journal.json.",
56
+ "This usually means they were hand-written instead of generated with `mise run db-generate`.",
57
+ "Fix: update src/db/schema.ts first, then run `mise run db-generate`.",
58
+ `Untracked files: ${untracked.map((t) => `${t}.sql`).join(", ")}`,
59
+ ].join("\n"),
60
+ ).toEqual([]);
61
+ });
62
+
63
+ it("every journal entry has a corresponding SQL file", () => {
64
+ const journal = readJournal();
65
+ const sqlFiles = new Set(
66
+ listMigrationFiles().map((f) => f.replace(".sql", "")),
67
+ );
68
+
69
+ const missing = journal.entries
70
+ .map((e) => e.tag)
71
+ .filter((tag) => !sqlFiles.has(tag));
72
+
73
+ expect(
74
+ missing,
75
+ [
76
+ "These journal entries have no matching SQL file.",
77
+ `Missing files: ${missing.map((t) => `${t}.sql`).join(", ")}`,
78
+ ].join("\n"),
79
+ ).toEqual([]);
80
+ });
81
+
82
+ it("journal entries have sequential idx values", () => {
83
+ const journal = readJournal();
84
+ for (let i = 0; i < journal.entries.length; i++) {
85
+ const entry = journal.entries[i];
86
+ if (entry) expect(entry.idx).toBe(i);
87
+ }
88
+ });
89
+
90
+ it("latest migration has a snapshot file", () => {
91
+ const journal = readJournal();
92
+ const lastEntry = journal.entries[journal.entries.length - 1];
93
+ if (!lastEntry) return;
94
+
95
+ const prefix = lastEntry.tag.split("_")[0];
96
+ const snapshotPath = resolve(
97
+ MIGRATIONS_DIR,
98
+ `meta/${prefix}_snapshot.json`,
99
+ );
100
+
101
+ let exists = false;
102
+ try {
103
+ readFileSync(snapshotPath);
104
+ exists = true;
105
+ } catch {
106
+ // file doesn't exist
107
+ }
108
+
109
+ expect(
110
+ exists,
111
+ [
112
+ `Missing snapshot for latest migration: meta/${prefix}_snapshot.json`,
113
+ "This means the migration was not generated by drizzle-kit.",
114
+ "Fix: run `mise run db-generate` to regenerate it properly.",
115
+ ].join("\n"),
116
+ ).toBe(true);
117
+ });
118
+ });
package/src/db/index.ts CHANGED
@@ -12,3 +12,55 @@ export function createDatabase(d1: D1Database) {
12
12
  }
13
13
 
14
14
  export { schema };
15
+
16
+ /**
17
+ * D1 enforces a lower SQL variable limit than standard SQLite (~999).
18
+ * Keep batch size well under the limit to leave room for other
19
+ * query parameters besides the IN-list.
20
+ */
21
+ const BATCH_SIZE = 50;
22
+
23
+ /**
24
+ * Run a query function in batches to avoid SQLite's variable limit.
25
+ * Splits `items` into chunks, calls `fn` for each chunk, and merges
26
+ * the resulting Maps.
27
+ *
28
+ * @param items - Array of IDs to batch
29
+ * @param fn - Async function that takes a chunk and returns a Map
30
+ * @returns Merged Map from all batches
31
+ */
32
+ export async function batchQuery<K, V>(
33
+ items: K[],
34
+ fn: (chunk: K[]) => Promise<Map<K, V>>,
35
+ ): Promise<Map<K, V>> {
36
+ if (items.length <= BATCH_SIZE) return fn(items);
37
+
38
+ const result = new Map<K, V>();
39
+ for (let i = 0; i < items.length; i += BATCH_SIZE) {
40
+ const chunk = items.slice(i, i + BATCH_SIZE);
41
+ const partial = await fn(chunk);
42
+ for (const [k, v] of partial) {
43
+ result.set(k, v);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * Like `batchQuery` but for functions that return an array of rows
51
+ * rather than a Map.
52
+ */
53
+ export async function batchQueryRows<K, R>(
54
+ items: K[],
55
+ fn: (chunk: K[]) => Promise<R[]>,
56
+ ): Promise<R[]> {
57
+ if (items.length <= BATCH_SIZE) return fn(items);
58
+
59
+ const result: R[] = [];
60
+ for (let i = 0; i < items.length; i += BATCH_SIZE) {
61
+ const chunk = items.slice(i, i + BATCH_SIZE);
62
+ const partial = await fn(chunk);
63
+ result.push(...partial);
64
+ }
65
+ return result;
66
+ }