@jant/core 0.3.26 → 0.3.28

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 (314) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +112 -173
  9. package/src/auth.ts +4 -1
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -265
  172. package/dist/auth.js +0 -36
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Toast Utility
3
+ *
4
+ * Shared showToast() for all client-side bridge modules.
5
+ * Appends a temporary notification to `#toast-container`.
6
+ */
7
+
8
+ const TOAST_ICONS = {
9
+ success:
10
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>',
11
+ error:
12
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>',
13
+ };
14
+
15
+ /**
16
+ * Show a toast notification.
17
+ *
18
+ * @param message - Text to display
19
+ * @param type - Visual style: "success" (default) or "error"
20
+ *
21
+ * @example
22
+ * showToast("Saved successfully.");
23
+ * showToast("Something went wrong", "error");
24
+ */
25
+ export function showToast(
26
+ message: string,
27
+ type: "success" | "error" = "success",
28
+ ): void {
29
+ if (!message) return;
30
+
31
+ const container = document.getElementById("toast-container");
32
+ if (!container) return;
33
+
34
+ const toast = document.createElement("div");
35
+ toast.className = `toast toast-${type}`;
36
+ toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
37
+ container.appendChild(toast);
38
+
39
+ setTimeout(() => {
40
+ toast.classList.add("toast-out");
41
+ toast.addEventListener("animationend", () => toast.remove());
42
+ }, 3000);
43
+ }
44
+
45
+ /**
46
+ * Show a persistent toast that stays until explicitly dismissed.
47
+ *
48
+ * @param id - Unique identifier for updating/dismissing later
49
+ * @param message - Text to display
50
+ * @param type - Visual style: "success" (default) or "error"
51
+ * @returns The toast element
52
+ *
53
+ * @example
54
+ * showPersistentToast("upload", "Uploading...");
55
+ */
56
+ export function showPersistentToast(
57
+ id: string,
58
+ message: string,
59
+ type: "success" | "error" = "success",
60
+ ): HTMLElement | null {
61
+ const container = document.getElementById("toast-container");
62
+ if (!container) return null;
63
+
64
+ const toast = document.createElement("div");
65
+ toast.className = `toast toast-${type}`;
66
+ toast.id = `toast-${id}`;
67
+ toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
68
+ container.appendChild(toast);
69
+
70
+ return toast;
71
+ }
72
+
73
+ /**
74
+ * Update the message of an existing persistent toast.
75
+ *
76
+ * @param id - The toast identifier
77
+ * @param message - New message text
78
+ *
79
+ * @example
80
+ * updateToast("upload", "Almost done...");
81
+ */
82
+ export function updateToast(id: string, message: string): void {
83
+ const toast = document.getElementById(`toast-${id}`);
84
+ if (!toast) return;
85
+
86
+ const span = toast.querySelector("span");
87
+ if (span) span.textContent = message;
88
+ }
89
+
90
+ /**
91
+ * Dismiss a persistent toast with fadeout animation.
92
+ *
93
+ * @param id - The toast identifier
94
+ *
95
+ * @example
96
+ * dismissToast("upload");
97
+ */
98
+ export function dismissToast(id: string): void {
99
+ const toast = document.getElementById(`toast-${id}`);
100
+ if (!toast) return;
101
+
102
+ toast.classList.add("toast-out");
103
+ toast.addEventListener("animationend", () => toast.remove());
104
+ }
105
+
106
+ /**
107
+ * Replace a persistent toast with an auto-dismissing one.
108
+ *
109
+ * @param id - The toast identifier
110
+ * @param message - New message text
111
+ * @param type - Visual style: "success" (default) or "error"
112
+ *
113
+ * @example
114
+ * replaceWithAutoClose("upload", "Published!", "success");
115
+ */
116
+ export function replaceWithAutoClose(
117
+ id: string,
118
+ message: string,
119
+ type: "success" | "error" = "success",
120
+ ): void {
121
+ const toast = document.getElementById(`toast-${id}`);
122
+ if (!toast) {
123
+ showToast(message, type);
124
+ return;
125
+ }
126
+
127
+ toast.className = `toast toast-${type}`;
128
+ toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
129
+
130
+ setTimeout(() => {
131
+ toast.classList.add("toast-out");
132
+ toast.addEventListener("animationend", () => toast.remove());
133
+ }, 3000);
134
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Upload Utilities
3
+ *
4
+ * Shared file validation and storage key generation for upload routes.
5
+ */
6
+
7
+ import { uuidv7 } from "uuidv7";
8
+
9
+ /** MIME types allowed for upload */
10
+ const ALLOWED_UPLOAD_TYPES = [
11
+ "image/jpeg",
12
+ "image/png",
13
+ "image/gif",
14
+ "image/webp",
15
+ "image/svg+xml",
16
+ ] as const;
17
+
18
+ /** Maximum file size in bytes (10MB) */
19
+ const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
20
+
21
+ /**
22
+ * Validates an uploaded file's type and size.
23
+ *
24
+ * @param file - The uploaded File object
25
+ * @returns null if valid, error message string if invalid
26
+ * @example
27
+ * ```ts
28
+ * const error = validateUploadFile(file);
29
+ * if (error) return dsToast(error, "error");
30
+ * ```
31
+ */
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.";
39
+ }
40
+ if (file.size > MAX_UPLOAD_SIZE) {
41
+ return "File too large (max 10MB).";
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Generates a unique storage key for an uploaded file.
48
+ * Format: `media/YYYY/MM/uuid.ext`
49
+ *
50
+ * @param originalFilename - Original filename to extract extension from
51
+ * @returns Object with generated id, filename, and storageKey
52
+ * @example
53
+ * ```ts
54
+ * const { id, filename, storageKey } = generateStorageKey("photo.jpg");
55
+ * // { id: "0192...", filename: "0192....jpg", storageKey: "media/2025/01/0192....jpg" }
56
+ * ```
57
+ */
58
+ export function generateStorageKey(originalFilename: string): {
59
+ id: string;
60
+ filename: string;
61
+ storageKey: string;
62
+ } {
63
+ const ext = originalFilename.split(".").pop() || "bin";
64
+ const id = uuidv7();
65
+ const date = new Date();
66
+ const year = date.getUTCFullYear();
67
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
68
+ const filename = `${id}.${ext}`;
69
+ const storageKey = `media/${year}/${month}/${filename}`;
70
+ return { id, filename, storageKey };
71
+ }
package/src/lib/url.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * URL Utilities
3
3
  */
4
4
 
5
+ import { pinyin } from "pinyin-pro";
6
+
5
7
  /**
6
8
  * Extracts the hostname (domain) from a URL string.
7
9
  *
@@ -98,7 +100,13 @@ export function isFullUrl(str: string): boolean {
98
100
  * ```
99
101
  */
100
102
  export function slugify(text: string): string {
101
- return text
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
+ );
108
+
109
+ return converted
102
110
  .toLowerCase()
103
111
  .trim()
104
112
  .replace(/[^\w\s-]/g, "")
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Version and environment detection
3
+ *
4
+ * In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define` config.
5
+ * In production (wrangler/esbuild), the typeof check evaluates to false safely.
6
+ *
7
+ * `__JANT_VERSION__` is replaced by Vite's `define` during both dev and lib build.
8
+ */
9
+
10
+ declare const __JANT_DEV__: boolean | undefined;
11
+ declare const __JANT_VERSION__: string;
12
+
13
+ export const IS_VITE_DEV =
14
+ typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
15
+
16
+ export const CORE_VERSION = __JANT_VERSION__;
package/src/lib/view.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * Theme components receive only View types -- no lib/ imports needed.
6
6
  */
7
7
 
8
- import type { Context } from "hono";
9
8
  import type {
10
9
  Post,
11
10
  PostWithMedia,
@@ -22,6 +21,7 @@ import type {
22
21
  Format,
23
22
  Status,
24
23
  NavItemType,
24
+ AppConfig,
25
25
  } from "../types.js";
26
26
  import { encode } from "./sqid.js";
27
27
  import {
@@ -38,7 +38,7 @@ import { getHtmlExcerpt } from "./excerpt.js";
38
38
  // =============================================================================
39
39
 
40
40
  /**
41
- * Central media config -- extracted once per request from env.
41
+ * Central media config -- extracted once per request from appConfig.
42
42
  */
43
43
  export interface MediaContext {
44
44
  r2PublicUrl?: string;
@@ -47,16 +47,16 @@ export interface MediaContext {
47
47
  }
48
48
 
49
49
  /**
50
- * Creates a MediaContext from Hono context environment variables.
50
+ * Creates a MediaContext from AppConfig.
51
51
  *
52
- * @param c - Hono context
53
- * @returns MediaContext with env values
52
+ * @param appConfig - Resolved app configuration
53
+ * @returns MediaContext with URL values
54
54
  */
55
- export function createMediaContext(c: Context): MediaContext {
55
+ export function createMediaContext(appConfig: AppConfig): MediaContext {
56
56
  return {
57
- r2PublicUrl: c.env.R2_PUBLIC_URL,
58
- imageTransformUrl: c.env.IMAGE_TRANSFORM_URL,
59
- s3PublicUrl: c.env.S3_PUBLIC_URL,
57
+ r2PublicUrl: appConfig.r2PublicUrl || undefined,
58
+ imageTransformUrl: appConfig.imageTransformUrl || undefined,
59
+ s3PublicUrl: appConfig.s3PublicUrl || undefined,
60
60
  };
61
61
  }
62
62
 
@@ -154,7 +154,6 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
154
154
  featured: post.featured === 1,
155
155
  pinned: post.pinned === 1,
156
156
  rating: post.rating ?? undefined,
157
- collectionId: post.collectionId ?? undefined,
158
157
  publishedAt: toISOString(post.publishedAt),
159
158
  publishedAtFormatted: formatDate(post.publishedAt),
160
159
  publishedAtTime: formatTime(post.publishedAt),
@@ -1,8 +1,9 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { Hono } from "hono";
3
3
  import { requireAuth, requireAuthApi } from "../auth.js";
4
+ import { errorHandler } from "../error-handler.js";
4
5
  import type { Bindings } from "../../types.js";
5
- import type { AppVariables } from "../../app.js";
6
+ import type { AppVariables } from "../../types/app-context.js";
6
7
 
7
8
  type Env = { Bindings: Bindings; Variables: AppVariables };
8
9
 
@@ -59,23 +60,12 @@ describe("requireAuth", () => {
59
60
  expect(res.status).toBe(302);
60
61
  expect(res.headers.get("Location")).toBe("/login");
61
62
  });
62
-
63
- it("redirects when auth is not configured", async () => {
64
- const app = new Hono<Env>();
65
- app.use("*", async (c, next) => {
66
- // auth not set (undefined)
67
- await next();
68
- });
69
- app.get("/dash", requireAuth(), (c) => c.text("Dashboard"));
70
-
71
- const res = await app.request("/dash", { redirect: "manual" });
72
- expect(res.status).toBe(302);
73
- });
74
63
  });
75
64
 
76
65
  describe("requireAuthApi", () => {
77
66
  it("allows authenticated requests", async () => {
78
67
  const app = new Hono<Env>();
68
+ app.onError(errorHandler);
79
69
  app.use("*", async (c, next) => {
80
70
  c.set("auth", createMockAuth(true));
81
71
  await next();
@@ -91,6 +81,7 @@ describe("requireAuthApi", () => {
91
81
 
92
82
  it("returns 401 for unauthenticated requests", async () => {
93
83
  const app = new Hono<Env>();
84
+ app.onError(errorHandler);
94
85
  app.use("*", async (c, next) => {
95
86
  c.set("auth", createMockAuth(false));
96
87
  await next();
@@ -102,25 +93,12 @@ describe("requireAuthApi", () => {
102
93
 
103
94
  const body = await res.json();
104
95
  expect(body.error).toBe("Unauthorized");
105
- });
106
-
107
- it("returns 500 when auth is not configured", async () => {
108
- const app = new Hono<Env>();
109
- app.use("*", async (c, next) => {
110
- // auth not set (undefined)
111
- await next();
112
- });
113
- app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
114
-
115
- const res = await app.request("/api/data");
116
- expect(res.status).toBe(500);
117
-
118
- const body = await res.json();
119
- expect(body.error).toBe("Authentication not configured");
96
+ expect(body.code).toBe("UNAUTHORIZED");
120
97
  });
121
98
 
122
99
  it("returns 401 when getSession throws", async () => {
123
100
  const app = new Hono<Env>();
101
+ app.onError(errorHandler);
124
102
  app.use("*", async (c, next) => {
125
103
  c.set("auth", {
126
104
  api: {
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { Hono } from "hono";
3
3
  import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
4
4
  import type { Bindings } from "../../types.js";
5
- import type { AppVariables } from "../../app.js";
5
+ import type { AppVariables } from "../../types/app-context.js";
6
6
 
7
7
  type Env = { Bindings: Bindings; Variables: AppVariables };
8
8
 
@@ -6,7 +6,8 @@
6
6
 
7
7
  import type { MiddlewareHandler } from "hono";
8
8
  import type { Bindings } from "../types.js";
9
- import type { AppVariables } from "../app.js";
9
+ import type { AppVariables } from "../types/app-context.js";
10
+ import { DomainError, UnauthorizedError } from "../lib/errors.js";
10
11
 
11
12
  type Env = { Bindings: Bindings; Variables: AppVariables };
12
13
 
@@ -16,10 +17,6 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
16
17
  */
17
18
  export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
18
19
  return async (c, next) => {
19
- if (!c.var.auth) {
20
- return c.redirect(redirectTo);
21
- }
22
-
23
20
  try {
24
21
  const session = await c.var.auth.api.getSession({
25
22
  headers: c.req.raw.headers,
@@ -42,22 +39,19 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
42
39
  */
43
40
  export function requireAuthApi(): MiddlewareHandler<Env> {
44
41
  return async (c, next) => {
45
- if (!c.var.auth) {
46
- return c.json({ error: "Authentication not configured" }, 500);
47
- }
48
-
49
42
  try {
50
43
  const session = await c.var.auth.api.getSession({
51
44
  headers: c.req.raw.headers,
52
45
  });
53
46
 
54
47
  if (!session?.user) {
55
- return c.json({ error: "Unauthorized" }, 401);
48
+ throw new UnauthorizedError();
56
49
  }
57
50
 
58
51
  await next();
59
- } catch {
60
- return c.json({ error: "Unauthorized" }, 401);
52
+ } catch (err) {
53
+ if (err instanceof DomainError) throw err;
54
+ throw new UnauthorizedError();
61
55
  }
62
56
  };
63
57
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Config Middleware
3
+ *
4
+ * Loads settings from DB, resolves app config and theme.
5
+ * Apply only to route groups that need config/theme data —
6
+ * skip for /health, /media/*, /favicon.ico, /api/auth/*, etc.
7
+ */
8
+
9
+ import type { MiddlewareHandler } from "hono";
10
+ import type { Bindings } from "../types.js";
11
+ import type { AppVariables } from "../types/app-context.js";
12
+ import { resolveConfig } from "../lib/resolve-config.js";
13
+ import { buildThemeStyle } from "../lib/theme.js";
14
+ import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
15
+ import { BUILTIN_FONT_THEMES } from "../ui/font-themes.js";
16
+
17
+ type Env = { Bindings: Bindings; Variables: AppVariables };
18
+
19
+ /**
20
+ * Middleware that loads settings, resolves app config, and builds theme CSS.
21
+ *
22
+ * Sets `allSettings`, `appConfig`, and `themeStyle` on the Hono context.
23
+ */
24
+ export function withConfig(): MiddlewareHandler<Env> {
25
+ return async (c, next) => {
26
+ const allSettings = await c.var.services.settings.getAll();
27
+ c.set("allSettings", allSettings);
28
+ const appConfig = resolveConfig(c.env, allSettings);
29
+ c.set("appConfig", appConfig);
30
+
31
+ // Resolve active color theme
32
+ const activeTheme = BUILTIN_COLOR_THEMES.find(
33
+ (t) => t.id === (appConfig.themeId || appConfig.defaultThemeId),
34
+ );
35
+
36
+ // Build font override CSS variables
37
+ const fontTheme = appConfig.fontThemeId
38
+ ? BUILTIN_FONT_THEMES.find((f) => f.id === appConfig.fontThemeId)
39
+ : undefined;
40
+ const fontOverrides: Record<string, string> = {};
41
+ if (fontTheme) {
42
+ fontOverrides["--font-body"] = fontTheme.bodyFontFamily;
43
+ fontOverrides["--font-heading"] = fontTheme.headingFontFamily;
44
+ }
45
+
46
+ const themeStyle = buildThemeStyle(activeTheme, fontOverrides);
47
+ c.set("themeStyle", themeStyle);
48
+
49
+ await next();
50
+ };
51
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Global Error Handler
3
+ *
4
+ * Maps DomainError subclasses to HTTP responses.
5
+ * API routes receive JSON; page routes fall through to Hono defaults.
6
+ */
7
+
8
+ import type { ErrorHandler } from "hono";
9
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
10
+ import type { Bindings } from "../types.js";
11
+ import type { AppVariables } from "../types/app-context.js";
12
+ import { DomainError, NotFoundError, ValidationError } from "../lib/errors.js";
13
+ import { dsToast } from "../lib/sse.js";
14
+
15
+ type Env = { Bindings: Bindings; Variables: AppVariables };
16
+
17
+ export const errorHandler: ErrorHandler<Env> = (err, c) => {
18
+ // API routes: always return JSON
19
+ if (c.req.path.startsWith("/api/")) {
20
+ if (err instanceof DomainError) {
21
+ const body: Record<string, unknown> = {
22
+ error: err.message,
23
+ code: err.code,
24
+ };
25
+
26
+ if (err instanceof ValidationError && err.details) {
27
+ body.details = err.details;
28
+ }
29
+
30
+ return c.json(body, err.statusCode as ContentfulStatusCode);
31
+ }
32
+
33
+ // Unknown API error
34
+ // eslint-disable-next-line no-console -- Server error logging is intentional
35
+ console.error("[Jant] Unhandled error:", err);
36
+ return c.json({ error: "Internal server error" }, 500);
37
+ }
38
+
39
+ // Datastar requests: return toast
40
+ if (c.req.header("datastar-request")) {
41
+ if (err instanceof DomainError) {
42
+ return dsToast(err.message, "error");
43
+ }
44
+ // eslint-disable-next-line no-console -- Server error logging is intentional
45
+ console.error("[Jant] Unhandled error:", err);
46
+ return dsToast("An unexpected error occurred", "error");
47
+ }
48
+
49
+ // Non-API routes: map NotFoundError to Hono's built-in 404
50
+ if (err instanceof NotFoundError) {
51
+ return c.notFound();
52
+ }
53
+
54
+ // Everything else: re-throw for Hono's default handling
55
+ throw err;
56
+ };
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type { MiddlewareHandler } from "hono";
11
11
  import type { Bindings } from "../types.js";
12
- import type { AppVariables } from "../app.js";
12
+ import type { AppVariables } from "../types/app-context.js";
13
13
 
14
14
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
15
 
package/src/preset.css CHANGED
@@ -16,6 +16,8 @@
16
16
  @theme {
17
17
  --radius-default: 0.5rem;
18
18
  --color-success: var(--success);
19
+ --default-font-family: var(--font-body);
20
+ --default-mono-font-family: var(--font-mono);
19
21
  }
20
22
 
21
23
  :root {
@@ -67,4 +69,8 @@
67
69
  a:hover {
68
70
  text-decoration-color: currentColor;
69
71
  }
72
+
73
+ :where(h1, h2, h3, h4, h5, h6) {
74
+ font-family: var(--font-heading);
75
+ }
70
76
  }