@jant/core 0.3.27 → 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 (313) 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 +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. 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
  }