@jant/core 0.2.17 → 0.2.19

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 (99) hide show
  1. package/dist/app.d.ts +1 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +307 -137
  4. package/dist/client.js +1 -0
  5. package/dist/i18n/context.d.ts +2 -2
  6. package/dist/i18n/context.js +1 -1
  7. package/dist/i18n/i18n.d.ts +1 -1
  8. package/dist/i18n/i18n.js +1 -1
  9. package/dist/i18n/index.d.ts +1 -1
  10. package/dist/i18n/index.js +1 -1
  11. package/dist/i18n/locales/en.d.ts.map +1 -1
  12. package/dist/i18n/locales/en.js +1 -1
  13. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  14. package/dist/i18n/locales/zh-Hans.js +1 -1
  15. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
  16. package/dist/i18n/locales/zh-Hant.js +1 -1
  17. package/dist/lib/config.d.ts +44 -10
  18. package/dist/lib/config.d.ts.map +1 -1
  19. package/dist/lib/config.js +69 -44
  20. package/dist/lib/constants.d.ts +2 -1
  21. package/dist/lib/constants.d.ts.map +1 -1
  22. package/dist/lib/constants.js +5 -2
  23. package/dist/lib/image-processor.js +0 -4
  24. package/dist/lib/media-upload.js +104 -0
  25. package/dist/lib/sse.d.ts +82 -13
  26. package/dist/lib/sse.d.ts.map +1 -1
  27. package/dist/lib/sse.js +115 -17
  28. package/dist/lib/theme.d.ts +44 -0
  29. package/dist/lib/theme.d.ts.map +1 -0
  30. package/dist/lib/theme.js +65 -0
  31. package/dist/routes/api/upload.js +16 -18
  32. package/dist/routes/dash/appearance.d.ts +13 -0
  33. package/dist/routes/dash/appearance.d.ts.map +1 -0
  34. package/dist/routes/dash/appearance.js +160 -0
  35. package/dist/routes/dash/collections.js +5 -13
  36. package/dist/routes/dash/media.js +17 -167
  37. package/dist/routes/dash/pages.js +4 -10
  38. package/dist/routes/dash/posts.js +4 -10
  39. package/dist/routes/dash/redirects.js +3 -7
  40. package/dist/routes/dash/settings.d.ts.map +1 -1
  41. package/dist/routes/dash/settings.js +52 -42
  42. package/dist/services/settings.d.ts +1 -0
  43. package/dist/services/settings.d.ts.map +1 -1
  44. package/dist/services/settings.js +3 -0
  45. package/dist/theme/color-themes.d.ts +30 -0
  46. package/dist/theme/color-themes.d.ts.map +1 -0
  47. package/dist/theme/color-themes.js +268 -0
  48. package/dist/theme/layouts/BaseLayout.d.ts +5 -0
  49. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  50. package/dist/theme/layouts/BaseLayout.js +70 -3
  51. package/dist/theme/layouts/DashLayout.d.ts +2 -0
  52. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  53. package/dist/theme/layouts/DashLayout.js +11 -1
  54. package/dist/theme/layouts/index.d.ts +1 -1
  55. package/dist/theme/layouts/index.d.ts.map +1 -1
  56. package/dist/types.d.ts +53 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js +52 -0
  59. package/package.json +1 -1
  60. package/src/app.tsx +260 -81
  61. package/src/client.ts +1 -0
  62. package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
  63. package/src/db/migrations/meta/0000_snapshot.json +9 -9
  64. package/src/db/migrations/meta/_journal.json +2 -30
  65. package/src/i18n/context.tsx +2 -2
  66. package/src/i18n/i18n.ts +1 -1
  67. package/src/i18n/index.ts +1 -1
  68. package/src/i18n/locales/en.po +328 -252
  69. package/src/i18n/locales/en.ts +1 -1
  70. package/src/i18n/locales/zh-Hans.po +315 -278
  71. package/src/i18n/locales/zh-Hans.ts +1 -1
  72. package/src/i18n/locales/zh-Hant.po +315 -278
  73. package/src/i18n/locales/zh-Hant.ts +1 -1
  74. package/src/lib/config.ts +73 -47
  75. package/src/lib/constants.ts +3 -0
  76. package/src/lib/image-processor.ts +0 -7
  77. package/src/lib/media-upload.ts +148 -0
  78. package/src/lib/sse.ts +156 -16
  79. package/src/lib/theme.ts +86 -0
  80. package/src/preset.css +9 -0
  81. package/src/routes/api/upload.ts +12 -18
  82. package/src/routes/dash/appearance.tsx +176 -0
  83. package/src/routes/dash/collections.tsx +5 -13
  84. package/src/routes/dash/media.tsx +16 -165
  85. package/src/routes/dash/pages.tsx +4 -10
  86. package/src/routes/dash/posts.tsx +4 -10
  87. package/src/routes/dash/redirects.tsx +3 -7
  88. package/src/routes/dash/settings.tsx +71 -55
  89. package/src/services/settings.ts +5 -0
  90. package/src/styles/components.css +93 -0
  91. package/src/theme/color-themes.ts +321 -0
  92. package/src/theme/layouts/BaseLayout.tsx +61 -1
  93. package/src/theme/layouts/DashLayout.tsx +14 -3
  94. package/src/theme/layouts/index.ts +5 -1
  95. package/src/types.ts +62 -1
  96. package/src/db/migrations/0001_add_search_fts.sql +0 -40
  97. package/src/db/migrations/0002_collection_path.sql +0 -2
  98. package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
  99. 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({
@@ -41,7 +42,7 @@ function DashLayoutContent({
41
42
  {/* Header */}
42
43
  <header class="border-b bg-card">
43
44
  <div class="container flex h-14 items-center justify-between">
44
- <a href="/dash" class="font-semibold">
45
+ <a id="site-name" href="/dash" class="font-semibold">
45
46
  {siteName}
46
47
  </a>
47
48
  <nav class="flex items-center gap-4">
@@ -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
@@ -43,6 +43,64 @@ export interface Bindings {
43
43
  SITE_LANGUAGE?: string;
44
44
  }
45
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
+
46
104
  // =============================================================================
47
105
  // Entity Types
48
106
  // =============================================================================
@@ -141,6 +199,7 @@ export interface UpdatePost {
141
199
  // =============================================================================
142
200
 
143
201
  import type { FC, PropsWithChildren } from "hono/jsx";
202
+ import type { ColorTheme } from "./theme/color-themes.js";
144
203
 
145
204
  /**
146
205
  * Props for overridable theme components
@@ -193,8 +252,10 @@ export interface JantTheme {
193
252
  name?: string;
194
253
  /** Component overrides */
195
254
  components?: ThemeComponents;
196
- /** CSS variable overrides */
255
+ /** CSS variable overrides (highest priority, always applied) */
197
256
  cssVariables?: Record<string, string>;
257
+ /** Replace built-in color themes with a custom list */
258
+ colorThemes?: ColorTheme[];
198
259
  }
199
260
 
200
261
  /**
@@ -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;
@@ -1,35 +0,0 @@
1
- -- Migration: Change media.id from integer to UUIDv7 (text)
2
- -- SQLite doesn't support altering primary key types, so we recreate the table
3
-
4
- -- Create new table with text id
5
- CREATE TABLE media_new (
6
- id TEXT PRIMARY KEY NOT NULL,
7
- post_id INTEGER REFERENCES posts(id),
8
- filename TEXT NOT NULL,
9
- original_name TEXT NOT NULL,
10
- mime_type TEXT NOT NULL,
11
- size INTEGER NOT NULL,
12
- r2_key TEXT NOT NULL,
13
- width INTEGER,
14
- height INTEGER,
15
- alt TEXT,
16
- created_at INTEGER NOT NULL
17
- );
18
-
19
- -- Migrate existing data (generate UUIDv7-like ids from old integer ids)
20
- -- For existing data, we use a deterministic format based on timestamp + old id
21
- INSERT INTO media_new (id, post_id, filename, original_name, mime_type, size, r2_key, width, height, alt, created_at)
22
- SELECT
23
- printf('%08x-%04x-7%03x-%04x-%012x',
24
- created_at,
25
- (id >> 16) & 0xFFFF,
26
- id & 0x0FFF,
27
- 0x8000 | (RANDOM() & 0x3FFF),
28
- ABS(RANDOM())
29
- ) as id,
30
- post_id, filename, original_name, mime_type, size, r2_key, width, height, alt, created_at
31
- FROM media;
32
-
33
- -- Drop old table and rename new one
34
- DROP TABLE media;
35
- ALTER TABLE media_new RENAME TO media;