@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
package/src/lib/upload.ts CHANGED
@@ -5,40 +5,289 @@
5
5
  */
6
6
 
7
7
  import { uuidv7 } from "uuidv7";
8
+ import type { MediaKind } from "../types/constants.js";
8
9
 
9
- /** MIME types allowed for upload */
10
- const ALLOWED_UPLOAD_TYPES = [
10
+ /** MIME types images */
11
+ const IMAGE_MIME_TYPES = [
11
12
  "image/jpeg",
12
13
  "image/png",
13
14
  "image/gif",
14
15
  "image/webp",
15
16
  "image/svg+xml",
17
+ "image/avif",
18
+ "image/bmp",
19
+ "image/x-icon",
16
20
  ] as const;
17
21
 
18
- /** Maximum file size in bytes (10MB) */
19
- const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
22
+ /** MIME types video */
23
+ const VIDEO_MIME_TYPES = [
24
+ "video/mp4",
25
+ "video/webm",
26
+ "video/quicktime",
27
+ "video/x-msvideo",
28
+ "video/x-matroska",
29
+ "video/mpeg",
30
+ "video/3gpp",
31
+ "video/x-flv",
32
+ "video/ogg",
33
+ ] as const;
34
+
35
+ /** MIME types — audio */
36
+ const AUDIO_MIME_TYPES = [
37
+ "audio/mpeg",
38
+ "audio/ogg",
39
+ "audio/wav",
40
+ "audio/mp4",
41
+ "audio/x-m4a",
42
+ "audio/flac",
43
+ "audio/aac",
44
+ "audio/webm",
45
+ "audio/x-aiff",
46
+ "audio/opus",
47
+ "audio/3gpp",
48
+ "audio/midi",
49
+ ] as const;
50
+
51
+ /** MIME types — documents (books, PDFs) */
52
+ const DOCUMENT_MIME_TYPES = [
53
+ "application/pdf",
54
+ "application/epub+zip",
55
+ "application/x-mobipocket-ebook",
56
+ "application/vnd.amazon.ebook",
57
+ ] as const;
58
+
59
+ /** MIME types — office documents */
60
+ const OFFICE_MIME_TYPES = [
61
+ "application/msword",
62
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
63
+ "application/vnd.ms-excel",
64
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
65
+ "application/vnd.ms-powerpoint",
66
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
67
+ "application/vnd.oasis.opendocument.text",
68
+ "application/vnd.oasis.opendocument.spreadsheet",
69
+ "application/vnd.oasis.opendocument.presentation",
70
+ "application/vnd.apple.pages",
71
+ "application/vnd.apple.numbers",
72
+ "application/vnd.apple.keynote",
73
+ ] as const;
74
+
75
+ /** MIME types — text & structured data */
76
+ const TEXT_MIME_TYPES = [
77
+ "text/plain",
78
+ "text/markdown",
79
+ "text/csv",
80
+ "text/x-tiptap+json",
81
+ "text/html",
82
+ "text/css",
83
+ "text/javascript",
84
+ "text/xml",
85
+ "text/rtf",
86
+ "text/tab-separated-values",
87
+ "text/calendar",
88
+ "application/json",
89
+ "application/xml",
90
+ "application/yaml",
91
+ "application/toml",
92
+ ] as const;
93
+
94
+ /** MIME types — archives */
95
+ const ARCHIVE_MIME_TYPES = [
96
+ "application/zip",
97
+ "application/x-tar",
98
+ "application/gzip",
99
+ "application/x-bzip2",
100
+ "application/x-7z-compressed",
101
+ "application/x-rar-compressed",
102
+ "application/zstd",
103
+ ] as const;
104
+
105
+ /** MIME types — fonts */
106
+ const FONT_MIME_TYPES = [
107
+ "font/ttf",
108
+ "font/otf",
109
+ "font/woff",
110
+ "font/woff2",
111
+ ] as const;
112
+
113
+ /** MIME types — 3D & design */
114
+ const THREE_D_MIME_TYPES = [
115
+ "model/gltf+json",
116
+ "model/gltf-binary",
117
+ "model/obj",
118
+ "application/x-figma",
119
+ "image/vnd.dxf",
120
+ ] as const;
121
+
122
+ /** MIME types — data & code */
123
+ const CODE_MIME_TYPES = [
124
+ "application/sql",
125
+ "application/wasm",
126
+ "application/x-ipynb+json",
127
+ "application/x-sh",
128
+ "application/x-python-code",
129
+ ] as const;
130
+
131
+ /** Lookup table from MIME type to category */
132
+ const MIME_CATEGORY_MAP = new Map<string, MediaCategory>([
133
+ ...IMAGE_MIME_TYPES.map((t) => [t, "image" as const] as const),
134
+ ...VIDEO_MIME_TYPES.map((t) => [t, "video" as const] as const),
135
+ ...AUDIO_MIME_TYPES.map((t) => [t, "audio" as const] as const),
136
+ ...DOCUMENT_MIME_TYPES.map((t) => [t, "document" as const] as const),
137
+ ...OFFICE_MIME_TYPES.map((t) => [t, "office" as const] as const),
138
+ ...TEXT_MIME_TYPES.map((t) => [t, "text" as const] as const),
139
+ ...ARCHIVE_MIME_TYPES.map((t) => [t, "archive" as const] as const),
140
+ ...FONT_MIME_TYPES.map((t) => [t, "font" as const] as const),
141
+ ...THREE_D_MIME_TYPES.map((t) => [t, "3d" as const] as const),
142
+ ...CODE_MIME_TYPES.map((t) => [t, "code" as const] as const),
143
+ ]);
144
+
145
+ /**
146
+ * Accept string for file inputs. Accepts all file types.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * <input type="file" accept={UPLOAD_ACCEPT} />
151
+ * ```
152
+ */
153
+ export const UPLOAD_ACCEPT = "*/*";
154
+
155
+ export type MediaCategory =
156
+ | "image"
157
+ | "video"
158
+ | "audio"
159
+ | "document"
160
+ | "office"
161
+ | "text"
162
+ | "archive"
163
+ | "font"
164
+ | "3d"
165
+ | "code";
166
+
167
+ /**
168
+ * Returns the media category for a given MIME type.
169
+ * Unrecognized types default to "archive".
170
+ *
171
+ * @param mimeType - The MIME type to classify
172
+ * @returns The media category
173
+ * @example
174
+ * ```ts
175
+ * getMediaCategory("video/mp4"); // "video"
176
+ * getMediaCategory("text/plain"); // "text"
177
+ * getMediaCategory("application/octet-stream"); // "archive"
178
+ * ```
179
+ */
180
+ export function getMediaCategory(mimeType: string): MediaCategory {
181
+ // Exact match from known types
182
+ const exact = MIME_CATEGORY_MAP.get(mimeType);
183
+ if (exact) return exact;
184
+
185
+ // Prefix-based fallback for unknown subtypes
186
+ if (mimeType.startsWith("image/")) return "image";
187
+ if (mimeType.startsWith("video/")) return "video";
188
+ if (mimeType.startsWith("audio/")) return "audio";
189
+ if (mimeType.startsWith("font/")) return "font";
190
+ if (mimeType.startsWith("model/")) return "3d";
191
+ if (mimeType.startsWith("text/")) return "text";
192
+
193
+ // Unknown types default to archive
194
+ return "archive";
195
+ }
196
+
197
+ /**
198
+ * Maps a MIME type to one of the five media kind categories.
199
+ * image/video/audio/text pass through; everything else becomes "document".
200
+ *
201
+ * @param mimeType - The MIME type to classify
202
+ * @returns The media kind
203
+ * @example
204
+ * ```ts
205
+ * toMediaKind("image/jpeg"); // "image"
206
+ * toMediaKind("application/pdf"); // "document"
207
+ * toMediaKind("text/plain"); // "text"
208
+ * ```
209
+ */
210
+ export function toMediaKind(mimeType: string): MediaKind {
211
+ const category = getMediaCategory(mimeType);
212
+ switch (category) {
213
+ case "image":
214
+ case "video":
215
+ case "audio":
216
+ case "text":
217
+ return category;
218
+ default:
219
+ return "document";
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Returns true if the given MIME type is an image type.
225
+ *
226
+ * @param mimeType - The MIME type to check
227
+ * @returns Whether the MIME type is an image
228
+ * @example
229
+ * ```ts
230
+ * isImageMimeType("image/jpeg"); // true
231
+ * isImageMimeType("video/mp4"); // false
232
+ * ```
233
+ */
234
+ export function isImageMimeType(mimeType: string): boolean {
235
+ return mimeType.startsWith("image/");
236
+ }
237
+
238
+ export interface ValidateUploadOptions {
239
+ /** When true, only image MIME types are accepted (e.g. for avatar uploads). */
240
+ imagesOnly?: boolean;
241
+ /** Max file size in MB. */
242
+ maxFileSizeMB: number;
243
+ }
20
244
 
21
245
  /**
22
246
  * Validates an uploaded file's type and size.
23
247
  *
24
248
  * @param file - The uploaded File object
249
+ * @param options - Validation constraints
25
250
  * @returns null if valid, error message string if invalid
26
251
  * @example
27
252
  * ```ts
28
- * const error = validateUploadFile(file);
253
+ * const error = validateUploadFile(file, { maxFileSizeMB: 500 });
29
254
  * if (error) return dsToast(error, "error");
30
255
  * ```
31
256
  */
32
- export function validateUploadFile(file: File): string | null {
33
- if (
34
- !ALLOWED_UPLOAD_TYPES.includes(
35
- file.type as (typeof ALLOWED_UPLOAD_TYPES)[number],
36
- )
37
- ) {
38
- return "File type not allowed.";
257
+ export function validateUploadFile(
258
+ file: File,
259
+ options: ValidateUploadOptions,
260
+ ): string | null {
261
+ return validateUploadFileMetadata(file.type, file.size, options);
262
+ }
263
+
264
+ /**
265
+ * Validates file metadata (type and size) without requiring a File object.
266
+ * Used by the multipart upload initiation endpoint which receives JSON metadata.
267
+ * All MIME types are accepted; unrecognized types are categorized as archive.
268
+ *
269
+ * @param contentType - The MIME type of the file
270
+ * @param size - The file size in bytes
271
+ * @param options - Validation constraints
272
+ * @returns null if valid, error message string if invalid
273
+ * @example
274
+ * ```ts
275
+ * const error = validateUploadFileMetadata("image/jpeg", 1024000, { maxFileSizeMB: 500 });
276
+ * ```
277
+ */
278
+ export function validateUploadFileMetadata(
279
+ contentType: string,
280
+ size: number,
281
+ options: ValidateUploadOptions,
282
+ ): string | null {
283
+ if (options?.imagesOnly) {
284
+ if (!isImageMimeType(contentType)) {
285
+ return "File type not allowed.";
286
+ }
39
287
  }
40
- if (file.size > MAX_UPLOAD_SIZE) {
41
- return "File too large (max 10MB).";
288
+ const maxMB = options.maxFileSizeMB;
289
+ if (size > maxMB * 1024 * 1024) {
290
+ return `File too large (max ${maxMB}MB).`;
42
291
  }
43
292
  return null;
44
293
  }
package/src/lib/url.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * URL Utilities
3
3
  */
4
4
 
5
- import { pinyin } from "pinyin-pro";
5
+ import limax from "limax";
6
6
 
7
7
  /**
8
8
  * Extracts the hostname (domain) from a URL string.
@@ -79,37 +79,52 @@ export function isFullUrl(str: string): boolean {
79
79
  /**
80
80
  * Converts text to a URL-friendly slug.
81
81
  *
82
- * Transforms text into a lowercase, hyphen-separated slug by:
83
- * - Converting to lowercase
84
- * - Removing special characters (keeping only word characters, spaces, and hyphens)
85
- * - Replacing whitespace and underscores with hyphens
86
- * - Removing leading and trailing hyphens
87
- *
88
- * Used for generating clean URLs from titles and names.
82
+ * Transforms text into a lowercase, hyphen-separated slug using limax for
83
+ * i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
89
84
  *
90
85
  * @param text - The text to convert to a slug
91
86
  * @returns The slugified string
92
87
  *
93
88
  * @example
94
89
  * ```ts
95
- * const slug = slugify("Hello World! This is a Test.");
90
+ * slugify("Hello World! This is a Test.");
96
91
  * // Returns: "hello-world-this-is-a-test"
97
92
  *
98
- * const slug = slugify(" Multiple Spaces ");
99
- * // Returns: "multiple-spaces"
93
+ * slugify("书评");
94
+ * // Returns: "shu-ping"
100
95
  * ```
101
96
  */
102
97
  export function slugify(text: string): string {
103
- // Replace CJK characters with their pinyin equivalents, preserving non-CJK text
104
- const converted = text.replace(
105
- /[\u4e00-\u9fff\u3400-\u4dbf]+/g,
106
- (match) => ` ${pinyin(match, { toneType: "none", separator: " " })} `,
107
- );
98
+ return limax(text, { tone: false }).replace(/_/g, "-");
99
+ }
108
100
 
109
- return converted
110
- .toLowerCase()
111
- .trim()
112
- .replace(/[^\w\s-]/g, "")
113
- .replace(/[\s_-]+/g, "-")
114
- .replace(/^-+|-+$/g, "");
101
+ const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
102
+
103
+ /**
104
+ * Sanitizes a URL by ensuring it uses a safe protocol.
105
+ *
106
+ * Returns the URL unchanged if it uses an allowed protocol (http:, https:, mailto:)
107
+ * or is a relative path. Returns an empty string for dangerous protocols like
108
+ * `javascript:`, `data:`, or `vbscript:`.
109
+ *
110
+ * @param url - The URL string to sanitize
111
+ * @returns The original URL if safe, or an empty string if the protocol is disallowed
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * sanitizeUrl("https://example.com"); // "https://example.com"
116
+ * sanitizeUrl("/about"); // "/about"
117
+ * sanitizeUrl("javascript:alert(1)"); // ""
118
+ * sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
119
+ * ```
120
+ */
121
+ export function sanitizeUrl(url: string): string {
122
+ try {
123
+ const parsed = new URL(url, "https://placeholder.invalid");
124
+ // Relative URLs resolve against the placeholder and get https: — allow them
125
+ if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
126
+ return "";
127
+ } catch {
128
+ return "";
129
+ }
115
130
  }