@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
package/dist/lib/sse.d.ts CHANGED
@@ -1,20 +1,21 @@
1
1
  /**
2
- * Server-Sent Events (SSE) utilities for Datastar v1.0.0-RC.7
2
+ * Datastar response utilities for v1.0.0-RC.7
3
3
  *
4
- * Generates SSE events compatible with the Datastar client's expected format.
4
+ * Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
5
5
  *
6
- * @see https://data-star.dev/
6
+ * **Non-SSE helpers** (preferred for single operations):
7
+ * - `dsRedirect(url)` — redirect via text/html
8
+ * - `dsToast(message, type)` — toast notification via text/html
9
+ * - `dsSignals(signals)` — signal patch via application/json
7
10
  *
8
- * @example
9
- * ```ts
10
- * app.post("/api/example", (c) => {
11
- * return sse(c, async (stream) => {
12
- * await stream.patchSignals({ loading: false });
13
- * await stream.patchElements('<div id="result">Done!</div>');
14
- * await stream.redirect("/success");
15
- * });
16
- * });
17
- * ```
11
+ * **SSE** (for multiple operations in one response):
12
+ * - `sse(c, handler)` — streaming SSE with full stream API
13
+ *
14
+ * Datastar auto-detects response type by Content-Type:
15
+ * - `text/html` dispatches as `datastar-patch-elements`
16
+ * - `application/json` → dispatches as `datastar-patch-signals`
17
+ *
18
+ * @see https://data-star.dev/
18
19
  */
19
20
  import type { Context } from "hono";
20
21
  /**
@@ -88,6 +89,21 @@ export interface SSEStream {
88
89
  * ```
89
90
  */
90
91
  remove(selector: string): void;
92
+ /**
93
+ * Show a toast notification
94
+ *
95
+ * Appends a toast element to `#toast-container` with auto-dismiss after 3s.
96
+ *
97
+ * @param message - The message to display
98
+ * @param type - Toast type: "success" (default) or "error"
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * await stream.toast("Settings saved successfully.");
103
+ * await stream.toast("Something went wrong.", "error");
104
+ * ```
105
+ */
106
+ toast(message: string, type?: "success" | "error"): void;
91
107
  }
92
108
  /**
93
109
  * Create an SSE response for Datastar
@@ -120,4 +136,57 @@ export interface SSEStream {
120
136
  export declare function sse(c: Context, handler: (stream: SSEStream) => Promise<void>, options?: {
121
137
  headers?: Record<string, string>;
122
138
  }): Response;
139
+ /**
140
+ * Datastar redirect via text/html
141
+ *
142
+ * Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
143
+ * Use instead of `sse()` when the only action is a redirect.
144
+ *
145
+ * @param url - The URL to redirect to
146
+ * @param options - Optional extra headers (e.g. Set-Cookie for auth)
147
+ * @returns Response with text/html content-type
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * return dsRedirect("/dash/posts");
152
+ *
153
+ * // With cookie forwarding (for auth)
154
+ * return dsRedirect("/dash", { headers: { "Set-Cookie": cookie } });
155
+ * ```
156
+ */
157
+ export declare function dsRedirect(url: string, options?: {
158
+ headers?: Record<string, string>;
159
+ }): Response;
160
+ /**
161
+ * Datastar toast notification via text/html
162
+ *
163
+ * Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
164
+ * Use instead of `sse()` when the only action is showing a toast.
165
+ *
166
+ * @param message - The message to display
167
+ * @param type - Toast type: "success" (default) or "error"
168
+ * @returns Response with text/html content-type
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * return dsToast("Settings saved successfully.");
173
+ * return dsToast("Something went wrong.", "error");
174
+ * ```
175
+ */
176
+ export declare function dsToast(message: string, type?: "success" | "error"): Response;
177
+ /**
178
+ * Datastar signal patch via application/json
179
+ *
180
+ * Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
181
+ * Use instead of `sse()` when the only action is updating signals.
182
+ *
183
+ * @param signals - Object containing signal values to update
184
+ * @returns Response with application/json content-type
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * return dsSignals({ _uploadError: "File too large" });
189
+ * ```
190
+ */
191
+ export declare function dsSignals(signals: Record<string, unknown>): Response;
123
192
  //# sourceMappingURL=sse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/lib/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GACpC,IAAI,CAAC;IAER;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CACX,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,SAAS,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,IAAI,CAAC;IAER;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,GAAG,CACjB,CAAC,EAAE,OAAO,EACV,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,QAAQ,CA2EV"}
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/lib/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GACpC,IAAI,CAAC;IAER;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CACX,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,SAAS,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,IAAI,CAAC;IAER;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,IAAI,CAAC;CAC1D;AA+CD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,GAAG,CACjB,CAAC,EAAE,OAAO,EACV,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,QAAQ,CAoFV;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,QAAQ,CASV;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,SAAS,GAAG,OAAmB,GACpC,QAAQ,CAQV;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAIpE"}
package/dist/lib/sse.js CHANGED
@@ -1,21 +1,39 @@
1
1
  /**
2
- * Server-Sent Events (SSE) utilities for Datastar v1.0.0-RC.7
2
+ * Datastar response utilities for v1.0.0-RC.7
3
3
  *
4
- * Generates SSE events compatible with the Datastar client's expected format.
4
+ * Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
5
5
  *
6
- * @see https://data-star.dev/
6
+ * **Non-SSE helpers** (preferred for single operations):
7
+ * - `dsRedirect(url)` — redirect via text/html
8
+ * - `dsToast(message, type)` — toast notification via text/html
9
+ * - `dsSignals(signals)` — signal patch via application/json
7
10
  *
8
- * @example
9
- * ```ts
10
- * app.post("/api/example", (c) => {
11
- * return sse(c, async (stream) => {
12
- * await stream.patchSignals({ loading: false });
13
- * await stream.patchElements('<div id="result">Done!</div>');
14
- * await stream.redirect("/success");
15
- * });
16
- * });
17
- * ```
18
- */ /**
11
+ * **SSE** (for multiple operations in one response):
12
+ * - `sse(c, handler)` — streaming SSE with full stream API
13
+ *
14
+ * Datastar auto-detects response type by Content-Type:
15
+ * - `text/html` dispatches as `datastar-patch-elements`
16
+ * - `application/json` → dispatches as `datastar-patch-signals`
17
+ *
18
+ * @see https://data-star.dev/
19
+ */ // ---------------------------------------------------------------------------
20
+ // Shared internal helpers (used by both SSE and non-SSE response builders)
21
+ // ---------------------------------------------------------------------------
22
+ /** Build the redirect script tag for Datastar patch-elements */ function buildRedirectScript(url) {
23
+ const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
24
+ return `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
25
+ }
26
+ /** Build a toast notification HTML element */ function buildToastHtml(message, type) {
27
+ const cls = type === "error" ? "toast-error" : "toast-success";
28
+ const icon = type === "error" ? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>';
29
+ const closeBtn = `<button class="toast-close" data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
30
+ const escapedMessage = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31
+ return `<div class="toast ${cls}" data-init="setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)">${icon}<span>${escapedMessage}</span>${closeBtn}</div>`;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // SSE helpers
35
+ // ---------------------------------------------------------------------------
36
+ /**
19
37
  * Format a single SSE event string
20
38
  *
21
39
  * @param eventType - The Datastar event type (e.g. "datastar-patch-elements")
@@ -88,10 +106,8 @@
88
106
  controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
89
107
  },
90
108
  redirect (url) {
91
- const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
92
- const script = `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
93
109
  const dataLines = [
94
- `elements ${script}`,
110
+ `elements ${buildRedirectScript(url)}`,
95
111
  "mode append",
96
112
  "selector body"
97
113
  ];
@@ -103,6 +119,14 @@
103
119
  `mode remove`,
104
120
  `selector ${selector}`
105
121
  ])));
122
+ },
123
+ toast (message, type = "success") {
124
+ const dataLines = [
125
+ `elements ${buildToastHtml(message, type)}`,
126
+ "mode append",
127
+ "selector #toast-container"
128
+ ];
129
+ controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
106
130
  }
107
131
  };
108
132
  await handler(stream);
@@ -119,3 +143,77 @@
119
143
  headers
120
144
  });
121
145
  }
146
+ // ---------------------------------------------------------------------------
147
+ // Non-SSE Datastar helpers (for single-operation responses)
148
+ // ---------------------------------------------------------------------------
149
+ /**
150
+ * Datastar redirect via text/html
151
+ *
152
+ * Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
153
+ * Use instead of `sse()` when the only action is a redirect.
154
+ *
155
+ * @param url - The URL to redirect to
156
+ * @param options - Optional extra headers (e.g. Set-Cookie for auth)
157
+ * @returns Response with text/html content-type
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * return dsRedirect("/dash/posts");
162
+ *
163
+ * // With cookie forwarding (for auth)
164
+ * return dsRedirect("/dash", { headers: { "Set-Cookie": cookie } });
165
+ * ```
166
+ */ export function dsRedirect(url, options) {
167
+ return new Response(buildRedirectScript(url), {
168
+ headers: {
169
+ "Content-Type": "text/html",
170
+ "Datastar-Mode": "append",
171
+ "Datastar-Selector": "body",
172
+ ...options?.headers
173
+ }
174
+ });
175
+ }
176
+ /**
177
+ * Datastar toast notification via text/html
178
+ *
179
+ * Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
180
+ * Use instead of `sse()` when the only action is showing a toast.
181
+ *
182
+ * @param message - The message to display
183
+ * @param type - Toast type: "success" (default) or "error"
184
+ * @returns Response with text/html content-type
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * return dsToast("Settings saved successfully.");
189
+ * return dsToast("Something went wrong.", "error");
190
+ * ```
191
+ */ export function dsToast(message, type = "success") {
192
+ return new Response(buildToastHtml(message, type), {
193
+ headers: {
194
+ "Content-Type": "text/html",
195
+ "Datastar-Mode": "append",
196
+ "Datastar-Selector": "#toast-container"
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * Datastar signal patch via application/json
202
+ *
203
+ * Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
204
+ * Use instead of `sse()` when the only action is updating signals.
205
+ *
206
+ * @param signals - Object containing signal values to update
207
+ * @returns Response with application/json content-type
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * return dsSignals({ _uploadError: "File too large" });
212
+ * ```
213
+ */ export function dsSignals(signals) {
214
+ return new Response(JSON.stringify(signals), {
215
+ headers: {
216
+ "Content-Type": "application/json"
217
+ }
218
+ });
219
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Theme Resolution Helpers
3
+ *
4
+ * Resolves the active color theme and builds CSS for injection into `<head>`.
5
+ */
6
+ import type { ColorTheme } from "../theme/color-themes.js";
7
+ import type { JantConfig } from "../types.js";
8
+ /**
9
+ * Get the list of available color themes.
10
+ *
11
+ * Returns `config.theme.colorThemes` if provided, otherwise the built-in list.
12
+ *
13
+ * @param config - The Jant configuration
14
+ * @returns Array of available color themes
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const themes = getAvailableThemes(c.var.config);
19
+ * ```
20
+ */
21
+ export declare function getAvailableThemes(config: JantConfig): ColorTheme[];
22
+ /**
23
+ * Build a `<style>` CSS string from a color theme and optional cssVariables overlay.
24
+ *
25
+ * Priority (lowest → highest):
26
+ * BaseCoat defaults → selected theme → cssVariables
27
+ *
28
+ * @param theme - The active color theme (undefined = no theme overrides)
29
+ * @param cssVariables - Extra CSS variable overrides from `createApp({ theme: { cssVariables } })`
30
+ * @returns CSS string to inject in `<head>`, or empty string if nothing to inject
31
+ *
32
+ * Uses `:root:root` and `:root.dark` selectors for higher specificity than
33
+ * BaseCoat defaults (`:root` and `.dark`). This ensures theme overrides win
34
+ * regardless of source order — important because Vite dev mode injects CSS
35
+ * as `<style>` tags after the theme `<style>`.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const css = buildThemeStyle(blueTheme, { "--radius": "0.5rem" });
40
+ * // => ":root:root { --primary: oklch(...); ... }\n:root.dark { ... }"
41
+ * ```
42
+ */
43
+ export declare function buildThemeStyle(theme: ColorTheme | undefined, cssVariables?: Record<string, string>): string;
44
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/lib/theme.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,EAAE,CAEnE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAkCR"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Theme Resolution Helpers
3
+ *
4
+ * Resolves the active color theme and builds CSS for injection into `<head>`.
5
+ */ import { BUILTIN_COLOR_THEMES } from "../theme/color-themes.js";
6
+ /**
7
+ * Get the list of available color themes.
8
+ *
9
+ * Returns `config.theme.colorThemes` if provided, otherwise the built-in list.
10
+ *
11
+ * @param config - The Jant configuration
12
+ * @returns Array of available color themes
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const themes = getAvailableThemes(c.var.config);
17
+ * ```
18
+ */ export function getAvailableThemes(config) {
19
+ return config.theme?.colorThemes ?? BUILTIN_COLOR_THEMES;
20
+ }
21
+ /**
22
+ * Build a `<style>` CSS string from a color theme and optional cssVariables overlay.
23
+ *
24
+ * Priority (lowest → highest):
25
+ * BaseCoat defaults → selected theme → cssVariables
26
+ *
27
+ * @param theme - The active color theme (undefined = no theme overrides)
28
+ * @param cssVariables - Extra CSS variable overrides from `createApp({ theme: { cssVariables } })`
29
+ * @returns CSS string to inject in `<head>`, or empty string if nothing to inject
30
+ *
31
+ * Uses `:root:root` and `:root.dark` selectors for higher specificity than
32
+ * BaseCoat defaults (`:root` and `.dark`). This ensures theme overrides win
33
+ * regardless of source order — important because Vite dev mode injects CSS
34
+ * as `<style>` tags after the theme `<style>`.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const css = buildThemeStyle(blueTheme, { "--radius": "0.5rem" });
39
+ * // => ":root:root { --primary: oklch(...); ... }\n:root.dark { ... }"
40
+ * ```
41
+ */ export function buildThemeStyle(theme, cssVariables) {
42
+ const lightVars = {
43
+ ...theme?.light ?? {},
44
+ ...cssVariables ?? {}
45
+ };
46
+ const darkVars = {
47
+ ...theme?.dark ?? {},
48
+ ...cssVariables ?? {}
49
+ };
50
+ const hasLight = Object.keys(lightVars).length > 0;
51
+ const hasDark = Object.keys(darkVars).length > 0;
52
+ if (!hasLight && !hasDark) return "";
53
+ const parts = [];
54
+ if (hasLight) {
55
+ const declarations = Object.entries(lightVars).map(([k, v])=>` ${k}: ${v};`).join("\n");
56
+ // :root:root has specificity (0,0,2) > BaseCoat's :root (0,0,1)
57
+ parts.push(`:root:root {\n${declarations}\n}`);
58
+ }
59
+ if (hasDark) {
60
+ const declarations = Object.entries(darkVars).map(([k, v])=>` ${k}: ${v};`).join("\n");
61
+ // :root.dark has specificity (0,1,1) > BaseCoat's .dark (0,1,0)
62
+ parts.push(`:root.dark {\n${declarations}\n}`);
63
+ }
64
+ return parts.join("\n");
65
+ }
@@ -7,7 +7,7 @@
7
7
  import { html } from "hono/html";
8
8
  import { requireAuthApi } from "../../middleware/auth.js";
9
9
  import { getMediaUrl, getImageUrl } from "../../lib/image.js";
10
- import { sse } from "../../lib/sse.js";
10
+ import { sse, dsSignals } from "../../lib/sse.js";
11
11
  export const uploadApiRoutes = new Hono();
12
12
  // Require auth for all upload routes
13
13
  uploadApiRoutes.use("*", requireAuthApi());
@@ -88,10 +88,8 @@ function formatSize(bytes) {
88
88
  uploadApiRoutes.post("/", async (c)=>{
89
89
  if (!c.env.R2) {
90
90
  if (wantsSSE(c)) {
91
- return sse(c, async (stream)=>{
92
- await stream.patchSignals({
93
- _uploadError: "R2 storage not configured"
94
- });
91
+ return dsSignals({
92
+ _uploadError: "R2 storage not configured"
95
93
  });
96
94
  }
97
95
  return c.json({
@@ -102,10 +100,8 @@ uploadApiRoutes.post("/", async (c)=>{
102
100
  const file = formData.get("file");
103
101
  if (!file) {
104
102
  if (wantsSSE(c)) {
105
- return sse(c, async (stream)=>{
106
- await stream.patchSignals({
107
- _uploadError: "No file provided"
108
- });
103
+ return dsSignals({
104
+ _uploadError: "No file provided"
109
105
  });
110
106
  }
111
107
  return c.json({
@@ -122,10 +118,8 @@ uploadApiRoutes.post("/", async (c)=>{
122
118
  ];
123
119
  if (!allowedTypes.includes(file.type)) {
124
120
  if (wantsSSE(c)) {
125
- return sse(c, async (stream)=>{
126
- await stream.patchSignals({
127
- _uploadError: "File type not allowed"
128
- });
121
+ return dsSignals({
122
+ _uploadError: "File type not allowed"
129
123
  });
130
124
  }
131
125
  return c.json({
@@ -136,10 +130,8 @@ uploadApiRoutes.post("/", async (c)=>{
136
130
  const maxSize = 10 * 1024 * 1024;
137
131
  if (file.size > maxSize) {
138
132
  if (wantsSSE(c)) {
139
- return sse(c, async (stream)=>{
140
- await stream.patchSignals({
141
- _uploadError: "File too large (max 10MB)"
142
- });
133
+ return dsSignals({
134
+ _uploadError: "File too large (max 10MB)"
143
135
  });
144
136
  }
145
137
  return c.json({
@@ -176,6 +168,7 @@ uploadApiRoutes.post("/", async (c)=>{
176
168
  mode: "outer",
177
169
  selector: "#upload-placeholder"
178
170
  });
171
+ await stream.toast("Upload successful!");
179
172
  });
180
173
  }
181
174
  // JSON response for API clients
@@ -190,7 +183,12 @@ uploadApiRoutes.post("/", async (c)=>{
190
183
  } catch (err) {
191
184
  // eslint-disable-next-line no-console -- Error logging is intentional
192
185
  console.error("Upload error:", err);
193
- // Return error - client will handle updating the placeholder
186
+ if (wantsSSE(c)) {
187
+ return sse(c, async (stream)=>{
188
+ await stream.remove("#upload-placeholder");
189
+ await stream.toast("Upload failed. Please try again.", "error");
190
+ });
191
+ }
194
192
  return c.json({
195
193
  error: "Upload failed"
196
194
  }, 500);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Dashboard Appearance Routes
3
+ */
4
+ import { Hono } from "hono";
5
+ import type { Bindings } from "../../types.js";
6
+ import type { AppVariables } from "../../app.js";
7
+ type Env = {
8
+ Bindings: Bindings;
9
+ Variables: AppVariables;
10
+ };
11
+ export declare const appearanceRoutes: Hono<Env, import("hono/types").BlankSchema, "/">;
12
+ export {};
13
+ //# sourceMappingURL=appearance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/appearance.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,gBAAgB,kDAAkB,CAAC"}
@@ -0,0 +1,160 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Dashboard Appearance Routes
4
+ */ import { Hono } from "hono";
5
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
6
+ import { DashLayout } from "../../theme/layouts/index.js";
7
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
8
+ import { getSiteName } from "../../lib/config.js";
9
+ import { SETTINGS_KEYS } from "../../lib/constants.js";
10
+ import { getAvailableThemes } from "../../lib/theme.js";
11
+ export const appearanceRoutes = new Hono();
12
+ function ThemeCard({ theme, selected }) {
13
+ const expr = `$theme === '${theme.id}'`;
14
+ const { preview } = theme;
15
+ return /*#__PURE__*/ _jsx("label", {
16
+ class: `block cursor-pointer rounded-lg border overflow-hidden transition-colors ${selected ? "border-primary" : "border-border"}`,
17
+ "data-class:border-primary": expr,
18
+ "data-class:border-border": `$theme !== '${theme.id}'`,
19
+ children: /*#__PURE__*/ _jsxs("div", {
20
+ class: "grid grid-cols-2",
21
+ children: [
22
+ /*#__PURE__*/ _jsxs("div", {
23
+ class: "p-5",
24
+ style: `background-color:${preview.lightBg};color:${preview.lightText}`,
25
+ children: [
26
+ /*#__PURE__*/ _jsx("input", {
27
+ type: "radio",
28
+ name: "theme",
29
+ value: theme.id,
30
+ "data-bind": "theme",
31
+ checked: selected || undefined,
32
+ class: "mb-1"
33
+ }),
34
+ /*#__PURE__*/ _jsx("h3", {
35
+ class: "font-bold text-lg",
36
+ children: theme.name
37
+ }),
38
+ /*#__PURE__*/ _jsxs("p", {
39
+ class: "text-sm mt-2 leading-relaxed",
40
+ children: [
41
+ "This is the ",
42
+ theme.name,
43
+ " theme in light mode. Links",
44
+ " ",
45
+ /*#__PURE__*/ _jsx("a", {
46
+ tabIndex: -1,
47
+ class: "underline",
48
+ style: `color:${preview.lightLink}`,
49
+ children: "look like this"
50
+ }),
51
+ ". We'll show the correct light or dark mode based on your visitor's settings."
52
+ ]
53
+ })
54
+ ]
55
+ }),
56
+ /*#__PURE__*/ _jsxs("div", {
57
+ class: "p-5",
58
+ style: `background-color:${preview.darkBg};color:${preview.darkText}`,
59
+ children: [
60
+ /*#__PURE__*/ _jsx("h3", {
61
+ class: "font-bold text-lg",
62
+ children: theme.name
63
+ }),
64
+ /*#__PURE__*/ _jsxs("p", {
65
+ class: "text-sm mt-2 leading-relaxed",
66
+ children: [
67
+ "This is the ",
68
+ theme.name,
69
+ " theme in dark mode. Links",
70
+ " ",
71
+ /*#__PURE__*/ _jsx("a", {
72
+ tabIndex: -1,
73
+ class: "underline",
74
+ style: `color:${preview.darkLink}`,
75
+ children: "look like this"
76
+ }),
77
+ ". We'll show the correct light or dark mode based on your visitor's settings."
78
+ ]
79
+ })
80
+ ]
81
+ })
82
+ ]
83
+ })
84
+ });
85
+ }
86
+ function AppearanceContent({ themes, currentThemeId }) {
87
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
88
+ const signals = JSON.stringify({
89
+ theme: currentThemeId
90
+ }).replace(/</g, "\\u003c");
91
+ return /*#__PURE__*/ _jsx("div", {
92
+ "data-signals": signals,
93
+ "data-on:change": "@post('/dash/appearance')",
94
+ class: "max-w-3xl",
95
+ children: /*#__PURE__*/ _jsxs("fieldset", {
96
+ children: [
97
+ /*#__PURE__*/ _jsx("legend", {
98
+ class: "text-lg font-semibold",
99
+ children: $__i18n._({
100
+ id: "rFmBG3",
101
+ message: "Color theme"
102
+ })
103
+ }),
104
+ /*#__PURE__*/ _jsx("p", {
105
+ class: "text-sm text-muted-foreground mb-4",
106
+ children: $__i18n._({
107
+ id: "07Epll",
108
+ message: "This will theme both your site and your dashboard. All color themes support dark mode."
109
+ })
110
+ }),
111
+ /*#__PURE__*/ _jsx("div", {
112
+ class: "flex flex-col gap-4",
113
+ children: themes.map((theme)=>/*#__PURE__*/ _jsx(ThemeCard, {
114
+ theme: theme,
115
+ selected: theme.id === currentThemeId
116
+ }, theme.id))
117
+ })
118
+ ]
119
+ })
120
+ });
121
+ }
122
+ // Appearance page
123
+ appearanceRoutes.get("/", async (c)=>{
124
+ const { settings } = c.var.services;
125
+ const siteName = await getSiteName(c);
126
+ const currentThemeId = await settings.get(SETTINGS_KEYS.THEME) ?? "default";
127
+ const themes = getAvailableThemes(c.var.config);
128
+ const saved = c.req.query("saved") !== undefined;
129
+ return c.html(/*#__PURE__*/ _jsx(DashLayout, {
130
+ c: c,
131
+ title: "Appearance",
132
+ siteName: siteName,
133
+ currentPath: "/dash/appearance",
134
+ toast: saved ? {
135
+ message: "Theme saved successfully."
136
+ } : undefined,
137
+ children: /*#__PURE__*/ _jsx(AppearanceContent, {
138
+ themes: themes,
139
+ currentThemeId: currentThemeId
140
+ })
141
+ }));
142
+ });
143
+ // Save theme
144
+ appearanceRoutes.post("/", async (c)=>{
145
+ const body = await c.req.json();
146
+ const { settings } = c.var.services;
147
+ const themes = getAvailableThemes(c.var.config);
148
+ // Validate theme ID
149
+ const validTheme = themes.find((t)=>t.id === body.theme);
150
+ if (!validTheme) {
151
+ return dsToast("Invalid theme selected.", "error");
152
+ }
153
+ if (validTheme.id === "default") {
154
+ await settings.remove(SETTINGS_KEYS.THEME);
155
+ } else {
156
+ await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
157
+ }
158
+ // Full page reload to apply the new theme CSS
159
+ return dsRedirect("/dash/appearance?saved");
160
+ });