@jant/core 0.3.25 → 0.3.27

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 (133) hide show
  1. package/dist/app.js +70 -563
  2. package/dist/auth.js +3 -0
  3. package/dist/client.js +1 -0
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/lib/avatar-upload.js +134 -0
  8. package/dist/lib/config.js +39 -0
  9. package/dist/lib/constants.js +10 -10
  10. package/dist/lib/favicon.js +102 -0
  11. package/dist/lib/image.js +13 -17
  12. package/dist/lib/media-helpers.js +2 -2
  13. package/dist/lib/navigation.js +23 -3
  14. package/dist/lib/render.js +10 -1
  15. package/dist/lib/schemas.js +31 -0
  16. package/dist/lib/timezones.js +388 -0
  17. package/dist/lib/view.js +1 -1
  18. package/dist/routes/api/posts.js +1 -1
  19. package/dist/routes/api/upload.js +3 -3
  20. package/dist/routes/auth/reset.js +221 -0
  21. package/dist/routes/auth/setup.js +194 -0
  22. package/dist/routes/auth/signin.js +176 -0
  23. package/dist/routes/dash/collections.js +23 -415
  24. package/dist/routes/dash/media.js +12 -392
  25. package/dist/routes/dash/pages.js +7 -330
  26. package/dist/routes/dash/redirects.js +18 -12
  27. package/dist/routes/dash/settings.js +198 -577
  28. package/dist/routes/feed/rss.js +2 -1
  29. package/dist/routes/feed/sitemap.js +4 -2
  30. package/dist/routes/pages/featured.js +5 -1
  31. package/dist/routes/pages/home.js +26 -1
  32. package/dist/routes/pages/latest.js +45 -0
  33. package/dist/services/post.js +30 -50
  34. package/dist/types/bindings.js +3 -0
  35. package/dist/types/config.js +147 -0
  36. package/dist/types/constants.js +27 -0
  37. package/dist/types/entities.js +3 -0
  38. package/dist/types/operations.js +3 -0
  39. package/dist/types/props.js +3 -0
  40. package/dist/types/views.js +5 -0
  41. package/dist/types.js +8 -111
  42. package/dist/ui/color-themes.js +33 -33
  43. package/dist/ui/compose/ComposeDialog.js +36 -21
  44. package/dist/ui/dash/PageForm.js +21 -15
  45. package/dist/ui/dash/PostForm.js +22 -16
  46. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  47. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  48. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  49. package/dist/ui/dash/media/MediaListContent.js +166 -0
  50. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  51. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  52. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  53. package/dist/ui/dash/settings/AccountContent.js +209 -0
  54. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  55. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  56. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  57. package/dist/ui/font-themes.js +36 -0
  58. package/dist/ui/layouts/BaseLayout.js +24 -2
  59. package/dist/ui/layouts/SiteLayout.js +47 -19
  60. package/package.json +1 -1
  61. package/src/app.tsx +95 -553
  62. package/src/auth.ts +4 -1
  63. package/src/client.ts +1 -0
  64. package/src/i18n/locales/en.po +240 -175
  65. package/src/i18n/locales/en.ts +1 -1
  66. package/src/i18n/locales/zh-Hans.po +240 -175
  67. package/src/i18n/locales/zh-Hans.ts +1 -1
  68. package/src/i18n/locales/zh-Hant.po +240 -175
  69. package/src/i18n/locales/zh-Hant.ts +1 -1
  70. package/src/lib/__tests__/config.test.ts +192 -0
  71. package/src/lib/__tests__/favicon.test.ts +151 -0
  72. package/src/lib/__tests__/image.test.ts +2 -6
  73. package/src/lib/__tests__/timezones.test.ts +61 -0
  74. package/src/lib/__tests__/view.test.ts +2 -2
  75. package/src/lib/avatar-upload.ts +165 -0
  76. package/src/lib/config.ts +47 -0
  77. package/src/lib/constants.ts +19 -11
  78. package/src/lib/favicon.ts +115 -0
  79. package/src/lib/image.ts +13 -21
  80. package/src/lib/media-helpers.ts +2 -2
  81. package/src/lib/navigation.ts +33 -2
  82. package/src/lib/render.tsx +15 -1
  83. package/src/lib/schemas.ts +39 -0
  84. package/src/lib/timezones.ts +325 -0
  85. package/src/lib/view.ts +1 -1
  86. package/src/routes/api/posts.ts +1 -1
  87. package/src/routes/api/upload.ts +2 -3
  88. package/src/routes/auth/reset.tsx +239 -0
  89. package/src/routes/auth/setup.tsx +189 -0
  90. package/src/routes/auth/signin.tsx +163 -0
  91. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  92. package/src/routes/dash/collections.tsx +17 -366
  93. package/src/routes/dash/media.tsx +12 -414
  94. package/src/routes/dash/pages.tsx +8 -348
  95. package/src/routes/dash/redirects.tsx +20 -14
  96. package/src/routes/dash/settings.tsx +243 -534
  97. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  98. package/src/routes/feed/rss.ts +3 -1
  99. package/src/routes/feed/sitemap.ts +4 -2
  100. package/src/routes/pages/featured.tsx +7 -1
  101. package/src/routes/pages/home.tsx +25 -2
  102. package/src/routes/pages/latest.tsx +59 -0
  103. package/src/services/post.ts +34 -66
  104. package/src/styles/components.css +0 -65
  105. package/src/styles/tokens.css +1 -1
  106. package/src/styles/ui.css +24 -40
  107. package/src/types/bindings.ts +30 -0
  108. package/src/types/config.ts +183 -0
  109. package/src/types/constants.ts +26 -0
  110. package/src/types/entities.ts +109 -0
  111. package/src/types/operations.ts +88 -0
  112. package/src/types/props.ts +115 -0
  113. package/src/types/views.ts +172 -0
  114. package/src/types.ts +8 -644
  115. package/src/ui/__tests__/font-themes.test.ts +34 -0
  116. package/src/ui/color-themes.ts +34 -34
  117. package/src/ui/compose/ComposeDialog.tsx +40 -21
  118. package/src/ui/dash/PageForm.tsx +25 -19
  119. package/src/ui/dash/PostForm.tsx +26 -20
  120. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  121. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  122. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  123. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  124. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  125. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  126. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  127. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  128. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  129. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  130. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  131. package/src/ui/font-themes.ts +54 -0
  132. package/src/ui/layouts/BaseLayout.tsx +17 -0
  133. package/src/ui/layouts/SiteLayout.tsx +45 -31
@@ -1,593 +1,49 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Dashboard Settings Routes
4
4
  *
5
5
  * Sub-pages: General, Appearance, Account
6
6
  */ import { Hono } from "hono";
7
- import { useLingui as $_useLingui } from "@jant/core/i18n";
8
7
  import { DashLayout } from "../../ui/layouts/DashLayout.js";
9
8
  import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
10
- import { getSiteLanguage, getSiteName, getConfigFallback } from "../../lib/config.js";
9
+ import { arrayBufferToBase64 } from "../../lib/favicon.js";
10
+ import { getSiteLanguage, getSiteName, getHomeDefaultView, getTimeZone, getSiteFooter, isNoIndex, getConfigFallback } from "../../lib/config.js";
11
11
  import { SETTINGS_KEYS } from "../../lib/constants.js";
12
12
  import { getAvailableThemes } from "../../lib/theme.js";
13
+ import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
14
+ import { TIMEZONES } from "../../lib/timezones.js";
15
+ import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
16
+ import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
17
+ import { AppearanceContent } from "../../ui/dash/settings/AppearanceContent.js";
18
+ import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
13
19
  /** Escape HTML special characters for safe insertion into HTML strings */ function escapeHtml(str) {
14
20
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
15
21
  }
16
22
  export const settingsRoutes = new Hono();
17
- function SettingsNav({ currentTab }) {
18
- const { i18n: $__i18n, _: $__ } = $_useLingui();
19
- const tabs = [
20
- {
21
- id: "general",
22
- label: $__i18n._({
23
- id: "Weq9zb",
24
- message: "General"
25
- }),
26
- href: "/dash/settings"
27
- },
28
- {
29
- id: "appearance",
30
- label: $__i18n._({
31
- id: "aAIQg2",
32
- message: "Appearance"
33
- }),
34
- href: "/dash/settings/appearance"
35
- },
36
- {
37
- id: "account",
38
- label: $__i18n._({
39
- id: "AeXO77",
40
- message: "Account"
41
- }),
42
- href: "/dash/settings/account"
43
- }
44
- ];
45
- return /*#__PURE__*/ _jsx("nav", {
46
- class: "flex gap-1 mb-6",
47
- children: tabs.map((tab)=>/*#__PURE__*/ _jsx("a", {
48
- href: tab.href,
49
- class: `px-3 py-2 text-sm rounded-md ${tab.id === currentTab ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`,
50
- children: tab.label
51
- }, tab.id))
52
- });
53
- }
54
- // ---------------------------------------------------------------------------
55
- // General tab
56
- // ---------------------------------------------------------------------------
57
- function GeneralContent({ siteName, siteDescription, siteLanguage, siteNameFallback, siteDescriptionFallback }) {
58
- const { i18n: $__i18n, _: $__ } = $_useLingui();
59
- const generalSignals = JSON.stringify({
60
- siteName,
61
- siteDescription,
62
- siteLanguage
63
- }).replace(/</g, "\\u003c");
64
- return /*#__PURE__*/ _jsxs(_Fragment, {
65
- children: [
66
- /*#__PURE__*/ _jsx("h1", {
67
- class: "text-2xl font-semibold mb-2",
68
- children: $__i18n._({
69
- id: "Tz0i8g",
70
- message: "Settings"
71
- })
72
- }),
73
- /*#__PURE__*/ _jsx(SettingsNav, {
74
- currentTab: "general"
75
- }),
76
- /*#__PURE__*/ _jsx("div", {
77
- class: "flex flex-col gap-6 max-w-lg",
78
- children: /*#__PURE__*/ _jsxs("form", {
79
- "data-signals": generalSignals,
80
- "data-on:submit__prevent": "@post('/dash/settings')",
81
- "data-indicator": "_loading",
82
- children: [
83
- /*#__PURE__*/ _jsxs("div", {
84
- class: "card",
85
- children: [
86
- /*#__PURE__*/ _jsx("header", {
87
- children: /*#__PURE__*/ _jsx("h2", {
88
- children: $__i18n._({
89
- id: "Weq9zb",
90
- message: "General"
91
- })
92
- })
93
- }),
94
- /*#__PURE__*/ _jsxs("section", {
95
- class: "flex flex-col gap-4",
96
- children: [
97
- /*#__PURE__*/ _jsxs("div", {
98
- class: "field",
99
- children: [
100
- /*#__PURE__*/ _jsx("label", {
101
- class: "label",
102
- children: $__i18n._({
103
- id: "SJmfuf",
104
- message: "Site Name"
105
- })
106
- }),
107
- /*#__PURE__*/ _jsx("input", {
108
- type: "text",
109
- "data-bind": "siteName",
110
- class: "input",
111
- placeholder: siteNameFallback
112
- })
113
- ]
114
- }),
115
- /*#__PURE__*/ _jsxs("div", {
116
- class: "field",
117
- children: [
118
- /*#__PURE__*/ _jsx("label", {
119
- class: "label",
120
- children: $__i18n._({
121
- id: "u2f7vd",
122
- message: "Site Description"
123
- })
124
- }),
125
- /*#__PURE__*/ _jsx("textarea", {
126
- "data-bind": "siteDescription",
127
- class: "textarea",
128
- rows: 3,
129
- placeholder: siteDescriptionFallback,
130
- children: siteDescription
131
- })
132
- ]
133
- }),
134
- /*#__PURE__*/ _jsxs("div", {
135
- class: "field",
136
- children: [
137
- /*#__PURE__*/ _jsx("label", {
138
- class: "label",
139
- children: $__i18n._({
140
- id: "vXIe7J",
141
- message: "Language"
142
- })
143
- }),
144
- /*#__PURE__*/ _jsxs("select", {
145
- "data-bind": "siteLanguage",
146
- class: "select",
147
- children: [
148
- /*#__PURE__*/ _jsx("option", {
149
- value: "en",
150
- selected: siteLanguage === "en",
151
- children: "English"
152
- }),
153
- /*#__PURE__*/ _jsx("option", {
154
- value: "zh-Hans",
155
- selected: siteLanguage === "zh-Hans",
156
- children: "简体中文"
157
- }),
158
- /*#__PURE__*/ _jsx("option", {
159
- value: "zh-Hant",
160
- selected: siteLanguage === "zh-Hant",
161
- children: "繁體中文"
162
- })
163
- ]
164
- })
165
- ]
166
- })
167
- ]
168
- })
169
- ]
170
- }),
171
- /*#__PURE__*/ _jsxs("button", {
172
- type: "submit",
173
- class: "btn mt-4",
174
- "data-attr-disabled": "$_loading",
175
- children: [
176
- /*#__PURE__*/ _jsx("span", {
177
- "data-show": "!$_loading",
178
- children: $__i18n._({
179
- id: "UGT5vp",
180
- message: "Save Settings"
181
- })
182
- }),
183
- /*#__PURE__*/ _jsx("span", {
184
- "data-show": "$_loading",
185
- children: $__i18n._({
186
- id: "k1ifdL",
187
- message: "Processing..."
188
- })
189
- })
190
- ]
191
- })
192
- ]
193
- })
194
- })
195
- ]
196
- });
197
- }
198
- // ---------------------------------------------------------------------------
199
- // Appearance tab
200
- // ---------------------------------------------------------------------------
201
- function ThemeCard({ theme, selected }) {
202
- const expr = `$theme === '${theme.id}'`;
203
- const { preview } = theme;
204
- return /*#__PURE__*/ _jsx("label", {
205
- class: `block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`,
206
- "data-class:border-primary": expr,
207
- "data-class:border-border": `$theme !== '${theme.id}'`,
208
- children: /*#__PURE__*/ _jsxs("div", {
209
- class: "grid grid-cols-2",
210
- children: [
211
- /*#__PURE__*/ _jsxs("div", {
212
- class: "p-5",
213
- style: `background-color:${preview.lightBg};color:${preview.lightText}`,
214
- children: [
215
- /*#__PURE__*/ _jsx("input", {
216
- type: "radio",
217
- name: "theme",
218
- value: theme.id,
219
- "data-bind": "theme",
220
- checked: selected || undefined,
221
- class: "mb-1"
222
- }),
223
- /*#__PURE__*/ _jsx("h3", {
224
- class: "font-bold text-lg",
225
- children: theme.name
226
- }),
227
- /*#__PURE__*/ _jsxs("p", {
228
- class: "text-sm mt-2 leading-relaxed",
229
- children: [
230
- "This is the ",
231
- theme.name,
232
- " theme in light mode. Links",
233
- " ",
234
- /*#__PURE__*/ _jsx("a", {
235
- tabIndex: -1,
236
- class: "underline",
237
- style: `color:${preview.lightLink}`,
238
- children: "look like this"
239
- }),
240
- ". We'll show the correct light or dark mode based on your visitor's settings."
241
- ]
242
- })
243
- ]
244
- }),
245
- /*#__PURE__*/ _jsxs("div", {
246
- class: "p-5",
247
- style: `background-color:${preview.darkBg};color:${preview.darkText}`,
248
- children: [
249
- /*#__PURE__*/ _jsx("h3", {
250
- class: "font-bold text-lg",
251
- children: theme.name
252
- }),
253
- /*#__PURE__*/ _jsxs("p", {
254
- class: "text-sm mt-2 leading-relaxed",
255
- children: [
256
- "This is the ",
257
- theme.name,
258
- " theme in dark mode. Links",
259
- " ",
260
- /*#__PURE__*/ _jsx("a", {
261
- tabIndex: -1,
262
- class: "underline",
263
- style: `color:${preview.darkLink}`,
264
- children: "look like this"
265
- }),
266
- ". We'll show the correct light or dark mode based on your visitor's settings."
267
- ]
268
- })
269
- ]
270
- })
271
- ]
272
- })
273
- });
274
- }
275
- function AppearanceContent({ themes, currentThemeId, customCSS }) {
276
- const { i18n: $__i18n, _: $__ } = $_useLingui();
277
- const themeSignals = JSON.stringify({
278
- theme: currentThemeId
279
- }).replace(/</g, "\\u003c");
280
- const cssSignals = JSON.stringify({
281
- customCSS
282
- }).replace(/</g, "\\u003c");
283
- return /*#__PURE__*/ _jsxs(_Fragment, {
284
- children: [
285
- /*#__PURE__*/ _jsx("h1", {
286
- class: "text-2xl font-semibold mb-2",
287
- children: $__i18n._({
288
- id: "Tz0i8g",
289
- message: "Settings"
290
- })
291
- }),
292
- /*#__PURE__*/ _jsx(SettingsNav, {
293
- currentTab: "appearance"
294
- }),
295
- /*#__PURE__*/ _jsx("div", {
296
- "data-signals": themeSignals,
297
- "data-on:change": "@post('/dash/settings/appearance')",
298
- class: "max-w-3xl",
299
- children: /*#__PURE__*/ _jsxs("fieldset", {
300
- children: [
301
- /*#__PURE__*/ _jsx("legend", {
302
- class: "text-lg font-semibold",
303
- children: $__i18n._({
304
- id: "rFmBG3",
305
- message: "Color theme"
306
- })
307
- }),
308
- /*#__PURE__*/ _jsx("p", {
309
- class: "text-sm text-muted-foreground mb-4",
310
- children: $__i18n._({
311
- id: "07Epll",
312
- message: "This will theme both your site and your dashboard. All color themes support dark mode."
313
- })
314
- }),
315
- /*#__PURE__*/ _jsx("div", {
316
- class: "flex flex-col gap-4",
317
- children: themes.map((theme)=>/*#__PURE__*/ _jsx(ThemeCard, {
318
- theme: theme,
319
- selected: theme.id === currentThemeId
320
- }, theme.id))
321
- })
322
- ]
323
- })
324
- }),
325
- /*#__PURE__*/ _jsxs("form", {
326
- "data-signals": cssSignals,
327
- "data-on:submit__prevent": "@post('/dash/settings/custom-css')",
328
- "data-indicator": "_cssLoading",
329
- class: "max-w-3xl mt-8",
330
- children: [
331
- /*#__PURE__*/ _jsxs("fieldset", {
332
- children: [
333
- /*#__PURE__*/ _jsx("legend", {
334
- class: "text-lg font-semibold",
335
- children: $__i18n._({
336
- id: "9+vGLh",
337
- message: "Custom CSS"
338
- })
339
- }),
340
- /*#__PURE__*/ _jsx("p", {
341
- class: "text-sm text-muted-foreground mb-4",
342
- children: $__i18n._({
343
- id: "vmQmHx",
344
- message: "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements."
345
- })
346
- }),
347
- /*#__PURE__*/ _jsx("textarea", {
348
- "data-bind": "customCSS",
349
- class: "textarea font-mono text-sm min-h-32",
350
- rows: 8,
351
- placeholder: $__i18n._({
352
- id: "wc+17X",
353
- message: "/* Your custom CSS here */"
354
- }),
355
- children: customCSS
356
- })
357
- ]
358
- }),
359
- /*#__PURE__*/ _jsxs("button", {
360
- type: "submit",
361
- class: "btn mt-4",
362
- "data-attr-disabled": "$_cssLoading",
363
- children: [
364
- /*#__PURE__*/ _jsx("span", {
365
- "data-show": "!$_cssLoading",
366
- children: $__i18n._({
367
- id: "NU2Fqi",
368
- message: "Save CSS"
369
- })
370
- }),
371
- /*#__PURE__*/ _jsx("span", {
372
- "data-show": "$_cssLoading",
373
- children: $__i18n._({
374
- id: "k1ifdL",
375
- message: "Processing..."
376
- })
377
- })
378
- ]
379
- })
380
- ]
381
- })
382
- ]
383
- });
384
- }
385
- // ---------------------------------------------------------------------------
386
- // Account tab
387
- // ---------------------------------------------------------------------------
388
- function AccountContent({ userName }) {
389
- const { i18n: $__i18n, _: $__ } = $_useLingui();
390
- const profileSignals = JSON.stringify({
391
- userName
392
- }).replace(/</g, "\\u003c");
393
- return /*#__PURE__*/ _jsxs(_Fragment, {
394
- children: [
395
- /*#__PURE__*/ _jsx("h1", {
396
- class: "text-2xl font-semibold mb-2",
397
- children: $__i18n._({
398
- id: "Tz0i8g",
399
- message: "Settings"
400
- })
401
- }),
402
- /*#__PURE__*/ _jsx(SettingsNav, {
403
- currentTab: "account"
404
- }),
405
- /*#__PURE__*/ _jsxs("div", {
406
- class: "flex flex-col gap-6 max-w-lg",
407
- children: [
408
- /*#__PURE__*/ _jsxs("form", {
409
- "data-signals": profileSignals,
410
- "data-on:submit__prevent": "@post('/dash/settings/account')",
411
- "data-indicator": "_profileLoading",
412
- children: [
413
- /*#__PURE__*/ _jsxs("div", {
414
- class: "card",
415
- children: [
416
- /*#__PURE__*/ _jsx("header", {
417
- children: /*#__PURE__*/ _jsx("h2", {
418
- children: $__i18n._({
419
- id: "vERlcd",
420
- message: "Profile"
421
- })
422
- })
423
- }),
424
- /*#__PURE__*/ _jsx("section", {
425
- class: "flex flex-col gap-4",
426
- children: /*#__PURE__*/ _jsxs("div", {
427
- class: "field",
428
- children: [
429
- /*#__PURE__*/ _jsx("label", {
430
- class: "label",
431
- children: $__i18n._({
432
- id: "6YtxFj",
433
- message: "Name"
434
- })
435
- }),
436
- /*#__PURE__*/ _jsx("input", {
437
- type: "text",
438
- "data-bind": "userName",
439
- class: "input",
440
- required: true
441
- })
442
- ]
443
- })
444
- })
445
- ]
446
- }),
447
- /*#__PURE__*/ _jsxs("button", {
448
- type: "submit",
449
- class: "btn mt-4",
450
- "data-attr-disabled": "$_profileLoading",
451
- children: [
452
- /*#__PURE__*/ _jsx("span", {
453
- "data-show": "!$_profileLoading",
454
- children: $__i18n._({
455
- id: "ssqvZi",
456
- message: "Save Profile"
457
- })
458
- }),
459
- /*#__PURE__*/ _jsx("span", {
460
- "data-show": "$_profileLoading",
461
- children: $__i18n._({
462
- id: "k1ifdL",
463
- message: "Processing..."
464
- })
465
- })
466
- ]
467
- })
468
- ]
469
- }),
470
- /*#__PURE__*/ _jsxs("form", {
471
- "data-signals": "{currentPassword: '', newPassword: '', confirmPassword: ''}",
472
- "data-on:submit__prevent": "@post('/dash/settings/password')",
473
- "data-indicator": "_passwordLoading",
474
- children: [
475
- /*#__PURE__*/ _jsxs("div", {
476
- class: "card",
477
- children: [
478
- /*#__PURE__*/ _jsx("header", {
479
- children: /*#__PURE__*/ _jsx("h2", {
480
- children: $__i18n._({
481
- id: "VhMDMg",
482
- message: "Change Password"
483
- })
484
- })
485
- }),
486
- /*#__PURE__*/ _jsxs("section", {
487
- class: "flex flex-col gap-4",
488
- children: [
489
- /*#__PURE__*/ _jsxs("div", {
490
- class: "field",
491
- children: [
492
- /*#__PURE__*/ _jsx("label", {
493
- class: "label",
494
- children: $__i18n._({
495
- id: "DCKkhU",
496
- message: "Current Password"
497
- })
498
- }),
499
- /*#__PURE__*/ _jsx("input", {
500
- type: "password",
501
- "data-bind": "currentPassword",
502
- class: "input",
503
- required: true,
504
- autocomplete: "current-password"
505
- })
506
- ]
507
- }),
508
- /*#__PURE__*/ _jsxs("div", {
509
- class: "field",
510
- children: [
511
- /*#__PURE__*/ _jsx("label", {
512
- class: "label",
513
- children: $__i18n._({
514
- id: "7vhWI8",
515
- message: "New Password"
516
- })
517
- }),
518
- /*#__PURE__*/ _jsx("input", {
519
- type: "password",
520
- "data-bind": "newPassword",
521
- class: "input",
522
- required: true,
523
- minlength: 8,
524
- autocomplete: "new-password"
525
- })
526
- ]
527
- }),
528
- /*#__PURE__*/ _jsxs("div", {
529
- class: "field",
530
- children: [
531
- /*#__PURE__*/ _jsx("label", {
532
- class: "label",
533
- children: $__i18n._({
534
- id: "yjkELF",
535
- message: "Confirm New Password"
536
- })
537
- }),
538
- /*#__PURE__*/ _jsx("input", {
539
- type: "password",
540
- "data-bind": "confirmPassword",
541
- class: "input",
542
- required: true,
543
- minlength: 8,
544
- autocomplete: "new-password"
545
- })
546
- ]
547
- })
548
- ]
549
- })
550
- ]
551
- }),
552
- /*#__PURE__*/ _jsxs("button", {
553
- type: "submit",
554
- class: "btn mt-4",
555
- "data-attr-disabled": "$_passwordLoading",
556
- children: [
557
- /*#__PURE__*/ _jsx("span", {
558
- "data-show": "!$_passwordLoading",
559
- children: $__i18n._({
560
- id: "VhMDMg",
561
- message: "Change Password"
562
- })
563
- }),
564
- /*#__PURE__*/ _jsx("span", {
565
- "data-show": "$_passwordLoading",
566
- children: $__i18n._({
567
- id: "k1ifdL",
568
- message: "Processing..."
569
- })
570
- })
571
- ]
572
- })
573
- ]
574
- })
575
- ]
576
- })
577
- ]
578
- });
579
- }
580
23
  // ===========================================================================
581
- // Route handlers
24
+ // General settings
582
25
  // ===========================================================================
583
- // General settings page
26
+ /** Resolve the avatar storage key to a URL */ async function resolveAvatarUrl(c) {
27
+ const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
28
+ if (!avatarKey) return "";
29
+ const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
30
+ return getMediaUrl(avatarKey, publicUrl);
31
+ }
584
32
  settingsRoutes.get("/", async (c)=>{
585
33
  const { settings } = c.var.services;
586
34
  const dbSiteName = await settings.get("SITE_NAME");
587
35
  const dbSiteDescription = await settings.get("SITE_DESCRIPTION");
588
- const siteLanguage = await getSiteLanguage(c);
36
+ const [siteLanguage, homeDefaultView, timeZone, siteFooter, noindex] = await Promise.all([
37
+ getSiteLanguage(c),
38
+ getHomeDefaultView(c),
39
+ getTimeZone(c),
40
+ getSiteFooter(c),
41
+ isNoIndex(c)
42
+ ]);
589
43
  const siteNameFallback = getConfigFallback(c, "SITE_NAME");
590
44
  const siteDescriptionFallback = getConfigFallback(c, "SITE_DESCRIPTION");
45
+ const siteAvatarUrl = await resolveAvatarUrl(c);
46
+ const showHeaderAvatar = await settings.get("SHOW_HEADER_AVATAR") === "true";
591
47
  const saved = c.req.query("saved") !== undefined;
592
48
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
593
49
  c: c,
@@ -601,12 +57,18 @@ settingsRoutes.get("/", async (c)=>{
601
57
  siteName: dbSiteName || "",
602
58
  siteDescription: dbSiteDescription || "",
603
59
  siteLanguage: siteLanguage,
60
+ homeDefaultView: homeDefaultView,
604
61
  siteNameFallback: siteNameFallback,
605
- siteDescriptionFallback: siteDescriptionFallback
62
+ siteDescriptionFallback: siteDescriptionFallback,
63
+ siteAvatarUrl: siteAvatarUrl,
64
+ showHeaderAvatar: showHeaderAvatar,
65
+ timeZone: timeZone,
66
+ siteFooter: siteFooter,
67
+ noindex: noindex,
68
+ timezones: TIMEZONES
606
69
  })
607
70
  }));
608
71
  });
609
- // Save general settings
610
72
  settingsRoutes.post("/", async (c)=>{
611
73
  const body = await c.req.json();
612
74
  const { settings } = c.var.services;
@@ -622,6 +84,18 @@ settingsRoutes.post("/", async (c)=>{
622
84
  await settings.remove("SITE_DESCRIPTION");
623
85
  }
624
86
  await settings.set("SITE_LANGUAGE", body.siteLanguage);
87
+ // Save homepage default view (only store if non-default)
88
+ if (body.homeDefaultView === "featured") {
89
+ await settings.set("HOME_DEFAULT_VIEW", body.homeDefaultView);
90
+ } else {
91
+ await settings.remove("HOME_DEFAULT_VIEW");
92
+ }
93
+ // Timezone
94
+ if (body.timeZone && body.timeZone !== "UTC") {
95
+ await settings.set("TIME_ZONE", body.timeZone);
96
+ } else {
97
+ await settings.remove("TIME_ZONE");
98
+ }
625
99
  const languageChanged = oldLanguage !== body.siteLanguage;
626
100
  const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
627
101
  return sse(c, async (stream)=>{
@@ -635,14 +109,147 @@ settingsRoutes.post("/", async (c)=>{
635
109
  selector: "title"
636
110
  });
637
111
  await stream.toast("Settings saved successfully.");
112
+ await stream.patchSignals({
113
+ _orig_siteName: body.siteName,
114
+ _orig_siteDescription: body.siteDescription,
115
+ _orig_siteLanguage: body.siteLanguage,
116
+ _orig_homeDefaultView: body.homeDefaultView,
117
+ _orig_timeZone: body.timeZone,
118
+ _generalDirty: false
119
+ });
638
120
  }
639
121
  });
640
122
  });
641
- // Appearance page
123
+ settingsRoutes.post("/footer", async (c)=>{
124
+ const body = await c.req.json();
125
+ const { settings } = c.var.services;
126
+ if (body.siteFooter?.trim()) {
127
+ await settings.set("SITE_FOOTER", body.siteFooter.trim());
128
+ } else {
129
+ await settings.remove("SITE_FOOTER");
130
+ }
131
+ return sse(c, async (stream)=>{
132
+ await stream.toast("Footer saved successfully.");
133
+ await stream.patchSignals({
134
+ _orig_siteFooter: body.siteFooter,
135
+ _footerDirty: false
136
+ });
137
+ });
138
+ });
139
+ settingsRoutes.post("/seo", async (c)=>{
140
+ const body = await c.req.json();
141
+ const { settings } = c.var.services;
142
+ // Checkbox "noindex" is the allow-indexing signal:
143
+ // checked (value "true") = indexing allowed -> remove NOINDEX
144
+ // unchecked (value "") = indexing blocked -> set NOINDEX=true
145
+ if (body.noindex === "true") {
146
+ await settings.remove("NOINDEX");
147
+ } else {
148
+ await settings.set("NOINDEX", "true");
149
+ }
150
+ return sse(c, async (stream)=>{
151
+ await stream.toast("SEO settings saved successfully.");
152
+ await stream.patchSignals({
153
+ _orig_noindex: body.noindex,
154
+ _seoDirty: false
155
+ });
156
+ });
157
+ });
158
+ // ===========================================================================
159
+ // Avatar upload & removal
160
+ // ===========================================================================
161
+ settingsRoutes.post("/avatar", async (c)=>{
162
+ const storage = c.var.storage;
163
+ if (!storage) {
164
+ return dsToast("Storage not configured.", "error");
165
+ }
166
+ const formData = await c.req.formData();
167
+ const file = formData.get("file");
168
+ if (!file) {
169
+ return dsToast("No file provided.", "error");
170
+ }
171
+ const allowedTypes = [
172
+ "image/jpeg",
173
+ "image/png",
174
+ "image/gif",
175
+ "image/webp",
176
+ "image/svg+xml"
177
+ ];
178
+ if (!allowedTypes.includes(file.type)) {
179
+ return dsToast("File type not allowed.", "error");
180
+ }
181
+ const maxSize = 10 * 1024 * 1024;
182
+ if (file.size > maxSize) {
183
+ return dsToast("File too large (max 10MB).", "error");
184
+ }
185
+ const { uuidv7 } = await import("uuidv7");
186
+ const ext = file.name.split(".").pop() || "bin";
187
+ const id = uuidv7();
188
+ const date = new Date();
189
+ const year = date.getUTCFullYear();
190
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
191
+ const filename = `${id}.${ext}`;
192
+ const storageKey = `media/${year}/${month}/${filename}`;
193
+ try {
194
+ await storage.put(storageKey, file.stream(), {
195
+ contentType: file.type
196
+ });
197
+ await c.var.services.media.create({
198
+ id,
199
+ filename,
200
+ originalName: file.name,
201
+ mimeType: file.type,
202
+ size: file.size,
203
+ storageKey,
204
+ provider: c.env.STORAGE_DRIVER || "r2"
205
+ });
206
+ await c.var.services.settings.set("SITE_AVATAR", storageKey);
207
+ // Store favicon variants as base64 in settings (small files, accessed every page load)
208
+ const faviconFile = formData.get("favicon");
209
+ const appleTouchFile = formData.get("appleTouch");
210
+ if (faviconFile) {
211
+ const b64 = arrayBufferToBase64(await faviconFile.arrayBuffer());
212
+ await c.var.services.settings.set("SITE_FAVICON_ICO", b64);
213
+ }
214
+ if (appleTouchFile) {
215
+ const b64 = arrayBufferToBase64(await appleTouchFile.arrayBuffer());
216
+ await c.var.services.settings.set("SITE_FAVICON_APPLE_TOUCH", b64);
217
+ }
218
+ return dsRedirect("/dash/settings?saved");
219
+ } catch {
220
+ return dsToast("Upload failed. Please try again.", "error");
221
+ }
222
+ });
223
+ settingsRoutes.post("/avatar/remove", async (c)=>{
224
+ await c.var.services.settings.remove("SITE_AVATAR");
225
+ await c.var.services.settings.remove("SITE_FAVICON_ICO");
226
+ await c.var.services.settings.remove("SITE_FAVICON_APPLE_TOUCH");
227
+ return dsRedirect("/dash/settings?saved");
228
+ });
229
+ settingsRoutes.post("/avatar/display", async (c)=>{
230
+ const body = await c.req.json();
231
+ const { settings } = c.var.services;
232
+ if (body.showHeaderAvatar === "true") {
233
+ await settings.set("SHOW_HEADER_AVATAR", "true");
234
+ } else {
235
+ await settings.remove("SHOW_HEADER_AVATAR");
236
+ }
237
+ return sse(c, async (stream)=>{
238
+ await stream.toast("Avatar display setting saved successfully.");
239
+ await stream.patchSignals({
240
+ _orig_showHeaderAvatar: body.showHeaderAvatar,
241
+ _avatarDisplayDirty: false
242
+ });
243
+ });
244
+ });
245
+ // ===========================================================================
246
+ // Appearance
247
+ // ===========================================================================
642
248
  settingsRoutes.get("/appearance", async (c)=>{
643
249
  const { settings } = c.var.services;
644
250
  const siteName = await getSiteName(c);
645
251
  const currentThemeId = await settings.get(SETTINGS_KEYS.THEME) ?? "default";
252
+ const currentFontThemeId = await settings.get("FONT_THEME") ?? "default";
646
253
  const customCSS = await settings.get(SETTINGS_KEYS.CUSTOM_CSS) ?? "";
647
254
  const themes = getAvailableThemes(c.var.config);
648
255
  const saved = c.req.query("saved") !== undefined;
@@ -657,11 +264,12 @@ settingsRoutes.get("/appearance", async (c)=>{
657
264
  children: /*#__PURE__*/ _jsx(AppearanceContent, {
658
265
  themes: themes,
659
266
  currentThemeId: currentThemeId,
267
+ fontThemes: BUILTIN_FONT_THEMES,
268
+ currentFontThemeId: currentFontThemeId,
660
269
  customCSS: customCSS
661
270
  })
662
271
  }));
663
272
  });
664
- // Save theme
665
273
  settingsRoutes.post("/appearance", async (c)=>{
666
274
  const body = await c.req.json();
667
275
  const { settings } = c.var.services;
@@ -677,7 +285,20 @@ settingsRoutes.post("/appearance", async (c)=>{
677
285
  }
678
286
  return dsRedirect("/dash/settings/appearance?saved");
679
287
  });
680
- // Save custom CSS
288
+ settingsRoutes.post("/font-theme", async (c)=>{
289
+ const body = await c.req.json();
290
+ const { settings } = c.var.services;
291
+ const validFont = BUILTIN_FONT_THEMES.find((f)=>f.id === body.fontTheme);
292
+ if (!validFont) {
293
+ return dsToast("Invalid font theme selected.", "error");
294
+ }
295
+ if (validFont.id === "default") {
296
+ await settings.remove("FONT_THEME");
297
+ } else {
298
+ await settings.set("FONT_THEME", validFont.id);
299
+ }
300
+ return dsRedirect("/dash/settings/appearance?saved");
301
+ });
681
302
  settingsRoutes.post("/custom-css", async (c)=>{
682
303
  const body = await c.req.json();
683
304
  const { settings } = c.var.services;
@@ -689,7 +310,9 @@ settingsRoutes.post("/custom-css", async (c)=>{
689
310
  }
690
311
  return dsToast("Custom CSS saved successfully.");
691
312
  });
692
- // Account page
313
+ // ===========================================================================
314
+ // Account
315
+ // ===========================================================================
693
316
  settingsRoutes.get("/account", async (c)=>{
694
317
  const siteName = await getSiteName(c);
695
318
  const session = await c.var.auth.api.getSession({
@@ -710,7 +333,6 @@ settingsRoutes.get("/account", async (c)=>{
710
333
  })
711
334
  }));
712
335
  });
713
- // Save account profile
714
336
  settingsRoutes.post("/account", async (c)=>{
715
337
  const body = await c.req.json();
716
338
  const name = body.userName?.trim();
@@ -729,7 +351,6 @@ settingsRoutes.post("/account", async (c)=>{
729
351
  }
730
352
  return dsToast("Profile saved successfully.");
731
353
  });
732
- // Change password
733
354
  settingsRoutes.post("/password", async (c)=>{
734
355
  const body = await c.req.json();
735
356
  if (body.newPassword !== body.confirmPassword) {