dsh-theme-studio 0.1.0 → 0.2.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/CHANGELOG.md +14 -0
- package/README.md +7 -3
- package/lib/io.d.ts +48 -0
- package/lib/io.js +146 -0
- package/lib/theme-studio.web.js +475 -91
- package/lib/themes.d.ts +3 -0
- package/lib/themes.js +84 -0
- package/lib/types.d.ts +5 -0
- package/lib/types.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- 4 new presets: Gruvbox, Solarized, Tokyo Night, Catppuccin.
|
|
6
|
+
- Dark-mode aware presets via `darkTokens`; the panel watches `body[data-ds-dark-theme]`.
|
|
7
|
+
- Separate dark-mode accent color.
|
|
8
|
+
- Contrast guard: a dark accent below the WCAG luminance floor is lightened in
|
|
9
|
+
steps, and the panel reports that it was adjusted.
|
|
10
|
+
- Animation toggle (disables UI transitions).
|
|
11
|
+
- Theme import/export as JSON, with validation; accepts both the envelope and a
|
|
12
|
+
bare preferences object, and reports a real error for non-theme input.
|
|
13
|
+
- Unified token resolution: switching a preset off now clears the properties it
|
|
14
|
+
had set instead of leaving them behind.
|
|
15
|
+
- 26 tests across theme data and color/IO logic.
|
|
16
|
+
|
|
3
17
|
## 0.1.0
|
|
4
18
|
|
|
5
19
|
- Initial release.
|
package/README.md
CHANGED
|
@@ -4,13 +4,17 @@ A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin that lets you cu
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
7
|
+
- **10 pre-built presets**: Ocean, Forest, Sunset, Monochrome, Nord, Dracula, Gruvbox, Solarized, Tokyo Night, Catppuccin
|
|
8
|
+
- **Dark-mode aware presets** — presets that ship a `darkTokens` palette switch automatically when dsh enters dark mode (watched via `MutationObserver` on `body[data-ds-dark-theme]`)
|
|
9
|
+
- **Custom accent color** picker with hex input, plus a **separate dark-mode accent**
|
|
10
|
+
- **Contrast guard** — a dark-mode accent that would be illegible on a dark surface is lightened in steps until it clears a WCAG luminance floor, and the panel says so rather than silently changing your pick
|
|
9
11
|
- **Density** control: Compact / Comfortable / Spacious
|
|
10
12
|
- **Border radius** control: Sharp / Rounded / Soft
|
|
11
13
|
- **Font family** selection: System / Monospace / Serif
|
|
14
|
+
- **Animation toggle** — turn off UI transitions for a snappier, low-motion interface
|
|
12
15
|
- **Custom CSS** textarea for advanced `--property: value;` overrides
|
|
13
|
-
- **
|
|
16
|
+
- **Import / export** — copy a theme to the clipboard as JSON, or paste / upload one back
|
|
17
|
+
- **Live preview** with badges, cards, and buttons, labelled with the current light/dark mode
|
|
14
18
|
- **Instant apply** — changes take effect immediately via CSS custom properties
|
|
15
19
|
- **Persistent** — preferences saved in `localStorage`, no server round-trips
|
|
16
20
|
|
package/lib/io.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme import/export and color utilities for dsh-theme-studio.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of DOM access so the logic is testable outside a browser.
|
|
5
|
+
*
|
|
6
|
+
* @module io
|
|
7
|
+
*/
|
|
8
|
+
import type { ThemePreferences } from './types.js';
|
|
9
|
+
/** The wire format for exported themes. */
|
|
10
|
+
export interface ExportedTheme {
|
|
11
|
+
$schema: 'dsh-theme-studio/v1';
|
|
12
|
+
name?: string;
|
|
13
|
+
preferences: ThemePreferences;
|
|
14
|
+
exportedAt: string;
|
|
15
|
+
}
|
|
16
|
+
/** Serialize preferences into a portable JSON string. */
|
|
17
|
+
export declare function exportTheme(prefs: ThemePreferences, name?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Parse a theme JSON document.
|
|
20
|
+
*
|
|
21
|
+
* Every field is validated and falls back to the default rather than throwing,
|
|
22
|
+
* so a partially-hand-edited file still applies what it can. The only hard
|
|
23
|
+
* failure is input that is not a JSON object at all — that returns `null` so
|
|
24
|
+
* the caller can report a real import error instead of silently applying defaults.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseTheme(raw: string): ThemePreferences | null;
|
|
27
|
+
/** Parse `#rgb` or `#rrggbb` into RGB components; `null` when unparseable. */
|
|
28
|
+
export declare function hexToRgb(hex: string): [number, number, number] | null;
|
|
29
|
+
/** WCAG relative luminance, 0 (black) → 1 (white). */
|
|
30
|
+
export declare function luminance(hex: string): number;
|
|
31
|
+
/** Convert RGB components back into a `#rrggbb` string. */
|
|
32
|
+
export declare function rgbToHex(r: number, g: number, b: number): string;
|
|
33
|
+
/** Blend a color toward white by `amount` (0–1). */
|
|
34
|
+
export declare function lighten(hex: string, amount: number): string;
|
|
35
|
+
/** Blend a color toward black by `amount` (0–1). */
|
|
36
|
+
export declare function darken(hex: string, amount: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Ensure an accent color stays legible on a dark background.
|
|
39
|
+
*
|
|
40
|
+
* A mid-tone accent that reads well on white can sit too close to a dark
|
|
41
|
+
* surface. Rather than silently swapping the user's color, we lighten it in
|
|
42
|
+
* steps until it clears a luminance floor and report whether we changed it, so
|
|
43
|
+
* the panel can say so instead of the user wondering why their pick looks off.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ensureDarkContrast(hex: string): {
|
|
46
|
+
color: string;
|
|
47
|
+
adjusted: boolean;
|
|
48
|
+
};
|
package/lib/io.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme import/export and color utilities for dsh-theme-studio.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of DOM access so the logic is testable outside a browser.
|
|
5
|
+
*
|
|
6
|
+
* @module io
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_PREFERENCES } from './types.js';
|
|
9
|
+
/** Serialize preferences into a portable JSON string. */
|
|
10
|
+
export function exportTheme(prefs, name) {
|
|
11
|
+
const payload = {
|
|
12
|
+
$schema: 'dsh-theme-studio/v1',
|
|
13
|
+
preferences: prefs,
|
|
14
|
+
exportedAt: new Date().toISOString(),
|
|
15
|
+
};
|
|
16
|
+
if (name !== undefined)
|
|
17
|
+
payload.name = name;
|
|
18
|
+
return JSON.stringify(payload, null, 2);
|
|
19
|
+
}
|
|
20
|
+
const DENSITIES = new Set(['compact', 'comfortable', 'spacious']);
|
|
21
|
+
const RADII = new Set(['sharp', 'rounded', 'soft']);
|
|
22
|
+
const FONTS = new Set(['system', 'mono', 'serif']);
|
|
23
|
+
function asColor(value) {
|
|
24
|
+
if (typeof value !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed === '')
|
|
28
|
+
return null;
|
|
29
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(trimmed) ? trimmed : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a theme JSON document.
|
|
33
|
+
*
|
|
34
|
+
* Every field is validated and falls back to the default rather than throwing,
|
|
35
|
+
* so a partially-hand-edited file still applies what it can. The only hard
|
|
36
|
+
* failure is input that is not a JSON object at all — that returns `null` so
|
|
37
|
+
* the caller can report a real import error instead of silently applying defaults.
|
|
38
|
+
*/
|
|
39
|
+
export function parseTheme(raw) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
48
|
+
return null;
|
|
49
|
+
const doc = parsed;
|
|
50
|
+
// Accept both the envelope and a bare preferences object.
|
|
51
|
+
const source = (doc['preferences'] !== null && typeof doc['preferences'] === 'object' && !Array.isArray(doc['preferences']))
|
|
52
|
+
? doc['preferences']
|
|
53
|
+
: doc;
|
|
54
|
+
const result = { ...DEFAULT_PREFERENCES };
|
|
55
|
+
if (typeof source['preset'] === 'string' || source['preset'] === null) {
|
|
56
|
+
result.preset = source['preset'];
|
|
57
|
+
}
|
|
58
|
+
result.accentColor = asColor(source['accentColor']);
|
|
59
|
+
result.darkAccentColor = asColor(source['darkAccentColor']);
|
|
60
|
+
if (typeof source['density'] === 'string' && DENSITIES.has(source['density'])) {
|
|
61
|
+
result.density = source['density'];
|
|
62
|
+
}
|
|
63
|
+
if (typeof source['radius'] === 'string' && RADII.has(source['radius'])) {
|
|
64
|
+
result.radius = source['radius'];
|
|
65
|
+
}
|
|
66
|
+
if (typeof source['fontFamily'] === 'string' && FONTS.has(source['fontFamily'])) {
|
|
67
|
+
result.fontFamily = source['fontFamily'];
|
|
68
|
+
}
|
|
69
|
+
if (typeof source['animations'] === 'boolean')
|
|
70
|
+
result.animations = source['animations'];
|
|
71
|
+
if (typeof source['customCss'] === 'string')
|
|
72
|
+
result.customCss = source['customCss'];
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** Parse `#rgb` or `#rrggbb` into RGB components; `null` when unparseable. */
|
|
76
|
+
export function hexToRgb(hex) {
|
|
77
|
+
const value = hex.trim().replace(/^#/, '');
|
|
78
|
+
if (value.length === 3) {
|
|
79
|
+
const [r, g, b] = value.split('');
|
|
80
|
+
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];
|
|
81
|
+
}
|
|
82
|
+
if (value.length === 6) {
|
|
83
|
+
return [
|
|
84
|
+
parseInt(value.slice(0, 2), 16),
|
|
85
|
+
parseInt(value.slice(2, 4), 16),
|
|
86
|
+
parseInt(value.slice(4, 6), 16),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** WCAG relative luminance, 0 (black) → 1 (white). */
|
|
92
|
+
export function luminance(hex) {
|
|
93
|
+
const rgb = hexToRgb(hex);
|
|
94
|
+
if (rgb === null)
|
|
95
|
+
return 0;
|
|
96
|
+
const [r, g, b] = rgb.map((c) => {
|
|
97
|
+
const s = c / 255;
|
|
98
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
99
|
+
});
|
|
100
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
101
|
+
}
|
|
102
|
+
/** Convert RGB components back into a `#rrggbb` string. */
|
|
103
|
+
export function rgbToHex(r, g, b) {
|
|
104
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
105
|
+
return '#' + [r, g, b].map((n) => clamp(n).toString(16).padStart(2, '0')).join('');
|
|
106
|
+
}
|
|
107
|
+
/** Blend a color toward white by `amount` (0–1). */
|
|
108
|
+
export function lighten(hex, amount) {
|
|
109
|
+
const rgb = hexToRgb(hex);
|
|
110
|
+
if (rgb === null)
|
|
111
|
+
return hex;
|
|
112
|
+
const [r, g, b] = rgb;
|
|
113
|
+
return rgbToHex(r + (255 - r) * amount, g + (255 - g) * amount, b + (255 - b) * amount);
|
|
114
|
+
}
|
|
115
|
+
/** Blend a color toward black by `amount` (0–1). */
|
|
116
|
+
export function darken(hex, amount) {
|
|
117
|
+
const rgb = hexToRgb(hex);
|
|
118
|
+
if (rgb === null)
|
|
119
|
+
return hex;
|
|
120
|
+
const [r, g, b] = rgb;
|
|
121
|
+
return rgbToHex(r * (1 - amount), g * (1 - amount), b * (1 - amount));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Ensure an accent color stays legible on a dark background.
|
|
125
|
+
*
|
|
126
|
+
* A mid-tone accent that reads well on white can sit too close to a dark
|
|
127
|
+
* surface. Rather than silently swapping the user's color, we lighten it in
|
|
128
|
+
* steps until it clears a luminance floor and report whether we changed it, so
|
|
129
|
+
* the panel can say so instead of the user wondering why their pick looks off.
|
|
130
|
+
*/
|
|
131
|
+
export function ensureDarkContrast(hex) {
|
|
132
|
+
const MIN_LUMINANCE = 0.18;
|
|
133
|
+
// An unparseable value can't be measured, so it can't be corrected either —
|
|
134
|
+
// returning it unadjusted keeps the panel from claiming it fixed something.
|
|
135
|
+
if (hexToRgb(hex) === null)
|
|
136
|
+
return { color: hex, adjusted: false };
|
|
137
|
+
if (luminance(hex) >= MIN_LUMINANCE)
|
|
138
|
+
return { color: hex, adjusted: false };
|
|
139
|
+
let current = hex;
|
|
140
|
+
for (let i = 0; i < 10; i += 1) {
|
|
141
|
+
current = lighten(current, 0.12);
|
|
142
|
+
if (luminance(current) >= MIN_LUMINANCE)
|
|
143
|
+
return { color: current, adjusted: true };
|
|
144
|
+
}
|
|
145
|
+
return { color: current, adjusted: true };
|
|
146
|
+
}
|
package/lib/theme-studio.web.js
CHANGED
|
@@ -10,6 +10,9 @@ var zh = {
|
|
|
10
10
|
accentSection: "\u5F3A\u8C03\u8272",
|
|
11
11
|
accentHint: "\u81EA\u5B9A\u4E49\u754C\u9762\u5F3A\u8C03\u8272\uFF0C\u8986\u76D6\u9884\u8BBE\u4E2D\u7684\u5F3A\u8C03\u8272",
|
|
12
12
|
accentPlaceholder: "#4B8BBE",
|
|
13
|
+
darkAccentSection: "\u6697\u8272\u6A21\u5F0F\u5F3A\u8C03\u8272",
|
|
14
|
+
darkAccentHint: "\u6697\u8272\u6A21\u5F0F\u4E0B\u4F7F\u7528\u7684\u5F3A\u8C03\u8272\uFF0C\u7559\u7A7A\u5219\u6CBF\u7528\u4E0A\u9762\u7684\u989C\u8272",
|
|
15
|
+
preserveContrast: "\u5DF2\u81EA\u52A8\u63D0\u4EAE\u4EE5\u4FDD\u8BC1\u6697\u8272\u4E0B\u53EF\u8BFB",
|
|
13
16
|
densitySection: "\u5BC6\u5EA6",
|
|
14
17
|
densityCompact: "\u7D27\u51D1",
|
|
15
18
|
densityComfortable: "\u8212\u9002",
|
|
@@ -25,6 +28,20 @@ var zh = {
|
|
|
25
28
|
customCssSection: "\u81EA\u5B9A\u4E49 CSS",
|
|
26
29
|
customCssHint: "\u76F4\u63A5\u8986\u76D6 CSS \u81EA\u5B9A\u4E49\u5C5E\u6027\uFF0C\u6BCF\u884C\u4E00\u4E2A `--property: value;`",
|
|
27
30
|
customCssPlaceholder: "--accent: #ff0000;\n--border: 1px solid red;",
|
|
31
|
+
behaviorSection: "\u884C\u4E3A",
|
|
32
|
+
animations: "\u754C\u9762\u52A8\u753B",
|
|
33
|
+
animationsHint: "\u5173\u95ED\u53EF\u51CF\u5C11\u52A8\u753B\u5E72\u6270\uFF0C\u63D0\u5347\u54CD\u5E94\u901F\u5EA6",
|
|
34
|
+
ioSection: "\u5BFC\u5165 / \u5BFC\u51FA",
|
|
35
|
+
export: "\u5BFC\u51FA\u4E3B\u9898",
|
|
36
|
+
import: "\u5BFC\u5165\u4E3B\u9898",
|
|
37
|
+
importHint: "\u7C98\u8D34\u5BFC\u51FA\u7684 JSON\uFF0C\u6216\u4E0A\u4F20 .json \u6587\u4EF6",
|
|
38
|
+
importPlaceholder: '{"preset":"nord","accentColor":"#5e81ac",...}',
|
|
39
|
+
importFile: "\u9009\u62E9\u6587\u4EF6",
|
|
40
|
+
importSuccess: "\u4E3B\u9898\u5DF2\u5BFC\u5165",
|
|
41
|
+
importFailed: "\u5BFC\u5165\u5931\u8D25\uFF1A\u4E0D\u662F\u6709\u6548\u7684\u4E3B\u9898 JSON",
|
|
42
|
+
exportSuccess: "\u4E3B\u9898\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F",
|
|
43
|
+
exportFailed: "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236",
|
|
44
|
+
copy: "\u590D\u5236",
|
|
28
45
|
preview: "\u9884\u89C8",
|
|
29
46
|
apply: "\u5E94\u7528",
|
|
30
47
|
reset: "\u91CD\u7F6E\u4E3A\u9ED8\u8BA4",
|
|
@@ -46,6 +63,9 @@ var en = {
|
|
|
46
63
|
accentSection: "Accent Color",
|
|
47
64
|
accentHint: "Custom accent color, overrides the preset",
|
|
48
65
|
accentPlaceholder: "#4B8BBE",
|
|
66
|
+
darkAccentSection: "Dark Mode Accent",
|
|
67
|
+
darkAccentHint: "Accent used in dark mode; leave empty to reuse the color above",
|
|
68
|
+
preserveContrast: "Lightened automatically for dark-mode readability",
|
|
49
69
|
densitySection: "Density",
|
|
50
70
|
densityCompact: "Compact",
|
|
51
71
|
densityComfortable: "Comfortable",
|
|
@@ -61,6 +81,20 @@ var en = {
|
|
|
61
81
|
customCssSection: "Custom CSS",
|
|
62
82
|
customCssHint: "Override CSS custom properties directly, one `--property: value;` per line",
|
|
63
83
|
customCssPlaceholder: "--accent: #ff0000;\n--border: 1px solid red;",
|
|
84
|
+
behaviorSection: "Behavior",
|
|
85
|
+
animations: "UI Animations",
|
|
86
|
+
animationsHint: "Turn off to reduce motion and speed up interaction",
|
|
87
|
+
ioSection: "Import / Export",
|
|
88
|
+
export: "Export Theme",
|
|
89
|
+
import: "Import Theme",
|
|
90
|
+
importHint: "Paste exported JSON, or upload a .json file",
|
|
91
|
+
importPlaceholder: '{"preset":"nord","accentColor":"#5e81ac",...}',
|
|
92
|
+
importFile: "Choose File",
|
|
93
|
+
importSuccess: "Theme imported",
|
|
94
|
+
importFailed: "Import failed: not a valid theme JSON",
|
|
95
|
+
exportSuccess: "Theme copied to clipboard",
|
|
96
|
+
exportFailed: "Copy failed, please copy manually",
|
|
97
|
+
copy: "Copy",
|
|
64
98
|
preview: "Preview",
|
|
65
99
|
apply: "Apply",
|
|
66
100
|
reset: "Reset to Default",
|
|
@@ -74,15 +108,17 @@ var en = {
|
|
|
74
108
|
};
|
|
75
109
|
|
|
76
110
|
// src/client/view.tsx
|
|
77
|
-
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
111
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
78
112
|
|
|
79
113
|
// src/types.ts
|
|
80
114
|
var DEFAULT_PREFERENCES = {
|
|
81
115
|
preset: null,
|
|
82
116
|
accentColor: null,
|
|
117
|
+
darkAccentColor: null,
|
|
83
118
|
density: "comfortable",
|
|
84
119
|
radius: "rounded",
|
|
85
120
|
fontFamily: "system",
|
|
121
|
+
animations: true,
|
|
86
122
|
customCss: ""
|
|
87
123
|
};
|
|
88
124
|
var STORAGE_KEY = "dsh-theme-studio";
|
|
@@ -159,6 +195,81 @@ var PRESETS = [
|
|
|
159
195
|
"--dsw-alias-state-business-primary": "#bd93f9",
|
|
160
196
|
"--dsw-alias-state-business-secondary": "#3a3a5c",
|
|
161
197
|
"--dsw-alias-interactive-bg-hover": "rgba(189,147,249,0.12)"
|
|
198
|
+
},
|
|
199
|
+
darkTokens: {
|
|
200
|
+
"--dsw-alias-state-business-secondary": "#44475a",
|
|
201
|
+
"--dsw-alias-interactive-bg-hover": "rgba(189,147,249,0.18)"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "gruvbox",
|
|
206
|
+
name: "Gruvbox",
|
|
207
|
+
description: "Retro groove with warm earthy tones",
|
|
208
|
+
tokens: {
|
|
209
|
+
"--accent": "#d65d0e",
|
|
210
|
+
"--accent-hover": "#cc241d",
|
|
211
|
+
"--dsw-alias-state-business-primary": "#d65d0e",
|
|
212
|
+
"--dsw-alias-state-business-secondary": "#fabd2f",
|
|
213
|
+
"--dsw-alias-interactive-bg-hover": "rgba(214,93,14,0.08)"
|
|
214
|
+
},
|
|
215
|
+
darkTokens: {
|
|
216
|
+
"--accent": "#fe8019",
|
|
217
|
+
"--accent-hover": "#fabd2f",
|
|
218
|
+
"--dsw-alias-state-business-primary": "#fe8019",
|
|
219
|
+
"--dsw-alias-state-business-secondary": "#3c3836"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "solarized",
|
|
224
|
+
name: "Solarized",
|
|
225
|
+
description: "Ethan Schoonover's precision palette",
|
|
226
|
+
tokens: {
|
|
227
|
+
"--accent": "#268bd2",
|
|
228
|
+
"--accent-hover": "#1e6fa8",
|
|
229
|
+
"--dsw-alias-state-business-primary": "#268bd2",
|
|
230
|
+
"--dsw-alias-state-business-secondary": "#eee8d5",
|
|
231
|
+
"--dsw-alias-interactive-bg-hover": "rgba(38,139,210,0.08)"
|
|
232
|
+
},
|
|
233
|
+
darkTokens: {
|
|
234
|
+
"--accent": "#839496",
|
|
235
|
+
"--accent-hover": "#93a1a1",
|
|
236
|
+
"--dsw-alias-state-business-primary": "#268bd2",
|
|
237
|
+
"--dsw-alias-state-business-secondary": "#073642"
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "tokyo-night",
|
|
242
|
+
name: "Tokyo Night",
|
|
243
|
+
description: "Inspired by the lights of downtown Tokyo at night",
|
|
244
|
+
tokens: {
|
|
245
|
+
"--accent": "#7aa2f7",
|
|
246
|
+
"--accent-hover": "#6183f0",
|
|
247
|
+
"--dsw-alias-state-business-primary": "#7aa2f7",
|
|
248
|
+
"--dsw-alias-state-business-secondary": "#bb9af7",
|
|
249
|
+
"--dsw-alias-interactive-bg-hover": "rgba(122,162,247,0.08)"
|
|
250
|
+
},
|
|
251
|
+
darkTokens: {
|
|
252
|
+
"--dsw-alias-state-business-secondary": "#1a1b26",
|
|
253
|
+
"--dsw-alias-interactive-bg-hover": "rgba(122,162,247,0.15)"
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "catppuccin",
|
|
258
|
+
name: "Catppuccin",
|
|
259
|
+
description: "Soothing pastel theme for high contrast",
|
|
260
|
+
tokens: {
|
|
261
|
+
"--accent": "#89b4fa",
|
|
262
|
+
"--accent-hover": "#74a8fc",
|
|
263
|
+
"--dsw-alias-state-business-primary": "#89b4fa",
|
|
264
|
+
"--dsw-alias-state-business-secondary": "#f5e0dc",
|
|
265
|
+
"--dsw-alias-interactive-bg-hover": "rgba(137,180,250,0.08)"
|
|
266
|
+
},
|
|
267
|
+
darkTokens: {
|
|
268
|
+
"--accent": "#cba6f7",
|
|
269
|
+
"--accent-hover": "#b48bef",
|
|
270
|
+
"--dsw-alias-state-business-primary": "#cba6f7",
|
|
271
|
+
"--dsw-alias-state-business-secondary": "#313244",
|
|
272
|
+
"--dsw-alias-interactive-bg-hover": "rgba(203,166,247,0.12)"
|
|
162
273
|
}
|
|
163
274
|
}
|
|
164
275
|
];
|
|
@@ -198,10 +309,116 @@ var FONT_TOKENS = {
|
|
|
198
309
|
mono: { "--dsh-font-family": "'JetBrains Mono', 'Fira Code', monospace" },
|
|
199
310
|
serif: { "--dsh-font-family": "'Georgia', 'Times New Roman', serif" }
|
|
200
311
|
};
|
|
312
|
+
var ANIMATION_ON_TOKENS = {
|
|
313
|
+
"--dsh-transition-fast": "0.15s ease",
|
|
314
|
+
"--dsh-transition-normal": "0.25s ease"
|
|
315
|
+
};
|
|
316
|
+
var ANIMATION_OFF_TOKENS = {
|
|
317
|
+
"--dsh-transition-fast": "0s",
|
|
318
|
+
"--dsh-transition-normal": "0s"
|
|
319
|
+
};
|
|
201
320
|
function getPreset(id) {
|
|
202
321
|
return PRESETS.find((p) => p.id === id);
|
|
203
322
|
}
|
|
204
323
|
|
|
324
|
+
// src/io.ts
|
|
325
|
+
function exportTheme(prefs, name) {
|
|
326
|
+
const payload = {
|
|
327
|
+
$schema: "dsh-theme-studio/v1",
|
|
328
|
+
preferences: prefs,
|
|
329
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
330
|
+
};
|
|
331
|
+
if (name !== void 0) payload.name = name;
|
|
332
|
+
return JSON.stringify(payload, null, 2);
|
|
333
|
+
}
|
|
334
|
+
var DENSITIES = /* @__PURE__ */ new Set(["compact", "comfortable", "spacious"]);
|
|
335
|
+
var RADII = /* @__PURE__ */ new Set(["sharp", "rounded", "soft"]);
|
|
336
|
+
var FONTS = /* @__PURE__ */ new Set(["system", "mono", "serif"]);
|
|
337
|
+
function asColor(value) {
|
|
338
|
+
if (typeof value !== "string") return null;
|
|
339
|
+
const trimmed = value.trim();
|
|
340
|
+
if (trimmed === "") return null;
|
|
341
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(trimmed) ? trimmed : null;
|
|
342
|
+
}
|
|
343
|
+
function parseTheme(raw) {
|
|
344
|
+
let parsed;
|
|
345
|
+
try {
|
|
346
|
+
parsed = JSON.parse(raw);
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
351
|
+
const doc = parsed;
|
|
352
|
+
const source = doc["preferences"] !== null && typeof doc["preferences"] === "object" && !Array.isArray(doc["preferences"]) ? doc["preferences"] : doc;
|
|
353
|
+
const result = { ...DEFAULT_PREFERENCES };
|
|
354
|
+
if (typeof source["preset"] === "string" || source["preset"] === null) {
|
|
355
|
+
result.preset = source["preset"];
|
|
356
|
+
}
|
|
357
|
+
result.accentColor = asColor(source["accentColor"]);
|
|
358
|
+
result.darkAccentColor = asColor(source["darkAccentColor"]);
|
|
359
|
+
if (typeof source["density"] === "string" && DENSITIES.has(source["density"])) {
|
|
360
|
+
result.density = source["density"];
|
|
361
|
+
}
|
|
362
|
+
if (typeof source["radius"] === "string" && RADII.has(source["radius"])) {
|
|
363
|
+
result.radius = source["radius"];
|
|
364
|
+
}
|
|
365
|
+
if (typeof source["fontFamily"] === "string" && FONTS.has(source["fontFamily"])) {
|
|
366
|
+
result.fontFamily = source["fontFamily"];
|
|
367
|
+
}
|
|
368
|
+
if (typeof source["animations"] === "boolean") result.animations = source["animations"];
|
|
369
|
+
if (typeof source["customCss"] === "string") result.customCss = source["customCss"];
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function hexToRgb(hex) {
|
|
373
|
+
const value = hex.trim().replace(/^#/, "");
|
|
374
|
+
if (value.length === 3) {
|
|
375
|
+
const [r, g, b] = value.split("");
|
|
376
|
+
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];
|
|
377
|
+
}
|
|
378
|
+
if (value.length === 6) {
|
|
379
|
+
return [
|
|
380
|
+
parseInt(value.slice(0, 2), 16),
|
|
381
|
+
parseInt(value.slice(2, 4), 16),
|
|
382
|
+
parseInt(value.slice(4, 6), 16)
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
function luminance(hex) {
|
|
388
|
+
const rgb = hexToRgb(hex);
|
|
389
|
+
if (rgb === null) return 0;
|
|
390
|
+
const [r, g, b] = rgb.map((c) => {
|
|
391
|
+
const s = c / 255;
|
|
392
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
393
|
+
});
|
|
394
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
395
|
+
}
|
|
396
|
+
function rgbToHex(r, g, b) {
|
|
397
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
398
|
+
return "#" + [r, g, b].map((n) => clamp(n).toString(16).padStart(2, "0")).join("");
|
|
399
|
+
}
|
|
400
|
+
function lighten(hex, amount) {
|
|
401
|
+
const rgb = hexToRgb(hex);
|
|
402
|
+
if (rgb === null) return hex;
|
|
403
|
+
const [r, g, b] = rgb;
|
|
404
|
+
return rgbToHex(
|
|
405
|
+
r + (255 - r) * amount,
|
|
406
|
+
g + (255 - g) * amount,
|
|
407
|
+
b + (255 - b) * amount
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
function ensureDarkContrast(hex) {
|
|
411
|
+
const MIN_LUMINANCE = 0.18;
|
|
412
|
+
if (hexToRgb(hex) === null) return { color: hex, adjusted: false };
|
|
413
|
+
if (luminance(hex) >= MIN_LUMINANCE) return { color: hex, adjusted: false };
|
|
414
|
+
let current = hex;
|
|
415
|
+
for (let i = 0; i < 10; i += 1) {
|
|
416
|
+
current = lighten(current, 0.12);
|
|
417
|
+
if (luminance(current) >= MIN_LUMINANCE) return { color: current, adjusted: true };
|
|
418
|
+
}
|
|
419
|
+
return { color: current, adjusted: true };
|
|
420
|
+
}
|
|
421
|
+
|
|
205
422
|
// src/client/ui.tsx
|
|
206
423
|
import {
|
|
207
424
|
createContext,
|
|
@@ -655,6 +872,21 @@ function Field({
|
|
|
655
872
|
|
|
656
873
|
// src/client/view.tsx
|
|
657
874
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
875
|
+
var MANAGED_PROPERTIES = [
|
|
876
|
+
"--accent",
|
|
877
|
+
"--accent-hover",
|
|
878
|
+
"--dsh-content-font-size",
|
|
879
|
+
"--dsh-spacing-unit",
|
|
880
|
+
"--dsh-radius-small",
|
|
881
|
+
"--dsh-radius-medium",
|
|
882
|
+
"--dsh-radius-large",
|
|
883
|
+
"--dsh-font-family",
|
|
884
|
+
"--dsh-transition-fast",
|
|
885
|
+
"--dsh-transition-normal",
|
|
886
|
+
"--dsw-alias-state-business-primary",
|
|
887
|
+
"--dsw-alias-state-business-secondary",
|
|
888
|
+
"--dsw-alias-interactive-bg-hover"
|
|
889
|
+
];
|
|
658
890
|
function loadPreferences() {
|
|
659
891
|
if (typeof localStorage === "undefined") return { ...DEFAULT_PREFERENCES };
|
|
660
892
|
try {
|
|
@@ -669,59 +901,71 @@ function savePreferences(prefs) {
|
|
|
669
901
|
if (typeof localStorage === "undefined") return;
|
|
670
902
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
|
671
903
|
}
|
|
672
|
-
function
|
|
673
|
-
if (typeof document === "undefined") return;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (prefs.accentColor !== null && prefs.accentColor.trim() !== "") {
|
|
681
|
-
allTokens["--accent"] = prefs.accentColor;
|
|
904
|
+
function isDarkMode() {
|
|
905
|
+
if (typeof document === "undefined") return false;
|
|
906
|
+
return document.body.hasAttribute("data-ds-dark-theme");
|
|
907
|
+
}
|
|
908
|
+
function observeDarkMode(onChange) {
|
|
909
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
910
|
+
return () => {
|
|
911
|
+
};
|
|
682
912
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
913
|
+
const observer = new MutationObserver(() => onChange(isDarkMode()));
|
|
914
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
|
|
915
|
+
return () => observer.disconnect();
|
|
916
|
+
}
|
|
917
|
+
function resolveTokens(prefs, dark) {
|
|
918
|
+
const tokens = {};
|
|
919
|
+
let contrastAdjusted = false;
|
|
920
|
+
const preset = prefs.preset !== null ? getPreset(prefs.preset) : void 0;
|
|
921
|
+
if (preset !== void 0) {
|
|
922
|
+
Object.assign(tokens, preset.tokens);
|
|
923
|
+
if (dark && preset.darkTokens !== void 0) Object.assign(tokens, preset.darkTokens);
|
|
688
924
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
925
|
+
const chosenAccent = dark ? prefs.darkAccentColor ?? prefs.accentColor : prefs.accentColor;
|
|
926
|
+
if (chosenAccent !== null && chosenAccent.trim() !== "") {
|
|
927
|
+
let accent = chosenAccent;
|
|
928
|
+
if (dark) {
|
|
929
|
+
const guarded = ensureDarkContrast(accent);
|
|
930
|
+
accent = guarded.color;
|
|
931
|
+
contrastAdjusted = guarded.adjusted;
|
|
695
932
|
}
|
|
933
|
+
tokens["--accent"] = accent;
|
|
696
934
|
}
|
|
935
|
+
Object.assign(tokens, DENSITY_TOKENS[prefs.density] ?? {});
|
|
936
|
+
Object.assign(tokens, RADIUS_TOKENS[prefs.radius] ?? {});
|
|
937
|
+
Object.assign(tokens, FONT_TOKENS[prefs.fontFamily] ?? {});
|
|
938
|
+
Object.assign(tokens, prefs.animations ? ANIMATION_ON_TOKENS : ANIMATION_OFF_TOKENS);
|
|
939
|
+
return { tokens, contrastAdjusted };
|
|
940
|
+
}
|
|
941
|
+
function parseCustomCss(css) {
|
|
942
|
+
const out = {};
|
|
943
|
+
for (const line of css.split("\n")) {
|
|
944
|
+
const match = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
|
|
945
|
+
if (match !== null) out[match[1]] = match[2];
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
function applyTheme(prefs, dark = isDarkMode()) {
|
|
950
|
+
if (typeof document === "undefined") return false;
|
|
951
|
+
const root = document.documentElement;
|
|
952
|
+
const { tokens, contrastAdjusted } = resolveTokens(prefs, dark);
|
|
953
|
+
const custom = parseCustomCss(prefs.customCss);
|
|
954
|
+
const merged = { ...tokens, ...custom };
|
|
955
|
+
for (const prop of MANAGED_PROPERTIES) {
|
|
956
|
+
if (!(prop in merged)) root.style.removeProperty(prop);
|
|
957
|
+
}
|
|
958
|
+
for (const [prop, value] of Object.entries(merged)) root.style.setProperty(prop, value);
|
|
959
|
+
return contrastAdjusted;
|
|
697
960
|
}
|
|
698
961
|
function clearTheme() {
|
|
699
962
|
if (typeof document === "undefined") return;
|
|
700
963
|
const root = document.documentElement;
|
|
701
|
-
const
|
|
702
|
-
"--accent",
|
|
703
|
-
"--accent-hover",
|
|
704
|
-
"--dsh-content-font-size",
|
|
705
|
-
"--dsh-spacing-unit",
|
|
706
|
-
"--dsh-radius-small",
|
|
707
|
-
"--dsh-radius-medium",
|
|
708
|
-
"--dsh-radius-large",
|
|
709
|
-
"--dsh-font-family",
|
|
710
|
-
"--dsw-alias-state-business-primary",
|
|
711
|
-
"--dsw-alias-state-business-secondary",
|
|
712
|
-
"--dsw-alias-interactive-bg-hover"
|
|
713
|
-
]);
|
|
714
|
-
for (const prop of known) root.style.removeProperty(prop);
|
|
715
|
-
const prefs = loadPreferences();
|
|
716
|
-
if (prefs.customCss.trim() !== "") {
|
|
717
|
-
for (const line of prefs.customCss.split("\n")) {
|
|
718
|
-
const match = line.match(/^\s*(--[\w-]+)\s*:/);
|
|
719
|
-
if (match !== null) root.style.removeProperty(match[1]);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
964
|
+
for (const prop of MANAGED_PROPERTIES) root.style.removeProperty(prop);
|
|
722
965
|
}
|
|
723
966
|
function PresetCard({ preset, selected, onClick }) {
|
|
724
967
|
const accent = preset.tokens["--accent"] ?? "#4B8BBE";
|
|
968
|
+
const secondary = preset.tokens["--dsw-alias-state-business-secondary"] ?? "#e5e5e5";
|
|
725
969
|
return /* @__PURE__ */ jsxs2(
|
|
726
970
|
"div",
|
|
727
971
|
{
|
|
@@ -735,12 +979,16 @@ function PresetCard({ preset, selected, onClick }) {
|
|
|
735
979
|
transition: "all 0.15s ease"
|
|
736
980
|
},
|
|
737
981
|
children: [
|
|
738
|
-
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom:
|
|
739
|
-
/* @__PURE__ */
|
|
982
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }, children: [
|
|
983
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 3 }, children: [
|
|
984
|
+
/* @__PURE__ */ jsx2("div", { style: { width: 14, height: 14, borderRadius: "50%", background: accent, border: "1px solid rgba(128,128,128,0.2)" } }),
|
|
985
|
+
/* @__PURE__ */ jsx2("div", { style: { width: 14, height: 14, borderRadius: "50%", background: secondary, border: "1px solid rgba(128,128,128,0.2)" } })
|
|
986
|
+
] }),
|
|
740
987
|
/* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: preset.name }),
|
|
741
|
-
|
|
988
|
+
preset.darkTokens !== void 0 && /* @__PURE__ */ jsx2(Badge, { color: "info", children: "\u25D0" }),
|
|
989
|
+
selected && /* @__PURE__ */ jsx2(Badge, { color: "success", children: "\u2713" })
|
|
742
990
|
] }),
|
|
743
|
-
/* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.6 }, children: preset.description })
|
|
991
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.6, lineHeight: 1.4 }, children: preset.description })
|
|
744
992
|
]
|
|
745
993
|
}
|
|
746
994
|
);
|
|
@@ -767,55 +1015,174 @@ function Segmented({ value, options, onChange }) {
|
|
|
767
1015
|
opt.value
|
|
768
1016
|
)) });
|
|
769
1017
|
}
|
|
770
|
-
function
|
|
771
|
-
return /* @__PURE__ */
|
|
772
|
-
/* @__PURE__ */
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1018
|
+
function ColorRow({ value, onChange, placeholder }) {
|
|
1019
|
+
return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: [
|
|
1020
|
+
/* @__PURE__ */ jsx2(
|
|
1021
|
+
"input",
|
|
1022
|
+
{
|
|
1023
|
+
type: "color",
|
|
1024
|
+
value: value ?? "#4B8BBE",
|
|
1025
|
+
onChange: (e) => onChange(e.target.value),
|
|
1026
|
+
style: { width: 40, height: 40, cursor: "pointer", borderRadius: 8, border: "1px solid var(--border, rgba(128,128,128,0.2))" }
|
|
1027
|
+
}
|
|
1028
|
+
),
|
|
1029
|
+
/* @__PURE__ */ jsx2(
|
|
1030
|
+
Input,
|
|
1031
|
+
{
|
|
1032
|
+
value: value ?? "",
|
|
1033
|
+
onChange: (e) => onChange(e.target.value.trim() === "" ? null : e.target.value),
|
|
1034
|
+
placeholder,
|
|
1035
|
+
style: { maxWidth: 200 }
|
|
1036
|
+
}
|
|
1037
|
+
),
|
|
1038
|
+
value !== null && /* @__PURE__ */ jsx2(Button, { variant: "ghost", size: "sm", onClick: () => onChange(null), children: "\u2715" })
|
|
1039
|
+
] });
|
|
1040
|
+
}
|
|
1041
|
+
function PreviewArea({ t, dark }) {
|
|
1042
|
+
return /* @__PURE__ */ jsx2(
|
|
1043
|
+
Card,
|
|
1044
|
+
{
|
|
1045
|
+
title: t("preview"),
|
|
1046
|
+
icon: "\u{1F441}",
|
|
1047
|
+
actions: /* @__PURE__ */ jsx2(Badge, { color: dark ? "info" : "warning", children: dark ? "\u{1F319} dark" : "\u2600 light" }),
|
|
1048
|
+
children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
|
|
1049
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
|
|
1050
|
+
/* @__PURE__ */ jsx2(Badge, { color: "info", children: "Info" }),
|
|
1051
|
+
/* @__PURE__ */ jsx2(Badge, { color: "success", children: "Success" }),
|
|
1052
|
+
/* @__PURE__ */ jsx2(Badge, { color: "warning", children: "Warning" }),
|
|
1053
|
+
/* @__PURE__ */ jsx2(Badge, { color: "error", children: "Error" })
|
|
1054
|
+
] }),
|
|
1055
|
+
/* @__PURE__ */ jsxs2(Card, { children: [
|
|
1056
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: 13, marginBottom: 8 }, children: t("text") }),
|
|
1057
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
|
|
1058
|
+
/* @__PURE__ */ jsx2(Button, { variant: "primary", size: "sm", children: t("button") }),
|
|
1059
|
+
/* @__PURE__ */ jsx2(Button, { variant: "secondary", size: "sm", children: "Secondary" }),
|
|
1060
|
+
/* @__PURE__ */ jsx2(Button, { variant: "danger", size: "sm", children: "Danger" })
|
|
1061
|
+
] })
|
|
1062
|
+
] })
|
|
784
1063
|
] })
|
|
785
|
-
|
|
786
|
-
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
787
1066
|
}
|
|
788
1067
|
function ThemePanelInner({ t }) {
|
|
789
1068
|
const toast = useToast();
|
|
790
1069
|
const [prefs, setPrefs] = useState2(loadPreferences);
|
|
1070
|
+
const [dark, setDark] = useState2(isDarkMode);
|
|
1071
|
+
const [contrastAdjusted, setContrastAdjusted] = useState2(false);
|
|
791
1072
|
const [confirmReset, setConfirmReset] = useState2(false);
|
|
1073
|
+
const [importText, setImportText] = useState2("");
|
|
1074
|
+
const [importOpen, setImportOpen] = useState2(false);
|
|
1075
|
+
const fileInput = useRef(null);
|
|
792
1076
|
useEffect2(() => {
|
|
793
|
-
applyTheme(prefs);
|
|
1077
|
+
setContrastAdjusted(applyTheme(prefs, dark));
|
|
794
1078
|
savePreferences(prefs);
|
|
795
|
-
}, [prefs]);
|
|
1079
|
+
}, [prefs, dark]);
|
|
1080
|
+
useEffect2(() => observeDarkMode(setDark), []);
|
|
796
1081
|
const update = useCallback2((key, value) => {
|
|
797
1082
|
setPrefs((p) => ({ ...p, [key]: value }));
|
|
798
1083
|
}, []);
|
|
799
1084
|
const handleReset = useCallback2(() => {
|
|
800
1085
|
clearTheme();
|
|
801
1086
|
setPrefs({ ...DEFAULT_PREFERENCES });
|
|
1087
|
+
setConfirmReset(false);
|
|
802
1088
|
toast("success", t("resetted"));
|
|
803
1089
|
}, [t, toast]);
|
|
804
|
-
|
|
805
|
-
|
|
1090
|
+
const handleExport = useCallback2(() => {
|
|
1091
|
+
const json = exportTheme(prefs);
|
|
1092
|
+
const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
1093
|
+
if (clipboard === void 0) {
|
|
1094
|
+
setImportText(json);
|
|
1095
|
+
setImportOpen(true);
|
|
1096
|
+
toast("warning", t("exportFailed"));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
clipboard.writeText(json).then(
|
|
1100
|
+
() => toast("success", t("exportSuccess")),
|
|
1101
|
+
() => {
|
|
1102
|
+
setImportText(json);
|
|
1103
|
+
setImportOpen(true);
|
|
1104
|
+
toast("warning", t("exportFailed"));
|
|
1105
|
+
}
|
|
1106
|
+
);
|
|
1107
|
+
}, [prefs, t, toast]);
|
|
1108
|
+
const runImport = useCallback2((raw) => {
|
|
1109
|
+
const parsed = parseTheme(raw);
|
|
1110
|
+
if (parsed === null) {
|
|
1111
|
+
toast("error", t("importFailed"));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
setPrefs(parsed);
|
|
1115
|
+
setImportOpen(false);
|
|
1116
|
+
setImportText("");
|
|
1117
|
+
toast("success", t("importSuccess"));
|
|
1118
|
+
}, [t, toast]);
|
|
1119
|
+
const handleFile = useCallback2((file) => {
|
|
1120
|
+
if (file === void 0) return;
|
|
1121
|
+
file.text().then(runImport, () => toast("error", t("importFailed")));
|
|
1122
|
+
}, [runImport, t, toast]);
|
|
1123
|
+
return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 16, maxWidth: 860 }, children: [
|
|
1124
|
+
/* @__PURE__ */ jsxs2("header", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
|
|
806
1125
|
/* @__PURE__ */ jsxs2("strong", { style: { fontSize: 15 }, children: [
|
|
807
1126
|
"\u{1F3A8} ",
|
|
808
1127
|
t("title")
|
|
809
1128
|
] }),
|
|
810
1129
|
/* @__PURE__ */ jsx2("span", { style: { fontSize: 12, opacity: 0.6 }, children: t("subtitle") }),
|
|
811
1130
|
/* @__PURE__ */ jsx2("span", { style: { flex: 1 } }),
|
|
1131
|
+
/* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: handleExport, children: [
|
|
1132
|
+
"\u2934 ",
|
|
1133
|
+
t("export")
|
|
1134
|
+
] }),
|
|
1135
|
+
/* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: () => setImportOpen((v) => !v), children: [
|
|
1136
|
+
"\u2935 ",
|
|
1137
|
+
t("import")
|
|
1138
|
+
] }),
|
|
812
1139
|
/* @__PURE__ */ jsxs2(Button, { variant: "danger", size: "sm", onClick: () => setConfirmReset(true), children: [
|
|
813
1140
|
"\u21BA ",
|
|
814
1141
|
t("reset")
|
|
815
1142
|
] })
|
|
816
1143
|
] }),
|
|
1144
|
+
importOpen && /* @__PURE__ */ jsxs2(Card, { title: t("import"), icon: "\u2935", children: [
|
|
1145
|
+
/* @__PURE__ */ jsx2(Field, { label: t("import"), hint: t("importHint"), children: /* @__PURE__ */ jsx2(
|
|
1146
|
+
Textarea,
|
|
1147
|
+
{
|
|
1148
|
+
value: importText,
|
|
1149
|
+
onChange: (e) => setImportText(e.target.value),
|
|
1150
|
+
placeholder: t("importPlaceholder"),
|
|
1151
|
+
rows: 5
|
|
1152
|
+
}
|
|
1153
|
+
) }),
|
|
1154
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, marginTop: 8 }, children: [
|
|
1155
|
+
/* @__PURE__ */ jsx2(
|
|
1156
|
+
Button,
|
|
1157
|
+
{
|
|
1158
|
+
variant: "primary",
|
|
1159
|
+
size: "sm",
|
|
1160
|
+
onClick: () => runImport(importText),
|
|
1161
|
+
disabled: importText.trim() === "",
|
|
1162
|
+
children: t("import")
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: () => fileInput.current?.click(), children: [
|
|
1166
|
+
"\u{1F4C4} ",
|
|
1167
|
+
t("importFile")
|
|
1168
|
+
] }),
|
|
1169
|
+
/* @__PURE__ */ jsx2(
|
|
1170
|
+
"input",
|
|
1171
|
+
{
|
|
1172
|
+
ref: fileInput,
|
|
1173
|
+
type: "file",
|
|
1174
|
+
accept: "application/json,.json",
|
|
1175
|
+
style: { display: "none" },
|
|
1176
|
+
onChange: (e) => {
|
|
1177
|
+
handleFile(e.target.files?.[0]);
|
|
1178
|
+
e.target.value = "";
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
)
|
|
1182
|
+
] })
|
|
1183
|
+
] }),
|
|
817
1184
|
/* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F3AD}", children: t("presetSection") }),
|
|
818
|
-
/* @__PURE__ */ jsxs2("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(
|
|
1185
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))", gap: 10 }, children: [
|
|
819
1186
|
/* @__PURE__ */ jsx2(
|
|
820
1187
|
PresetCard,
|
|
821
1188
|
{
|
|
@@ -836,28 +1203,28 @@ function ThemePanelInner({ t }) {
|
|
|
836
1203
|
] }),
|
|
837
1204
|
/* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F308}", children: t("accentSection") }),
|
|
838
1205
|
/* @__PURE__ */ jsxs2(Card, { children: [
|
|
839
|
-
/* @__PURE__ */
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1206
|
+
/* @__PURE__ */ jsx2(
|
|
1207
|
+
ColorRow,
|
|
1208
|
+
{
|
|
1209
|
+
value: prefs.accentColor,
|
|
1210
|
+
onChange: (v) => update("accentColor", v),
|
|
1211
|
+
placeholder: t("accentPlaceholder")
|
|
1212
|
+
}
|
|
1213
|
+
),
|
|
1214
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.5, marginTop: 6 }, children: t("accentHint") }),
|
|
1215
|
+
/* @__PURE__ */ jsx2("div", { style: { height: 14 } }),
|
|
1216
|
+
/* @__PURE__ */ jsx2(Field, { label: t("darkAccentSection"), hint: t("darkAccentHint"), children: /* @__PURE__ */ jsx2(
|
|
1217
|
+
ColorRow,
|
|
1218
|
+
{
|
|
1219
|
+
value: prefs.darkAccentColor,
|
|
1220
|
+
onChange: (v) => update("darkAccentColor", v),
|
|
1221
|
+
placeholder: t("accentPlaceholder")
|
|
1222
|
+
}
|
|
1223
|
+
) }),
|
|
1224
|
+
contrastAdjusted && /* @__PURE__ */ jsx2("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsxs2(Badge, { color: "warning", children: [
|
|
1225
|
+
"\u26A0 ",
|
|
1226
|
+
t("preserveContrast")
|
|
1227
|
+
] }) })
|
|
861
1228
|
] }),
|
|
862
1229
|
/* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F4D0}", children: t("densitySection") }),
|
|
863
1230
|
/* @__PURE__ */ jsx2(
|
|
@@ -898,6 +1265,23 @@ function ThemePanelInner({ t }) {
|
|
|
898
1265
|
]
|
|
899
1266
|
}
|
|
900
1267
|
),
|
|
1268
|
+
/* @__PURE__ */ jsx2(SectionTitle, { icon: "\u2699", children: t("behaviorSection") }),
|
|
1269
|
+
/* @__PURE__ */ jsxs2(Card, { children: [
|
|
1270
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
|
|
1271
|
+
/* @__PURE__ */ jsx2(
|
|
1272
|
+
"input",
|
|
1273
|
+
{
|
|
1274
|
+
id: "theme-animations",
|
|
1275
|
+
type: "checkbox",
|
|
1276
|
+
checked: prefs.animations,
|
|
1277
|
+
onChange: (e) => update("animations", e.target.checked),
|
|
1278
|
+
style: { width: 16, height: 16, cursor: "pointer" }
|
|
1279
|
+
}
|
|
1280
|
+
),
|
|
1281
|
+
/* @__PURE__ */ jsx2("label", { htmlFor: "theme-animations", style: { fontSize: 13, cursor: "pointer" }, children: t("animations") })
|
|
1282
|
+
] }),
|
|
1283
|
+
/* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.5, marginTop: 6, marginLeft: 26 }, children: t("animationsHint") })
|
|
1284
|
+
] }),
|
|
901
1285
|
/* @__PURE__ */ jsx2(SectionTitle, { icon: "\u270F\uFE0F", children: t("customCssSection") }),
|
|
902
1286
|
/* @__PURE__ */ jsx2(Card, { children: /* @__PURE__ */ jsx2(Field, { label: t("customCssSection"), hint: t("customCssHint"), children: /* @__PURE__ */ jsx2(
|
|
903
1287
|
Textarea,
|
|
@@ -908,7 +1292,7 @@ function ThemePanelInner({ t }) {
|
|
|
908
1292
|
rows: 4
|
|
909
1293
|
}
|
|
910
1294
|
) }) }),
|
|
911
|
-
/* @__PURE__ */ jsx2(PreviewArea, { t }),
|
|
1295
|
+
/* @__PURE__ */ jsx2(PreviewArea, { t, dark }),
|
|
912
1296
|
confirmReset && /* @__PURE__ */ jsx2(
|
|
913
1297
|
ConfirmDialog,
|
|
914
1298
|
{
|
package/lib/themes.d.ts
CHANGED
|
@@ -15,5 +15,8 @@ export declare const DENSITY_TOKENS: Record<string, Record<string, string>>;
|
|
|
15
15
|
export declare const RADIUS_TOKENS: Record<string, Record<string, string>>;
|
|
16
16
|
/** Font family → CSS custom property overrides */
|
|
17
17
|
export declare const FONT_TOKENS: Record<string, Record<string, string>>;
|
|
18
|
+
/** Animation → CSS custom property overrides */
|
|
19
|
+
export declare const ANIMATION_ON_TOKENS: Record<string, string>;
|
|
20
|
+
export declare const ANIMATION_OFF_TOKENS: Record<string, string>;
|
|
18
21
|
/** Look up a preset by id. */
|
|
19
22
|
export declare function getPreset(id: string): ThemePreset | undefined;
|
package/lib/themes.js
CHANGED
|
@@ -79,6 +79,81 @@ export const PRESETS = [
|
|
|
79
79
|
'--dsw-alias-state-business-secondary': '#3a3a5c',
|
|
80
80
|
'--dsw-alias-interactive-bg-hover': 'rgba(189,147,249,0.12)',
|
|
81
81
|
},
|
|
82
|
+
darkTokens: {
|
|
83
|
+
'--dsw-alias-state-business-secondary': '#44475a',
|
|
84
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(189,147,249,0.18)',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'gruvbox',
|
|
89
|
+
name: 'Gruvbox',
|
|
90
|
+
description: 'Retro groove with warm earthy tones',
|
|
91
|
+
tokens: {
|
|
92
|
+
'--accent': '#d65d0e',
|
|
93
|
+
'--accent-hover': '#cc241d',
|
|
94
|
+
'--dsw-alias-state-business-primary': '#d65d0e',
|
|
95
|
+
'--dsw-alias-state-business-secondary': '#fabd2f',
|
|
96
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(214,93,14,0.08)',
|
|
97
|
+
},
|
|
98
|
+
darkTokens: {
|
|
99
|
+
'--accent': '#fe8019',
|
|
100
|
+
'--accent-hover': '#fabd2f',
|
|
101
|
+
'--dsw-alias-state-business-primary': '#fe8019',
|
|
102
|
+
'--dsw-alias-state-business-secondary': '#3c3836',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'solarized',
|
|
107
|
+
name: 'Solarized',
|
|
108
|
+
description: 'Ethan Schoonover\'s precision palette',
|
|
109
|
+
tokens: {
|
|
110
|
+
'--accent': '#268bd2',
|
|
111
|
+
'--accent-hover': '#1e6fa8',
|
|
112
|
+
'--dsw-alias-state-business-primary': '#268bd2',
|
|
113
|
+
'--dsw-alias-state-business-secondary': '#eee8d5',
|
|
114
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(38,139,210,0.08)',
|
|
115
|
+
},
|
|
116
|
+
darkTokens: {
|
|
117
|
+
'--accent': '#839496',
|
|
118
|
+
'--accent-hover': '#93a1a1',
|
|
119
|
+
'--dsw-alias-state-business-primary': '#268bd2',
|
|
120
|
+
'--dsw-alias-state-business-secondary': '#073642',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'tokyo-night',
|
|
125
|
+
name: 'Tokyo Night',
|
|
126
|
+
description: 'Inspired by the lights of downtown Tokyo at night',
|
|
127
|
+
tokens: {
|
|
128
|
+
'--accent': '#7aa2f7',
|
|
129
|
+
'--accent-hover': '#6183f0',
|
|
130
|
+
'--dsw-alias-state-business-primary': '#7aa2f7',
|
|
131
|
+
'--dsw-alias-state-business-secondary': '#bb9af7',
|
|
132
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(122,162,247,0.08)',
|
|
133
|
+
},
|
|
134
|
+
darkTokens: {
|
|
135
|
+
'--dsw-alias-state-business-secondary': '#1a1b26',
|
|
136
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(122,162,247,0.15)',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'catppuccin',
|
|
141
|
+
name: 'Catppuccin',
|
|
142
|
+
description: 'Soothing pastel theme for high contrast',
|
|
143
|
+
tokens: {
|
|
144
|
+
'--accent': '#89b4fa',
|
|
145
|
+
'--accent-hover': '#74a8fc',
|
|
146
|
+
'--dsw-alias-state-business-primary': '#89b4fa',
|
|
147
|
+
'--dsw-alias-state-business-secondary': '#f5e0dc',
|
|
148
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(137,180,250,0.08)',
|
|
149
|
+
},
|
|
150
|
+
darkTokens: {
|
|
151
|
+
'--accent': '#cba6f7',
|
|
152
|
+
'--accent-hover': '#b48bef',
|
|
153
|
+
'--dsw-alias-state-business-primary': '#cba6f7',
|
|
154
|
+
'--dsw-alias-state-business-secondary': '#313244',
|
|
155
|
+
'--dsw-alias-interactive-bg-hover': 'rgba(203,166,247,0.12)',
|
|
156
|
+
},
|
|
82
157
|
},
|
|
83
158
|
];
|
|
84
159
|
/** Density → CSS custom property overrides */
|
|
@@ -120,6 +195,15 @@ export const FONT_TOKENS = {
|
|
|
120
195
|
mono: { '--dsh-font-family': "'JetBrains Mono', 'Fira Code', monospace" },
|
|
121
196
|
serif: { '--dsh-font-family': "'Georgia', 'Times New Roman', serif" },
|
|
122
197
|
};
|
|
198
|
+
/** Animation → CSS custom property overrides */
|
|
199
|
+
export const ANIMATION_ON_TOKENS = {
|
|
200
|
+
'--dsh-transition-fast': '0.15s ease',
|
|
201
|
+
'--dsh-transition-normal': '0.25s ease',
|
|
202
|
+
};
|
|
203
|
+
export const ANIMATION_OFF_TOKENS = {
|
|
204
|
+
'--dsh-transition-fast': '0s',
|
|
205
|
+
'--dsh-transition-normal': '0s',
|
|
206
|
+
};
|
|
123
207
|
/** Look up a preset by id. */
|
|
124
208
|
export function getPreset(id) {
|
|
125
209
|
return PRESETS.find((p) => p.id === id);
|
package/lib/types.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ThemePreset {
|
|
|
10
10
|
description: string;
|
|
11
11
|
/** CSS custom property → value, applied to document.documentElement.style */
|
|
12
12
|
tokens: Record<string, string>;
|
|
13
|
+
/** Tokens only applied when dsh is in dark mode (body[data-ds-dark-theme]) */
|
|
14
|
+
darkTokens?: Record<string, string>;
|
|
13
15
|
}
|
|
14
16
|
/** User-selectable density preference. */
|
|
15
17
|
export type Density = 'compact' | 'comfortable' | 'spacious';
|
|
@@ -21,9 +23,12 @@ export type FontFamily = 'system' | 'mono' | 'serif';
|
|
|
21
23
|
export interface ThemePreferences {
|
|
22
24
|
preset: string | null;
|
|
23
25
|
accentColor: string | null;
|
|
26
|
+
/** Separate accent color for dark mode; null = use accentColor */
|
|
27
|
+
darkAccentColor: string | null;
|
|
24
28
|
density: Density;
|
|
25
29
|
radius: Radius;
|
|
26
30
|
fontFamily: FontFamily;
|
|
31
|
+
animations: boolean;
|
|
27
32
|
customCss: string;
|
|
28
33
|
}
|
|
29
34
|
/** Default preferences — nothing overridden, dsh's built-in theme wins. */
|
package/lib/types.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
export const DEFAULT_PREFERENCES = {
|
|
8
8
|
preset: null,
|
|
9
9
|
accentColor: null,
|
|
10
|
+
darkAccentColor: null,
|
|
10
11
|
density: 'comfortable',
|
|
11
12
|
radius: 'rounded',
|
|
12
13
|
fontFamily: 'system',
|
|
14
|
+
animations: true,
|
|
13
15
|
customCss: '',
|
|
14
16
|
};
|
|
15
17
|
/** localStorage key for persisting preferences. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-theme-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Customize the dsh UI theme: pick presets, set accent colors, adjust density, border radius, and font family, or write custom CSS variable overrides.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|