@jant/core 0.2.16 → 0.2.18

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 (120) hide show
  1. package/dist/app.d.ts +5 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +332 -119
  4. package/dist/i18n/context.d.ts +2 -2
  5. package/dist/i18n/context.js +1 -1
  6. package/dist/i18n/i18n.d.ts +1 -1
  7. package/dist/i18n/i18n.js +1 -1
  8. package/dist/i18n/index.d.ts +1 -1
  9. package/dist/i18n/index.js +1 -1
  10. package/dist/i18n/locales/en.d.ts.map +1 -1
  11. package/dist/i18n/locales/en.js +1 -1
  12. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  13. package/dist/i18n/locales/zh-Hans.js +1 -1
  14. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
  15. package/dist/i18n/locales/zh-Hant.js +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/lib/config.d.ts +83 -0
  19. package/dist/lib/config.d.ts.map +1 -0
  20. package/dist/lib/config.js +104 -0
  21. package/dist/lib/constants.d.ts +2 -1
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/constants.js +5 -2
  24. package/dist/lib/sse.d.ts +15 -0
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +13 -0
  27. package/dist/lib/theme.d.ts +44 -0
  28. package/dist/lib/theme.d.ts.map +1 -0
  29. package/dist/lib/theme.js +65 -0
  30. package/dist/routes/dash/appearance.d.ts +13 -0
  31. package/dist/routes/dash/appearance.d.ts.map +1 -0
  32. package/dist/routes/dash/appearance.js +164 -0
  33. package/dist/routes/dash/collections.d.ts.map +1 -1
  34. package/dist/routes/dash/collections.js +5 -4
  35. package/dist/routes/dash/index.d.ts.map +1 -1
  36. package/dist/routes/dash/index.js +2 -1
  37. package/dist/routes/dash/media.d.ts.map +1 -1
  38. package/dist/routes/dash/media.js +3 -2
  39. package/dist/routes/dash/pages.d.ts.map +1 -1
  40. package/dist/routes/dash/pages.js +5 -4
  41. package/dist/routes/dash/posts.d.ts.map +1 -1
  42. package/dist/routes/dash/posts.js +5 -4
  43. package/dist/routes/dash/redirects.d.ts.map +1 -1
  44. package/dist/routes/dash/redirects.js +3 -2
  45. package/dist/routes/dash/settings.d.ts.map +1 -1
  46. package/dist/routes/dash/settings.js +39 -38
  47. package/dist/routes/pages/archive.d.ts.map +1 -1
  48. package/dist/routes/pages/archive.js +2 -1
  49. package/dist/routes/pages/collection.d.ts.map +1 -1
  50. package/dist/routes/pages/collection.js +2 -1
  51. package/dist/routes/pages/home.d.ts.map +1 -1
  52. package/dist/routes/pages/home.js +2 -1
  53. package/dist/routes/pages/page.d.ts.map +1 -1
  54. package/dist/routes/pages/page.js +2 -1
  55. package/dist/routes/pages/post.d.ts.map +1 -1
  56. package/dist/routes/pages/post.js +2 -1
  57. package/dist/routes/pages/search.d.ts.map +1 -1
  58. package/dist/routes/pages/search.js +2 -1
  59. package/dist/services/settings.d.ts +1 -0
  60. package/dist/services/settings.d.ts.map +1 -1
  61. package/dist/services/settings.js +3 -0
  62. package/dist/theme/color-themes.d.ts +30 -0
  63. package/dist/theme/color-themes.d.ts.map +1 -0
  64. package/dist/theme/color-themes.js +268 -0
  65. package/dist/theme/layouts/BaseLayout.d.ts +5 -0
  66. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  67. package/dist/theme/layouts/BaseLayout.js +70 -3
  68. package/dist/theme/layouts/DashLayout.d.ts +2 -0
  69. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  70. package/dist/theme/layouts/DashLayout.js +10 -1
  71. package/dist/theme/layouts/index.d.ts +1 -1
  72. package/dist/theme/layouts/index.d.ts.map +1 -1
  73. package/dist/types.d.ts +64 -32
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +52 -0
  76. package/package.json +1 -1
  77. package/src/app.tsx +286 -59
  78. package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
  79. package/src/db/migrations/meta/0000_snapshot.json +9 -9
  80. package/src/db/migrations/meta/_journal.json +2 -30
  81. package/src/i18n/context.tsx +2 -2
  82. package/src/i18n/i18n.ts +1 -1
  83. package/src/i18n/index.ts +1 -1
  84. package/src/i18n/locales/en.po +328 -252
  85. package/src/i18n/locales/en.ts +1 -1
  86. package/src/i18n/locales/zh-Hans.po +315 -278
  87. package/src/i18n/locales/zh-Hans.ts +1 -1
  88. package/src/i18n/locales/zh-Hant.po +315 -278
  89. package/src/i18n/locales/zh-Hant.ts +1 -1
  90. package/src/index.ts +0 -2
  91. package/src/lib/config.ts +120 -0
  92. package/src/lib/constants.ts +3 -0
  93. package/src/lib/sse.ts +38 -0
  94. package/src/lib/theme.ts +86 -0
  95. package/src/preset.css +9 -0
  96. package/src/routes/dash/appearance.tsx +180 -0
  97. package/src/routes/dash/collections.tsx +5 -4
  98. package/src/routes/dash/index.tsx +2 -1
  99. package/src/routes/dash/media.tsx +3 -2
  100. package/src/routes/dash/pages.tsx +5 -4
  101. package/src/routes/dash/posts.tsx +5 -4
  102. package/src/routes/dash/redirects.tsx +3 -2
  103. package/src/routes/dash/settings.tsx +51 -49
  104. package/src/routes/pages/archive.tsx +2 -1
  105. package/src/routes/pages/collection.tsx +2 -1
  106. package/src/routes/pages/home.tsx +2 -1
  107. package/src/routes/pages/page.tsx +2 -1
  108. package/src/routes/pages/post.tsx +2 -1
  109. package/src/routes/pages/search.tsx +2 -1
  110. package/src/services/settings.ts +5 -0
  111. package/src/styles/components.css +93 -0
  112. package/src/theme/color-themes.ts +321 -0
  113. package/src/theme/layouts/BaseLayout.tsx +61 -1
  114. package/src/theme/layouts/DashLayout.tsx +13 -2
  115. package/src/theme/layouts/index.ts +5 -1
  116. package/src/types.ts +74 -34
  117. package/src/db/migrations/0001_add_search_fts.sql +0 -40
  118. package/src/db/migrations/0002_collection_path.sql +0 -2
  119. package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
  120. package/src/db/migrations/0004_media_uuid.sql +0 -35
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Built-in Color Themes
3
+ *
4
+ * Each theme defines CSS variable overrides for light and dark modes,
5
+ * plus preview colors for the theme picker UI.
6
+ */
7
+
8
+ /**
9
+ * A color theme definition with light and dark mode CSS variable overrides.
10
+ */
11
+ export interface ColorTheme {
12
+ /** Stored in DB settings, e.g. "beach" */
13
+ id: string;
14
+ /** Display name, e.g. "Beach" */
15
+ name: string;
16
+ /** CSS variable overrides for :root (light mode) */
17
+ light: Record<string, string>;
18
+ /** CSS variable overrides for .dark (dark mode) */
19
+ dark: Record<string, string>;
20
+ /** Preview colors (hex) for theme picker cards */
21
+ preview: {
22
+ lightBg: string;
23
+ lightText: string;
24
+ lightLink: string;
25
+ darkBg: string;
26
+ darkText: string;
27
+ darkLink: string;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Create a comprehensive color theme from key colors.
33
+ * Derives card, popover, muted, secondary, accent, and sidebar variables.
34
+ */
35
+ function defineTheme(opts: {
36
+ id: string;
37
+ name: string;
38
+ preview: ColorTheme["preview"];
39
+ light: {
40
+ bg: string;
41
+ fg: string;
42
+ primary: string;
43
+ primaryFg: string;
44
+ muted: string;
45
+ mutedFg: string;
46
+ border: string;
47
+ };
48
+ dark: {
49
+ bg: string;
50
+ fg: string;
51
+ primary: string;
52
+ primaryFg: string;
53
+ muted: string;
54
+ mutedFg: string;
55
+ border: string;
56
+ };
57
+ }): ColorTheme {
58
+ const { light, dark } = opts;
59
+ return {
60
+ id: opts.id,
61
+ name: opts.name,
62
+ preview: opts.preview,
63
+ light: {
64
+ "--background": light.bg,
65
+ "--foreground": light.fg,
66
+ "--card": light.bg,
67
+ "--card-foreground": light.fg,
68
+ "--popover": light.bg,
69
+ "--popover-foreground": light.fg,
70
+ "--primary": light.primary,
71
+ "--primary-foreground": light.primaryFg,
72
+ "--secondary": light.muted,
73
+ "--secondary-foreground": light.fg,
74
+ "--muted": light.muted,
75
+ "--muted-foreground": light.mutedFg,
76
+ "--accent": light.muted,
77
+ "--accent-foreground": light.fg,
78
+ "--border": light.border,
79
+ "--input": light.border,
80
+ "--ring": light.primary,
81
+ "--sidebar": light.bg,
82
+ "--sidebar-foreground": light.fg,
83
+ "--sidebar-primary": light.primary,
84
+ "--sidebar-primary-foreground": light.primaryFg,
85
+ "--sidebar-accent": light.muted,
86
+ "--sidebar-accent-foreground": light.fg,
87
+ "--sidebar-border": light.border,
88
+ "--sidebar-ring": light.primary,
89
+ },
90
+ dark: {
91
+ "--background": dark.bg,
92
+ "--foreground": dark.fg,
93
+ "--card": dark.bg,
94
+ "--card-foreground": dark.fg,
95
+ "--popover": dark.bg,
96
+ "--popover-foreground": dark.fg,
97
+ "--primary": dark.primary,
98
+ "--primary-foreground": dark.primaryFg,
99
+ "--secondary": dark.muted,
100
+ "--secondary-foreground": dark.fg,
101
+ "--muted": dark.muted,
102
+ "--muted-foreground": dark.mutedFg,
103
+ "--accent": dark.muted,
104
+ "--accent-foreground": dark.fg,
105
+ "--border": dark.border,
106
+ "--input": dark.border,
107
+ "--ring": dark.primary,
108
+ "--sidebar": dark.bg,
109
+ "--sidebar-foreground": dark.fg,
110
+ "--sidebar-primary": dark.primary,
111
+ "--sidebar-primary-foreground": dark.primaryFg,
112
+ "--sidebar-accent": dark.muted,
113
+ "--sidebar-accent-foreground": dark.fg,
114
+ "--sidebar-border": dark.border,
115
+ "--sidebar-ring": dark.primary,
116
+ },
117
+ };
118
+ }
119
+
120
+ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
121
+ {
122
+ id: "default",
123
+ name: "Default",
124
+ light: {},
125
+ dark: {},
126
+ preview: {
127
+ lightBg: "#ffffff",
128
+ lightText: "#1e1e1e",
129
+ lightLink: "#1e1e1e",
130
+ darkBg: "#262626",
131
+ darkText: "#fafafa",
132
+ darkLink: "#eaeaea",
133
+ },
134
+ },
135
+
136
+ defineTheme({
137
+ id: "beach",
138
+ name: "Beach",
139
+ preview: {
140
+ lightBg: "#f9f3ea",
141
+ lightText: "#3d3527",
142
+ lightLink: "#2d6a59",
143
+ darkBg: "#2d4553",
144
+ darkText: "#e2d6c4",
145
+ darkLink: "#7cc5a2",
146
+ },
147
+ light: {
148
+ bg: "oklch(0.97 0.01 85)",
149
+ fg: "oklch(0.28 0.02 65)",
150
+ primary: "oklch(0.46 0.1 170)",
151
+ primaryFg: "oklch(0.98 0.005 85)",
152
+ muted: "oklch(0.93 0.015 85)",
153
+ mutedFg: "oklch(0.52 0.015 65)",
154
+ border: "oklch(0.88 0.018 85)",
155
+ },
156
+ dark: {
157
+ bg: "oklch(0.27 0.03 210)",
158
+ fg: "oklch(0.88 0.015 80)",
159
+ primary: "oklch(0.72 0.1 165)",
160
+ primaryFg: "oklch(0.22 0.03 210)",
161
+ muted: "oklch(0.33 0.025 210)",
162
+ mutedFg: "oklch(0.65 0.015 80)",
163
+ border: "oklch(0.38 0.02 210)",
164
+ },
165
+ }),
166
+
167
+ defineTheme({
168
+ id: "gameboy",
169
+ name: "Gameboy",
170
+ preview: {
171
+ lightBg: "#d3d7c0",
172
+ lightText: "#2b3326",
173
+ lightLink: "#466740",
174
+ darkBg: "#1b1f18",
175
+ darkText: "#a6b09a",
176
+ darkLink: "#6d9660",
177
+ },
178
+ light: {
179
+ bg: "oklch(0.87 0.03 130)",
180
+ fg: "oklch(0.25 0.04 140)",
181
+ primary: "oklch(0.4 0.08 145)",
182
+ primaryFg: "oklch(0.92 0.02 130)",
183
+ muted: "oklch(0.83 0.035 130)",
184
+ mutedFg: "oklch(0.48 0.03 140)",
185
+ border: "oklch(0.79 0.035 130)",
186
+ },
187
+ dark: {
188
+ bg: "oklch(0.18 0.02 140)",
189
+ fg: "oklch(0.78 0.025 130)",
190
+ primary: "oklch(0.6 0.08 145)",
191
+ primaryFg: "oklch(0.15 0.02 140)",
192
+ muted: "oklch(0.24 0.02 140)",
193
+ mutedFg: "oklch(0.58 0.02 130)",
194
+ border: "oklch(0.3 0.02 140)",
195
+ },
196
+ }),
197
+
198
+ defineTheme({
199
+ id: "grayscale",
200
+ name: "Grayscale",
201
+ preview: {
202
+ lightBg: "#efefef",
203
+ lightText: "#3a3a3a",
204
+ lightLink: "#555555",
205
+ darkBg: "#1e1e1e",
206
+ darkText: "#c8c8c8",
207
+ darkLink: "#999999",
208
+ },
209
+ light: {
210
+ bg: "oklch(0.96 0 0)",
211
+ fg: "oklch(0.3 0 0)",
212
+ primary: "oklch(0.4 0 0)",
213
+ primaryFg: "oklch(0.96 0 0)",
214
+ muted: "oklch(0.92 0 0)",
215
+ mutedFg: "oklch(0.55 0 0)",
216
+ border: "oklch(0.87 0 0)",
217
+ },
218
+ dark: {
219
+ bg: "oklch(0.18 0 0)",
220
+ fg: "oklch(0.82 0 0)",
221
+ primary: "oklch(0.7 0 0)",
222
+ primaryFg: "oklch(0.18 0 0)",
223
+ muted: "oklch(0.24 0 0)",
224
+ mutedFg: "oklch(0.6 0 0)",
225
+ border: "oklch(0.3 0 0)",
226
+ },
227
+ }),
228
+
229
+ defineTheme({
230
+ id: "halloween",
231
+ name: "Halloween",
232
+ preview: {
233
+ lightBg: "#f9f2e3",
234
+ lightText: "#352200",
235
+ lightLink: "#cc5500",
236
+ darkBg: "#1e1000",
237
+ darkText: "#dfc390",
238
+ darkLink: "#ff8c00",
239
+ },
240
+ light: {
241
+ bg: "oklch(0.97 0.015 75)",
242
+ fg: "oklch(0.25 0.04 55)",
243
+ primary: "oklch(0.6 0.2 50)",
244
+ primaryFg: "oklch(0.98 0.01 75)",
245
+ muted: "oklch(0.93 0.02 75)",
246
+ mutedFg: "oklch(0.5 0.025 55)",
247
+ border: "oklch(0.88 0.025 75)",
248
+ },
249
+ dark: {
250
+ bg: "oklch(0.16 0.03 50)",
251
+ fg: "oklch(0.85 0.025 75)",
252
+ primary: "oklch(0.72 0.19 55)",
253
+ primaryFg: "oklch(0.14 0.03 50)",
254
+ muted: "oklch(0.22 0.025 50)",
255
+ mutedFg: "oklch(0.62 0.02 75)",
256
+ border: "oklch(0.28 0.025 50)",
257
+ },
258
+ }),
259
+
260
+ defineTheme({
261
+ id: "notepad",
262
+ name: "Notepad",
263
+ preview: {
264
+ lightBg: "#fdfce8",
265
+ lightText: "#333333",
266
+ lightLink: "#2060b8",
267
+ darkBg: "#2a291a",
268
+ darkText: "#d2d2b8",
269
+ darkLink: "#6695cc",
270
+ },
271
+ light: {
272
+ bg: "oklch(0.985 0.018 95)",
273
+ fg: "oklch(0.27 0 0)",
274
+ primary: "oklch(0.5 0.17 260)",
275
+ primaryFg: "oklch(0.985 0.01 95)",
276
+ muted: "oklch(0.94 0.022 95)",
277
+ mutedFg: "oklch(0.52 0 0)",
278
+ border: "oklch(0.88 0.025 95)",
279
+ },
280
+ dark: {
281
+ bg: "oklch(0.2 0.02 90)",
282
+ fg: "oklch(0.87 0.015 95)",
283
+ primary: "oklch(0.65 0.14 260)",
284
+ primaryFg: "oklch(0.98 0.01 95)",
285
+ muted: "oklch(0.26 0.018 90)",
286
+ mutedFg: "oklch(0.62 0.012 95)",
287
+ border: "oklch(0.32 0.018 90)",
288
+ },
289
+ }),
290
+
291
+ defineTheme({
292
+ id: "sonnet",
293
+ name: "Sonnet",
294
+ preview: {
295
+ lightBg: "#f7eef5",
296
+ lightText: "#2e1e2c",
297
+ lightLink: "#9845c8",
298
+ darkBg: "#1d1428",
299
+ darkText: "#d4c2d0",
300
+ darkLink: "#c080fc",
301
+ },
302
+ light: {
303
+ bg: "oklch(0.97 0.012 325)",
304
+ fg: "oklch(0.25 0.02 310)",
305
+ primary: "oklch(0.55 0.2 300)",
306
+ primaryFg: "oklch(0.98 0.008 325)",
307
+ muted: "oklch(0.93 0.016 325)",
308
+ mutedFg: "oklch(0.52 0.015 310)",
309
+ border: "oklch(0.88 0.016 325)",
310
+ },
311
+ dark: {
312
+ bg: "oklch(0.18 0.025 300)",
313
+ fg: "oklch(0.87 0.012 325)",
314
+ primary: "oklch(0.72 0.18 300)",
315
+ primaryFg: "oklch(0.98 0.008 325)",
316
+ muted: "oklch(0.24 0.022 300)",
317
+ mutedFg: "oklch(0.62 0.012 325)",
318
+ border: "oklch(0.3 0.022 300)",
319
+ },
320
+ }),
321
+ ];
@@ -12,11 +12,17 @@ import type { Context } from "hono";
12
12
  import { Script, Link, ViteClient } from "vite-ssr-components/hono";
13
13
  import { I18nProvider } from "../../i18n/index.js";
14
14
 
15
+ export interface ToastProps {
16
+ message: string;
17
+ type?: "success" | "error";
18
+ }
19
+
15
20
  export interface BaseLayoutProps {
16
21
  title: string;
17
22
  description?: string;
18
23
  lang?: string;
19
24
  c?: Context;
25
+ toast?: ToastProps;
20
26
  }
21
27
 
22
28
  export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
@@ -24,6 +30,7 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
24
30
  description,
25
31
  lang,
26
32
  c,
33
+ toast,
27
34
  children,
28
35
  }) => {
29
36
  // Read lang from Hono context if available, otherwise use prop or default
@@ -32,6 +39,9 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
32
39
  // Automatically wrap with I18nProvider if Context is provided
33
40
  const content = c ? <I18nProvider c={c}>{children}</I18nProvider> : children;
34
41
 
42
+ // Read theme style from Hono context if available
43
+ const themeStyle = c ? c.get("themeStyle") : undefined;
44
+
35
45
  return (
36
46
  <html lang={resolvedLang}>
37
47
  <head>
@@ -41,9 +51,59 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
41
51
  {description && <meta name="description" content={description} />}
42
52
  <ViteClient />
43
53
  <Link href="/src/style.css" rel="stylesheet" />
54
+ {themeStyle && <style>{themeStyle}</style>}
44
55
  <Script src="/src/client.ts" />
45
56
  </head>
46
- <body class="bg-background text-foreground antialiased">{content}</body>
57
+ <body class="bg-background text-foreground antialiased">
58
+ {content}
59
+ <div id="toast-container" class="toast-container">
60
+ {toast && (
61
+ <div
62
+ class={`toast ${toast.type === "error" ? "toast-error" : "toast-success"}`}
63
+ data-init="history.replaceState({}, '', location.pathname); setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)"
64
+ >
65
+ {toast.type === "error" ? (
66
+ <svg
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ fill="none"
69
+ viewBox="0 0 24 24"
70
+ stroke-width="2"
71
+ stroke="currentColor"
72
+ >
73
+ <circle cx="12" cy="12" r="10" />
74
+ <path d="m15 9-6 6M9 9l6 6" />
75
+ </svg>
76
+ ) : (
77
+ <svg
78
+ xmlns="http://www.w3.org/2000/svg"
79
+ fill="none"
80
+ viewBox="0 0 24 24"
81
+ stroke-width="2"
82
+ stroke="currentColor"
83
+ >
84
+ <circle cx="12" cy="12" r="10" />
85
+ <path d="m9 12 2 2 4-4" />
86
+ </svg>
87
+ )}
88
+ <span>{toast.message}</span>
89
+ <button
90
+ class="toast-close"
91
+ data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"
92
+ >
93
+ <svg
94
+ xmlns="http://www.w3.org/2000/svg"
95
+ fill="none"
96
+ viewBox="0 0 24 24"
97
+ stroke-width="2"
98
+ stroke="currentColor"
99
+ >
100
+ <path d="M18 6 6 18M6 6l12 12" />
101
+ </svg>
102
+ </button>
103
+ </div>
104
+ )}
105
+ </div>
106
+ </body>
47
107
  </html>
48
108
  );
49
109
  };
@@ -7,13 +7,14 @@
7
7
  import type { FC, PropsWithChildren } from "hono/jsx";
8
8
  import type { Context } from "hono";
9
9
  import { useLingui } from "@lingui/react/macro";
10
- import { BaseLayout } from "./BaseLayout.js";
10
+ import { BaseLayout, type ToastProps } from "./BaseLayout.js";
11
11
 
12
12
  export interface DashLayoutProps {
13
13
  c: Context;
14
14
  title: string;
15
15
  siteName: string;
16
16
  currentPath?: string;
17
+ toast?: ToastProps;
17
18
  }
18
19
 
19
20
  function DashLayoutContent({
@@ -134,6 +135,15 @@ function DashLayoutContent({
134
135
  comment: "@context: Dashboard navigation - site settings",
135
136
  })}
136
137
  </a>
138
+ <a
139
+ href="/dash/appearance"
140
+ class={navClass("/dash/appearance", /^\/dash\/appearance/)}
141
+ >
142
+ {t({
143
+ message: "Appearance",
144
+ comment: "@context: Dashboard navigation - appearance settings",
145
+ })}
146
+ </a>
137
147
  </nav>
138
148
  </aside>
139
149
 
@@ -149,10 +159,11 @@ export const DashLayout: FC<PropsWithChildren<DashLayoutProps>> = ({
149
159
  title,
150
160
  siteName,
151
161
  currentPath,
162
+ toast,
152
163
  children,
153
164
  }) => {
154
165
  return (
155
- <BaseLayout title={`${title} - ${siteName}`} c={c}>
166
+ <BaseLayout title={`${title} - ${siteName}`} c={c} toast={toast}>
156
167
  <DashLayoutContent siteName={siteName} currentPath={currentPath}>
157
168
  {children}
158
169
  </DashLayoutContent>
@@ -1,2 +1,6 @@
1
- export { BaseLayout, type BaseLayoutProps } from "./BaseLayout.js";
1
+ export {
2
+ BaseLayout,
3
+ type BaseLayoutProps,
4
+ type ToastProps,
5
+ } from "./BaseLayout.js";
2
6
  export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
package/src/types.ts CHANGED
@@ -37,8 +37,70 @@ export interface Bindings {
37
37
  IMAGE_TRANSFORM_URL?: string;
38
38
  DEMO_EMAIL?: string;
39
39
  DEMO_PASSWORD?: string;
40
+ // Site configuration (optional - can be overridden in DB)
41
+ SITE_NAME?: string;
42
+ SITE_DESCRIPTION?: string;
43
+ SITE_LANGUAGE?: string;
40
44
  }
41
45
 
46
+ // =============================================================================
47
+ // Configuration System
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Configuration Registry - Single Source of Truth
52
+ *
53
+ * All available configuration fields with their metadata.
54
+ * Add new fields here, and they'll automatically work everywhere.
55
+ *
56
+ * Priority logic:
57
+ * - envOnly: false → User-configurable (DB > ENV > Default)
58
+ * - envOnly: true → Environment-only (ENV > Default)
59
+ */
60
+ export const CONFIG_FIELDS = {
61
+ // User-configurable (can be modified in dashboard)
62
+ SITE_NAME: {
63
+ defaultValue: "Jant",
64
+ envOnly: false,
65
+ },
66
+ SITE_DESCRIPTION: {
67
+ defaultValue: "A microblog powered by Jant",
68
+ envOnly: false,
69
+ },
70
+ SITE_LANGUAGE: {
71
+ defaultValue: "en",
72
+ envOnly: false,
73
+ },
74
+
75
+ // Environment-only (deployment/infrastructure config)
76
+ SITE_URL: {
77
+ defaultValue: "",
78
+ envOnly: true,
79
+ },
80
+ AUTH_SECRET: {
81
+ defaultValue: "",
82
+ envOnly: true,
83
+ },
84
+ R2_PUBLIC_URL: {
85
+ defaultValue: "",
86
+ envOnly: true,
87
+ },
88
+ IMAGE_TRANSFORM_URL: {
89
+ defaultValue: "",
90
+ envOnly: true,
91
+ },
92
+ DEMO_EMAIL: {
93
+ defaultValue: "",
94
+ envOnly: true,
95
+ },
96
+ DEMO_PASSWORD: {
97
+ defaultValue: "",
98
+ envOnly: true,
99
+ },
100
+ } as const;
101
+
102
+ export type ConfigKey = keyof typeof CONFIG_FIELDS;
103
+
42
104
  // =============================================================================
43
105
  // Entity Types
44
106
  // =============================================================================
@@ -137,6 +199,7 @@ export interface UpdatePost {
137
199
  // =============================================================================
138
200
 
139
201
  import type { FC, PropsWithChildren } from "hono/jsx";
202
+ import type { ColorTheme } from "./theme/color-themes.js";
140
203
 
141
204
  /**
142
205
  * Props for overridable theme components
@@ -189,46 +252,23 @@ export interface JantTheme {
189
252
  name?: string;
190
253
  /** Component overrides */
191
254
  components?: ThemeComponents;
192
- /** CSS variable overrides */
255
+ /** CSS variable overrides (highest priority, always applied) */
193
256
  cssVariables?: Record<string, string>;
194
- }
195
-
196
- /**
197
- * Site configuration
198
- */
199
- export interface SiteConfig {
200
- /** Site name */
201
- name?: string;
202
- /** Site description */
203
- description?: string;
204
- /** Default language */
205
- language?: string;
206
- /** Site URL (usually set via env) */
207
- url?: string;
208
- }
209
-
210
- /**
211
- * Feature toggles
212
- */
213
- export interface FeatureConfig {
214
- /** Enable search (default: true) */
215
- search?: boolean;
216
- /** Enable RSS feed (default: true) */
217
- rss?: boolean;
218
- /** Enable sitemap (default: true) */
219
- sitemap?: boolean;
220
- /** Enable i18n (default: true) */
221
- i18n?: boolean;
257
+ /** Replace built-in color themes with a custom list */
258
+ colorThemes?: ColorTheme[];
222
259
  }
223
260
 
224
261
  /**
225
262
  * Main Jant configuration
263
+ *
264
+ * Configuration Philosophy:
265
+ * - Use environment variables for runtime config (API keys, feature flags, site settings)
266
+ * - Use code config (this object) for compile-time customization (theme components)
267
+ *
268
+ * Site-level settings (name, description, language) are configured via
269
+ * environment variables, not here. See lib/config.ts for details.
226
270
  */
227
271
  export interface JantConfig {
228
- /** Site configuration */
229
- site?: SiteConfig;
230
- /** Theme configuration */
272
+ /** Theme configuration (components, CSS overrides) */
231
273
  theme?: JantTheme;
232
- /** Feature toggles */
233
- features?: FeatureConfig;
234
274
  }
@@ -1,40 +0,0 @@
1
- -- FTS5 Full-Text Search for posts
2
- -- This creates a virtual table that indexes post titles and content
3
-
4
- CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
5
- title,
6
- content,
7
- content='posts',
8
- content_rowid='id'
9
- );
10
- --> statement-breakpoint
11
-
12
- -- Populate FTS table with existing posts
13
- INSERT INTO posts_fts(rowid, title, content)
14
- SELECT id, COALESCE(title, ''), COALESCE(content, '') FROM posts WHERE deleted_at IS NULL;
15
- --> statement-breakpoint
16
-
17
- -- Trigger to keep FTS in sync on INSERT
18
- CREATE TRIGGER posts_fts_insert AFTER INSERT ON posts
19
- WHEN NEW.deleted_at IS NULL
20
- BEGIN
21
- INSERT INTO posts_fts(rowid, title, content)
22
- VALUES (NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, ''));
23
- END;
24
- --> statement-breakpoint
25
-
26
- -- Trigger to keep FTS in sync on UPDATE
27
- CREATE TRIGGER posts_fts_update AFTER UPDATE ON posts
28
- BEGIN
29
- DELETE FROM posts_fts WHERE rowid = OLD.id;
30
- INSERT INTO posts_fts(rowid, title, content)
31
- SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, '')
32
- WHERE NEW.deleted_at IS NULL;
33
- END;
34
- --> statement-breakpoint
35
-
36
- -- Trigger to remove from FTS on DELETE
37
- CREATE TRIGGER posts_fts_delete AFTER DELETE ON posts
38
- BEGIN
39
- DELETE FROM posts_fts WHERE rowid = OLD.id;
40
- END;
@@ -1,2 +0,0 @@
1
- -- Rename slug to path in collections table
2
- ALTER TABLE collections RENAME COLUMN slug TO path;
@@ -1,21 +0,0 @@
1
- -- Make collections.path nullable
2
- -- SQLite doesn't support ALTER COLUMN, so we need to recreate the table
3
-
4
- PRAGMA foreign_keys=OFF;
5
-
6
- CREATE TABLE collections_new (
7
- id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
8
- path TEXT UNIQUE,
9
- title TEXT NOT NULL,
10
- description TEXT,
11
- created_at INTEGER NOT NULL,
12
- updated_at INTEGER NOT NULL
13
- );
14
-
15
- INSERT INTO collections_new SELECT * FROM collections;
16
-
17
- DROP TABLE collections;
18
-
19
- ALTER TABLE collections_new RENAME TO collections;
20
-
21
- PRAGMA foreign_keys=ON;