@jant/core 0.3.34 → 0.3.36

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 (156) hide show
  1. package/dist/client/assets/module-RjUF93sV.js +716 -0
  2. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  3. package/dist/client/assets/url-8Dj-5CLW.js +1 -0
  4. package/dist/client/client.css +1 -1
  5. package/dist/client/client.js +3109 -2294
  6. package/dist/index.js +3327 -3031
  7. package/package.json +13 -4
  8. package/src/__tests__/helpers/app.ts +1 -1
  9. package/src/__tests__/helpers/db.ts +6 -0
  10. package/src/app.tsx +1 -5
  11. package/src/{lib → client}/avatar-upload.ts +1 -1
  12. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  13. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  14. package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
  15. package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
  16. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
  17. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  18. package/src/client/components/collection-sidebar-types.ts +45 -0
  19. package/src/{ui → client}/components/collection-types.ts +3 -4
  20. package/src/{ui → client}/components/compose-types.ts +3 -1
  21. package/src/{ui → client}/components/jant-collection-form.ts +301 -182
  22. package/src/client/components/jant-collection-sidebar.ts +801 -0
  23. package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
  24. package/src/client/components/jant-compose-editor.ts +1249 -0
  25. package/src/client/components/jant-compose-fullscreen.ts +338 -0
  26. package/src/client/components/jant-media-lightbox.ts +257 -0
  27. package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
  28. package/src/{ui → client}/components/jant-post-form.ts +57 -8
  29. package/src/{ui → client}/components/jant-settings-general.ts +2 -2
  30. package/src/{ui → client}/components/nav-manager-types.ts +3 -0
  31. package/src/{ui → client}/components/post-form-template.ts +35 -31
  32. package/src/{ui → client}/components/post-form-types.ts +7 -3
  33. package/src/{lib → client}/compose-bridge.ts +9 -7
  34. package/src/client/lazy-slugify.ts +51 -0
  35. package/src/{lib → client}/media-upload.ts +16 -3
  36. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  37. package/src/client/page-slug-bridge.ts +42 -0
  38. package/src/{lib → client}/post-form-bridge.ts +2 -2
  39. package/src/{lib → client}/settings-bridge.ts +3 -3
  40. package/src/client/tiptap/bubble-menu.ts +205 -0
  41. package/src/client/tiptap/create-editor.ts +40 -0
  42. package/src/client/tiptap/exitable-marks.ts +73 -0
  43. package/src/client/tiptap/extensions.ts +60 -0
  44. package/src/client/tiptap/image-node.ts +488 -0
  45. package/src/client/tiptap/link-toolbar.ts +371 -0
  46. package/src/client/tiptap/more-break.ts +50 -0
  47. package/src/client/tiptap/paste-image.ts +140 -0
  48. package/src/client/tiptap/slash-commands.ts +328 -0
  49. package/src/{types → client/types}/sortablejs.d.ts +1 -1
  50. package/src/client.ts +24 -17
  51. package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
  52. package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
  53. package/src/db/schema.ts +6 -1
  54. package/src/i18n/locales/en.po +641 -215
  55. package/src/i18n/locales/en.ts +1 -1
  56. package/src/i18n/locales/zh-Hans.po +642 -204
  57. package/src/i18n/locales/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/zh-Hant.po +642 -204
  59. package/src/i18n/locales/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  61. package/src/lib/__tests__/schemas.test.ts +9 -6
  62. package/src/lib/__tests__/url.test.ts +2 -2
  63. package/src/lib/__tests__/view.test.ts +9 -9
  64. package/src/lib/emoji-catalog.ts +146 -0
  65. package/src/lib/feed.ts +1 -1
  66. package/src/lib/media-helpers.ts +10 -9
  67. package/src/lib/render.tsx +4 -3
  68. package/src/lib/resolve-config.ts +8 -1
  69. package/src/lib/schemas.ts +2 -3
  70. package/src/lib/summary.ts +92 -0
  71. package/src/lib/timeline.ts +2 -0
  72. package/src/lib/tiptap-render.ts +196 -0
  73. package/src/lib/upload.ts +97 -9
  74. package/src/lib/url.ts +7 -23
  75. package/src/lib/view.ts +33 -19
  76. package/src/middleware/error-handler.ts +3 -3
  77. package/src/preset.css +38 -0
  78. package/src/routes/api/collections.ts +20 -3
  79. package/src/routes/api/posts.ts +48 -33
  80. package/src/routes/api/upload.ts +7 -5
  81. package/src/routes/auth/reset.tsx +5 -4
  82. package/src/routes/auth/setup.tsx +26 -11
  83. package/src/routes/auth/signin.tsx +10 -7
  84. package/src/routes/compose.tsx +20 -11
  85. package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
  86. package/src/routes/dash/index.tsx +7 -1
  87. package/src/routes/dash/media.tsx +3 -0
  88. package/src/routes/dash/pages.tsx +8 -2
  89. package/src/routes/dash/posts.tsx +6 -2
  90. package/src/routes/dash/redirects.tsx +15 -9
  91. package/src/routes/dash/settings.tsx +336 -32
  92. package/src/routes/feed/__tests__/rss.test.ts +245 -6
  93. package/src/routes/feed/rss.ts +70 -6
  94. package/src/routes/pages/__tests__/featured.test.ts +6 -7
  95. package/src/routes/pages/archive.tsx +11 -7
  96. package/src/routes/pages/collection.tsx +32 -15
  97. package/src/routes/pages/collections.tsx +11 -2
  98. package/src/routes/pages/featured.tsx +1 -1
  99. package/src/routes/pages/home.tsx +1 -1
  100. package/src/services/__tests__/post.test.ts +124 -33
  101. package/src/services/__tests__/settings.test.ts +3 -3
  102. package/src/services/page.ts +16 -3
  103. package/src/services/post.ts +96 -37
  104. package/src/services/search.ts +4 -2
  105. package/src/services/settings.ts +6 -2
  106. package/src/styles/components.css +240 -60
  107. package/src/styles/tokens.css +10 -0
  108. package/src/styles/ui.css +1157 -81
  109. package/src/types/bindings.ts +5 -0
  110. package/src/types/config.ts +23 -1
  111. package/src/types/constants.ts +3 -0
  112. package/src/types/entities.ts +9 -2
  113. package/src/types/operations.ts +9 -3
  114. package/src/types/props.ts +3 -3
  115. package/src/types/views.ts +3 -2
  116. package/src/ui/compose/ComposeDialog.tsx +24 -7
  117. package/src/ui/dash/PageForm.tsx +2 -0
  118. package/src/ui/dash/PostList.tsx +5 -5
  119. package/src/ui/dash/StatusBadge.tsx +13 -5
  120. package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
  121. package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
  122. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  123. package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
  124. package/src/ui/dash/media/MediaListContent.tsx +9 -4
  125. package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
  126. package/src/ui/dash/pages/PagesContent.tsx +2 -1
  127. package/src/ui/dash/posts/PostForm.tsx +19 -7
  128. package/src/ui/dash/settings/AccountContent.tsx +133 -138
  129. package/src/ui/dash/settings/AvatarContent.tsx +70 -0
  130. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  131. package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
  132. package/src/ui/layouts/DashLayout.tsx +157 -75
  133. package/src/ui/layouts/SiteLayout.tsx +13 -13
  134. package/src/ui/pages/ArchivePage.tsx +10 -7
  135. package/src/ui/pages/CollectionPage.tsx +6 -35
  136. package/src/ui/pages/CollectionsPage.tsx +2 -1
  137. package/src/ui/pages/FeaturedPage.tsx +2 -1
  138. package/src/ui/pages/HomePage.tsx +1 -1
  139. package/src/ui/pages/SearchPage.tsx +1 -1
  140. package/src/ui/shared/CollectionsSidebar.tsx +228 -3
  141. package/src/ui/shared/MediaGallery.tsx +179 -41
  142. package/src/lib/collections-reorder.ts +0 -28
  143. package/src/routes/dash/appearance.tsx +0 -240
  144. package/src/routes/dash/collections.tsx +0 -211
  145. package/src/ui/components/jant-compose-editor.ts +0 -814
  146. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  147. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  148. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  149. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  150. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  151. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  152. /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
  153. /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
  154. /package/src/{ui → client}/components/settings-types.ts +0 -0
  155. /package/src/{lib → client}/image-processor.ts +0 -0
  156. /package/src/{lib → client}/toast.ts +0 -0
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Dashboard Settings Routes
3
3
  *
4
- * Sub-pages: General, Account
4
+ * Unified settings hub — root page with iOS-style grouped list,
5
+ * plus sub-pages for General, Avatar, Navigation, Color Theme,
6
+ * Font Theme, Custom CSS, and Account.
5
7
  */
6
8
 
7
9
  import { Hono } from "hono";
@@ -14,18 +16,47 @@ import { getI18n } from "../../i18n/index.js";
14
16
  import { TIMEZONES } from "../../lib/timezones.js";
15
17
  import { escapeHtml } from "../../lib/html.js";
16
18
  import { ValidationError } from "../../lib/errors.js";
19
+ import { SETTINGS_KEYS } from "../../lib/constants.js";
20
+ import { getAvailableThemes } from "../../lib/theme.js";
21
+ import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
22
+ import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
17
23
  import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
24
+ import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
18
25
  import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
26
+ import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
27
+ import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
28
+ import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
29
+ import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
19
30
 
20
31
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
32
 
22
33
  export const settingsRoutes = new Hono<Env>();
23
34
 
24
35
  // ===========================================================================
25
- // General settings
36
+ // Settings root — iOS-style grouped list
26
37
  // ===========================================================================
27
38
 
28
39
  settingsRoutes.get("/", async (c) => {
40
+ const siteName = c.var.appConfig.siteName;
41
+
42
+ return c.html(
43
+ <DashLayout
44
+ c={c}
45
+ title="Settings"
46
+ siteName={siteName}
47
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
48
+ currentPath="/dash/settings"
49
+ >
50
+ <SettingsRootContent />
51
+ </DashLayout>,
52
+ );
53
+ });
54
+
55
+ // ===========================================================================
56
+ // General settings
57
+ // ===========================================================================
58
+
59
+ settingsRoutes.get("/general", async (c) => {
29
60
  const { allSettings, appConfig } = c.var;
30
61
 
31
62
  const dbSiteName = allSettings["SITE_NAME"] ?? "";
@@ -36,10 +67,16 @@ settingsRoutes.get("/", async (c) => {
36
67
  return c.html(
37
68
  <DashLayout
38
69
  c={c}
39
- title="Settings"
70
+ title="General"
40
71
  siteName={dbSiteName || appConfig.fallbacks.siteName}
72
+ siteAvatarUrl={appConfig.siteAvatarUrl}
41
73
  currentPath="/dash/settings"
42
- toast={saved ? { message: "Settings saved successfully." } : undefined}
74
+ breadcrumb={{
75
+ parent: "Settings",
76
+ parentHref: "/dash/settings",
77
+ current: "General",
78
+ }}
79
+ toast={saved ? { message: "Settings updated." } : undefined}
43
80
  >
44
81
  <GeneralContent
45
82
  siteName={dbSiteName || ""}
@@ -47,8 +84,6 @@ settingsRoutes.get("/", async (c) => {
47
84
  siteLanguage={appConfig.siteLanguage}
48
85
  siteNameFallback={appConfig.fallbacks.siteName}
49
86
  siteDescriptionFallback={appConfig.fallbacks.siteDescription}
50
- siteAvatarUrl={appConfig.siteAvatarUrl}
51
- showHeaderAvatar={appConfig.showHeaderAvatar}
52
87
  timeZone={appConfig.timeZone}
53
88
  siteFooter={appConfig.siteFooter}
54
89
  noindex={appConfig.noindex}
@@ -58,7 +93,7 @@ settingsRoutes.get("/", async (c) => {
58
93
  );
59
94
  });
60
95
 
61
- settingsRoutes.post("/", async (c) => {
96
+ settingsRoutes.post("/general", async (c) => {
62
97
  const i18n = getI18n(c);
63
98
  const body = await c.req.json<{
64
99
  siteName: string;
@@ -82,14 +117,14 @@ settingsRoutes.post("/", async (c) => {
82
117
  if (languageChanged) {
83
118
  return c.json({
84
119
  status: "redirect" as const,
85
- url: "/dash/settings?saved",
120
+ url: "/dash/settings/general?saved",
86
121
  });
87
122
  }
88
123
  return c.json({
89
124
  status: "ok" as const,
90
125
  toast: i18n._(
91
126
  msg({
92
- message: "Settings saved successfully.",
127
+ message: "Settings updated.",
93
128
  comment: "@context: Toast after saving general settings",
94
129
  }),
95
130
  ),
@@ -99,20 +134,20 @@ settingsRoutes.post("/", async (c) => {
99
134
 
100
135
  return sse(c, async (stream) => {
101
136
  if (languageChanged) {
102
- await stream.redirect("/dash/settings?saved");
137
+ await stream.redirect("/dash/settings/general?saved");
103
138
  } else {
104
139
  const escaped = escapeHtml(displayName);
105
140
  await stream.patchElements(
106
141
  `<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`,
107
142
  );
108
- await stream.patchElements(`Settings - ${escaped}`, {
143
+ await stream.patchElements(`General - ${escaped}`, {
109
144
  mode: "inner",
110
145
  selector: "title",
111
146
  });
112
147
  await stream.toast(
113
148
  i18n._(
114
149
  msg({
115
- message: "Settings saved successfully.",
150
+ message: "Settings updated.",
116
151
  comment: "@context: Toast after saving general settings",
117
152
  }),
118
153
  ),
@@ -129,7 +164,7 @@ settingsRoutes.post("/", async (c) => {
129
164
  });
130
165
  });
131
166
 
132
- settingsRoutes.post("/seo", async (c) => {
167
+ settingsRoutes.post("/general/seo", async (c) => {
133
168
  const i18n = getI18n(c);
134
169
  const body = await c.req.json<{ noindex: string }>();
135
170
  const { settings } = c.var.services;
@@ -150,7 +185,7 @@ settingsRoutes.post("/seo", async (c) => {
150
185
  status: "ok" as const,
151
186
  toast: i18n._(
152
187
  msg({
153
- message: "SEO settings saved successfully.",
188
+ message: "SEO settings updated.",
154
189
  comment: "@context: Toast after saving SEO settings",
155
190
  }),
156
191
  ),
@@ -161,7 +196,7 @@ settingsRoutes.post("/seo", async (c) => {
161
196
  await stream.toast(
162
197
  i18n._(
163
198
  msg({
164
- message: "SEO settings saved successfully.",
199
+ message: "SEO settings updated.",
165
200
  comment: "@context: Toast after saving SEO settings",
166
201
  }),
167
202
  ),
@@ -174,9 +209,35 @@ settingsRoutes.post("/seo", async (c) => {
174
209
  });
175
210
 
176
211
  // ===========================================================================
177
- // Avatar upload & removal
212
+ // Avatar
178
213
  // ===========================================================================
179
214
 
215
+ settingsRoutes.get("/avatar", async (c) => {
216
+ const siteName = c.var.appConfig.siteName;
217
+ const saved = c.req.query("saved") !== undefined;
218
+
219
+ return c.html(
220
+ <DashLayout
221
+ c={c}
222
+ title="Avatar"
223
+ siteName={siteName}
224
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
225
+ currentPath="/dash/settings"
226
+ breadcrumb={{
227
+ parent: "Settings",
228
+ parentHref: "/dash/settings",
229
+ current: "Avatar",
230
+ }}
231
+ toast={saved ? { message: "Avatar updated." } : undefined}
232
+ >
233
+ <AvatarContent
234
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
235
+ showHeaderAvatar={c.var.appConfig.showHeaderAvatar}
236
+ />
237
+ </DashLayout>,
238
+ );
239
+ });
240
+
180
241
  settingsRoutes.post("/avatar", async (c) => {
181
242
  const i18n = getI18n(c);
182
243
  const storage = c.var.storage;
@@ -184,7 +245,7 @@ settingsRoutes.post("/avatar", async (c) => {
184
245
  return dsToast(
185
246
  i18n._(
186
247
  msg({
187
- message: "Storage not configured.",
248
+ message: "File storage isn't set up. Check your server config.",
188
249
  comment: "@context: Error toast when file storage is not set up",
189
250
  }),
190
251
  ),
@@ -198,7 +259,7 @@ settingsRoutes.post("/avatar", async (c) => {
198
259
  return dsToast(
199
260
  i18n._(
200
261
  msg({
201
- message: "No file provided.",
262
+ message: "No file selected. Choose a file to upload.",
202
263
  comment: "@context: Error toast when no file was selected for upload",
203
264
  }),
204
265
  ),
@@ -222,10 +283,11 @@ settingsRoutes.post("/avatar", async (c) => {
222
283
  media: c.var.services.media,
223
284
  storage,
224
285
  storageProvider: c.var.appConfig.storageDriver,
286
+ maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
225
287
  },
226
288
  );
227
289
 
228
- return dsRedirect("/dash/settings?saved");
290
+ return dsRedirect("/dash/settings/avatar?saved");
229
291
  } catch (e) {
230
292
  if (e instanceof ValidationError) {
231
293
  return dsToast(e.message, "error");
@@ -233,7 +295,7 @@ settingsRoutes.post("/avatar", async (c) => {
233
295
  return dsToast(
234
296
  i18n._(
235
297
  msg({
236
- message: "Upload failed. Please try again.",
298
+ message: "Upload didn't go through. Try again in a moment.",
237
299
  comment: "@context: Error toast when avatar upload fails",
238
300
  }),
239
301
  ),
@@ -248,10 +310,13 @@ settingsRoutes.post("/avatar/remove", async (c) => {
248
310
  // ── JSON response mode (used by Lit settings bridge) ──────────────
249
311
  const wantsJson = c.req.header("accept")?.includes("application/json");
250
312
  if (wantsJson) {
251
- return c.json({ status: "redirect" as const, url: "/dash/settings?saved" });
313
+ return c.json({
314
+ status: "redirect" as const,
315
+ url: "/dash/settings/avatar?saved",
316
+ });
252
317
  }
253
318
 
254
- return dsRedirect("/dash/settings?saved");
319
+ return dsRedirect("/dash/settings/avatar?saved");
255
320
  });
256
321
 
257
322
  settingsRoutes.post("/avatar/display", async (c) => {
@@ -272,7 +337,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
272
337
  status: "ok" as const,
273
338
  toast: i18n._(
274
339
  msg({
275
- message: "Avatar display setting saved successfully.",
340
+ message: "Avatar display updated.",
276
341
  comment: "@context: Toast after saving avatar display preference",
277
342
  }),
278
343
  ),
@@ -283,7 +348,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
283
348
  await stream.toast(
284
349
  i18n._(
285
350
  msg({
286
- message: "Avatar display setting saved successfully.",
351
+ message: "Avatar display updated.",
287
352
  comment: "@context: Toast after saving avatar display preference",
288
353
  }),
289
354
  ),
@@ -295,6 +360,238 @@ settingsRoutes.post("/avatar/display", async (c) => {
295
360
  });
296
361
  });
297
362
 
363
+ // ===========================================================================
364
+ // Navigation (moved from appearance routes)
365
+ // ===========================================================================
366
+
367
+ settingsRoutes.get("/navigation", async (c) => {
368
+ const [navItems, availablePages] = await Promise.all([
369
+ c.var.services.navItems.list(),
370
+ c.var.services.pages.listNotInNav(),
371
+ ]);
372
+ const siteName = c.var.appConfig.siteName;
373
+ const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
374
+ const homeDefaultView = c.var.appConfig.homeDefaultView;
375
+
376
+ return c.html(
377
+ <DashLayout
378
+ c={c}
379
+ title="Navigation"
380
+ siteName={siteName}
381
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
382
+ currentPath="/dash/settings"
383
+ breadcrumb={{
384
+ parent: "Settings",
385
+ parentHref: "/dash/settings",
386
+ current: "Navigation",
387
+ }}
388
+ >
389
+ <NavigationContent
390
+ navItems={navItems}
391
+ availablePages={availablePages}
392
+ headerNavMaxVisible={headerNavMaxVisible}
393
+ homeDefaultView={homeDefaultView}
394
+ siteName={siteName}
395
+ />
396
+ </DashLayout>,
397
+ );
398
+ });
399
+
400
+ settingsRoutes.post("/navigation/nav-max-visible", async (c) => {
401
+ const body = await c.req.json<{ value: number }>();
402
+ const { settings } = c.var.services;
403
+
404
+ const navMax = Math.max(0, Math.min(5, body.value ?? 3));
405
+ if (navMax !== 3) {
406
+ await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
407
+ } else {
408
+ await settings.remove("HEADER_NAV_MAX_VISIBLE");
409
+ }
410
+
411
+ return c.json({ ok: true });
412
+ });
413
+
414
+ settingsRoutes.post("/navigation/home-default-view", async (c) => {
415
+ const body = await c.req.json<{ value: string }>();
416
+ const { settings } = c.var.services;
417
+
418
+ if (body.value === "featured") {
419
+ await settings.set("HOME_DEFAULT_VIEW", "featured");
420
+ } else {
421
+ await settings.remove("HOME_DEFAULT_VIEW");
422
+ }
423
+
424
+ return c.json({ ok: true });
425
+ });
426
+
427
+ // ===========================================================================
428
+ // Color Theme (moved from appearance routes)
429
+ // ===========================================================================
430
+
431
+ settingsRoutes.get("/color-theme", async (c) => {
432
+ const siteName = c.var.appConfig.siteName;
433
+ const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
434
+ const currentThemeId =
435
+ c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
436
+ const themes = getAvailableThemes();
437
+ const saved = c.req.query("saved") !== undefined;
438
+
439
+ return c.html(
440
+ <DashLayout
441
+ c={c}
442
+ title="Color Theme"
443
+ siteName={siteName}
444
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
445
+ currentPath="/dash/settings"
446
+ breadcrumb={{
447
+ parent: "Settings",
448
+ parentHref: "/dash/settings",
449
+ current: "Color Theme",
450
+ }}
451
+ toast={saved ? { message: "Theme updated." } : undefined}
452
+ >
453
+ <ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
454
+ </DashLayout>,
455
+ );
456
+ });
457
+
458
+ settingsRoutes.post("/color-theme", async (c) => {
459
+ const i18n = getI18n(c);
460
+ const body = await c.req.json<{ theme: string }>();
461
+ const { settings } = c.var.services;
462
+ const themes = getAvailableThemes();
463
+
464
+ const validTheme = themes.find((t) => t.id === body.theme);
465
+ if (!validTheme) {
466
+ return dsToast(
467
+ i18n._(
468
+ msg({
469
+ message: "That theme isn't available. Pick another one.",
470
+ comment: "@context: Error toast when selected theme is not valid",
471
+ }),
472
+ ),
473
+ "error",
474
+ );
475
+ }
476
+
477
+ const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
478
+ if (validTheme.id === defaultThemeId) {
479
+ await settings.remove(SETTINGS_KEYS.THEME);
480
+ } else {
481
+ await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
482
+ }
483
+
484
+ return dsRedirect("/dash/settings/color-theme?saved");
485
+ });
486
+
487
+ // ===========================================================================
488
+ // Font Theme (moved from appearance routes)
489
+ // ===========================================================================
490
+
491
+ settingsRoutes.get("/font-theme", async (c) => {
492
+ const siteName = c.var.appConfig.siteName;
493
+ const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
494
+ const saved = c.req.query("saved") !== undefined;
495
+
496
+ return c.html(
497
+ <DashLayout
498
+ c={c}
499
+ title="Font Theme"
500
+ siteName={siteName}
501
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
502
+ currentPath="/dash/settings"
503
+ breadcrumb={{
504
+ parent: "Settings",
505
+ parentHref: "/dash/settings",
506
+ current: "Font Theme",
507
+ }}
508
+ toast={saved ? { message: "Font theme updated." } : undefined}
509
+ >
510
+ <FontThemeContent
511
+ fontThemes={BUILTIN_FONT_THEMES}
512
+ currentFontThemeId={currentFontThemeId}
513
+ />
514
+ </DashLayout>,
515
+ );
516
+ });
517
+
518
+ settingsRoutes.post("/font-theme", async (c) => {
519
+ const i18n = getI18n(c);
520
+ const body = await c.req.json<{ fontTheme: string }>();
521
+ const { settings } = c.var.services;
522
+
523
+ const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
524
+ if (!validFont) {
525
+ return dsToast(
526
+ i18n._(
527
+ msg({
528
+ message: "That font theme isn't available. Pick another one.",
529
+ comment:
530
+ "@context: Error toast when selected font theme is not valid",
531
+ }),
532
+ ),
533
+ "error",
534
+ );
535
+ }
536
+
537
+ if (validFont.id === "default") {
538
+ await settings.remove("FONT_THEME");
539
+ } else {
540
+ await settings.set("FONT_THEME", validFont.id);
541
+ }
542
+
543
+ return dsRedirect("/dash/settings/font-theme?saved");
544
+ });
545
+
546
+ // ===========================================================================
547
+ // Custom CSS (moved from appearance routes)
548
+ // ===========================================================================
549
+
550
+ settingsRoutes.get("/custom-css", async (c) => {
551
+ const siteName = c.var.appConfig.siteName;
552
+ const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
553
+
554
+ return c.html(
555
+ <DashLayout
556
+ c={c}
557
+ title="Custom CSS"
558
+ siteName={siteName}
559
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
560
+ currentPath="/dash/settings"
561
+ breadcrumb={{
562
+ parent: "Settings",
563
+ parentHref: "/dash/settings",
564
+ current: "Custom CSS",
565
+ }}
566
+ >
567
+ <AdvancedContent customCSS={customCSS} />
568
+ </DashLayout>,
569
+ );
570
+ });
571
+
572
+ settingsRoutes.post("/custom-css", async (c) => {
573
+ const i18n = getI18n(c);
574
+ const body = await c.req.json<{ customCSS: string }>();
575
+ const { settings } = c.var.services;
576
+
577
+ const css = body.customCSS?.trim() ?? "";
578
+
579
+ if (css) {
580
+ await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
581
+ } else {
582
+ await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
583
+ }
584
+
585
+ return dsToast(
586
+ i18n._(
587
+ msg({
588
+ message: "Custom CSS updated.",
589
+ comment: "@context: Toast after saving custom CSS",
590
+ }),
591
+ ),
592
+ );
593
+ });
594
+
298
595
  // ===========================================================================
299
596
  // Account
300
597
  // ===========================================================================
@@ -310,10 +607,16 @@ settingsRoutes.get("/account", async (c) => {
310
607
  return c.html(
311
608
  <DashLayout
312
609
  c={c}
313
- title="Settings"
610
+ title="Account"
314
611
  siteName={siteName}
612
+ siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
315
613
  currentPath="/dash/settings"
316
- toast={saved ? { message: "Profile saved successfully." } : undefined}
614
+ breadcrumb={{
615
+ parent: "Settings",
616
+ parentHref: "/dash/settings",
617
+ current: "Account",
618
+ }}
619
+ toast={saved ? { message: "Profile updated." } : undefined}
317
620
  >
318
621
  <AccountContent userName={userName} />
319
622
  </DashLayout>,
@@ -329,7 +632,7 @@ settingsRoutes.post("/account", async (c) => {
329
632
  return dsToast(
330
633
  i18n._(
331
634
  msg({
332
- message: "Name is required.",
635
+ message: "A display name is required.",
333
636
  comment: "@context: Error toast when display name is empty",
334
637
  }),
335
638
  ),
@@ -346,7 +649,7 @@ settingsRoutes.post("/account", async (c) => {
346
649
  return dsToast(
347
650
  i18n._(
348
651
  msg({
349
- message: "Failed to update profile.",
652
+ message: "Couldn't update your profile. Try again in a moment.",
350
653
  comment: "@context: Error toast when profile update fails",
351
654
  }),
352
655
  ),
@@ -357,7 +660,7 @@ settingsRoutes.post("/account", async (c) => {
357
660
  return dsToast(
358
661
  i18n._(
359
662
  msg({
360
- message: "Profile saved successfully.",
663
+ message: "Profile updated.",
361
664
  comment: "@context: Toast after saving user profile",
362
665
  }),
363
666
  ),
@@ -376,7 +679,8 @@ settingsRoutes.post("/password", async (c) => {
376
679
  return dsToast(
377
680
  i18n._(
378
681
  msg({
379
- message: "Passwords do not match.",
682
+ message:
683
+ "Passwords don't match. Make sure both fields are identical.",
380
684
  comment:
381
685
  "@context: Error toast when new password and confirmation differ",
382
686
  }),
@@ -398,7 +702,7 @@ settingsRoutes.post("/password", async (c) => {
398
702
  return dsToast(
399
703
  i18n._(
400
704
  msg({
401
- message: "Current password is incorrect.",
705
+ message: "Current password doesn't match. Try again.",
402
706
  comment:
403
707
  "@context: Error toast when current password verification fails",
404
708
  }),
@@ -411,7 +715,7 @@ settingsRoutes.post("/password", async (c) => {
411
715
  await stream.toast(
412
716
  i18n._(
413
717
  msg({
414
- message: "Password changed successfully.",
718
+ message: "Password changed.",
415
719
  comment: "@context: Toast after changing account password",
416
720
  }),
417
721
  ),