@jant/core 0.3.27 → 0.3.29

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/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  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 -267
  172. package/dist/auth.js +0 -39
  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,157 @@
1
+ /**
2
+ * Unified App Configuration
3
+ *
4
+ * Resolves all configuration from environment + DB settings into a single
5
+ * immutable object. Created once per request in middleware, then accessed
6
+ * via `c.var.appConfig` everywhere else.
7
+ *
8
+ * Priority: DB > ENV > Default (for user-configurable fields)
9
+ * ENV > Default (for envOnly fields)
10
+ */
11
+
12
+ import type { Bindings } from "../types/bindings.js";
13
+ import type { AppConfig } from "../types/config.js";
14
+ import { CONFIG_FIELDS } from "../types/config.js";
15
+ import { getPublicUrlForProvider, getMediaUrl } from "./image.js";
16
+
17
+ /**
18
+ * Resolve a single config value following priority rules.
19
+ *
20
+ * @param key - CONFIG_FIELDS key
21
+ * @param allSettings - DB settings map
22
+ * @param env - Worker bindings
23
+ * @returns Resolved string value
24
+ */
25
+ function resolve(
26
+ key: string,
27
+ allSettings: Record<string, string>,
28
+ env: Bindings,
29
+ ): string {
30
+ const field = CONFIG_FIELDS[key as keyof typeof CONFIG_FIELDS];
31
+ if (!field) return "";
32
+
33
+ // User-configurable: DB > ENV > Default
34
+ if (!field.envOnly) {
35
+ const dbValue = allSettings[key];
36
+ if (dbValue) return dbValue;
37
+ }
38
+
39
+ // ENV > Default
40
+ const envValue = env[key as keyof Bindings];
41
+ if (envValue && typeof envValue === "string") return envValue;
42
+
43
+ return field.defaultValue;
44
+ }
45
+
46
+ /**
47
+ * Resolve a fallback value (ENV > Default), skipping the database.
48
+ * Used for placeholder values in forms.
49
+ *
50
+ * @param key - CONFIG_FIELDS key
51
+ * @param env - Worker bindings
52
+ * @returns Fallback value
53
+ */
54
+ function resolveFallback(key: string, env: Bindings): string {
55
+ const field = CONFIG_FIELDS[key as keyof typeof CONFIG_FIELDS];
56
+ if (!field) return "";
57
+
58
+ const envValue = env[key as keyof Bindings];
59
+ if (envValue && typeof envValue === "string") return envValue;
60
+
61
+ return field.defaultValue;
62
+ }
63
+
64
+ /**
65
+ * Build a complete AppConfig from environment bindings and DB settings.
66
+ *
67
+ * Pure function — no side effects, no DB access.
68
+ *
69
+ * @param env - Cloudflare Worker bindings
70
+ * @param allSettings - All DB settings (from `services.settings.getAll()`)
71
+ * @returns Fully resolved AppConfig
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const allSettings = await services.settings.getAll();
76
+ * const appConfig = resolveConfig(c.env, allSettings);
77
+ * ```
78
+ */
79
+ export function resolveConfig(
80
+ env: Bindings,
81
+ allSettings: Record<string, string>,
82
+ ): AppConfig {
83
+ const storageDriver = env.STORAGE_DRIVER || "r2";
84
+ const r2PublicUrl = env.R2_PUBLIC_URL || "";
85
+ const s3PublicUrl = env.S3_PUBLIC_URL || "";
86
+ const imageTransformUrl = env.IMAGE_TRANSFORM_URL || "";
87
+
88
+ // Resolve avatar URL from storage key
89
+ const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
90
+ let siteAvatarUrl = "";
91
+ if (siteAvatar) {
92
+ const publicUrl = getPublicUrlForProvider(
93
+ storageDriver,
94
+ r2PublicUrl,
95
+ s3PublicUrl,
96
+ );
97
+ siteAvatarUrl = getMediaUrl(siteAvatar, publicUrl);
98
+ }
99
+
100
+ // Description is "explicit" when set in DB or ENV (not just the default)
101
+ const dbDescription = allSettings["SITE_DESCRIPTION"];
102
+ const envDescription = env.SITE_DESCRIPTION;
103
+ const siteDescriptionExplicit = !!(
104
+ dbDescription ||
105
+ (typeof envDescription === "string" && envDescription)
106
+ );
107
+
108
+ return {
109
+ // Site identity (DB > ENV > Default)
110
+ siteName: resolve("SITE_NAME", allSettings, env),
111
+ siteDescription: resolve("SITE_DESCRIPTION", allSettings, env),
112
+ siteDescriptionExplicit,
113
+ siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
114
+ homeDefaultView: resolve("HOME_DEFAULT_VIEW", allSettings, env),
115
+ timeZone: resolve("TIME_ZONE", allSettings, env),
116
+ siteFooter: resolve("SITE_FOOTER", allSettings, env),
117
+ noindex: resolve("NOINDEX", allSettings, env) === "true",
118
+
119
+ // Infrastructure (ENV only)
120
+ siteUrl: env.SITE_URL || "",
121
+ authConfigured: !!env.AUTH_SECRET,
122
+
123
+ // Media (ENV only)
124
+ storageDriver,
125
+ r2PublicUrl,
126
+ s3PublicUrl,
127
+ imageTransformUrl,
128
+
129
+ // Pagination/Feed (ENV only)
130
+ pageSize: parseInt(env.PAGE_SIZE ?? "20", 10) || 20,
131
+ rssFeedLimit: parseInt(env.RSS_FEED_LIMIT ?? "50", 10) || 50,
132
+
133
+ // Demo (ENV only)
134
+ demoEmail: env.DEMO_EMAIL || "",
135
+ demoPassword: env.DEMO_PASSWORD || "",
136
+
137
+ // Theme (DB internal)
138
+ themeId: allSettings["THEME"] ?? "",
139
+ defaultThemeId:
140
+ env.DEFAULT_THEME || CONFIG_FIELDS.DEFAULT_THEME.defaultValue,
141
+ fontThemeId: allSettings["FONT_THEME"] ?? "",
142
+ customCSS: allSettings["CUSTOM_CSS"] ?? "",
143
+
144
+ // Site appearance (DB internal)
145
+ siteAvatar,
146
+ showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
147
+ siteAvatarUrl,
148
+ faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
149
+
150
+ // Dashboard form placeholders (ENV > Default, without DB)
151
+ fallbacks: {
152
+ siteName: resolveFallback("SITE_NAME", env),
153
+ siteDescription: resolveFallback("SITE_DESCRIPTION", env),
154
+ defaultTheme: resolveFallback("DEFAULT_THEME", env),
155
+ },
156
+ };
157
+ }
@@ -16,6 +16,7 @@ import {
16
16
  NAV_ITEM_TYPES,
17
17
  MAX_MEDIA_ATTACHMENTS,
18
18
  } from "../types.js";
19
+ import { ValidationError } from "./errors.js";
19
20
 
20
21
  /**
21
22
  * Post format enum schema
@@ -79,16 +80,14 @@ export const CreatePostSchema = z.object({
79
80
  url: z.url().optional().or(z.literal("")),
80
81
  quoteText: z.string().optional(),
81
82
  rating: RatingSchema,
82
- collectionId: z.coerce
83
- .number()
84
- .int()
85
- .min(0)
83
+ collectionIds: z
84
+ .array(z.coerce.number().int().positive())
86
85
  .optional()
87
- .or(z.literal("").transform(() => undefined))
88
- .transform((v) => (v === 0 ? undefined : v)),
86
+ .or(z.literal("").transform(() => undefined)),
89
87
  replyToId: z.string().optional(), // Sqid format
90
88
  publishedAt: z.number().int().positive().optional(),
91
89
  mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional(),
90
+ mediaAlts: z.record(z.string(), z.string()).optional(),
92
91
  });
93
92
 
94
93
  /**
@@ -103,7 +102,13 @@ export const CreatePageSchema = z.object({
103
102
  slug: z
104
103
  .string()
105
104
  .min(1)
106
- .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
105
+ .transform(normalizeSlug)
106
+ .pipe(
107
+ z
108
+ .string()
109
+ .min(1)
110
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
111
+ ),
107
112
  title: z.string().optional(),
108
113
  body: z.string().optional(),
109
114
  status: StatusSchema.optional(),
@@ -137,15 +142,18 @@ export const CreateCollectionSchema = z.object({
137
142
  slug: z
138
143
  .string()
139
144
  .min(1)
140
- .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
145
+ .transform(normalizeSlug)
146
+ .pipe(
147
+ z
148
+ .string()
149
+ .min(1)
150
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
151
+ ),
141
152
  title: z.string().min(1),
142
153
  description: z.string().optional(),
143
154
  icon: z.string().optional(),
144
155
  sortOrder: SortOrderSchema.optional(),
145
156
  position: z.coerce.number().int().min(0).optional(),
146
- showDivider: z
147
- .union([z.boolean(), z.literal("on").transform(() => true)])
148
- .optional(),
149
157
  });
150
158
 
151
159
  /**
@@ -188,6 +196,42 @@ export const ResetPasswordSchema = z
188
196
  path: ["confirmPassword"],
189
197
  });
190
198
 
199
+ // =============================================================================
200
+ // Slug Normalization
201
+ // =============================================================================
202
+
203
+ /**
204
+ * Normalize a string into a valid slug format.
205
+ * Lowercases, replaces non-alphanumeric characters with dashes,
206
+ * collapses consecutive dashes, and trims leading/trailing dashes.
207
+ *
208
+ * @param s - Raw input string
209
+ * @returns Normalized slug
210
+ * @example
211
+ * ```ts
212
+ * normalizeSlug("My Cool Page!") // "my-cool-page"
213
+ * normalizeSlug(" hello world ") // "hello-world"
214
+ * ```
215
+ */
216
+ export function normalizeSlug(s: string): string {
217
+ return s
218
+ .toLowerCase()
219
+ .replace(/[^a-z0-9-]/g, "-")
220
+ .replace(/-{2,}/g, "-")
221
+ .replace(/^-|-$/g, "");
222
+ }
223
+
224
+ // =============================================================================
225
+ // Reorder Schemas
226
+ // =============================================================================
227
+
228
+ /**
229
+ * Reorder request schema for simple ID-based reordering
230
+ */
231
+ export const ReorderSchema = z.object({
232
+ ids: z.array(z.coerce.number().int().positive()),
233
+ });
234
+
191
235
  // =============================================================================
192
236
  // Form Data Helpers
193
237
  // =============================================================================
@@ -208,7 +252,7 @@ export function parseFormData<T>(
208
252
  ): T {
209
253
  const value = formData.get(key);
210
254
  if (value === null) {
211
- throw new Error(`Missing required field: ${key}`);
255
+ throw new ValidationError(`Missing required field: ${key}`);
212
256
  }
213
257
  return schema.parse(value);
214
258
  }
@@ -247,3 +291,23 @@ export function validateMediaCount(mediaIds: string[]): string | null {
247
291
  }
248
292
  return null;
249
293
  }
294
+
295
+ /**
296
+ * Parse and validate data against a Zod schema, throwing ValidationError on failure.
297
+ *
298
+ * @param schema - Zod schema to validate against
299
+ * @param data - Data to validate
300
+ * @returns Validated data
301
+ * @example
302
+ * ```ts
303
+ * const body = parseValidated(CreatePageSchema, await c.req.json());
304
+ * ```
305
+ */
306
+ export function parseValidated<T>(schema: z.ZodSchema<T>, data: unknown): T {
307
+ const result = schema.safeParse(data);
308
+ if (!result.success) {
309
+ const firstMessage = result.error.issues[0]?.message ?? "Validation failed";
310
+ throw new ValidationError(firstMessage, result.error.flatten());
311
+ }
312
+ return result.data;
313
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Settings Bridge
3
+ *
4
+ * Handles server communication for the Lit settings components.
5
+ * Listens for `jant:settings-save` and `jant:avatar-remove` events,
6
+ * POSTs to the server, and handles the response (toast, DOM updates).
7
+ */
8
+
9
+ import type {
10
+ SettingsSaveDetail,
11
+ AvatarRemoveDetail,
12
+ } from "../ui/components/settings-types.js";
13
+ import type { JantSettingsGeneral } from "../ui/components/jant-settings-general.js";
14
+ import type { JantSettingsAvatar } from "../ui/components/jant-settings-avatar.js";
15
+ import { showToast } from "./toast.js";
16
+
17
+ function updateSidebarSiteName(siteName: string) {
18
+ const el = document.getElementById("site-name");
19
+ if (el) el.textContent = siteName;
20
+ const titleEl = document.querySelector("title");
21
+ if (titleEl) titleEl.textContent = `Settings - ${siteName}`;
22
+ }
23
+
24
+ // ── Settings save handler ───────────────────────────────────────────
25
+
26
+ document.addEventListener("jant:settings-save", async (e: Event) => {
27
+ const event = e as CustomEvent<SettingsSaveDetail>;
28
+ const { endpoint, data, section } = event.detail;
29
+
30
+ const generalEl = document.querySelector<JantSettingsGeneral>(
31
+ "jant-settings-general",
32
+ );
33
+ const avatarEl = document.querySelector<JantSettingsAvatar>(
34
+ "jant-settings-avatar",
35
+ );
36
+
37
+ try {
38
+ const res = await fetch(endpoint, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ Accept: "application/json",
43
+ },
44
+ body: JSON.stringify(data),
45
+ });
46
+
47
+ if (!res.ok) {
48
+ throw new Error(`HTTP ${res.status}`);
49
+ }
50
+
51
+ const json = await res.json();
52
+
53
+ if (json.status === "redirect") {
54
+ window.location.href = json.url;
55
+ return;
56
+ }
57
+
58
+ if (json.toast) {
59
+ showToast(json.toast);
60
+ }
61
+
62
+ // Update sidebar site name when general settings are saved
63
+ if (section === "general" && json.siteName) {
64
+ updateSidebarSiteName(json.siteName);
65
+ }
66
+
67
+ // Notify the component that save succeeded
68
+ if (section === "avatar-display") {
69
+ avatarEl?.saved();
70
+ } else {
71
+ generalEl?.sectionSaved(section);
72
+ }
73
+ } catch {
74
+ showToast("Failed to save. Please try again.", "error");
75
+
76
+ if (section === "avatar-display") {
77
+ avatarEl?.saveError();
78
+ } else {
79
+ generalEl?.sectionError(section);
80
+ }
81
+ }
82
+ });
83
+
84
+ // ── Avatar remove handler ───────────────────────────────────────────
85
+
86
+ document.addEventListener("jant:avatar-remove", async (e: Event) => {
87
+ const event = e as CustomEvent<AvatarRemoveDetail>;
88
+ const { endpoint } = event.detail;
89
+
90
+ try {
91
+ const res = await fetch(endpoint, {
92
+ method: "POST",
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ Accept: "application/json",
96
+ },
97
+ });
98
+
99
+ if (!res.ok) {
100
+ throw new Error(`HTTP ${res.status}`);
101
+ }
102
+
103
+ const json = await res.json();
104
+
105
+ if (json.status === "redirect") {
106
+ window.location.href = json.url;
107
+ return;
108
+ }
109
+ } catch {
110
+ showToast("Failed to remove avatar. Please try again.", "error");
111
+ }
112
+ });
113
+
114
+ // ── Initialize form data from server-rendered JSON ──────────────────
115
+
116
+ function initSettingsData() {
117
+ const el = document.querySelector<JantSettingsGeneral>(
118
+ "jant-settings-general",
119
+ );
120
+ if (!el) return;
121
+
122
+ const dataEl = document.getElementById("settings-initial-data");
123
+ if (!dataEl?.textContent) return;
124
+
125
+ try {
126
+ const data = JSON.parse(dataEl.textContent);
127
+ el.initData(data);
128
+ } catch {
129
+ // Data parsing failed, form will use defaults
130
+ }
131
+ }
132
+
133
+ // Run after Lit components have upgraded
134
+ if (document.readyState === "loading") {
135
+ document.addEventListener("DOMContentLoaded", initSettingsData);
136
+ } else {
137
+ // Use microtask to let custom elements upgrade first
138
+ queueMicrotask(initSettingsData);
139
+ }
@@ -71,6 +71,40 @@ export interface S3DriverConfig {
71
71
  region: string;
72
72
  }
73
73
 
74
+ /** Constructor for an S3 command object */
75
+ interface S3CommandCtor<TInput> {
76
+ new (input: TInput): unknown;
77
+ }
78
+
79
+ /** Input for PutObject */
80
+ interface PutObjectInput {
81
+ Bucket: string;
82
+ Key: string;
83
+ Body: Uint8Array;
84
+ ContentType?: string;
85
+ }
86
+
87
+ /** Input for GetObject / DeleteObject */
88
+ interface ObjectKeyInput {
89
+ Bucket: string;
90
+ Key: string;
91
+ }
92
+
93
+ /** Subset of GetObjectOutput used by the S3 driver */
94
+ interface S3GetObjectOutput {
95
+ Body?: { transformToWebStream(): ReadableStream };
96
+ ContentType?: string;
97
+ }
98
+
99
+ /** Lazy-loaded S3 client bundle */
100
+ interface S3ClientBundle {
101
+ send: (command: unknown) => Promise<unknown>;
102
+ PutObjectCommand: S3CommandCtor<PutObjectInput>;
103
+ GetObjectCommand: S3CommandCtor<ObjectKeyInput>;
104
+ DeleteObjectCommand: S3CommandCtor<ObjectKeyInput>;
105
+ bucket: string;
106
+ }
107
+
74
108
  /**
75
109
  * Creates an S3-compatible storage driver using the AWS SDK.
76
110
  *
@@ -82,18 +116,7 @@ export interface S3DriverConfig {
82
116
  */
83
117
  export function createS3Driver(config: S3DriverConfig): StorageDriver {
84
118
  // Lazy-load the AWS SDK to avoid bundling it when using R2
85
- let clientPromise: Promise<{
86
- send: (command: unknown) => Promise<unknown>;
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
88
- S3Client: any;
89
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
90
- PutObjectCommand: any;
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
92
- GetObjectCommand: any;
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic import type
94
- DeleteObjectCommand: any;
95
- bucket: string;
96
- }> | null = null;
119
+ let clientPromise: Promise<S3ClientBundle> | null = null;
97
120
 
98
121
  function getClient() {
99
122
  if (!clientPromise) {
@@ -110,7 +133,6 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
110
133
  });
111
134
  return {
112
135
  send: (cmd: unknown) => client.send(cmd as never),
113
- S3Client: sdk.S3Client,
114
136
  PutObjectCommand: sdk.PutObjectCommand,
115
137
  GetObjectCommand: sdk.GetObjectCommand,
116
138
  DeleteObjectCommand: sdk.DeleteObjectCommand,
@@ -163,11 +185,10 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
163
185
  Bucket: s3.bucket,
164
186
  Key: key,
165
187
  });
166
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AWS SDK response type
167
- const response = (await s3.send(command)) as any;
188
+ const response = (await s3.send(command)) as S3GetObjectOutput;
168
189
  if (!response.Body) return null;
169
190
  return {
170
- body: response.Body.transformToWebStream() as ReadableStream,
191
+ body: response.Body.transformToWebStream(),
171
192
  contentType: response.ContentType ?? undefined,
172
193
  };
173
194
  } catch (err: unknown) {
package/src/lib/theme.ts CHANGED
@@ -6,23 +6,21 @@
6
6
 
7
7
  import type { ColorTheme } from "../ui/color-themes.js";
8
8
  import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
9
- import type { JantConfig } from "../types.js";
10
9
 
11
10
  /**
12
11
  * Get the list of available color themes.
13
12
  *
14
- * Returns `config.colorThemes` if provided, otherwise the built-in list.
13
+ * Returns the built-in color theme list.
15
14
  *
16
- * @param config - The Jant configuration
17
15
  * @returns Array of available color themes
18
16
  *
19
17
  * @example
20
18
  * ```typescript
21
- * const themes = getAvailableThemes(c.var.config);
19
+ * const themes = getAvailableThemes();
22
20
  * ```
23
21
  */
24
- export function getAvailableThemes(config: JantConfig): ColorTheme[] {
25
- return config.colorThemes ?? BUILTIN_COLOR_THEMES;
22
+ export function getAvailableThemes(): ColorTheme[] {
23
+ return BUILTIN_COLOR_THEMES;
26
24
  }
27
25
 
28
26
  /**
@@ -32,7 +30,7 @@ export function getAvailableThemes(config: JantConfig): ColorTheme[] {
32
30
  * BaseCoat defaults → selected theme → cssVariables
33
31
  *
34
32
  * @param theme - The active color theme (undefined = no theme overrides)
35
- * @param cssVariables - Extra CSS variable overrides from `createApp({ cssVariables })`
33
+ * @param cssVariables - Extra CSS variable overrides
36
34
  * @returns CSS string to inject in `<head>`, or empty string if nothing to inject
37
35
  *
38
36
  * Uses `:root:root` and `:root.dark` selectors for higher specificity than
@@ -7,14 +7,12 @@
7
7
 
8
8
  import type { Context } from "hono";
9
9
  import type { Bindings, TimelineItemView } from "../types.js";
10
- import type { AppVariables } from "../app.js";
10
+ import type { AppVariables } from "../types/app-context.js";
11
11
  import { buildMediaMap } from "./media-helpers.js";
12
12
  import { createMediaContext, toPostView, toPostViews } from "./view.js";
13
13
 
14
14
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
15
 
16
- const DEFAULT_PAGE_SIZE = 20;
17
-
18
16
  /**
19
17
  * Result from assembling a timeline page.
20
18
  */
@@ -30,7 +28,7 @@ export interface TimelineResult {
30
28
  * Fetches posts using offset-based pagination, batch-loads media, identifies
31
29
  * threads, and returns render-ready `TimelineItemView[]` with page info.
32
30
  *
33
- * @param c - Hono context (provides services + env)
31
+ * @param c - Hono context (provides services + appConfig)
34
32
  * @param options - Optional page number (1-indexed, defaults to 1)
35
33
  * @returns Assembled timeline items with pagination info
36
34
  *
@@ -44,9 +42,7 @@ export async function assembleTimeline(
44
42
  c: Context<Env>,
45
43
  options?: { page?: number },
46
44
  ): Promise<TimelineResult> {
47
- const pageSize =
48
- parseInt(c.env.PAGE_SIZE ?? String(DEFAULT_PAGE_SIZE), 10) ||
49
- DEFAULT_PAGE_SIZE;
45
+ const pageSize = c.var.appConfig.pageSize;
50
46
 
51
47
  const page = Math.max(1, options?.page ?? 1);
52
48
  const offset = (page - 1) * pageSize;
@@ -73,7 +69,7 @@ export async function assembleTimeline(
73
69
  // Batch load media attachments
74
70
  const postIds = posts.map((p) => p.id);
75
71
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
76
- const mediaCtx = createMediaContext(c);
72
+ const mediaCtx = createMediaContext(c.var.appConfig);
77
73
  const mediaMap = buildMediaMap(
78
74
  rawMediaMap,
79
75
  mediaCtx.r2PublicUrl,