dsh-cool-theme 0.3.1 → 0.5.0

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 (66) hide show
  1. package/README.md +9 -10
  2. package/README.zh.md +9 -10
  3. package/lib/client.js +2694 -215
  4. package/lib/index.js +304 -6
  5. package/lib/types/client/components.d.ts +43 -3
  6. package/lib/types/client/components.d.ts.map +1 -1
  7. package/lib/types/client/custom.d.ts +124 -0
  8. package/lib/types/client/custom.d.ts.map +1 -0
  9. package/lib/types/client/locales.d.ts +102 -0
  10. package/lib/types/client/locales.d.ts.map +1 -1
  11. package/lib/types/client/presets/aura.d.ts +26 -0
  12. package/lib/types/client/presets/aura.d.ts.map +1 -0
  13. package/lib/types/client/presets/catppuccin-frappe.d.ts +26 -0
  14. package/lib/types/client/presets/catppuccin-frappe.d.ts.map +1 -0
  15. package/lib/types/client/presets/catppuccin-macchiato.d.ts +26 -0
  16. package/lib/types/client/presets/catppuccin-macchiato.d.ts.map +1 -0
  17. package/lib/types/client/presets/cobalt2.d.ts +26 -0
  18. package/lib/types/client/presets/cobalt2.d.ts.map +1 -0
  19. package/lib/types/client/presets/cursor.d.ts +26 -0
  20. package/lib/types/client/presets/cursor.d.ts.map +1 -0
  21. package/lib/types/client/presets/everforest.d.ts +26 -0
  22. package/lib/types/client/presets/everforest.d.ts.map +1 -0
  23. package/lib/types/client/presets/flexoki.d.ts +26 -0
  24. package/lib/types/client/presets/flexoki.d.ts.map +1 -0
  25. package/lib/types/client/presets/kanagawa.d.ts +26 -0
  26. package/lib/types/client/presets/kanagawa.d.ts.map +1 -0
  27. package/lib/types/client/presets/lucent-orng.d.ts +26 -0
  28. package/lib/types/client/presets/lucent-orng.d.ts.map +1 -0
  29. package/lib/types/client/presets/material.d.ts +26 -0
  30. package/lib/types/client/presets/material.d.ts.map +1 -0
  31. package/lib/types/client/presets/matrix.d.ts +26 -0
  32. package/lib/types/client/presets/matrix.d.ts.map +1 -0
  33. package/lib/types/client/presets/mercury.d.ts +26 -0
  34. package/lib/types/client/presets/mercury.d.ts.map +1 -0
  35. package/lib/types/client/presets/nightowl.d.ts +26 -0
  36. package/lib/types/client/presets/nightowl.d.ts.map +1 -0
  37. package/lib/types/client/presets/opencode.d.ts +26 -0
  38. package/lib/types/client/presets/opencode.d.ts.map +1 -0
  39. package/lib/types/client/presets/orng.d.ts +26 -0
  40. package/lib/types/client/presets/orng.d.ts.map +1 -0
  41. package/lib/types/client/presets/osaka-jade.d.ts +26 -0
  42. package/lib/types/client/presets/osaka-jade.d.ts.map +1 -0
  43. package/lib/types/client/presets/palenight.d.ts +26 -0
  44. package/lib/types/client/presets/palenight.d.ts.map +1 -0
  45. package/lib/types/client/presets/synthwave84.d.ts +26 -0
  46. package/lib/types/client/presets/synthwave84.d.ts.map +1 -0
  47. package/lib/types/client/presets/system.d.ts +26 -0
  48. package/lib/types/client/presets/system.d.ts.map +1 -0
  49. package/lib/types/client/presets/vercel.d.ts +26 -0
  50. package/lib/types/client/presets/vercel.d.ts.map +1 -0
  51. package/lib/types/client/presets/vesper.d.ts +26 -0
  52. package/lib/types/client/presets/vesper.d.ts.map +1 -0
  53. package/lib/types/client/presets.d.ts +537 -0
  54. package/lib/types/client/presets.d.ts.map +1 -1
  55. package/lib/types/client/theme-files.d.ts +37 -0
  56. package/lib/types/client/theme-files.d.ts.map +1 -0
  57. package/lib/types/client/theme.d.ts.map +1 -1
  58. package/lib/types/client/tokens.d.ts +16 -3
  59. package/lib/types/client/tokens.d.ts.map +1 -1
  60. package/lib/types/contract.d.ts +43 -0
  61. package/lib/types/contract.d.ts.map +1 -1
  62. package/lib/types/host/index.d.ts +6 -3
  63. package/lib/types/host/index.d.ts.map +1 -1
  64. package/lib/types/host/theme-files.d.ts +38 -0
  65. package/lib/types/host/theme-files.d.ts.map +1 -0
  66. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -1,11 +1,309 @@
1
- //#region src/host/index.ts
1
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ //#region src/contract.ts
5
+ /**
6
+ * The Host's theme-file API, all below one prefix route. Kept here so the two
7
+ * halves cannot drift on a path.
8
+ */
9
+ const THEME_API_PREFIX = "/cool-theme/api";
10
+ const THEME_API_PATH_THEMES = `${THEME_API_PREFIX}/themes`;
11
+ //#endregion
12
+ //#region src/host/theme-files.ts
13
+ /**
14
+ * Theme files owned by the Host.
15
+ *
16
+ * One directory per theme below `$DSH_HOME/cool-theme/themes/<id>/`, holding the
17
+ * document at `theme.json` and — once themes carry media — the originals under
18
+ * `assets/`. Keeping a theme a self-contained directory is what makes the later
19
+ * export a copy of that directory rather than a reassembly of scattered rows.
20
+ */
21
+ /** Directory name of the theme document inside one theme's own directory. */
22
+ const DOCUMENT_NAME = "theme.json";
23
+ /**
24
+ * Theme ids become directory names, so they are restricted to a conservative
25
+ * slug: anything else could escape or alias the themes root.
26
+ */
27
+ const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
28
+ function isThemeId(value) {
29
+ return typeof value === "string" && ID_PATTERN.test(value);
30
+ }
31
+ /**
32
+ * The harness home, resolved the way the rest of DSH resolves it: an explicit
33
+ * `DSH_HOME`, else `~/.dsh`.
34
+ */
35
+ function harnessHome() {
36
+ const configured = process.env.DSH_HOME?.trim();
37
+ return configured && configured !== "" ? configured : join(homedir(), ".dsh");
38
+ }
39
+ /** Root of every stored theme. */
40
+ function themesRoot() {
41
+ return join(harnessHome(), "cool-theme", "themes");
42
+ }
43
+ /** One theme's own directory. Callers must have validated `id`. */
44
+ function themeDir(id) {
45
+ return join(themesRoot(), id);
46
+ }
47
+ function documentPath(id) {
48
+ return join(themeDir(id), DOCUMENT_NAME);
49
+ }
50
+ function isRecord$1(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ /**
54
+ * Read one document back into the shape this plugin writes. Returns null when
55
+ * the file is missing or unusable; a corrupt theme must not take the roster
56
+ * down with it, so the caller skips it and keeps the rest.
57
+ */
58
+ async function readDocument(id) {
59
+ let raw;
60
+ try {
61
+ raw = await readFile(documentPath(id), "utf8");
62
+ } catch {
63
+ return null;
64
+ }
65
+ try {
66
+ const parsed = JSON.parse(raw);
67
+ if (!isRecord$1(parsed)) return null;
68
+ if (!isThemeId(parsed.id) || parsed.id !== id) return null;
69
+ if (typeof parsed.name !== "string" || typeof parsed.base !== "string") return null;
70
+ const assets = Array.isArray(parsed.assets) ? parsed.assets.filter(isAsset) : [];
71
+ return {
72
+ v: typeof parsed.v === "number" ? parsed.v : 1,
73
+ id: parsed.id,
74
+ name: parsed.name,
75
+ base: parsed.base,
76
+ theme: parsed.theme,
77
+ assets,
78
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
79
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString()
80
+ };
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ function isAsset(value) {
86
+ if (!isRecord$1(value)) return false;
87
+ return typeof value.name === "string" && (value.kind === "image" || value.kind === "video") && typeof value.mime === "string" && typeof value.bytes === "number";
88
+ }
89
+ /**
90
+ * Write one document where a reader either sees the previous file or the next
91
+ * one — never a half-written one: the bytes land in a sibling temp file that is
92
+ * renamed over the target.
93
+ */
94
+ async function writeDocument(doc) {
95
+ const dir = themeDir(doc.id);
96
+ await mkdir(dir, { recursive: true });
97
+ const temp = join(dir, `${DOCUMENT_NAME}.tmp`);
98
+ await writeFile(temp, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
99
+ await rename(temp, documentPath(doc.id));
100
+ }
101
+ /** Every stored theme, oldest by creation time first; unreadable directories are skipped. */
102
+ async function listThemes() {
103
+ let entries;
104
+ try {
105
+ entries = await readdir(themesRoot());
106
+ } catch {
107
+ return [];
108
+ }
109
+ const documents = [];
110
+ for (const id of entries) {
111
+ if (!isThemeId(id)) continue;
112
+ const doc = await readDocument(id);
113
+ if (doc !== null) documents.push(doc);
114
+ }
115
+ documents.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0);
116
+ return documents;
117
+ }
2
118
  /**
3
- * Host entry for dsh-cool-theme.
4
- * No host routes needed — theme is purely client-side (localStorage + overrideTokens).
5
- * Keep a minimal apply so the cordis patch has a mount point.
119
+ * Insert or replace one document. `createdAt` is the caller's when the theme is
120
+ * new and the stored value when it already exists, so a later rename or edit
121
+ * cannot rewrite when the theme was made.
6
122
  */
123
+ async function putTheme(input) {
124
+ const existing = await readDocument(input.id);
125
+ const now = (/* @__PURE__ */ new Date()).toISOString();
126
+ const doc = {
127
+ v: 1,
128
+ id: input.id,
129
+ name: input.name,
130
+ base: input.base,
131
+ theme: input.theme,
132
+ assets: input.assets,
133
+ createdAt: existing?.createdAt ?? now,
134
+ updatedAt: now
135
+ };
136
+ await writeDocument(doc);
137
+ return doc;
138
+ }
139
+ /** Remove one theme directory. A missing directory is already the goal. */
140
+ async function deleteTheme(id) {
141
+ await rm(themeDir(id), {
142
+ recursive: true,
143
+ force: true
144
+ });
145
+ }
146
+ //#endregion
147
+ //#region src/host/index.ts
7
148
  const name = "dsh-cool-theme";
8
- const inject = [];
9
- function apply(_ctx) {}
149
+ const inject = ["webServer"];
150
+ /** Refuse cross-site callers: this API reads and writes the user's own files. */
151
+ function sameOrigin(req) {
152
+ const origin = req.headers.origin;
153
+ if (typeof origin !== "string" || origin === "" || origin === "null") return true;
154
+ const host = req.headers.host;
155
+ if (typeof host !== "string" || host === "") return false;
156
+ try {
157
+ return new URL(origin).host === host;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ function sendJson(res, status, body) {
163
+ res.writeHead(status, {
164
+ "content-type": "application/json; charset=utf-8",
165
+ "cache-control": "no-store"
166
+ });
167
+ res.end(JSON.stringify(body));
168
+ }
169
+ /** Answer 405 unless the request uses one of the allowed methods. */
170
+ function methodAllowed(req, res, allowed) {
171
+ const method = req.method ?? "GET";
172
+ if (allowed.includes(method)) return true;
173
+ res.writeHead(405, { allow: allowed.join(", ") });
174
+ res.end();
175
+ return false;
176
+ }
177
+ /** Read a bounded request body; a theme document is small by construction. */
178
+ async function readBody(req, limit = 1e6) {
179
+ const chunks = [];
180
+ let size = 0;
181
+ for await (const chunk of req) {
182
+ const buf = chunk;
183
+ size += buf.byteLength;
184
+ if (size > limit) throw new Error("request body too large");
185
+ chunks.push(buf);
186
+ }
187
+ return Buffer.concat(chunks).toString("utf8");
188
+ }
189
+ function isRecord(value) {
190
+ return typeof value === "object" && value !== null && !Array.isArray(value);
191
+ }
192
+ /** The roster, newest first, as the browser half reads it. */
193
+ async function handleList(res) {
194
+ sendJson(res, 200, {
195
+ ok: true,
196
+ themes: await listThemes()
197
+ });
198
+ }
199
+ /**
200
+ * Upsert the posted documents. The browser half sends exactly the entries it
201
+ * changed, so one round trip covers save, rename and duplicate.
202
+ */
203
+ async function handlePut(req, res) {
204
+ let payload;
205
+ try {
206
+ payload = JSON.parse(await readBody(req));
207
+ } catch {
208
+ sendJson(res, 400, {
209
+ ok: false,
210
+ error: "invalid JSON body"
211
+ });
212
+ return;
213
+ }
214
+ const incoming = isRecord(payload) && Array.isArray(payload.themes) ? payload.themes : null;
215
+ if (incoming === null) {
216
+ sendJson(res, 400, {
217
+ ok: false,
218
+ error: "expected { themes: [...] }"
219
+ });
220
+ return;
221
+ }
222
+ const saved = [];
223
+ for (const raw of incoming) {
224
+ if (!isRecord(raw) || !isThemeId(raw.id)) {
225
+ sendJson(res, 400, {
226
+ ok: false,
227
+ error: "theme id must be a safe slug"
228
+ });
229
+ return;
230
+ }
231
+ if (typeof raw.name !== "string" || raw.name.trim() === "") {
232
+ sendJson(res, 400, {
233
+ ok: false,
234
+ error: "theme name must be a non-empty string"
235
+ });
236
+ return;
237
+ }
238
+ if (typeof raw.base !== "string") {
239
+ sendJson(res, 400, {
240
+ ok: false,
241
+ error: "theme base must be a string"
242
+ });
243
+ return;
244
+ }
245
+ saved.push(await putTheme({
246
+ id: raw.id,
247
+ name: raw.name,
248
+ base: raw.base,
249
+ theme: raw.theme,
250
+ assets: Array.isArray(raw.assets) ? raw.assets : []
251
+ }));
252
+ }
253
+ sendJson(res, 200, {
254
+ ok: true,
255
+ themes: saved
256
+ });
257
+ }
258
+ /** Delete one theme directory. */
259
+ async function handleDelete(res, id) {
260
+ await deleteTheme(id);
261
+ sendJson(res, 200, { ok: true });
262
+ }
263
+ function apply(ctx) {
264
+ const route = (handler) => ctx.webServer.register({
265
+ kind: "prefix",
266
+ path: THEME_API_PREFIX,
267
+ handler: (req, res) => {
268
+ if (!sameOrigin(req)) {
269
+ sendJson(res, 403, {
270
+ ok: false,
271
+ error: "cross-origin request refused"
272
+ });
273
+ return;
274
+ }
275
+ return handler(req, res);
276
+ }
277
+ });
278
+ ctx.effect(() => {
279
+ const dispose = route(async (req, res) => {
280
+ const pathname = new URL(req.url ?? "/", "http://dsh.invalid").pathname;
281
+ try {
282
+ if (pathname === THEME_API_PATH_THEMES) {
283
+ if (!methodAllowed(req, res, ["GET", "POST"])) return;
284
+ if (req.method === "GET") return await handleList(res);
285
+ return await handlePut(req, res);
286
+ }
287
+ const rest = pathname.startsWith(`${THEME_API_PATH_THEMES}/`) ? pathname.slice(THEME_API_PATH_THEMES.length + 1) : null;
288
+ if (rest !== null && isThemeId(rest)) {
289
+ if (!methodAllowed(req, res, ["DELETE"])) return;
290
+ return await handleDelete(res, rest);
291
+ }
292
+ sendJson(res, 404, {
293
+ ok: false,
294
+ error: "unknown route"
295
+ });
296
+ } catch (error) {
297
+ sendJson(res, 500, {
298
+ ok: false,
299
+ error: error instanceof Error ? error.message : String(error)
300
+ });
301
+ }
302
+ });
303
+ return () => {
304
+ dispose();
305
+ };
306
+ }, "dsh-cool-theme: theme-file routes");
307
+ }
10
308
  //#endregion
11
309
  export { apply, inject, name };
@@ -1,14 +1,54 @@
1
1
  /**
2
- * Theme settings UI: scheme selector + preset picker.
2
+ * Theme settings UI: scheme selector, preset picker, and the custom colour editor.
3
3
  */
4
4
  import * as React from 'react';
5
5
  import { type PresetId } from './presets.js';
6
6
  import { type ThemeKey } from './locales.js';
7
+ import { type CustomTheme, type SavedTheme } from './custom.js';
8
+ /** Reserved menu value that switches to the user-defined theme. */
9
+ export declare const CUSTOM_SELECTION = "custom";
10
+ export type Selection = PresetId | typeof CUSTOM_SELECTION;
11
+ export type CustomState = {
12
+ base: PresetId;
13
+ theme: CustomTheme;
14
+ };
7
15
  export declare function ThemePanel(props: {
8
16
  theme: any;
9
- getPreset: () => PresetId;
10
- setPreset: (id: PresetId) => void;
17
+ getSelection: () => Selection;
18
+ setSelection: (s: Selection) => void;
19
+ getCustom: () => CustomState;
20
+ setCustom: (theme: CustomTheme) => void;
21
+ /** Pick the preset future custom themes seed from, without applying it. */
22
+ setCustomBase: (base: PresetId) => void;
23
+ resetCustom: () => CustomTheme;
24
+ saved: {
25
+ /** Cached roster; empty until `refresh` resolves. */
26
+ list: () => SavedTheme[];
27
+ activeId: () => string | null;
28
+ load: (id: string) => CustomTheme | null;
29
+ /** Point the "in use" badge at an entry, or clear it entirely. */
30
+ setActive: (id: string | null) => void;
31
+ /** Re-read the roster from the Host, migrating the legacy browser list once. */
32
+ refresh: () => Promise<SavedTheme[]>;
33
+ /** Persist the current draft as a new named entry and make it active. */
34
+ create: (name: string) => Promise<SavedTheme[]>;
35
+ /** Write the current draft (and name) back to an existing entry. */
36
+ update: (id: string, name: string) => Promise<SavedTheme[]>;
37
+ remove: (id: string) => Promise<SavedTheme[]>;
38
+ };
39
+ /**
40
+ * The shell's own Toast, or null when the host does not share its primitives
41
+ * module with plugins. Read at render time: the async resolution lands long
42
+ * before the first action a banner can report.
43
+ */
44
+ getHostToast: () => ((props: any) => any) | null;
11
45
  t: (key: ThemeKey) => string;
46
+ /**
47
+ * The shell's own close seat, or null on a host that does not offer it. The
48
+ * settings modal is shell-owned, so this is both the only way to leave after
49
+ * a confirmed discard and the gate on whether guarding a close is worthwhile.
50
+ */
51
+ onRequestClose: (() => void) | null;
12
52
  }): React.DetailedReactHTMLElement<{
13
53
  style: {
14
54
  display: "flex";
@@ -1 +1 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/client/components.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6G5C,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,KAAK,EAAE,GAAG,CAAA;IACV,SAAS,EAAE,MAAM,QAAQ,CAAA;IACzB,SAAS,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,IAAI,CAAA;IACjC,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAA;CAC7B;;;;;;;gBAyDA"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/client/components.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EACL,KAAK,WAAW,EAGhB,KAAK,UAAU,EAIhB,MAAM,aAAa,CAAA;AAEpB,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,WAAmB,CAAA;AAChD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,gBAAgB,CAAA;AAE1D,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAA;AAmZhE,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,KAAK,EAAE,GAAG,CAAA;IACV,YAAY,EAAE,MAAM,SAAS,CAAA;IAC7B,YAAY,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACpC,SAAS,EAAE,MAAM,WAAW,CAAA;IAC5B,SAAS,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,2EAA2E;IAC3E,aAAa,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;IACvC,WAAW,EAAE,MAAM,WAAW,CAAA;IAC9B,KAAK,EAAE;QACL,qDAAqD;QACrD,IAAI,EAAE,MAAM,UAAU,EAAE,CAAA;QACxB,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAA;QACxC,kEAAkE;QAClE,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;QACtC,gFAAgF;QAChF,OAAO,EAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;QACpC,yEAAyE;QACzE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;QAC/C,oEAAoE;QACpE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;QAC3D,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;KAC9C,CAAA;IACD;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAA;IAChD,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAA;IAC5B;;;;OAIG;IACH,cAAc,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;CACpC;;;;;;;gBAwuBA"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Custom theme: a `PresetDef` generated at runtime from a small set of seeds.
3
+ *
4
+ * The colour maths is deliberately IDENTICAL to the shipped presets — the same
5
+ * `buildScale` / `buildSemanticScales` helpers with the same steps and weights,
6
+ * and the same `t = step / 1000` interpolation. A custom theme is therefore not
7
+ * a special case: it produces a complete 73-primitive + 9-shiki map that flows
8
+ * through `resolvePreset` -> `primitiveOverrides` unchanged. The one structural
9
+ * difference is the neutral ramp, which is built per appearance (see below).
10
+ *
11
+ * Seed groups (6):
12
+ * neutral merged `--dsw-static-neutral-bluish-*` + `--dsw-static-neutral-*`,
13
+ * one ramp PER appearance
14
+ * accent merged `--dsw-static-deepseek-*` + `--dsw-static-blue-*`
15
+ * green / amber / red
16
+ * shiki 9 syntax tokens
17
+ */
18
+ import type { PresetDef } from './presets.js';
19
+ import type { ThemeAsset } from '../contract.js';
20
+ export type { PresetDef };
21
+ export type Mode = 'light' | 'dark';
22
+ /** One seed colour per appearance. */
23
+ export type SeedPair = {
24
+ light: string;
25
+ dark: string;
26
+ };
27
+ /** The nine `--shiki-token-*` slots, in stylesheet order. */
28
+ export declare const SHIKI_KEYS: readonly ["constant", "string", "comment", "keyword", "parameter", "function", "string-expression", "punctuation", "link"];
29
+ export type ShikiKey = (typeof SHIKI_KEYS)[number];
30
+ /** The two endpoints of ONE appearance's neutral ramp: step 00 and step 1000. */
31
+ export type NeutralRamp = {
32
+ lightest: string;
33
+ darkest: string;
34
+ };
35
+ /** Everything a user can edit. 2*2 + 4*2 + 9*2 = 30 colours. */
36
+ export type CustomTheme = {
37
+ /**
38
+ * One neutral ramp PER appearance. DSH's alias layer picks a different step
39
+ * per appearance — light reads step 00 for surfaces and step 1000 for text,
40
+ * dark reads step 950 for surfaces and step 50 for text — so a ramp shared by
41
+ * both appearances ties light text to dark surfaces and cannot be tuned. Each
42
+ * ramp still runs lightest -> darkest; only the endpoints are per-appearance.
43
+ *
44
+ * Presets keep the single shared ramp they were authored with; only the
45
+ * runtime-generated custom theme splits it.
46
+ */
47
+ neutral: Record<Mode, NeutralRamp>;
48
+ /** `--dsw-static-deepseek-*` and `--dsw-static-blue-*` share this base. */
49
+ accent: SeedPair;
50
+ green: SeedPair;
51
+ amber: SeedPair;
52
+ red: SeedPair;
53
+ shiki: Record<ShikiKey, SeedPair>;
54
+ };
55
+ /**
56
+ * Parse a colour into `#RRGGBB`, or null when it is not a format we understand.
57
+ *
58
+ * Accepted: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`, `rgb()`/`rgba()` with
59
+ * numbers or percentages, and `hsl()`/`hsla()`. Alpha is parsed but dropped —
60
+ * every token these seeds drive is an opaque theme colour.
61
+ *
62
+ * `primitives.ts` stores `rgb(r, g, b)` while presets store hex, so both shapes
63
+ * have to work; the extra forms are there for what users may type.
64
+ */
65
+ export declare function tryToHex(input: unknown): string | null;
66
+ /** `tryToHex` with a black fallback, for call sites that need a guaranteed value. */
67
+ export declare function toHex(input: unknown): string;
68
+ /**
69
+ * Recover the seed inputs from an already-generated preset.
70
+ *
71
+ * Exact by construction: `buildScale` writes the `start`/`end` arguments to
72
+ * steps 00/1000, and `buildTintShadeScale` writes the base to step 500. This is
73
+ * what lets a user start from any preset without re-declaring its palette.
74
+ */
75
+ export declare function extractSeeds(base: PresetDef): CustomTheme;
76
+ /**
77
+ * Bump when the `CustomTheme` shape changes so old blobs can be migrated.
78
+ * v2 split the neutral ramp per appearance; v1 blobs stay readable.
79
+ */
80
+ export declare const CUSTOM_SCHEMA_VERSION = 2;
81
+ /** Self-contained stored form: records the preset the seeds were derived from. */
82
+ export type CustomEnvelope = {
83
+ v: number;
84
+ base: string;
85
+ theme: unknown;
86
+ };
87
+ export declare function encodeCustom(base: string, theme: CustomTheme): string;
88
+ /** Parse a stored blob. Returns null for absent/corrupt/foreign-version data. */
89
+ export declare function parseEnvelope(raw: string | null): CustomEnvelope | null;
90
+ /** Repair a partially-invalid object against `fallback`, keeping every valid colour. */
91
+ export declare function normalizeCustom(raw: unknown, fallback: CustomTheme): CustomTheme;
92
+ /**
93
+ * Turn seeds into a full `PresetDef` using the presets' own helpers.
94
+ *
95
+ * Unlike the presets (which ship one ramp for both appearances), the ramp is
96
+ * built once per appearance, so the light text and the dark text are no longer
97
+ * the two ends of the same colour axis.
98
+ */
99
+ export declare function buildCustomPreset(v: CustomTheme): PresetDef;
100
+ /** A named custom theme the user saved. */
101
+ export type SavedTheme = {
102
+ id: string;
103
+ name: string;
104
+ /** Preset the seeds derive from; used for the "reset" fallback. */
105
+ base: string;
106
+ theme: CustomTheme;
107
+ /**
108
+ * Media stored beside the seeds. Empty until themes carry a background, but
109
+ * always carried through a rewrite so a later edit cannot drop one.
110
+ */
111
+ assets: ThemeAsset[];
112
+ };
113
+ /** Collision-resistant enough for a local list, and stable across reloads. */
114
+ export declare function newThemeId(): string;
115
+ export declare function encodeList(list: SavedTheme[]): string;
116
+ /**
117
+ * Parse the saved list. Entries with a corrupt theme are repaired against
118
+ * `fallbackFor(base)` rather than dropped, so one bad record cannot lose the
119
+ * rest of the list; only structurally unusable entries are skipped.
120
+ */
121
+ export declare function decodeList(raw: string | null, fallbackFor: (base: string) => CustomTheme): SavedTheme[];
122
+ /** `${base} 1`, `${base} 2`, … skipping names already taken. */
123
+ export declare function nextThemeName(base: string, taken: string[]): string;
124
+ //# sourceMappingURL=custom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../../src/client/custom.ts"],"names":[],"mappings":"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"}
@@ -10,9 +10,60 @@ export declare const zh: {
10
10
  'appearance.desc': string;
11
11
  'presets.title': string;
12
12
  'presets.desc': string;
13
+ 'presets.disabled': string;
13
14
  'scheme.light': string;
14
15
  'scheme.dark': string;
15
16
  'scheme.system': string;
17
+ 'custom.title': string;
18
+ 'custom.toggle.desc': string;
19
+ 'custom.group.seeds': string;
20
+ 'custom.group.shiki': string;
21
+ 'custom.mode.label': string;
22
+ 'custom.neutral.title': string;
23
+ 'custom.neutral.desc': string;
24
+ 'custom.neutral.lightest': string;
25
+ 'custom.neutral.darkest': string;
26
+ 'custom.neutral.roleBackground': string;
27
+ 'custom.neutral.roleForeground': string;
28
+ 'custom.accent.title': string;
29
+ 'custom.accent.desc': string;
30
+ 'custom.green.title': string;
31
+ 'custom.green.desc': string;
32
+ 'custom.amber.title': string;
33
+ 'custom.amber.desc': string;
34
+ 'custom.red.title': string;
35
+ 'custom.red.desc': string;
36
+ 'custom.shiki.desc': string;
37
+ 'custom.seed.light': string;
38
+ 'custom.seed.dark': string;
39
+ 'custom.save': string;
40
+ 'custom.add': string;
41
+ 'custom.edit': string;
42
+ 'custom.name.placeholder': string;
43
+ 'custom.defaultName': string;
44
+ 'custom.inUse': string;
45
+ 'custom.delete': string;
46
+ 'custom.cancel': string;
47
+ 'custom.toast.saved': string;
48
+ 'custom.toast.failed': string;
49
+ 'custom.toast.unavailable': string;
50
+ 'custom.delete.title': string;
51
+ 'custom.delete.desc': string;
52
+ 'custom.unsaved.title': string;
53
+ 'custom.unsaved.desc': string;
54
+ 'custom.unsaved.confirm': string;
55
+ 'custom.unsaved.leaveDesc': string;
56
+ 'custom.unsaved.leaveConfirm': string;
57
+ 'custom.unsaved.cancel': string;
58
+ 'shiki.constant': string;
59
+ 'shiki.string': string;
60
+ 'shiki.comment': string;
61
+ 'shiki.keyword': string;
62
+ 'shiki.parameter': string;
63
+ 'shiki.function': string;
64
+ 'shiki.string-expression': string;
65
+ 'shiki.punctuation': string;
66
+ 'shiki.link': string;
16
67
  };
17
68
  /** The theme settings namespace key union. */
18
69
  export type ThemeKey = keyof typeof zh;
@@ -23,8 +74,59 @@ export declare const en: {
23
74
  'appearance.desc': string;
24
75
  'presets.title': string;
25
76
  'presets.desc': string;
77
+ 'presets.disabled': string;
26
78
  'scheme.light': string;
27
79
  'scheme.dark': string;
28
80
  'scheme.system': string;
81
+ 'custom.title': string;
82
+ 'custom.toggle.desc': string;
83
+ 'custom.group.seeds': string;
84
+ 'custom.group.shiki': string;
85
+ 'custom.mode.label': string;
86
+ 'custom.neutral.title': string;
87
+ 'custom.neutral.desc': string;
88
+ 'custom.neutral.lightest': string;
89
+ 'custom.neutral.darkest': string;
90
+ 'custom.neutral.roleBackground': string;
91
+ 'custom.neutral.roleForeground': string;
92
+ 'custom.accent.title': string;
93
+ 'custom.accent.desc': string;
94
+ 'custom.green.title': string;
95
+ 'custom.green.desc': string;
96
+ 'custom.amber.title': string;
97
+ 'custom.amber.desc': string;
98
+ 'custom.red.title': string;
99
+ 'custom.red.desc': string;
100
+ 'custom.shiki.desc': string;
101
+ 'custom.seed.light': string;
102
+ 'custom.seed.dark': string;
103
+ 'custom.save': string;
104
+ 'custom.add': string;
105
+ 'custom.edit': string;
106
+ 'custom.name.placeholder': string;
107
+ 'custom.defaultName': string;
108
+ 'custom.inUse': string;
109
+ 'custom.delete': string;
110
+ 'custom.cancel': string;
111
+ 'custom.toast.saved': string;
112
+ 'custom.toast.failed': string;
113
+ 'custom.toast.unavailable': string;
114
+ 'custom.delete.title': string;
115
+ 'custom.delete.desc': string;
116
+ 'custom.unsaved.title': string;
117
+ 'custom.unsaved.desc': string;
118
+ 'custom.unsaved.confirm': string;
119
+ 'custom.unsaved.leaveDesc': string;
120
+ 'custom.unsaved.leaveConfirm': string;
121
+ 'custom.unsaved.cancel': string;
122
+ 'shiki.constant': string;
123
+ 'shiki.string': string;
124
+ 'shiki.comment': string;
125
+ 'shiki.keyword': string;
126
+ 'shiki.parameter': string;
127
+ 'shiki.function': string;
128
+ 'shiki.string-expression': string;
129
+ 'shiki.punctuation': string;
130
+ 'shiki.link': string;
29
131
  };
30
132
  //# sourceMappingURL=locales.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;CASmB,CAAA;AAElC,8CAA8C;AAC9C,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,EAAE,CAAA;AAEtC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;CASqB,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DmB,CAAA;AAElC,8CAA8C;AAC9C,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,EAAE,CAAA;AAEtC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DqB,CAAA"}
@@ -0,0 +1,26 @@
1
+ export declare const aura: {
2
+ readonly label: "Aura";
3
+ readonly light: {
4
+ readonly '--shiki-token-constant': "#7C3AED";
5
+ readonly '--shiki-token-string': "#059669";
6
+ readonly '--shiki-token-comment': "#6B7280";
7
+ readonly '--shiki-token-keyword': "#C026D3";
8
+ readonly '--shiki-token-parameter': "#B45309";
9
+ readonly '--shiki-token-function': "#7C3AED";
10
+ readonly '--shiki-token-string-expression': "#059669";
11
+ readonly '--shiki-token-punctuation': "#4B5563";
12
+ readonly '--shiki-token-link': "#7C3AED";
13
+ };
14
+ readonly dark: {
15
+ readonly '--shiki-token-constant': "#FFCA85";
16
+ readonly '--shiki-token-string': "#61FFCA";
17
+ readonly '--shiki-token-comment': "#6C6F93";
18
+ readonly '--shiki-token-keyword': "#FF61EF";
19
+ readonly '--shiki-token-parameter': "#FFCA85";
20
+ readonly '--shiki-token-function': "#A277FF";
21
+ readonly '--shiki-token-string-expression': "#61FFCA";
22
+ readonly '--shiki-token-punctuation': "#B4B7CF";
23
+ readonly '--shiki-token-link': "#A277FF";
24
+ };
25
+ };
26
+ //# sourceMappingURL=aura.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aura.d.ts","sourceRoot":"","sources":["../../../../src/client/presets/aura.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;CA8BP,CAAA"}