@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.
- package/dist/app.d.ts +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +307 -137
- package/dist/client.js +1 -0
- package/dist/i18n/context.d.ts +2 -2
- package/dist/i18n/context.js +1 -1
- package/dist/i18n/i18n.d.ts +1 -1
- package/dist/i18n/i18n.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/config.d.ts +44 -10
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +69 -44
- package/dist/lib/constants.d.ts +2 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +5 -2
- package/dist/lib/image-processor.js +0 -4
- package/dist/lib/media-upload.js +104 -0
- package/dist/lib/sse.d.ts +82 -13
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +115 -17
- package/dist/lib/theme.d.ts +44 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +65 -0
- package/dist/routes/api/upload.js +16 -18
- package/dist/routes/dash/appearance.d.ts +13 -0
- package/dist/routes/dash/appearance.d.ts.map +1 -0
- package/dist/routes/dash/appearance.js +160 -0
- package/dist/routes/dash/collections.js +5 -13
- package/dist/routes/dash/media.js +17 -167
- package/dist/routes/dash/pages.js +4 -10
- package/dist/routes/dash/posts.js +4 -10
- package/dist/routes/dash/redirects.js +3 -7
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +52 -42
- package/dist/services/settings.d.ts +1 -0
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/services/settings.js +3 -0
- package/dist/theme/color-themes.d.ts +30 -0
- package/dist/theme/color-themes.d.ts.map +1 -0
- package/dist/theme/color-themes.js +268 -0
- package/dist/theme/layouts/BaseLayout.d.ts +5 -0
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +70 -3
- package/dist/theme/layouts/DashLayout.d.ts +2 -0
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +11 -1
- package/dist/theme/layouts/index.d.ts +1 -1
- package/dist/theme/layouts/index.d.ts.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +52 -0
- package/package.json +1 -1
- package/src/app.tsx +260 -81
- package/src/client.ts +1 -0
- package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
- package/src/db/migrations/meta/0000_snapshot.json +9 -9
- package/src/db/migrations/meta/_journal.json +2 -30
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +1 -1
- package/src/i18n/locales/en.po +328 -252
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +315 -278
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +315 -278
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/config.ts +73 -47
- package/src/lib/constants.ts +3 -0
- package/src/lib/image-processor.ts +0 -7
- package/src/lib/media-upload.ts +148 -0
- package/src/lib/sse.ts +156 -16
- package/src/lib/theme.ts +86 -0
- package/src/preset.css +9 -0
- package/src/routes/api/upload.ts +12 -18
- package/src/routes/dash/appearance.tsx +176 -0
- package/src/routes/dash/collections.tsx +5 -13
- package/src/routes/dash/media.tsx +16 -165
- package/src/routes/dash/pages.tsx +4 -10
- package/src/routes/dash/posts.tsx +4 -10
- package/src/routes/dash/redirects.tsx +3 -7
- package/src/routes/dash/settings.tsx +71 -55
- package/src/services/settings.ts +5 -0
- package/src/styles/components.css +93 -0
- package/src/theme/color-themes.ts +321 -0
- package/src/theme/layouts/BaseLayout.tsx +61 -1
- package/src/theme/layouts/DashLayout.tsx +14 -3
- package/src/theme/layouts/index.ts +5 -1
- package/src/types.ts +62 -1
- package/src/db/migrations/0001_add_search_fts.sql +0 -40
- package/src/db/migrations/0002_collection_path.sql +0 -2
- package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
- package/src/db/migrations/0004_media_uuid.sql +0 -35
package/dist/lib/sse.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Datastar response utilities for v1.0.0-RC.7
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
package/dist/lib/sse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/lib/sse.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
2
|
+
* Datastar response utilities for v1.0.0-RC.7
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 ${
|
|
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
|
|
92
|
-
|
|
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
|
|
106
|
-
|
|
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
|
|
126
|
-
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
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
|
+
});
|