dsh-cool-theme 0.7.0 → 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 -17
- package/README.zh.md +8 -16
- package/lib/client.js +644 -44
- 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 +4 -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/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -250,7 +250,7 @@ window.__ModuleLoader__.load({
|
|
|
250
250
|
return map;
|
|
251
251
|
}
|
|
252
252
|
/** Mix two colors linearly in RGB. weight 0 -> colorA, 1 -> colorB */
|
|
253
|
-
function mix(colorA, colorB, weight) {
|
|
253
|
+
function mix$1(colorA, colorB, weight) {
|
|
254
254
|
const a = hexToRgb(colorA);
|
|
255
255
|
const b = hexToRgb(colorB);
|
|
256
256
|
const w = Math.max(0, Math.min(1, weight));
|
|
@@ -321,7 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
321
321
|
for (const step of steps) {
|
|
322
322
|
const w = weights[step];
|
|
323
323
|
if (w === void 0) continue;
|
|
324
|
-
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);
|
|
325
325
|
map[`${prefix}-${step}`] = color;
|
|
326
326
|
}
|
|
327
327
|
return map;
|
|
@@ -354,7 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
354
354
|
...buildGreenScale(opts.green),
|
|
355
355
|
...buildAmberScale(opts.amber),
|
|
356
356
|
...buildRedScale(opts.red),
|
|
357
|
-
"--dsw-static-deepseek-700-delete": mix("#000000", opts.deepseek, .7)
|
|
357
|
+
"--dsw-static-deepseek-700-delete": mix$1("#000000", opts.deepseek, .7)
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
360
|
//#endregion
|
|
@@ -2189,6 +2189,226 @@ window.__ModuleLoader__.load({
|
|
|
2189
2189
|
return out;
|
|
2190
2190
|
}
|
|
2191
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
|
|
2192
2412
|
//#region src/client/css/shiki.ts
|
|
2193
2413
|
/**
|
|
2194
2414
|
* Shiki token palette: values behind --shiki-* custom properties emitted by
|
|
@@ -2254,15 +2474,24 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2254
2474
|
}
|
|
2255
2475
|
};
|
|
2256
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
|
+
*/
|
|
2257
2483
|
function buildOverrides(src) {
|
|
2258
2484
|
const resolved = resolvePreset(src);
|
|
2259
2485
|
if (!resolved) return {};
|
|
2260
|
-
return
|
|
2486
|
+
return {
|
|
2487
|
+
...primitiveOverrides(resolved.light, resolved.dark),
|
|
2488
|
+
...accentOverrides(resolved.light, resolved.dark)
|
|
2489
|
+
};
|
|
2261
2490
|
}
|
|
2262
2491
|
function buildFullCssFallback(src) {
|
|
2263
2492
|
const resolved = resolvePreset(src);
|
|
2264
2493
|
if (!resolved) return "";
|
|
2265
|
-
return buildPrimitivesCss(resolved.light, resolved.dark)
|
|
2494
|
+
return `${buildPrimitivesCss(resolved.light, resolved.dark)}\n\n${buildAccentCss(resolved.light, resolved.dark)}`;
|
|
2266
2495
|
}
|
|
2267
2496
|
//#endregion
|
|
2268
2497
|
//#region src/client/style-injector.ts
|
|
@@ -2459,6 +2688,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2459
2688
|
var r = y(e);
|
|
2460
2689
|
return "hsl(" + r.h + ", " + r.s + "%, " + r.l + "%)";
|
|
2461
2690
|
};
|
|
2691
|
+
var q = function(e) {
|
|
2692
|
+
var r = y(e);
|
|
2693
|
+
return "hsla(" + r.h + ", " + r.s + "%, " + r.l + "%, " + r.a + ")";
|
|
2694
|
+
};
|
|
2462
2695
|
var I = function(e) {
|
|
2463
2696
|
var r = e.h, n = e.s, t = e.v, o = e.a;
|
|
2464
2697
|
r = r / 360 * 6, n /= 100, t /= 100;
|
|
@@ -2672,6 +2905,67 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2672
2905
|
var Z = function(r) {
|
|
2673
2906
|
return react.default.createElement(J, l({}, r, { colorModel: Q }));
|
|
2674
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
|
+
};
|
|
2675
2969
|
var ke = /^#?([0-9A-F]{3,8})$/i;
|
|
2676
2970
|
var qe = function(r) {
|
|
2677
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, [
|
|
@@ -2770,11 +3064,51 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2770
3064
|
const HEX = /^#([0-9a-f]{3,8})$/i;
|
|
2771
3065
|
const RGB_FN = /^rgba?\(([^)]*)\)$/i;
|
|
2772
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
|
+
};
|
|
2773
3105
|
function isRecord$1(value) {
|
|
2774
3106
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2775
3107
|
}
|
|
2776
3108
|
const clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
2777
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)));
|
|
2778
3112
|
/** Accepts both `a, b, c` and the modern `a b c / d` argument syntax. */
|
|
2779
3113
|
function fnArgs(body) {
|
|
2780
3114
|
return body.replace(/\//g, " ").split(/[\s,]+/).filter(Boolean);
|
|
@@ -2803,8 +3137,9 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2803
3137
|
* Parse a colour into `#RRGGBB`, or null when it is not a format we understand.
|
|
2804
3138
|
*
|
|
2805
3139
|
* Accepted: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`, `rgb()`/`rgba()` with
|
|
2806
|
-
* numbers or percentages, and `hsl()`/`hsla()`. Alpha is parsed but dropped
|
|
2807
|
-
* 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.
|
|
2808
3143
|
*
|
|
2809
3144
|
* `primitives.ts` stores `rgb(r, g, b)` while presets store hex, so both shapes
|
|
2810
3145
|
* have to work; the extra forms are there for what users may type.
|
|
@@ -2849,6 +3184,38 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2849
3184
|
function toHex(input) {
|
|
2850
3185
|
return tryToHex(input) ?? "#000000";
|
|
2851
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
|
+
}
|
|
2852
3219
|
function readSeed(map, fallback, key) {
|
|
2853
3220
|
return toHex(map[key] ?? fallback[key]);
|
|
2854
3221
|
}
|
|
@@ -2889,14 +3256,22 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2889
3256
|
green: pair("--dsw-static-green-500"),
|
|
2890
3257
|
amber: pair("--dsw-static-amber-500"),
|
|
2891
3258
|
red: pair("--dsw-static-red-500"),
|
|
3259
|
+
border: {
|
|
3260
|
+
light: { ...DEFAULT_BORDER.light },
|
|
3261
|
+
dark: { ...DEFAULT_BORDER.dark }
|
|
3262
|
+
},
|
|
2892
3263
|
shiki
|
|
2893
3264
|
};
|
|
2894
3265
|
}
|
|
2895
|
-
/**
|
|
2896
|
-
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
|
+
];
|
|
2897
3272
|
function encodeCustom(base, theme) {
|
|
2898
3273
|
return JSON.stringify({
|
|
2899
|
-
v:
|
|
3274
|
+
v: 3,
|
|
2900
3275
|
base,
|
|
2901
3276
|
theme
|
|
2902
3277
|
});
|
|
@@ -2907,9 +3282,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2907
3282
|
try {
|
|
2908
3283
|
const parsed = JSON.parse(raw);
|
|
2909
3284
|
if (!parsed || typeof parsed !== "object") return null;
|
|
2910
|
-
|
|
3285
|
+
const version = parsed.v;
|
|
3286
|
+
if (typeof version !== "number" || !READABLE_SCHEMA_VERSIONS.includes(version)) return null;
|
|
2911
3287
|
return {
|
|
2912
|
-
v:
|
|
3288
|
+
v: 3,
|
|
2913
3289
|
base: String(parsed.base ?? ""),
|
|
2914
3290
|
theme: parsed.theme
|
|
2915
3291
|
};
|
|
@@ -2952,6 +3328,13 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2952
3328
|
darkest: pick(v.darkest, fb.darkest)
|
|
2953
3329
|
};
|
|
2954
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
|
+
};
|
|
2955
3338
|
const shikiSrc = src.shiki ?? {};
|
|
2956
3339
|
const shiki = SHIKI_KEYS.reduce((acc, k) => {
|
|
2957
3340
|
acc[k] = pickPair(shikiSrc[k], fallback.shiki[k]);
|
|
@@ -2967,6 +3350,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2967
3350
|
green: pickPair(src.green, fallback.green),
|
|
2968
3351
|
amber: pickPair(src.amber, fallback.amber),
|
|
2969
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
|
+
},
|
|
2970
3357
|
shiki
|
|
2971
3358
|
};
|
|
2972
3359
|
}
|
|
@@ -2976,6 +3363,10 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2976
3363
|
* Unlike the presets (which ship one ramp for both appearances), the ramp is
|
|
2977
3364
|
* built once per appearance, so the light text and the dark text are no longer
|
|
2978
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.
|
|
2979
3370
|
*/
|
|
2980
3371
|
function buildCustomPreset(v) {
|
|
2981
3372
|
/** One appearance's ramp, published under both prefixes DSH reads from. */
|
|
@@ -2998,16 +3389,20 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
2998
3389
|
for (const k of SHIKI_KEYS) out[`--shiki-token-${k}`] = v.shiki[k][mode];
|
|
2999
3390
|
return out;
|
|
3000
3391
|
};
|
|
3392
|
+
/** Every border alias, derived from this appearance's one border seed. */
|
|
3393
|
+
const border = (mode) => borderTokens(mode, v.border[mode]);
|
|
3001
3394
|
return {
|
|
3002
3395
|
label: "Custom",
|
|
3003
3396
|
light: {
|
|
3004
3397
|
...ramp("light"),
|
|
3005
3398
|
...semantic("light"),
|
|
3399
|
+
...border("light"),
|
|
3006
3400
|
...shiki("light")
|
|
3007
3401
|
},
|
|
3008
3402
|
dark: {
|
|
3009
3403
|
...ramp("dark"),
|
|
3010
3404
|
...semantic("dark"),
|
|
3405
|
+
...border("dark"),
|
|
3011
3406
|
...shiki("dark")
|
|
3012
3407
|
}
|
|
3013
3408
|
};
|
|
@@ -3025,8 +3420,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3025
3420
|
if (!raw) return [];
|
|
3026
3421
|
try {
|
|
3027
3422
|
const parsed = JSON.parse(raw);
|
|
3028
|
-
|
|
3029
|
-
if (!
|
|
3423
|
+
if (!parsed || typeof parsed.v !== "number" || !READABLE_SCHEMA_VERSIONS.includes(parsed.v)) return [];
|
|
3424
|
+
if (!Array.isArray(parsed.items)) return [];
|
|
3030
3425
|
const out = [];
|
|
3031
3426
|
for (const item of parsed.items) {
|
|
3032
3427
|
const e = item;
|
|
@@ -3230,6 +3625,96 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3230
3625
|
return legacy.filter((entry) => !known.has(entry.id));
|
|
3231
3626
|
}
|
|
3232
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
|
|
3233
3718
|
//#region src/client/components.tsx
|
|
3234
3719
|
/**
|
|
3235
3720
|
* Theme settings UI: scheme selector, preset picker, and the custom colour editor.
|
|
@@ -3245,11 +3730,61 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3245
3730
|
*/
|
|
3246
3731
|
const SEED_MODE_CONTROL_ID = "ct-seed-mode";
|
|
3247
3732
|
/**
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
*
|
|
3251
|
-
|
|
3252
|
-
|
|
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.
|
|
3253
3788
|
*/
|
|
3254
3789
|
const ICON_SIZE = 14;
|
|
3255
3790
|
/**
|
|
@@ -3270,11 +3805,16 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3270
3805
|
function sameRamp(a, b) {
|
|
3271
3806
|
return a?.lightest === b?.lightest && a?.darkest === b?.darkest;
|
|
3272
3807
|
}
|
|
3273
|
-
/** 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. */
|
|
3274
3813
|
function sameSeeds(a, b) {
|
|
3275
3814
|
if (!sameRamp(a.neutral?.light, b.neutral?.light) || !sameRamp(a.neutral?.dark, b.neutral?.dark)) return false;
|
|
3276
3815
|
if (!samePair(a.accent, b.accent) || !samePair(a.green, b.green)) return false;
|
|
3277
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;
|
|
3278
3818
|
return SHIKI_KEYS.every((k) => samePair(a.shiki?.[k], b.shiki?.[k]));
|
|
3279
3819
|
}
|
|
3280
3820
|
/** The two roles one appearance's neutral ramp fills, in display order. */
|
|
@@ -3423,7 +3963,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3423
3963
|
}, entry.name)), react.createElement("div", {
|
|
3424
3964
|
className: "ct-list-actions",
|
|
3425
3965
|
onClick: (e) => e.stopPropagation()
|
|
3426
|
-
}, 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 })));
|
|
3427
3967
|
}
|
|
3428
3968
|
/**
|
|
3429
3969
|
* Which appearance the colour dots edit. `system` resolves through the same
|
|
@@ -3499,29 +4039,40 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3499
4039
|
const pickerAnchorRef = react.useRef(null);
|
|
3500
4040
|
/** The popover node itself; outside-pointerdown detection needs its bounds. */
|
|
3501
4041
|
const pickerPopRef = react.useRef(null);
|
|
3502
|
-
/**
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
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) {
|
|
3508
4055
|
const rect = anchor.getBoundingClientRect();
|
|
3509
4056
|
const viewW = window.innerWidth;
|
|
3510
4057
|
const viewH = window.innerHeight;
|
|
3511
4058
|
return {
|
|
3512
|
-
left: Math.min(Math.max(
|
|
3513
|
-
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)
|
|
3514
4061
|
};
|
|
3515
4062
|
}
|
|
3516
|
-
/**
|
|
3517
|
-
|
|
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) {
|
|
3518
4068
|
if (pickerDotKey === key) {
|
|
3519
4069
|
closePicker();
|
|
3520
4070
|
return;
|
|
3521
4071
|
}
|
|
3522
4072
|
pickerAnchorRef.current = anchor;
|
|
4073
|
+
pickerHeightRef.current = height;
|
|
3523
4074
|
setPickerDotKey(key);
|
|
3524
|
-
setPickerPos(placePicker(anchor));
|
|
4075
|
+
setPickerPos(placePicker(anchor, height));
|
|
3525
4076
|
}
|
|
3526
4077
|
function closePicker() {
|
|
3527
4078
|
setPickerDotKey(null);
|
|
@@ -3546,7 +4097,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3546
4097
|
const anchor = pickerAnchorRef.current;
|
|
3547
4098
|
if (!anchor) return;
|
|
3548
4099
|
cancelAnimationFrame(raf);
|
|
3549
|
-
raf = requestAnimationFrame(() => setPickerPos(placePicker(anchor)));
|
|
4100
|
+
raf = requestAnimationFrame(() => setPickerPos(placePicker(anchor, pickerHeightRef.current)));
|
|
3550
4101
|
};
|
|
3551
4102
|
window.addEventListener("scroll", update, true);
|
|
3552
4103
|
window.addEventListener("resize", update);
|
|
@@ -3555,7 +4106,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3555
4106
|
window.removeEventListener("resize", update);
|
|
3556
4107
|
cancelAnimationFrame(raf);
|
|
3557
4108
|
};
|
|
3558
|
-
}, [pickerOpen]);
|
|
4109
|
+
}, [pickerOpen, pickerDotKey]);
|
|
3559
4110
|
/** Re-read both the list and the active pointer after any list mutation. */
|
|
3560
4111
|
function syncSaved() {
|
|
3561
4112
|
setList(saved.list());
|
|
@@ -3939,6 +4490,12 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3939
4490
|
}))
|
|
3940
4491
|
];
|
|
3941
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;
|
|
3942
4499
|
/** A group row: its name on the left, every seed's dot on the right. */
|
|
3943
4500
|
const dotRow = (key, label, dots) => react.createElement("div", {
|
|
3944
4501
|
className: "ct-seed-row",
|
|
@@ -3951,11 +4508,11 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3951
4508
|
children: react.createElement("button", {
|
|
3952
4509
|
type: "button",
|
|
3953
4510
|
className: "ct-seed-dot",
|
|
3954
|
-
style: { background: d.value },
|
|
4511
|
+
style: d.swatch ?? { background: d.value },
|
|
3955
4512
|
"aria-haspopup": "dialog",
|
|
3956
4513
|
"aria-expanded": pickerDotKey === d.key,
|
|
3957
4514
|
"aria-label": `${d.name} — ${d.hint}`,
|
|
3958
|
-
onClick: (e) => togglePicker(d.key, e.currentTarget)
|
|
4515
|
+
onClick: (e) => togglePicker(d.key, e.currentTarget, seedPickerHeight(d))
|
|
3959
4516
|
})
|
|
3960
4517
|
}))));
|
|
3961
4518
|
const pair = (key, title, desc) => ({
|
|
@@ -3990,9 +4547,46 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
3990
4547
|
})
|
|
3991
4548
|
};
|
|
3992
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
|
+
};
|
|
3993
4586
|
const seedDots = [
|
|
3994
4587
|
pair("accent", "custom.accent.title", "custom.accent.desc"),
|
|
3995
4588
|
...neutralDots,
|
|
4589
|
+
borderDot(),
|
|
3996
4590
|
pair("green", "custom.green.title", "custom.green.desc"),
|
|
3997
4591
|
pair("amber", "custom.amber.title", "custom.amber.desc"),
|
|
3998
4592
|
pair("red", "custom.red.title", "custom.red.desc")
|
|
@@ -4056,17 +4650,18 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4056
4650
|
onClick: () => void saveEdit()
|
|
4057
4651
|
}, t("custom.save")));
|
|
4058
4652
|
const pickerDot = pickerDotKey ? [...seedDots, ...shikiDots].find((d) => d.key === pickerDotKey) ?? null : null;
|
|
4653
|
+
const pickerAlpha = pickerDot?.alpha;
|
|
4059
4654
|
const pickerPop = pickerDot && pickerPos ? react.createElement("div", {
|
|
4060
4655
|
key: "color-picker",
|
|
4061
|
-
className: "ct-color-pop",
|
|
4656
|
+
className: pickerAlpha ? "ct-color-pop ct-color-pop-alpha" : "ct-color-pop",
|
|
4062
4657
|
ref: pickerPopRef,
|
|
4063
4658
|
style: {
|
|
4064
4659
|
left: pickerPos.left,
|
|
4065
4660
|
top: pickerPos.top
|
|
4066
4661
|
}
|
|
4067
|
-
}, react.createElement(Z, {
|
|
4068
|
-
color: pickerDot.value,
|
|
4069
|
-
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())
|
|
4070
4665
|
}), react.createElement(Oe, {
|
|
4071
4666
|
className: "ct-hex-input",
|
|
4072
4667
|
color: pickerDot.value,
|
|
@@ -4106,13 +4701,13 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4106
4701
|
className: "ct-add-btn",
|
|
4107
4702
|
disabled: importing,
|
|
4108
4703
|
onClick: startImport
|
|
4109
|
-
}, react.createElement(
|
|
4704
|
+
}, react.createElement(IconImport, { size: ICON_SIZE }), t("custom.import")), react.createElement("button", {
|
|
4110
4705
|
key: "add",
|
|
4111
4706
|
type: "button",
|
|
4112
4707
|
className: "ct-add-btn",
|
|
4113
4708
|
disabled: editing !== null,
|
|
4114
4709
|
onClick: startAdd
|
|
4115
|
-
}, react.createElement(
|
|
4710
|
+
}, react.createElement(IconPlus, { size: ICON_SIZE }), t("custom.add")), react.createElement("input", {
|
|
4116
4711
|
key: "import-input",
|
|
4117
4712
|
ref: importInputRef,
|
|
4118
4713
|
type: "file",
|
|
@@ -4212,6 +4807,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4212
4807
|
"custom.amber.desc": "警告状态",
|
|
4213
4808
|
"custom.red.title": "错误",
|
|
4214
4809
|
"custom.red.desc": "错误状态",
|
|
4810
|
+
"custom.border.title": "边框",
|
|
4811
|
+
"custom.border.desc": "分隔线与描边的颜色和透明度;较强的边框会自动加深",
|
|
4215
4812
|
"custom.shiki.desc": "9 个语法标记的颜色",
|
|
4216
4813
|
"custom.seed.light": "浅色",
|
|
4217
4814
|
"custom.seed.dark": "深色",
|
|
@@ -4287,6 +4884,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4287
4884
|
"custom.amber.desc": "Warning states",
|
|
4288
4885
|
"custom.red.title": "Error",
|
|
4289
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",
|
|
4290
4889
|
"custom.shiki.desc": "Colors for the nine syntax tokens",
|
|
4291
4890
|
"custom.seed.light": "Light",
|
|
4292
4891
|
"custom.seed.dark": "Dark",
|
|
@@ -4353,6 +4952,8 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4353
4952
|
".ct-seed-dot:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3);}",
|
|
4354
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;}",
|
|
4355
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;}",
|
|
4356
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;}",
|
|
4357
4958
|
".ct-color-pop .ct-hex-input:focus{border-color:var(--dsw-alias-border-l3);}",
|
|
4358
4959
|
".ct-list{margin-top:4px;display:flex;flex-direction:column;gap:8px;}",
|
|
@@ -4727,7 +5328,6 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4727
5328
|
onRequestClose: typeof props?.close === "function" ? props.close : null
|
|
4728
5329
|
}));
|
|
4729
5330
|
});
|
|
4730
|
-
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>";
|
|
4731
5331
|
let paletteObs = null;
|
|
4732
5332
|
function patchThemeNavIcon() {
|
|
4733
5333
|
if (typeof document === "undefined") return;
|
|
@@ -4755,7 +5355,7 @@ html[data-ds-dark-theme], body[data-ds-dark-theme] {
|
|
|
4755
5355
|
holder.style.display = "inline-flex";
|
|
4756
5356
|
holder.style.flex = "none";
|
|
4757
5357
|
holder.setAttribute("aria-hidden", "true");
|
|
4758
|
-
holder.innerHTML =
|
|
5358
|
+
holder.innerHTML = THEME_NAV_MARKUP;
|
|
4759
5359
|
svg.parentNode?.insertBefore(holder, svg.nextSibling);
|
|
4760
5360
|
}
|
|
4761
5361
|
}
|