@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,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,