dsh-cool-theme 0.6.2 → 0.8.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.
- package/README.md +9 -16
- package/README.zh.md +8 -15
- package/lib/client.js +685 -45
- package/lib/types/client/components.d.ts +3 -0
- package/lib/types/client/components.d.ts.map +1 -1
- package/lib/types/client/css/accents.d.ts +47 -0
- package/lib/types/client/css/accents.d.ts.map +1 -0
- package/lib/types/client/css/primitives.d.ts +5 -4
- package/lib/types/client/css/primitives.d.ts.map +1 -1
- package/lib/types/client/custom.d.ts +59 -6
- package/lib/types/client/custom.d.ts.map +1 -1
- package/lib/types/client/icons.d.ts +37 -0
- package/lib/types/client/icons.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/theme.d.ts.map +1 -1
- package/lib/types/client/tokens.d.ts +6 -0
- package/lib/types/client/tokens.d.ts.map +1 -1
- package/lib/types/contract.d.ts +1 -0
- package/lib/types/contract.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -40,6 +40,7 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
const CUSTOM_LIST_STORAGE_KEY = "cool-theme-custom-list";
|
|
41
41
|
const CUSTOM_ACTIVE_STORAGE_KEY = "cool-theme-custom-active";
|
|
42
42
|
const CUSTOM_MIGRATED_STORAGE_KEY = "cool-theme-custom-migrated";
|
|
43
|
+
const SIDEBAR_TRANSPARENT_STORAGE_KEY = "cool-theme-sidebar-transparent";
|
|
43
44
|
const THEME_API_PATH_THEMES = `/cool-theme/api/themes`;
|
|
44
45
|
const THEME_ARCHIVE_EXTENSION = ".zip";
|
|
45
46
|
/**
|
|
@@ -249,7 +250,7 @@ window.__ModuleLoader__.load({
|
|
|
249
250
|
return map;
|
|
250
251
|
}
|
|
251
252
|
/** Mix two colors linearly in RGB. weight 0 -> colorA, 1 -> colorB */
|
|
252
|
-
function mix(colorA, colorB, weight) {
|
|
253
|
+
function mix$1(colorA, colorB, weight) {
|
|
253
254
|
const a = hexToRgb(colorA);
|
|
254
255
|
const b = hexToRgb(colorB);
|
|
255
256
|
const w = Math.max(0, Math.min(1, weight));
|
|
@@ -320,7 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
320
321
|
for (const step of steps) {
|
|
321
322
|
const w = weights[step];
|
|
322
323
|
if (w === void 0) continue;
|
|
323
|
-
const color = step === 500 || step === "500" ? base.toUpperCase() : typeof step === "number" && step > 500 || step === "950" ? mix("#000000", base, w) : mix("#FFFFFF", base, w);
|
|
324
|
+
const color = step === 500 || step === "500" ? base.toUpperCase() : typeof step === "number" && step > 500 || step === "950" ? mix$1("#000000", base, w) : mix$1("#FFFFFF", base, w);
|
|
324
325
|
map[`${prefix}-${step}`] = color;
|
|
325
326
|
}
|
|
326
327
|
return map;
|
|
@@ -353,7 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
353
354
|
...buildGreenScale(opts.green),
|
|
354
355
|
...buildAmberScale(opts.amber),
|
|
355
356
|
...buildRedScale(opts.red),
|
|
356
|
-
"--dsw-static-deepseek-700-delete": mix("#000000", opts.deepseek, .7)
|
|
357
|
+
"--dsw-static-deepseek-700-delete": mix$1("#000000", opts.deepseek, .7)
|
|
357
358
|
};
|
|
358
359
|
}
|
|
359
360
|
//#endregion
|
|
@@ -2188,6 +2189,226 @@ window.__ModuleLoader__.load({
|
|
|
2188
2189
|
return out;
|
|
2189
2190
|
}
|
|
2190
2191
|
//#endregion
|
|
2192
|
+
//#region src/client/css/accents.ts
|
|
2193
|
+
/**
|
|
2194
|
+
* Accent candidates, most specific first. Light takes `deepseek-500` and dark
|
|
2195
|
+
* takes `deepseek-400` — the exact steps DSH picks for `state-business-primary`
|
|
2196
|
+
* and `button-info-fill`, so the themed controls land on the colour the shell
|
|
2197
|
+
* already uses for its own emphasis. The blue scale is the fallback for a
|
|
2198
|
+
* palette that renames or omits the DeepSeek one.
|
|
2199
|
+
*/
|
|
2200
|
+
const ACCENT_KEYS = {
|
|
2201
|
+
light: [
|
|
2202
|
+
"--dsw-static-deepseek-500",
|
|
2203
|
+
"--dsw-static-deepseek-450",
|
|
2204
|
+
"--dsw-static-blue-500",
|
|
2205
|
+
"--dsw-static-blue-450"
|
|
2206
|
+
],
|
|
2207
|
+
dark: [
|
|
2208
|
+
"--dsw-static-deepseek-400",
|
|
2209
|
+
"--dsw-static-deepseek-450",
|
|
2210
|
+
"--dsw-static-blue-400",
|
|
2211
|
+
"--dsw-static-blue-450"
|
|
2212
|
+
]
|
|
2213
|
+
};
|
|
2214
|
+
/**
|
|
2215
|
+
* The step next to the accent on the light→dark axis, which DSH itself uses as
|
|
2216
|
+
* the filled button's hover (`button-info-fill: deepseek-500` with
|
|
2217
|
+
* `button-info-hover: deepseek-400` in light, the pair reversed in dark).
|
|
2218
|
+
*/
|
|
2219
|
+
const HOVER_KEYS = {
|
|
2220
|
+
light: [
|
|
2221
|
+
"--dsw-static-deepseek-400",
|
|
2222
|
+
"--dsw-static-deepseek-450",
|
|
2223
|
+
"--dsw-static-blue-400",
|
|
2224
|
+
"--dsw-static-blue-450"
|
|
2225
|
+
],
|
|
2226
|
+
dark: [
|
|
2227
|
+
"--dsw-static-deepseek-500",
|
|
2228
|
+
"--dsw-static-deepseek-450",
|
|
2229
|
+
"--dsw-static-blue-500",
|
|
2230
|
+
"--dsw-static-blue-450"
|
|
2231
|
+
]
|
|
2232
|
+
};
|
|
2233
|
+
/**
|
|
2234
|
+
* The palette's extremes, used as the two ink candidates. A preset keeps its
|
|
2235
|
+
* neutral ramp identical in both appearances (the alias layer forks, the
|
|
2236
|
+
* primitives do not), so `neutral-bluish-00` is the lightest colour of the theme
|
|
2237
|
+
* in light *and* dark — which is also the label the shell prints on a Tooltip.
|
|
2238
|
+
*/
|
|
2239
|
+
const LIGHTEST_INK_KEY = "--dsw-static-neutral-bluish-00";
|
|
2240
|
+
const DARKEST_INK_KEY = "--dsw-static-neutral-bluish-1000";
|
|
2241
|
+
/**
|
|
2242
|
+
* Alpha of the accent on the interaction overlays. Taken from the literals DSH
|
|
2243
|
+
* ships (`#2631480f`/`#2631481a` light, `#ffffff14`/`#ffffff24` dark), so only
|
|
2244
|
+
* the hue changes and hover weight stays exactly as the shell tuned it.
|
|
2245
|
+
*/
|
|
2246
|
+
const OVERLAY_ALPHA = {
|
|
2247
|
+
light: {
|
|
2248
|
+
hover: 6,
|
|
2249
|
+
active: 10
|
|
2250
|
+
},
|
|
2251
|
+
dark: {
|
|
2252
|
+
hover: 8,
|
|
2253
|
+
active: 14
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
/** Share of the palette's darkest neutral mixed into the accent for the tooltip. */
|
|
2257
|
+
const TOOLTIP_INK_WEIGHT = .55;
|
|
2258
|
+
/** Shifting applied per retry when the tooltip label is not yet legible. */
|
|
2259
|
+
const TOOLTIP_SHIFT_STEP = .25;
|
|
2260
|
+
/** Retry cap, so a palette whose lightest ink sits next to the bubble cannot spin here. */
|
|
2261
|
+
const TOOLTIP_SHIFT_RETRIES = 6;
|
|
2262
|
+
/** WCAG 2.1 AA contrast for normal text. */
|
|
2263
|
+
const TOOLTIP_MIN_CONTRAST = 4.5;
|
|
2264
|
+
const BLACK = [
|
|
2265
|
+
0,
|
|
2266
|
+
0,
|
|
2267
|
+
0
|
|
2268
|
+
];
|
|
2269
|
+
const WHITE = [
|
|
2270
|
+
255,
|
|
2271
|
+
255,
|
|
2272
|
+
255
|
|
2273
|
+
];
|
|
2274
|
+
/**
|
|
2275
|
+
* Read one of `keys` from the palette as RGB. `null` when the palette carries
|
|
2276
|
+
* none of them, or carries a value this layer cannot reason about (a `var()`,
|
|
2277
|
+
* a `color-mix()`, a named colour): without a concrete triple there is no way to
|
|
2278
|
+
* derive an ink or a tooltip that is guaranteed to stay readable.
|
|
2279
|
+
*/
|
|
2280
|
+
function pickRgb(palette, keys) {
|
|
2281
|
+
for (const key of keys) {
|
|
2282
|
+
const value = palette[key];
|
|
2283
|
+
if (value === void 0) continue;
|
|
2284
|
+
const rgb = parseColor(value);
|
|
2285
|
+
if (rgb) return rgb;
|
|
2286
|
+
}
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
function parseColor(value) {
|
|
2290
|
+
const text = value.trim();
|
|
2291
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text);
|
|
2292
|
+
if (hex) {
|
|
2293
|
+
const digits = hex[1].length === 3 ? [...hex[1]].map((d) => d + d).join("") : hex[1];
|
|
2294
|
+
return [
|
|
2295
|
+
Number.parseInt(digits.slice(0, 2), 16),
|
|
2296
|
+
Number.parseInt(digits.slice(2, 4), 16),
|
|
2297
|
+
Number.parseInt(digits.slice(4, 6), 16)
|
|
2298
|
+
];
|
|
2299
|
+
}
|
|
2300
|
+
const fn = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(text);
|
|
2301
|
+
if (fn) return [
|
|
2302
|
+
Number(fn[1]),
|
|
2303
|
+
Number(fn[2]),
|
|
2304
|
+
Number(fn[3])
|
|
2305
|
+
];
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
function linear(channel) {
|
|
2309
|
+
const c = channel / 255;
|
|
2310
|
+
return c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
2311
|
+
}
|
|
2312
|
+
/** WCAG relative luminance. */
|
|
2313
|
+
function luminance(rgb) {
|
|
2314
|
+
return .2126 * linear(rgb[0]) + .7152 * linear(rgb[1]) + .0722 * linear(rgb[2]);
|
|
2315
|
+
}
|
|
2316
|
+
function contrastRatio(a, b) {
|
|
2317
|
+
const first = luminance(a);
|
|
2318
|
+
const second = luminance(b);
|
|
2319
|
+
const [high, low] = first >= second ? [first, second] : [second, first];
|
|
2320
|
+
return (high + .05) / (low + .05);
|
|
2321
|
+
}
|
|
2322
|
+
/** Mix in RGB, weight 0 → `from`, 1 → `to` (the space the preset ramps use). */
|
|
2323
|
+
function mix(from, to, weight) {
|
|
2324
|
+
return [
|
|
2325
|
+
from[0] * (1 - weight) + to[0] * weight,
|
|
2326
|
+
from[1] * (1 - weight) + to[1] * weight,
|
|
2327
|
+
from[2] * (1 - weight) + to[2] * weight
|
|
2328
|
+
];
|
|
2329
|
+
}
|
|
2330
|
+
function toHex$1(rgb) {
|
|
2331
|
+
const channel = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
|
|
2332
|
+
return `#${channel(rgb[0])}${channel(rgb[1])}${channel(rgb[2])}`;
|
|
2333
|
+
}
|
|
2334
|
+
function accentPalette(palette, mode) {
|
|
2335
|
+
const accent = pickRgb(palette, ACCENT_KEYS[mode]);
|
|
2336
|
+
const lightest = pickRgb(palette, [LIGHTEST_INK_KEY]);
|
|
2337
|
+
const darkest = pickRgb(palette, [DARKEST_INK_KEY]);
|
|
2338
|
+
if (!accent || !lightest || !darkest) return null;
|
|
2339
|
+
const tooltipTarget = contrastRatio(lightest, BLACK) >= contrastRatio(lightest, WHITE) ? BLACK : WHITE;
|
|
2340
|
+
let tooltipBg = mix(accent, darkest, TOOLTIP_INK_WEIGHT);
|
|
2341
|
+
let retry = 0;
|
|
2342
|
+
while (retry < TOOLTIP_SHIFT_RETRIES && contrastRatio(tooltipBg, lightest) < TOOLTIP_MIN_CONTRAST) {
|
|
2343
|
+
tooltipBg = mix(tooltipBg, tooltipTarget, TOOLTIP_SHIFT_STEP);
|
|
2344
|
+
retry += 1;
|
|
2345
|
+
}
|
|
2346
|
+
const onAccent = contrastRatio(accent, lightest) >= contrastRatio(accent, darkest) ? lightest : darkest;
|
|
2347
|
+
const accentHex = toHex$1(accent);
|
|
2348
|
+
return {
|
|
2349
|
+
accent: accentHex,
|
|
2350
|
+
hover: toHex$1(pickRgb(palette, HOVER_KEYS[mode]) ?? accent),
|
|
2351
|
+
onAccent: toHex$1(onAccent),
|
|
2352
|
+
tooltipBg: toHex$1(tooltipBg),
|
|
2353
|
+
hoverOverlay: `color-mix(in srgb, ${accentHex} ${OVERLAY_ALPHA[mode].hover}%, transparent)`,
|
|
2354
|
+
activeOverlay: `color-mix(in srgb, ${accentHex} ${OVERLAY_ALPHA[mode].active}%, transparent)`
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* The alias tokens that carry the accent. Empty when either appearance has no
|
|
2359
|
+
* usable accent: publishing half a layer would leave the two appearances
|
|
2360
|
+
* disagreeing about the theme's brand colour.
|
|
2361
|
+
*/
|
|
2362
|
+
function accentOverrides(light, dark) {
|
|
2363
|
+
const lightPalette = accentPalette(light, "light");
|
|
2364
|
+
const darkPalette = accentPalette(dark, "dark");
|
|
2365
|
+
if (!lightPalette || !darkPalette) return {};
|
|
2366
|
+
return {
|
|
2367
|
+
"--dsw-alias-brand-primary": {
|
|
2368
|
+
light: lightPalette.accent,
|
|
2369
|
+
dark: darkPalette.accent
|
|
2370
|
+
},
|
|
2371
|
+
"--dsw-alias-button-primary-fill": {
|
|
2372
|
+
light: lightPalette.accent,
|
|
2373
|
+
dark: darkPalette.accent
|
|
2374
|
+
},
|
|
2375
|
+
"--dsw-alias-button-primary-hover": {
|
|
2376
|
+
light: lightPalette.hover,
|
|
2377
|
+
dark: darkPalette.hover
|
|
2378
|
+
},
|
|
2379
|
+
"--dsw-alias-label-primary-foreground": {
|
|
2380
|
+
light: lightPalette.onAccent,
|
|
2381
|
+
dark: darkPalette.onAccent
|
|
2382
|
+
},
|
|
2383
|
+
"--dsw-alias-tooltip-bg": {
|
|
2384
|
+
light: lightPalette.tooltipBg,
|
|
2385
|
+
dark: darkPalette.tooltipBg
|
|
2386
|
+
},
|
|
2387
|
+
"--dsw-alias-interactive-bg-hover": {
|
|
2388
|
+
light: lightPalette.hoverOverlay,
|
|
2389
|
+
dark: darkPalette.hoverOverlay
|
|
2390
|
+
},
|
|
2391
|
+
"--dsw-alias-interactive-bg-active": {
|
|
2392
|
+
light: lightPalette.activeOverlay,
|
|
2393
|
+
dark: darkPalette.activeOverlay
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* The same layer as CSS, for the fallback used when the client exposes no
|
|
2399
|
+
* `theme.overrideTokens`. DSH declares its aliases on `body`, so the light block
|
|
2400
|
+
* rides on `:root` (a pseudo-class outranks it) and the dark block adds
|
|
2401
|
+
* `html[data-ds-dark-theme] body` to outrank `body[data-ds-dark-theme]`
|
|
2402
|
+
* regardless of where the injected sheet lands in the cascade.
|
|
2403
|
+
*/
|
|
2404
|
+
function buildAccentCss(light, dark) {
|
|
2405
|
+
const overrides = accentOverrides(light, dark);
|
|
2406
|
+
const names = Object.keys(overrides);
|
|
2407
|
+
if (names.length === 0) return "";
|
|
2408
|
+
const block = (selector, mode) => `${selector} {\n${names.map((name) => ` ${name}: ${overrides[name][mode]};`).join("\n")}\n}`;
|
|
2409
|
+
return `${block(":root, body", "light")}\n\n${block("html[data-ds-dark-theme] body, body[data-ds-dark-theme]", "dark")}`;
|
|
2410
|
+
}
|
|
2411
|
+
//#endregion
|
|
2191
2412
|
//#region src/client/css/shiki.ts
|
|
2192
2413
|
/**
|
|
2193
2414
|
* Shiki token palette: values behind --shiki-* custom properties emitted by
|
|
@@ -2253,15 +2474,24 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2253
2474
|
}
|
|
2254
2475
|
};
|
|
2255
2476
|
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Every token a theme publishes, in one layer: the primitive ramps it retints,
|
|
2479
|
+
* plus the alias tokens that move DSH's interactive components onto the theme's
|
|
2480
|
+
* accent (see css/accents.ts). The accent goes last so it wins over any alias a
|
|
2481
|
+
* custom theme's own map happens to carry.
|
|
2482
|
+
*/
|
|
2256
2483
|
function buildOverrides(src) {
|
|
2257
2484
|
const resolved = resolvePreset(src);
|
|
2258
2485
|
if (!resolved) return {};
|
|
2259
|
-
return
|
|
2486
|
+
return {
|
|
2487
|
+
...primitiveOverrides(resolved.light, resolved.dark),
|
|
2488
|
+
...accentOverrides(resolved.light, resolved.dark)
|
|
2489
|
+
};
|
|
2260
2490
|
}
|
|
2261
2491
|
function buildFullCssFallback(src) {
|
|
2262
2492
|
const resolved = resolvePreset(src);
|
|
2263
2493
|
if (!resolved) return "";
|
|
2264
|
-
return buildPrimitivesCss(resolved.light, resolved.dark)
|
|
2494
|
+
return `${buildPrimitivesCss(resolved.light, resolved.dark)}\n\n${buildAccentCss(resolved.light, resolved.dark)}`;
|
|
2265
2495
|
}
|
|
2266
2496
|
//#endregion
|
|
2267
2497
|
//#region src/client/style-injector.ts
|
|
@@ -2458,6 +2688,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2458
2688
|
var r = y(e);
|
|
2459
2689
|
return "hsl(" + r.h + ", " + r.s + "%, " + r.l + "%)";
|
|
2460
2690
|
};
|
|
2691
|
+
var q = function(e) {
|
|
2692
|
+
var r = y(e);
|
|
2693
|
+
return "hsla(" + r.h + ", " + r.s + "%, " + r.l + "%, " + r.a + ")";
|
|
2694
|
+
};
|
|
2461
2695
|
var I = function(e) {
|
|
2462
2696
|
var r = e.h, n = e.s, t = e.v, o = e.a;
|
|
2463
2697
|
r = r / 360 * 6, n /= 100, t /= 100;
|
|
@@ -2671,6 +2905,67 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2671
2905
|
var Z = function(r) {
|
|
2672
2906
|
return react.default.createElement(J, l({}, r, { colorModel: Q }));
|
|
2673
2907
|
};
|
|
2908
|
+
var ee = function(r) {
|
|
2909
|
+
var n = r.className, t = r.hsva, o = r.onChange, a = r.onChangeEnd, u = { backgroundImage: "linear-gradient(90deg, " + q(Object.assign({}, t, { a: 0 })) + ", " + q(Object.assign({}, t, { a: 1 })) + ")" }, l = m(["react-colorful__alpha", n]), c = b(100 * t.a);
|
|
2910
|
+
return react.default.createElement("div", { className: l }, react.default.createElement("div", {
|
|
2911
|
+
className: "react-colorful__alpha-gradient",
|
|
2912
|
+
style: u
|
|
2913
|
+
}), react.default.createElement(g, {
|
|
2914
|
+
onMove: function(e) {
|
|
2915
|
+
o({ a: e.left });
|
|
2916
|
+
},
|
|
2917
|
+
onKey: function(e) {
|
|
2918
|
+
o({ a: s(t.a + e.left) });
|
|
2919
|
+
},
|
|
2920
|
+
onEnd: a,
|
|
2921
|
+
"aria-label": "Alpha",
|
|
2922
|
+
"aria-valuetext": c + "%",
|
|
2923
|
+
"aria-valuenow": c,
|
|
2924
|
+
"aria-valuemin": "0",
|
|
2925
|
+
"aria-valuemax": "100"
|
|
2926
|
+
}, react.default.createElement(p, {
|
|
2927
|
+
className: "react-colorful__alpha-pointer",
|
|
2928
|
+
left: t.a,
|
|
2929
|
+
color: q(t)
|
|
2930
|
+
})));
|
|
2931
|
+
};
|
|
2932
|
+
var re = function(n) {
|
|
2933
|
+
var t = n.className, o = n.colorModel, a = n.color, u = void 0 === a ? o.defaultColor : a, i = n.onChange, s = n.onChangeEnd, f = c(n, [
|
|
2934
|
+
"className",
|
|
2935
|
+
"colorModel",
|
|
2936
|
+
"color",
|
|
2937
|
+
"onChange",
|
|
2938
|
+
"onChangeEnd"
|
|
2939
|
+
]), d = (0, react.useRef)(null);
|
|
2940
|
+
G(d);
|
|
2941
|
+
var v = X(o, u, i, s), h = v[0], g = v[1], p = v[2], b = m(["react-colorful", t]);
|
|
2942
|
+
return react.default.createElement("div", l({}, f, {
|
|
2943
|
+
ref: d,
|
|
2944
|
+
className: b
|
|
2945
|
+
}), react.default.createElement(S, {
|
|
2946
|
+
hsva: h,
|
|
2947
|
+
onChange: g,
|
|
2948
|
+
onChangeEnd: p
|
|
2949
|
+
}), react.default.createElement(R, {
|
|
2950
|
+
hue: h.h,
|
|
2951
|
+
onChange: g,
|
|
2952
|
+
onChangeEnd: p
|
|
2953
|
+
}), react.default.createElement(ee, {
|
|
2954
|
+
hsva: h,
|
|
2955
|
+
onChange: g,
|
|
2956
|
+
onChangeEnd: p,
|
|
2957
|
+
className: "react-colorful__last-control"
|
|
2958
|
+
}));
|
|
2959
|
+
};
|
|
2960
|
+
var ne = {
|
|
2961
|
+
defaultColor: "0001",
|
|
2962
|
+
toHsva: E,
|
|
2963
|
+
fromHsva: w,
|
|
2964
|
+
equal: P
|
|
2965
|
+
};
|
|
2966
|
+
var te = function(r) {
|
|
2967
|
+
return react.default.createElement(re, l({}, r, { colorModel: ne }));
|
|
2968
|
+
};
|
|
2674
2969
|
var ke = /^#?([0-9A-F]{3,8})$/i;
|
|
2675
2970
|
var qe = function(r) {
|
|
2676
2971
|
var n = r.color, u = void 0 === n ? "" : n, s = r.onChange, f = r.onBlur, d = r.escape, v = r.validate, h = r.format, g = r.process, m = c(r, [
|
|
@@ -2769,11 +3064,51 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2769
3064
|
const HEX = /^#([0-9a-f]{3,8})$/i;
|
|
2770
3065
|
const RGB_FN = /^rgba?\(([^)]*)\)$/i;
|
|
2771
3066
|
const HSL_FN = /^hsla?\(([^)]*)\)$/i;
|
|
3067
|
+
/**
|
|
3068
|
+
* DSH's own `--dsw-alias-border-l1`: a 4% black hairline in light and a 6% white
|
|
3069
|
+
* one in dark. A fresh custom theme starts here, so turning custom mode on
|
|
3070
|
+
* leaves the borders exactly as the shell paints them until the seed is moved.
|
|
3071
|
+
*/
|
|
3072
|
+
const DEFAULT_BORDER = {
|
|
3073
|
+
light: {
|
|
3074
|
+
color: "#000000",
|
|
3075
|
+
alpha: 4
|
|
3076
|
+
},
|
|
3077
|
+
dark: {
|
|
3078
|
+
color: "#FFFFFF",
|
|
3079
|
+
alpha: 6
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
/**
|
|
3083
|
+
* How far above the seed each derived level sits, in percentage points — DSH's
|
|
3084
|
+
* own spacing. Its light ladder runs 4 / 10 / 12 / 16 and its dark one
|
|
3085
|
+
* 6 / 12 / 16 / 20, so these offsets reproduce the shipped values exactly at the
|
|
3086
|
+
* default seed while letting one picker move every level together.
|
|
3087
|
+
*
|
|
3088
|
+
* `thin` tracks l2 where DSH does not thin it (light) and collapses onto l1
|
|
3089
|
+
* where it does (dark), which is exactly how the shell's pair is authored.
|
|
3090
|
+
*/
|
|
3091
|
+
const BORDER_ALPHA_STEPS = {
|
|
3092
|
+
light: {
|
|
3093
|
+
l2: 6,
|
|
3094
|
+
l3: 8,
|
|
3095
|
+
l4: 12,
|
|
3096
|
+
thin: 6
|
|
3097
|
+
},
|
|
3098
|
+
dark: {
|
|
3099
|
+
l2: 6,
|
|
3100
|
+
l3: 10,
|
|
3101
|
+
l4: 14,
|
|
3102
|
+
thin: 0
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
2772
3105
|
function isRecord$1(value) {
|
|
2773
3106
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2774
3107
|
}
|
|
2775
3108
|
const clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
2776
3109
|
const clamp01 = (n) => Math.max(0, Math.min(1, n));
|
|
3110
|
+
/** An opacity percentage, folded into the 0–100 the seed stores. */
|
|
3111
|
+
const clampAlpha = (n) => Math.max(0, Math.min(100, Math.round(n)));
|
|
2777
3112
|
/** Accepts both `a, b, c` and the modern `a b c / d` argument syntax. */
|
|
2778
3113
|
function fnArgs(body) {
|
|
2779
3114
|
return body.replace(/\//g, " ").split(/[\s,]+/).filter(Boolean);
|
|
@@ -2802,8 +3137,9 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2802
3137
|
* Parse a colour into `#RRGGBB`, or null when it is not a format we understand.
|
|
2803
3138
|
*
|
|
2804
3139
|
* Accepted: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`, `rgb()`/`rgba()` with
|
|
2805
|
-
* numbers or percentages, and `hsl()`/`hsla()`. Alpha is parsed but dropped
|
|
2806
|
-
* every
|
|
3140
|
+
* numbers or percentages, and `hsl()`/`hsla()`. Alpha is parsed but dropped:
|
|
3141
|
+
* every seed stores an opaque colour, and the one overlay — the border — keeps
|
|
3142
|
+
* its opacity in a field of its own.
|
|
2807
3143
|
*
|
|
2808
3144
|
* `primitives.ts` stores `rgb(r, g, b)` while presets store hex, so both shapes
|
|
2809
3145
|
* have to work; the extra forms are there for what users may type.
|
|
@@ -2848,6 +3184,38 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2848
3184
|
function toHex(input) {
|
|
2849
3185
|
return tryToHex(input) ?? "#000000";
|
|
2850
3186
|
}
|
|
3187
|
+
/** A stored opacity: a finite number folded into 0–100, anything else the fallback. */
|
|
3188
|
+
function pickAlpha(value, fallback) {
|
|
3189
|
+
return typeof value === "number" && Number.isFinite(value) ? clampAlpha(value) : fallback;
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* One border alias value, at the seed's own opacity or an override of it.
|
|
3193
|
+
*
|
|
3194
|
+
* Written as `rgba()` rather than an 8-digit hex because the seed stores the
|
|
3195
|
+
* opacity as a percentage and `rgba()` is the form DSH's own border tokens take.
|
|
3196
|
+
*/
|
|
3197
|
+
function borderCss(seed, alpha = seed.alpha) {
|
|
3198
|
+
const hex = toHex(seed.color);
|
|
3199
|
+
const channel = (from) => parseInt(hex.slice(from, from + 2), 16);
|
|
3200
|
+
return `rgba(${channel(1)}, ${channel(3)}, ${channel(5)}, ${clampAlpha(alpha) / 100})`;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* The whole border ladder, from one seed. l1 is the seed itself and every
|
|
3204
|
+
* stronger level is the same hue at DSH's own spacing above it, so a single
|
|
3205
|
+
* picker moves sidebars, tab bars, cards and buttons together, and a stronger
|
|
3206
|
+
* level can never end up weaker than a subtler one. The alpha is clamped, so a
|
|
3207
|
+
* near-opaque seed flattens the top of the ladder instead of overflowing it.
|
|
3208
|
+
*/
|
|
3209
|
+
function borderTokens(mode, seed) {
|
|
3210
|
+
const step = BORDER_ALPHA_STEPS[mode];
|
|
3211
|
+
return {
|
|
3212
|
+
"--dsw-alias-border-l1": borderCss(seed),
|
|
3213
|
+
"--dsw-alias-border-l2": borderCss(seed, seed.alpha + step.l2),
|
|
3214
|
+
"--dsw-alias-border-l3": borderCss(seed, seed.alpha + step.l3),
|
|
3215
|
+
"--dsw-alias-border-l4": borderCss(seed, seed.alpha + step.l4),
|
|
3216
|
+
"--dsw-alias-border-l2-darkmode-thin": borderCss(seed, seed.alpha + step.thin)
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
2851
3219
|
function readSeed(map, fallback, key) {
|
|
2852
3220
|
return toHex(map[key] ?? fallback[key]);
|
|
2853
3221
|
}
|
|
@@ -2888,14 +3256,22 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2888
3256
|
green: pair("--dsw-static-green-500"),
|
|
2889
3257
|
amber: pair("--dsw-static-amber-500"),
|
|
2890
3258
|
red: pair("--dsw-static-red-500"),
|
|
3259
|
+
border: {
|
|
3260
|
+
light: { ...DEFAULT_BORDER.light },
|
|
3261
|
+
dark: { ...DEFAULT_BORDER.dark }
|
|
3262
|
+
},
|
|
2891
3263
|
shiki
|
|
2892
3264
|
};
|
|
2893
3265
|
}
|
|
2894
|
-
/**
|
|
2895
|
-
const
|
|
3266
|
+
/** Every stored version this build still reads; anything else is foreign data. */
|
|
3267
|
+
const READABLE_SCHEMA_VERSIONS = [
|
|
3268
|
+
1,
|
|
3269
|
+
2,
|
|
3270
|
+
3
|
|
3271
|
+
];
|
|
2896
3272
|
function encodeCustom(base, theme) {
|
|
2897
3273
|
return JSON.stringify({
|
|
2898
|
-
v:
|
|
3274
|
+
v: 3,
|
|
2899
3275
|
base,
|
|
2900
3276
|
theme
|
|
2901
3277
|
});
|
|
@@ -2906,9 +3282,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2906
3282
|
try {
|
|
2907
3283
|
const parsed = JSON.parse(raw);
|
|
2908
3284
|
if (!parsed || typeof parsed !== "object") return null;
|
|
2909
|
-
|
|
3285
|
+
const version = parsed.v;
|
|
3286
|
+
if (typeof version !== "number" || !READABLE_SCHEMA_VERSIONS.includes(version)) return null;
|
|
2910
3287
|
return {
|
|
2911
|
-
v:
|
|
3288
|
+
v: 3,
|
|
2912
3289
|
base: String(parsed.base ?? ""),
|
|
2913
3290
|
theme: parsed.theme
|
|
2914
3291
|
};
|
|
@@ -2951,6 +3328,13 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2951
3328
|
darkest: pick(v.darkest, fb.darkest)
|
|
2952
3329
|
};
|
|
2953
3330
|
};
|
|
3331
|
+
const pickBorder = (value, fb) => {
|
|
3332
|
+
const v = value ?? {};
|
|
3333
|
+
return {
|
|
3334
|
+
color: pick(v.color, fb.color),
|
|
3335
|
+
alpha: pickAlpha(v.alpha, fb.alpha)
|
|
3336
|
+
};
|
|
3337
|
+
};
|
|
2954
3338
|
const shikiSrc = src.shiki ?? {};
|
|
2955
3339
|
const shiki = SHIKI_KEYS.reduce((acc, k) => {
|
|
2956
3340
|
acc[k] = pickPair(shikiSrc[k], fallback.shiki[k]);
|
|
@@ -2966,6 +3350,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2966
3350
|
green: pickPair(src.green, fallback.green),
|
|
2967
3351
|
amber: pickPair(src.amber, fallback.amber),
|
|
2968
3352
|
red: pickPair(src.red, fallback.red),
|
|
3353
|
+
border: {
|
|
3354
|
+
light: pickBorder(src.border?.light, fallback.border.light),
|
|
3355
|
+
dark: pickBorder(src.border?.dark, fallback.border.dark)
|
|
3356
|
+
},
|
|
2969
3357
|
shiki
|
|
2970
3358
|
};
|
|
2971
3359
|
}
|
|
@@ -2975,6 +3363,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2975
3363
|
* Unlike the presets (which ship one ramp for both appearances), the ramp is
|
|
2976
3364
|
* built once per appearance, so the light text and the dark text are no longer
|
|
2977
3365
|
* the two ends of the same colour axis.
|
|
3366
|
+
*
|
|
3367
|
+
* The border aliases are the one set of tokens a seed publishes directly
|
|
3368
|
+
* instead of through a `--dsw-static-*` ramp: DSH keeps its border levels as
|
|
3369
|
+
* alpha overlays in the base stylesheets, so they have no scale to hang off.
|
|
2978
3370
|
*/
|
|
2979
3371
|
function buildCustomPreset(v) {
|
|
2980
3372
|
/** One appearance's ramp, published under both prefixes DSH reads from. */
|
|
@@ -2997,16 +3389,20 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2997
3389
|
for (const k of SHIKI_KEYS) out[`--shiki-token-${k}`] = v.shiki[k][mode];
|
|
2998
3390
|
return out;
|
|
2999
3391
|
};
|
|
3392
|
+
/** Every border alias, derived from this appearance's one border seed. */
|
|
3393
|
+
const border = (mode) => borderTokens(mode, v.border[mode]);
|
|
3000
3394
|
return {
|
|
3001
3395
|
label: "Custom",
|
|
3002
3396
|
light: {
|
|
3003
3397
|
...ramp("light"),
|
|
3004
3398
|
...semantic("light"),
|
|
3399
|
+
...border("light"),
|
|
3005
3400
|
...shiki("light")
|
|
3006
3401
|
},
|
|
3007
3402
|
dark: {
|
|
3008
3403
|
...ramp("dark"),
|
|
3009
3404
|
...semantic("dark"),
|
|
3405
|
+
...border("dark"),
|
|
3010
3406
|
...shiki("dark")
|
|
3011
3407
|
}
|
|
3012
3408
|
};
|
|
@@ -3024,8 +3420,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3024
3420
|
if (!raw) return [];
|
|
3025
3421
|
try {
|
|
3026
3422
|
const parsed = JSON.parse(raw);
|
|
3027
|
-
|
|
3028
|
-
if (!
|
|
3423
|
+
if (!parsed || typeof parsed.v !== "number" || !READABLE_SCHEMA_VERSIONS.includes(parsed.v)) return [];
|
|
3424
|
+
if (!Array.isArray(parsed.items)) return [];
|
|
3029
3425
|
const out = [];
|
|
3030
3426
|
for (const item of parsed.items) {
|
|
3031
3427
|
const e = item;
|
|
@@ -3229,6 +3625,96 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3229
3625
|
return legacy.filter((entry) => !known.has(entry.id));
|
|
3230
3626
|
}
|
|
3231
3627
|
//#endregion
|
|
3628
|
+
//#region src/client/assets/delete.svg
|
|
3629
|
+
var delete_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n</svg>\n";
|
|
3630
|
+
//#endregion
|
|
3631
|
+
//#region src/client/assets/edit.svg
|
|
3632
|
+
var edit_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\n <path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" />\n <path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\" />\n </g>\n</svg>\n";
|
|
3633
|
+
//#endregion
|
|
3634
|
+
//#region src/client/assets/export.svg
|
|
3635
|
+
var export_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 3v12m5-7l-5-5l-5 5m14 7v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n</svg>\n";
|
|
3636
|
+
//#endregion
|
|
3637
|
+
//#region src/client/assets/import.svg
|
|
3638
|
+
var import_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\n <path d=\"M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <path d=\"m7 10l5 5l5-5\" />\n </g>\n</svg>\n";
|
|
3639
|
+
//#endregion
|
|
3640
|
+
//#region src/client/assets/plus.svg
|
|
3641
|
+
var plus_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7v14\" />\n</svg>\n";
|
|
3642
|
+
//#endregion
|
|
3643
|
+
//#region src/client/assets/theme.svg
|
|
3644
|
+
var theme_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\n <path d=\"M12 22a1 1 0 0 1 0-20a10 9 0 0 1 10 9a5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z\" />\n <circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\" />\n <circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\" />\n <circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\" />\n <circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\" />\n </g>\n</svg>\n";
|
|
3645
|
+
//#endregion
|
|
3646
|
+
//#region src/client/icons.tsx
|
|
3647
|
+
/**
|
|
3648
|
+
* The plugin's own icon set.
|
|
3649
|
+
*
|
|
3650
|
+
* Every glyph lives in `assets/` as an SVG, and that file is the source of
|
|
3651
|
+
* truth for its artwork: `tsdown` inlines it as text (the `loader` map in
|
|
3652
|
+
* tsdown.config.ts, described by assets.d.ts), and the wrapper below narrows the
|
|
3653
|
+
* markup to the same `{ size, className }` surface the shell's icons expose, so
|
|
3654
|
+
* a call site swaps one for the other without touching its layout.
|
|
3655
|
+
*
|
|
3656
|
+
* Two properties of the artwork the wrapper keeps: the strokes draw in
|
|
3657
|
+
* `currentColor`, so whichever control owns an icon goes on painting it — hover,
|
|
3658
|
+
* disabled and the danger face included — with no CSS here; and React owns the
|
|
3659
|
+
* `<svg>` root, so `size` stays a real prop instead of riding on the asset's own
|
|
3660
|
+
* `1em` box.
|
|
3661
|
+
*/
|
|
3662
|
+
/**
|
|
3663
|
+
* Splits the root `<svg>` off an asset. The halves are needed separately because
|
|
3664
|
+
* React draws the root — that is where `size` and `className` land — while the
|
|
3665
|
+
* inside ships exactly as the file wrote it.
|
|
3666
|
+
*
|
|
3667
|
+
* A source that is not one `<svg>` carrying a `viewBox` is a mistake in this repo
|
|
3668
|
+
* and not something a user can reach, so it throws instead of quietly drawing an
|
|
3669
|
+
* empty box.
|
|
3670
|
+
*/
|
|
3671
|
+
function parseAsset(source, file) {
|
|
3672
|
+
const match = /<svg\b[^>]*\bviewBox="([^"]+)"[^>]*>([\s\S]*)<\/svg>/.exec(source);
|
|
3673
|
+
if (match === null) throw new Error(`dsh-cool-theme: assets/${file} is not a single <svg> with a viewBox`);
|
|
3674
|
+
return {
|
|
3675
|
+
viewBox: match[1],
|
|
3676
|
+
inner: match[2]
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Builds one icon component from an asset.
|
|
3681
|
+
*
|
|
3682
|
+
* The inner markup is this repo's own build-time text, never user input — which
|
|
3683
|
+
* is what makes `dangerouslySetInnerHTML` safe here, and the only way to keep the
|
|
3684
|
+
* paths byte-identical to the set's own drawing, `stroke-width` and all.
|
|
3685
|
+
*/
|
|
3686
|
+
function icon(displayName, file, source) {
|
|
3687
|
+
const { viewBox, inner } = parseAsset(source, file);
|
|
3688
|
+
const Icon = ({ size = 16, className }) => react.createElement("svg", {
|
|
3689
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3690
|
+
width: size,
|
|
3691
|
+
height: size,
|
|
3692
|
+
viewBox,
|
|
3693
|
+
className,
|
|
3694
|
+
"aria-hidden": "true",
|
|
3695
|
+
dangerouslySetInnerHTML: { __html: inner }
|
|
3696
|
+
});
|
|
3697
|
+
Icon.displayName = displayName;
|
|
3698
|
+
return Icon;
|
|
3699
|
+
}
|
|
3700
|
+
const IconDelete = icon("IconDelete", "delete.svg", delete_default);
|
|
3701
|
+
const IconEdit = icon("IconEdit", "edit.svg", edit_default);
|
|
3702
|
+
const IconExport = icon("IconExport", "export.svg", export_default);
|
|
3703
|
+
const IconImport = icon("IconImport", "import.svg", import_default);
|
|
3704
|
+
const IconPlus = icon("IconPlus", "plus.svg", plus_default);
|
|
3705
|
+
/** The palette is the one glyph whose seat belongs to the shell; see below. */
|
|
3706
|
+
const THEME_ASSET = parseAsset(theme_default, "theme.svg");
|
|
3707
|
+
const THEME_NAV_ICON_SIZE = 16;
|
|
3708
|
+
/**
|
|
3709
|
+
* The palette as markup instead of as a component, because its only consumer
|
|
3710
|
+
* patches shell DOM rather than rendering it: `patchThemeNavIcon` in theme.tsx
|
|
3711
|
+
* hides the gear the settings nav draws for an unknown section and writes this
|
|
3712
|
+
* into the seat beside it. The string therefore travels whole, with width and
|
|
3713
|
+
* height pinned to the shell's 16px icon slot — the asset's own `1em` would
|
|
3714
|
+
* resolve against whatever font the nav happens to carry.
|
|
3715
|
+
*/
|
|
3716
|
+
const THEME_NAV_MARKUP = `<svg xmlns="http://www.w3.org/2000/svg" width="${THEME_NAV_ICON_SIZE}" height="${THEME_NAV_ICON_SIZE}" viewBox="${THEME_ASSET.viewBox}" aria-hidden="true">${THEME_ASSET.inner}</svg>`;
|
|
3717
|
+
//#endregion
|
|
3232
3718
|
//#region src/client/components.tsx
|
|
3233
3719
|
/**
|
|
3234
3720
|
* Theme settings UI: scheme selector, preset picker, and the custom colour editor.
|
|
@@ -3244,11 +3730,61 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3244
3730
|
*/
|
|
3245
3731
|
const SEED_MODE_CONTROL_ID = "ct-seed-mode";
|
|
3246
3732
|
/**
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
|
|
3251
|
-
|
|
3733
|
+
* Key of the border swatch in the shared colour popover. It is the one seed that
|
|
3734
|
+
* is not a plain fill — it carries an opacity too — which is why its picker is
|
|
3735
|
+
* the alpha-aware one and one control taller (see `seedPickerHeight`).
|
|
3736
|
+
*/
|
|
3737
|
+
const BORDER_SEED_KEY = "border";
|
|
3738
|
+
/**
|
|
3739
|
+
* The colour popover's box. It comes in two heights: the picker itself is a
|
|
3740
|
+
* saturation panel plus a hue slider, and the border seed's picker carries an
|
|
3741
|
+
* opacity slider under those. These are estimates for choosing which side of
|
|
3742
|
+
* the swatch to open on — the popover is `position:fixed`, so being taller than
|
|
3743
|
+
* estimated only means it hangs higher.
|
|
3744
|
+
*/
|
|
3745
|
+
const PICKER_WIDTH = 224;
|
|
3746
|
+
const PICKER_HEIGHT = 240;
|
|
3747
|
+
/** One more 24px control in the picker, one more 24px of popover. */
|
|
3748
|
+
const PICKER_HEIGHT_WITH_ALPHA = 264;
|
|
3749
|
+
const PICKER_GAP = 8;
|
|
3750
|
+
const PICKER_MARGIN = 8;
|
|
3751
|
+
/**
|
|
3752
|
+
* The alpha checkerboard behind a translucent swatch. Both squares are the same
|
|
3753
|
+
* grey at different alphas, so the pattern reads on a light and a dark panel
|
|
3754
|
+
* alike without needing a colour per appearance.
|
|
3755
|
+
*/
|
|
3756
|
+
const ALPHA_CHECKERBOARD = "repeating-conic-gradient(rgba(128, 128, 128, .45) 0% 25%, rgba(128, 128, 128, .18) 0% 50%)";
|
|
3757
|
+
/**
|
|
3758
|
+
* The picker carries opacity as an 8-bit hex channel while a seed keeps it as a
|
|
3759
|
+
* whole percentage, so the two have to be converted at the popover's edge. A
|
|
3760
|
+
* byte is finer than one percent, so every percentage a seed can hold survives
|
|
3761
|
+
* the round trip unchanged.
|
|
3762
|
+
*/
|
|
3763
|
+
const opacityToByte = (percent) => Math.round(percent / 100 * 255);
|
|
3764
|
+
const byteToOpacity = (byte) => Math.round(parseInt(byte, 16) / 255 * 100);
|
|
3765
|
+
/**
|
|
3766
|
+
* A seed as the picker reads it: `#RRGGBB`, plus `#RRGGBBAA` when there is an
|
|
3767
|
+
* opacity to carry. An opaque colour stays six digits, which is the form the
|
|
3768
|
+
* picker itself hands back.
|
|
3769
|
+
*/
|
|
3770
|
+
function withAlpha(hex, alpha) {
|
|
3771
|
+
if (alpha >= 100) return hex;
|
|
3772
|
+
return `${hex}${opacityToByte(alpha).toString(16).padStart(2, "0").toUpperCase()}`;
|
|
3773
|
+
}
|
|
3774
|
+
/** A picker value back in seed shape: the opaque colour and its opacity. */
|
|
3775
|
+
function splitAlpha(value) {
|
|
3776
|
+
const hex = value.replace(/^#/, "");
|
|
3777
|
+
return {
|
|
3778
|
+
color: `#${hex.slice(0, 6)}`.toUpperCase(),
|
|
3779
|
+
alpha: hex.length >= 8 ? byteToOpacity(hex.slice(6, 8)) : 100
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* The one glyph size this panel draws at, passed explicitly by every call site.
|
|
3784
|
+
* The two halves of a row disagree on their own: the shell's chevron falls back
|
|
3785
|
+
* to 14 while this plugin's icons default to 16, so leaving it implicit would
|
|
3786
|
+
* silently mix two scales in the same panel. 14 is what the shell's own settings
|
|
3787
|
+
* pages pass.
|
|
3252
3788
|
*/
|
|
3253
3789
|
const ICON_SIZE = 14;
|
|
3254
3790
|
/**
|
|
@@ -3269,11 +3805,16 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3269
3805
|
function sameRamp(a, b) {
|
|
3270
3806
|
return a?.lightest === b?.lightest && a?.darkest === b?.darkest;
|
|
3271
3807
|
}
|
|
3272
|
-
/** Structural equality
|
|
3808
|
+
/** Structural equality of one border seed: both its channels have to match. */
|
|
3809
|
+
function sameBorder(a, b) {
|
|
3810
|
+
return a?.color === b?.color && a?.alpha === b?.alpha;
|
|
3811
|
+
}
|
|
3812
|
+
/** Structural equality over every seed, ignoring key order. */
|
|
3273
3813
|
function sameSeeds(a, b) {
|
|
3274
3814
|
if (!sameRamp(a.neutral?.light, b.neutral?.light) || !sameRamp(a.neutral?.dark, b.neutral?.dark)) return false;
|
|
3275
3815
|
if (!samePair(a.accent, b.accent) || !samePair(a.green, b.green)) return false;
|
|
3276
3816
|
if (!samePair(a.amber, b.amber) || !samePair(a.red, b.red)) return false;
|
|
3817
|
+
if (!sameBorder(a.border?.light, b.border?.light) || !sameBorder(a.border?.dark, b.border?.dark)) return false;
|
|
3277
3818
|
return SHIKI_KEYS.every((k) => samePair(a.shiki?.[k], b.shiki?.[k]));
|
|
3278
3819
|
}
|
|
3279
3820
|
/** The two roles one appearance's neutral ramp fills, in display order. */
|
|
@@ -3422,7 +3963,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3422
3963
|
}, entry.name)), react.createElement("div", {
|
|
3423
3964
|
className: "ct-list-actions",
|
|
3424
3965
|
onClick: (e) => e.stopPropagation()
|
|
3425
|
-
}, active ? react.createElement("span", { className: "ct-list-badge" }, t("custom.inUse")) : null, action(t("custom.edit"), react.createElement(
|
|
3966
|
+
}, active ? react.createElement("span", { className: "ct-list-badge" }, t("custom.inUse")) : null, action(t("custom.edit"), react.createElement(IconEdit, { size: ICON_SIZE }), onEdit, { leave: true }), action(t("custom.export"), react.createElement(IconExport, { size: ICON_SIZE }), onExport), action(t("custom.delete"), react.createElement(IconDelete, { size: ICON_SIZE }), onDelete, { danger: true })));
|
|
3426
3967
|
}
|
|
3427
3968
|
/**
|
|
3428
3969
|
* Which appearance the colour dots edit. `system` resolves through the same
|
|
@@ -3438,7 +3979,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3438
3979
|
}
|
|
3439
3980
|
}
|
|
3440
3981
|
function ThemePanel(props) {
|
|
3441
|
-
const { theme, getSelection, setSelection, getCustom, setCustom, setCustomBase, resetCustom, saved, t, onRequestClose } = props;
|
|
3982
|
+
const { theme, getSelection, setSelection, getCustom, setCustom, setCustomBase, resetCustom, saved, getSidebarTransparent, setSidebarTransparent, t, onRequestClose } = props;
|
|
3442
3983
|
let initScheme = "system";
|
|
3443
3984
|
try {
|
|
3444
3985
|
const snap = theme?.getTheme();
|
|
@@ -3457,6 +3998,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3457
3998
|
});
|
|
3458
3999
|
const [list, setList] = react.useState(() => saved.list());
|
|
3459
4000
|
const [activeId, setActiveId] = react.useState(() => saved.activeId());
|
|
4001
|
+
const [sidebarTransparent, setSidebarTransparentState] = react.useState(() => getSidebarTransparent());
|
|
3460
4002
|
const [pendingDelete, setPendingDelete] = react.useState(null);
|
|
3461
4003
|
const [importing, setImporting] = react.useState(false);
|
|
3462
4004
|
/** The hidden picker the import button opens. */
|
|
@@ -3497,29 +4039,40 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3497
4039
|
const pickerAnchorRef = react.useRef(null);
|
|
3498
4040
|
/** The popover node itself; outside-pointerdown detection needs its bounds. */
|
|
3499
4041
|
const pickerPopRef = react.useRef(null);
|
|
3500
|
-
/**
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
4042
|
+
/**
|
|
4043
|
+
* Height of the box the open popover was placed with, captured beside its
|
|
4044
|
+
* anchor. The two popovers differ by one control, and a scroll or resize has
|
|
4045
|
+
* to rebuild the same box; the clicked dot is not in that scope, so its own
|
|
4046
|
+
* fact travels here rather than being re-derived from the dot's key.
|
|
4047
|
+
*/
|
|
4048
|
+
const pickerHeightRef = react.useRef(PICKER_HEIGHT);
|
|
4049
|
+
/**
|
|
4050
|
+
* Viewport coordinates that put the popover beside its anchor swatch. The
|
|
4051
|
+
* caller passes the height of the box being opened, because the border seed's
|
|
4052
|
+
* popover carries one more row than every other seed's.
|
|
4053
|
+
*/
|
|
4054
|
+
function placePicker(anchor, height) {
|
|
3506
4055
|
const rect = anchor.getBoundingClientRect();
|
|
3507
4056
|
const viewW = window.innerWidth;
|
|
3508
4057
|
const viewH = window.innerHeight;
|
|
3509
4058
|
return {
|
|
3510
|
-
left: Math.min(Math.max(
|
|
3511
|
-
top: rect.bottom +
|
|
4059
|
+
left: Math.min(Math.max(PICKER_MARGIN, rect.right - PICKER_WIDTH), Math.max(PICKER_MARGIN, viewW - PICKER_WIDTH - PICKER_MARGIN)),
|
|
4060
|
+
top: rect.bottom + PICKER_GAP + height <= viewH - PICKER_MARGIN ? rect.bottom + PICKER_GAP : Math.max(PICKER_MARGIN, rect.top - PICKER_GAP - height)
|
|
3512
4061
|
};
|
|
3513
4062
|
}
|
|
3514
|
-
/**
|
|
3515
|
-
|
|
4063
|
+
/**
|
|
4064
|
+
* Toggle one seed's popover; `anchor` is the swatch that was clicked and
|
|
4065
|
+
* `height` the height of the box it opens.
|
|
4066
|
+
*/
|
|
4067
|
+
function togglePicker(key, anchor, height) {
|
|
3516
4068
|
if (pickerDotKey === key) {
|
|
3517
4069
|
closePicker();
|
|
3518
4070
|
return;
|
|
3519
4071
|
}
|
|
3520
4072
|
pickerAnchorRef.current = anchor;
|
|
4073
|
+
pickerHeightRef.current = height;
|
|
3521
4074
|
setPickerDotKey(key);
|
|
3522
|
-
setPickerPos(placePicker(anchor));
|
|
4075
|
+
setPickerPos(placePicker(anchor, height));
|
|
3523
4076
|
}
|
|
3524
4077
|
function closePicker() {
|
|
3525
4078
|
setPickerDotKey(null);
|
|
@@ -3544,7 +4097,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3544
4097
|
const anchor = pickerAnchorRef.current;
|
|
3545
4098
|
if (!anchor) return;
|
|
3546
4099
|
cancelAnimationFrame(raf);
|
|
3547
|
-
raf = requestAnimationFrame(() => setPickerPos(placePicker(anchor)));
|
|
4100
|
+
raf = requestAnimationFrame(() => setPickerPos(placePicker(anchor, pickerHeightRef.current)));
|
|
3548
4101
|
};
|
|
3549
4102
|
window.addEventListener("scroll", update, true);
|
|
3550
4103
|
window.addEventListener("resize", update);
|
|
@@ -3553,7 +4106,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3553
4106
|
window.removeEventListener("resize", update);
|
|
3554
4107
|
cancelAnimationFrame(raf);
|
|
3555
4108
|
};
|
|
3556
|
-
}, [pickerOpen]);
|
|
4109
|
+
}, [pickerOpen, pickerDotKey]);
|
|
3557
4110
|
/** Re-read both the list and the active pointer after any list mutation. */
|
|
3558
4111
|
function syncSaved() {
|
|
3559
4112
|
setList(saved.list());
|
|
@@ -3916,6 +4469,17 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3916
4469
|
onSelect: (v) => pickPreset(v),
|
|
3917
4470
|
disabled: customOn
|
|
3918
4471
|
})),
|
|
4472
|
+
react.createElement("div", {
|
|
4473
|
+
className: "ct-row",
|
|
4474
|
+
key: "sidebar-transparency"
|
|
4475
|
+
}, react.createElement("div", { className: "ct-row-main" }, react.createElement("div", { className: "ct-row-title" }, t("sidebar.title")), react.createElement("div", { className: "ct-row-desc" }, t("sidebar.desc"))), react.createElement(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
4476
|
+
checked: sidebarTransparent,
|
|
4477
|
+
label: t("sidebar.title"),
|
|
4478
|
+
onChange: (on) => {
|
|
4479
|
+
setSidebarTransparentState(on);
|
|
4480
|
+
setSidebarTransparent(on);
|
|
4481
|
+
}
|
|
4482
|
+
})),
|
|
3919
4483
|
react.createElement("div", {
|
|
3920
4484
|
className: "ct-row ct-row-flush",
|
|
3921
4485
|
key: "custom-toggle"
|
|
@@ -3926,6 +4490,12 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3926
4490
|
}))
|
|
3927
4491
|
];
|
|
3928
4492
|
if (customOn) {
|
|
4493
|
+
/**
|
|
4494
|
+
* Height of the box one seed's dot opens. Only the border seed's picker
|
|
4495
|
+
* carries an opacity slider, and that seed is the one that declares it, so
|
|
4496
|
+
* the height follows `SeedDot.alpha` rather than a name.
|
|
4497
|
+
*/
|
|
4498
|
+
const seedPickerHeight = (dot) => dot.alpha ? PICKER_HEIGHT_WITH_ALPHA : PICKER_HEIGHT;
|
|
3929
4499
|
/** A group row: its name on the left, every seed's dot on the right. */
|
|
3930
4500
|
const dotRow = (key, label, dots) => react.createElement("div", {
|
|
3931
4501
|
className: "ct-seed-row",
|
|
@@ -3938,11 +4508,11 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3938
4508
|
children: react.createElement("button", {
|
|
3939
4509
|
type: "button",
|
|
3940
4510
|
className: "ct-seed-dot",
|
|
3941
|
-
style: { background: d.value },
|
|
4511
|
+
style: d.swatch ?? { background: d.value },
|
|
3942
4512
|
"aria-haspopup": "dialog",
|
|
3943
4513
|
"aria-expanded": pickerDotKey === d.key,
|
|
3944
4514
|
"aria-label": `${d.name} — ${d.hint}`,
|
|
3945
|
-
onClick: (e) => togglePicker(d.key, e.currentTarget)
|
|
4515
|
+
onClick: (e) => togglePicker(d.key, e.currentTarget, seedPickerHeight(d))
|
|
3946
4516
|
})
|
|
3947
4517
|
}))));
|
|
3948
4518
|
const pair = (key, title, desc) => ({
|
|
@@ -3977,9 +4547,46 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3977
4547
|
})
|
|
3978
4548
|
};
|
|
3979
4549
|
});
|
|
4550
|
+
/**
|
|
4551
|
+
* The border swatch. It is the one seed that is an overlay rather than a
|
|
4552
|
+
* fill, so it is also the only one whose picker carries an opacity slider
|
|
4553
|
+
* (`SeedDot.alpha`) and whose swatch previews that opacity over a
|
|
4554
|
+
* checkerboard. DSH's whole border ladder follows from it: l1 is the seed
|
|
4555
|
+
* and every stronger level is derived while the palette is built.
|
|
4556
|
+
*/
|
|
4557
|
+
const borderDot = () => {
|
|
4558
|
+
const seed = custom.border[seedMode];
|
|
4559
|
+
const set = (next) => editCustom({
|
|
4560
|
+
...custom,
|
|
4561
|
+
border: {
|
|
4562
|
+
...custom.border,
|
|
4563
|
+
[seedMode]: {
|
|
4564
|
+
...seed,
|
|
4565
|
+
...next
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
});
|
|
4569
|
+
const rgba = borderCss(seed);
|
|
4570
|
+
return {
|
|
4571
|
+
key: BORDER_SEED_KEY,
|
|
4572
|
+
name: t("custom.border.title"),
|
|
4573
|
+
hint: `${t("custom.border.desc")} — ${seed.alpha}%`,
|
|
4574
|
+
value: seed.color,
|
|
4575
|
+
onChange: (v) => set({ color: v }),
|
|
4576
|
+
alpha: {
|
|
4577
|
+
value: seed.alpha,
|
|
4578
|
+
onPicked: (v) => set(splitAlpha(v))
|
|
4579
|
+
},
|
|
4580
|
+
swatch: {
|
|
4581
|
+
backgroundImage: `linear-gradient(${rgba}, ${rgba}), ${ALPHA_CHECKERBOARD}`,
|
|
4582
|
+
backgroundSize: "auto, 10px 10px"
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4585
|
+
};
|
|
3980
4586
|
const seedDots = [
|
|
3981
4587
|
pair("accent", "custom.accent.title", "custom.accent.desc"),
|
|
3982
4588
|
...neutralDots,
|
|
4589
|
+
borderDot(),
|
|
3983
4590
|
pair("green", "custom.green.title", "custom.green.desc"),
|
|
3984
4591
|
pair("amber", "custom.amber.title", "custom.amber.desc"),
|
|
3985
4592
|
pair("red", "custom.red.title", "custom.red.desc")
|
|
@@ -4043,17 +4650,18 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4043
4650
|
onClick: () => void saveEdit()
|
|
4044
4651
|
}, t("custom.save")));
|
|
4045
4652
|
const pickerDot = pickerDotKey ? [...seedDots, ...shikiDots].find((d) => d.key === pickerDotKey) ?? null : null;
|
|
4653
|
+
const pickerAlpha = pickerDot?.alpha;
|
|
4046
4654
|
const pickerPop = pickerDot && pickerPos ? react.createElement("div", {
|
|
4047
4655
|
key: "color-picker",
|
|
4048
|
-
className: "ct-color-pop",
|
|
4656
|
+
className: pickerAlpha ? "ct-color-pop ct-color-pop-alpha" : "ct-color-pop",
|
|
4049
4657
|
ref: pickerPopRef,
|
|
4050
4658
|
style: {
|
|
4051
4659
|
left: pickerPos.left,
|
|
4052
4660
|
top: pickerPos.top
|
|
4053
4661
|
}
|
|
4054
|
-
}, react.createElement(Z, {
|
|
4055
|
-
color: pickerDot.value,
|
|
4056
|
-
onChange: (v) => pickerDot.onChange(v.toUpperCase())
|
|
4662
|
+
}, react.createElement(pickerAlpha ? te : Z, {
|
|
4663
|
+
color: pickerAlpha ? withAlpha(pickerDot.value, pickerAlpha.value) : pickerDot.value,
|
|
4664
|
+
onChange: pickerAlpha ? pickerAlpha.onPicked : (v) => pickerDot.onChange(v.toUpperCase())
|
|
4057
4665
|
}), react.createElement(Oe, {
|
|
4058
4666
|
className: "ct-hex-input",
|
|
4059
4667
|
color: pickerDot.value,
|
|
@@ -4093,13 +4701,13 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4093
4701
|
className: "ct-add-btn",
|
|
4094
4702
|
disabled: importing,
|
|
4095
4703
|
onClick: startImport
|
|
4096
|
-
}, react.createElement(
|
|
4704
|
+
}, react.createElement(IconImport, { size: ICON_SIZE }), t("custom.import")), react.createElement("button", {
|
|
4097
4705
|
key: "add",
|
|
4098
4706
|
type: "button",
|
|
4099
4707
|
className: "ct-add-btn",
|
|
4100
4708
|
disabled: editing !== null,
|
|
4101
4709
|
onClick: startAdd
|
|
4102
|
-
}, react.createElement(
|
|
4710
|
+
}, react.createElement(IconPlus, { size: ICON_SIZE }), t("custom.add")), react.createElement("input", {
|
|
4103
4711
|
key: "import-input",
|
|
4104
4712
|
ref: importInputRef,
|
|
4105
4713
|
type: "file",
|
|
@@ -4175,6 +4783,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4175
4783
|
"presets.title": "预设主题",
|
|
4176
4784
|
"presets.desc": "一键套用主流配色方案",
|
|
4177
4785
|
"presets.disabled": "自定义主题已启用,关闭后才能选择预设",
|
|
4786
|
+
"sidebar.title": "侧边栏透明",
|
|
4787
|
+
"sidebar.desc": "侧边栏面板透明效果,仅 Mac 端生效",
|
|
4178
4788
|
"scheme.light": "浅色",
|
|
4179
4789
|
"scheme.dark": "深色",
|
|
4180
4790
|
"scheme.system": "跟随系统",
|
|
@@ -4197,6 +4807,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4197
4807
|
"custom.amber.desc": "警告状态",
|
|
4198
4808
|
"custom.red.title": "错误",
|
|
4199
4809
|
"custom.red.desc": "错误状态",
|
|
4810
|
+
"custom.border.title": "边框",
|
|
4811
|
+
"custom.border.desc": "分隔线与描边的颜色和透明度;较强的边框会自动加深",
|
|
4200
4812
|
"custom.shiki.desc": "9 个语法标记的颜色",
|
|
4201
4813
|
"custom.seed.light": "浅色",
|
|
4202
4814
|
"custom.seed.dark": "深色",
|
|
@@ -4248,6 +4860,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4248
4860
|
"presets.title": "Preset themes",
|
|
4249
4861
|
"presets.desc": "Apply popular color schemes in one click",
|
|
4250
4862
|
"presets.disabled": "Custom theme is on — turn it off to choose a preset",
|
|
4863
|
+
"sidebar.title": "Transparent sidebar",
|
|
4864
|
+
"sidebar.desc": "Sidebar panel transparency effect, macOS only",
|
|
4251
4865
|
"scheme.light": "Light",
|
|
4252
4866
|
"scheme.dark": "Dark",
|
|
4253
4867
|
"scheme.system": "System",
|
|
@@ -4270,6 +4884,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4270
4884
|
"custom.amber.desc": "Warning states",
|
|
4271
4885
|
"custom.red.title": "Error",
|
|
4272
4886
|
"custom.red.desc": "Error states",
|
|
4887
|
+
"custom.border.title": "Border",
|
|
4888
|
+
"custom.border.desc": "Color and opacity for separators and outlines; the stronger border is derived automatically",
|
|
4273
4889
|
"custom.shiki.desc": "Colors for the nine syntax tokens",
|
|
4274
4890
|
"custom.seed.light": "Light",
|
|
4275
4891
|
"custom.seed.dark": "Dark",
|
|
@@ -4336,6 +4952,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4336
4952
|
".ct-seed-dot:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3);}",
|
|
4337
4953
|
".ct-color-pop{position:fixed;z-index:1200;box-sizing:border-box;width:224px;padding:12px;border:1px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3);display:flex;flex-direction:column;gap:10px;}",
|
|
4338
4954
|
".ct-color-pop .react-colorful{width:198px;height:170px;}",
|
|
4955
|
+
".ct-color-pop-alpha .react-colorful{height:194px;}",
|
|
4956
|
+
".ct-color-pop .react-colorful__pointer{width:24px;height:24px;}",
|
|
4339
4957
|
".ct-color-pop .ct-hex-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:center;outline:none;}",
|
|
4340
4958
|
".ct-color-pop .ct-hex-input:focus{border-color:var(--dsw-alias-border-l3);}",
|
|
4341
4959
|
".ct-list{margin-top:4px;display:flex;flex-direction:column;gap:8px;}",
|
|
@@ -4434,6 +5052,25 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4434
5052
|
else localStorage.setItem(key, value);
|
|
4435
5053
|
} catch {}
|
|
4436
5054
|
}
|
|
5055
|
+
const SIDEBAR_OPAQUE_CSS = "html[data-platform=\"darwin\"] [class*=\"sidebarCol\"]:not([class*=\"sidebarCollapsed\"]){background:var(--dsw-specific-sidebar-fill) !important;}";
|
|
5056
|
+
let sidebarOpaqueDisposer = null;
|
|
5057
|
+
/**
|
|
5058
|
+
* Whether the sidebar keeps DSH's translucency. Only an explicit '1' turns it
|
|
5059
|
+
* on: an absent key means opaque, which is the state a fresh install starts in.
|
|
5060
|
+
*/
|
|
5061
|
+
function sidebarTransparent() {
|
|
5062
|
+
return readRaw(SIDEBAR_TRANSPARENT_STORAGE_KEY) === "1";
|
|
5063
|
+
}
|
|
5064
|
+
/** Paint the sidebar column opaque (on), or hand it back to DSH (off). */
|
|
5065
|
+
function applySidebarOpaque(opaque) {
|
|
5066
|
+
if (opaque) {
|
|
5067
|
+
if (!sidebarOpaqueDisposer) sidebarOpaqueDisposer = injector.insert(SIDEBAR_OPAQUE_CSS);
|
|
5068
|
+
} else if (sidebarOpaqueDisposer) sidebarOpaqueDisposer = release(sidebarOpaqueDisposer);
|
|
5069
|
+
}
|
|
5070
|
+
function setSidebarTransparent(on) {
|
|
5071
|
+
writeRaw(SIDEBAR_TRANSPARENT_STORAGE_KEY, on ? "1" : "0");
|
|
5072
|
+
applySidebarOpaque(!on);
|
|
5073
|
+
}
|
|
4437
5074
|
function getSelection() {
|
|
4438
5075
|
for (const key of [THEME_STORAGE_KEY, THEME_STORAGE_KEY_LEGACY]) {
|
|
4439
5076
|
const cur = readRaw(key);
|
|
@@ -4657,6 +5294,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4657
5294
|
if (env && isPresetId(env.base)) lastPreset = env.base;
|
|
4658
5295
|
}
|
|
4659
5296
|
apply(initialSelection);
|
|
5297
|
+
applySidebarOpaque(!sidebarTransparent());
|
|
4660
5298
|
ctx.effect(() => () => {
|
|
4661
5299
|
disposed = true;
|
|
4662
5300
|
cancelApply();
|
|
@@ -4664,6 +5302,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4664
5302
|
baselineDisposer = release(baselineDisposer);
|
|
4665
5303
|
overrideDispose = release(overrideDispose);
|
|
4666
5304
|
fallbackDispose = release(fallbackDispose);
|
|
5305
|
+
sidebarOpaqueDisposer = release(sidebarOpaqueDisposer);
|
|
4667
5306
|
injector.disposeAll();
|
|
4668
5307
|
});
|
|
4669
5308
|
slots.inject("settings.section", () => {
|
|
@@ -4684,10 +5323,11 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4684
5323
|
setCustomBase,
|
|
4685
5324
|
resetCustom,
|
|
4686
5325
|
saved,
|
|
5326
|
+
getSidebarTransparent: sidebarTransparent,
|
|
5327
|
+
setSidebarTransparent,
|
|
4687
5328
|
onRequestClose: typeof props?.close === "function" ? props.close : null
|
|
4688
5329
|
}));
|
|
4689
5330
|
});
|
|
4690
|
-
const PALETTE_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M12 22a1 1 0 0 1 0-20a10 9 0 0 1 10 9a5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z\"/><circle cx=\"13.5\" cy=\"6.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"17.5\" cy=\"10.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"6.5\" cy=\"12.5\" r=\".5\" fill=\"currentColor\"/><circle cx=\"8.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/></g></svg>";
|
|
4691
5331
|
let paletteObs = null;
|
|
4692
5332
|
function patchThemeNavIcon() {
|
|
4693
5333
|
if (typeof document === "undefined") return;
|
|
@@ -4715,7 +5355,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4715
5355
|
holder.style.display = "inline-flex";
|
|
4716
5356
|
holder.style.flex = "none";
|
|
4717
5357
|
holder.setAttribute("aria-hidden", "true");
|
|
4718
|
-
holder.innerHTML =
|
|
5358
|
+
holder.innerHTML = THEME_NAV_MARKUP;
|
|
4719
5359
|
svg.parentNode?.insertBefore(holder, svg.nextSibling);
|
|
4720
5360
|
}
|
|
4721
5361
|
}
|