dsh-theme-studio 0.1.0 → 0.3.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/lib/themes.d.ts CHANGED
@@ -1,19 +1,13 @@
1
1
  /**
2
- * Pre-built theme presets for dsh-theme-studio.
2
+ * Theme presets for dsh-theme-studio.
3
3
  *
4
- * Each preset overrides a set of CSS custom properties on document.documentElement.
5
- * The property names target dsh's `--dsw-alias-*` semantic layer so the overrides
6
- * cascade through every component that reads the design tokens.
4
+ * Each preset only sets tokens listed in `VERIFIED_TOKENS` real dsh design
5
+ * tokens, read out of the shipped stylesheets. The accent bundle comes from
6
+ * `accentTokens()` so a preset and a hand-picked color produce the same shape.
7
7
  *
8
8
  * @module themes
9
9
  */
10
10
  import type { ThemePreset } from './types.js';
11
11
  export declare const PRESETS: readonly ThemePreset[];
12
- /** Density → CSS custom property overrides */
13
- export declare const DENSITY_TOKENS: Record<string, Record<string, string>>;
14
- /** Radius → CSS custom property overrides */
15
- export declare const RADIUS_TOKENS: Record<string, Record<string, string>>;
16
- /** Font family → CSS custom property overrides */
17
- export declare const FONT_TOKENS: Record<string, Record<string, string>>;
18
12
  /** Look up a preset by id. */
19
13
  export declare function getPreset(id: string): ThemePreset | undefined;
package/lib/themes.js CHANGED
@@ -1,125 +1,37 @@
1
1
  /**
2
- * Pre-built theme presets for dsh-theme-studio.
2
+ * Theme presets for dsh-theme-studio.
3
3
  *
4
- * Each preset overrides a set of CSS custom properties on document.documentElement.
5
- * The property names target dsh's `--dsw-alias-*` semantic layer so the overrides
6
- * cascade through every component that reads the design tokens.
4
+ * Each preset only sets tokens listed in `VERIFIED_TOKENS` real dsh design
5
+ * tokens, read out of the shipped stylesheets. The accent bundle comes from
6
+ * `accentTokens()` so a preset and a hand-picked color produce the same shape.
7
7
  *
8
8
  * @module themes
9
9
  */
10
+ import { accentTokens } from './tokens.js';
11
+ /** Every preset is built from one accent color per mode. */
12
+ function preset(id, name, description, lightAccent, darkAccent) {
13
+ const base = { id, name, description, tokens: accentTokens(lightAccent) };
14
+ // A dark entry equal to the light one would be a no-op that still shows the
15
+ // "adapts to dark" badge, so it is only kept when the color actually changes.
16
+ if (darkAccent !== undefined && darkAccent.toLowerCase() !== lightAccent.toLowerCase()) {
17
+ base.darkTokens = accentTokens(darkAccent);
18
+ }
19
+ return base;
20
+ }
10
21
  export const PRESETS = [
11
- {
12
- id: 'ocean',
13
- name: 'Ocean',
14
- description: 'Deep blue accent with cool neutrals',
15
- tokens: {
16
- '--accent': '#2563eb',
17
- '--accent-hover': '#1d4ed8',
18
- '--dsw-alias-state-business-primary': '#2563eb',
19
- '--dsw-alias-state-business-secondary': '#dbeafe',
20
- '--dsw-alias-interactive-bg-hover': 'rgba(37,99,235,0.08)',
21
- },
22
- },
23
- {
24
- id: 'forest',
25
- name: 'Forest',
26
- description: 'Green accent with earthy neutrals',
27
- tokens: {
28
- '--accent': '#16a34a',
29
- '--accent-hover': '#15803d',
30
- '--dsw-alias-state-business-primary': '#16a34a',
31
- '--dsw-alias-state-business-secondary': '#dcfce7',
32
- '--dsw-alias-interactive-bg-hover': 'rgba(22,163,74,0.08)',
33
- },
34
- },
35
- {
36
- id: 'sunset',
37
- name: 'Sunset',
38
- description: 'Warm orange accent with amber highlights',
39
- tokens: {
40
- '--accent': '#ea580c',
41
- '--accent-hover': '#c2410c',
42
- '--dsw-alias-state-business-primary': '#ea580c',
43
- '--dsw-alias-state-business-secondary': '#fed7aa',
44
- '--dsw-alias-interactive-bg-hover': 'rgba(234,88,12,0.08)',
45
- },
46
- },
47
- {
48
- id: 'monochrome',
49
- name: 'Monochrome',
50
- description: 'Grayscale only — no color accent',
51
- tokens: {
52
- '--accent': '#404040',
53
- '--accent-hover': '#262626',
54
- '--dsw-alias-state-business-primary': '#404040',
55
- '--dsw-alias-state-business-secondary': '#e5e5e5',
56
- '--dsw-alias-interactive-bg-hover': 'rgba(64,64,64,0.08)',
57
- },
58
- },
59
- {
60
- id: 'nord',
61
- name: 'Nord',
62
- description: 'Frost blue inspired by the Nord palette',
63
- tokens: {
64
- '--accent': '#5e81ac',
65
- '--accent-hover': '#4c6f96',
66
- '--dsw-alias-state-business-primary': '#5e81ac',
67
- '--dsw-alias-state-business-secondary': '#e5ebf1',
68
- '--dsw-alias-interactive-bg-hover': 'rgba(94,129,172,0.08)',
69
- },
70
- },
71
- {
72
- id: 'dracula',
73
- name: 'Dracula',
74
- description: 'Purple accent with dark mode flair',
75
- tokens: {
76
- '--accent': '#bd93f9',
77
- '--accent-hover': '#a678e8',
78
- '--dsw-alias-state-business-primary': '#bd93f9',
79
- '--dsw-alias-state-business-secondary': '#3a3a5c',
80
- '--dsw-alias-interactive-bg-hover': 'rgba(189,147,249,0.12)',
81
- },
82
- },
22
+ preset('ocean', 'Ocean', 'Deep blue, close to dsh default', '#4d6bfe'),
23
+ preset('forest', 'Forest', 'Calm green', '#16a34a', '#22c55e'),
24
+ preset('sunset', 'Sunset', 'Warm orange', '#ea580c', '#fb923c'),
25
+ preset('monochrome', 'Monochrome', 'No color accent at all', '#5c5c5c', '#9e9e9e'),
26
+ preset('nord', 'Nord', 'Frost blue', '#5e81ac', '#88c0d0'),
27
+ preset('dracula', 'Dracula', 'Purple, brighter on dark', '#7c5cd6', '#bd93f9'),
28
+ preset('gruvbox', 'Gruvbox', 'Retro warm earth tones', '#b57614', '#fe8019'),
29
+ preset('solarized', 'Solarized', 'Schoonover precision palette', '#268bd2', '#5fa8d3'),
30
+ preset('tokyo-night', 'Tokyo Night', 'City lights blue', '#3d59a1', '#7aa2f7'),
31
+ preset('catppuccin', 'Catppuccin', 'Soft pastel', '#7287fd', '#cba6f7'),
32
+ preset('rose', 'Rosé', 'Muted pink', '#b3276b', '#f472b6'),
33
+ preset('ember', 'Ember', 'Deep red', '#b91c1c', '#ef4444'),
83
34
  ];
84
- /** Density → CSS custom property overrides */
85
- export const DENSITY_TOKENS = {
86
- compact: {
87
- '--dsh-content-font-size': '13px',
88
- '--dsh-spacing-unit': '4px',
89
- },
90
- comfortable: {
91
- '--dsh-content-font-size': '14px',
92
- '--dsh-spacing-unit': '6px',
93
- },
94
- spacious: {
95
- '--dsh-content-font-size': '15px',
96
- '--dsh-spacing-unit': '8px',
97
- },
98
- };
99
- /** Radius → CSS custom property overrides */
100
- export const RADIUS_TOKENS = {
101
- sharp: {
102
- '--dsh-radius-small': '2px',
103
- '--dsh-radius-medium': '4px',
104
- '--dsh-radius-large': '6px',
105
- },
106
- rounded: {
107
- '--dsh-radius-small': '6px',
108
- '--dsh-radius-medium': '10px',
109
- '--dsh-radius-large': '14px',
110
- },
111
- soft: {
112
- '--dsh-radius-small': '10px',
113
- '--dsh-radius-medium': '16px',
114
- '--dsh-radius-large': '22px',
115
- },
116
- };
117
- /** Font family → CSS custom property overrides */
118
- export const FONT_TOKENS = {
119
- system: { '--dsh-font-family': "system-ui, -apple-system, sans-serif" },
120
- mono: { '--dsh-font-family': "'JetBrains Mono', 'Fira Code', monospace" },
121
- serif: { '--dsh-font-family': "'Georgia', 'Times New Roman', serif" },
122
- };
123
35
  /** Look up a preset by id. */
124
36
  export function getPreset(id) {
125
37
  return PRESETS.find((p) => p.id === id);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The verified dsh design-token surface this plugin is allowed to write.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * An earlier version of this plugin invented its own variable names
7
+ * (`--accent`, `--border`, `--dsh-radius-medium`, `--dsh-transition-fast`).
8
+ * None of them exist in dsh, so nothing the panel did had any effect on the
9
+ * real UI — while the panel's own preview, which read the same invented
10
+ * fallbacks, changed color and made it look like it worked.
11
+ *
12
+ * Every name below was read out of the shipped dsh stylesheets
13
+ * (`@deepseek-ai/dsh-client-ui-theme/lib/client.js`), which define the palette
14
+ * on `body` and `body[data-ds-dark-theme]`. Anything not on this list must not
15
+ * be written by this plugin, and `tests/tokens.test.mjs` enforces that.
16
+ *
17
+ * Two consequences that are load-bearing:
18
+ *
19
+ * 1. The palette lives on `body`, not `:root`/`html`. A custom property set on
20
+ * `documentElement` is inherited INTO body, but body's own declaration wins
21
+ * — so this plugin must write to `document.body`, which `applyTheme` does.
22
+ * 2. dsh hardcodes `border-radius` per component (including many `50%` circles
23
+ * and `corner-shape: round`), and ships no motion tokens at all. Radius
24
+ * therefore has no faithful token target and is deliberately not offered;
25
+ * animation-reduction is done with a stylesheet instead (see `motionCss`).
26
+ *
27
+ * @module tokens
28
+ */
29
+ /**
30
+ * Token names confirmed present in dsh, kept here as data so the guard test can
31
+ * check every map this plugin builds against it.
32
+ */
33
+ export declare const VERIFIED_TOKENS: readonly string[];
34
+ /** True when `name` is a token this plugin has verified against dsh. */
35
+ export declare function isVerifiedToken(name: string): boolean;
36
+ /** Turn `#rrggbb` into an `rgba()` tint at the given alpha. */
37
+ export declare function tint(hex: string, alpha: number): string;
38
+ /**
39
+ * The accent bundle: every token that should follow one accent color.
40
+ *
41
+ * Applying one color to all of these keeps the primary button, links, active
42
+ * nav item, focus accents and hover tint in step. Deriving the hover tint from
43
+ * the accent rather than using a fixed value is what makes a custom color look
44
+ * deliberate instead of half-applied.
45
+ */
46
+ export declare function accentTokens(hex: string): Record<string, string>;
47
+ /**
48
+ * Density → content font size.
49
+ *
50
+ * `--dsh-content-font-size` is the verified knob: dsh's own theme bootstrap
51
+ * writes it on `body`, and 22 shipped stylesheets read it, so changing it moves
52
+ * the whole content column.
53
+ */
54
+ export declare const DENSITY_TOKENS: Record<string, Record<string, string>>;
55
+ /** Font family → the two verified family tokens. */
56
+ export declare const FONT_TOKENS: Record<string, Record<string, string>>;
57
+ /**
58
+ * CSS that suppresses motion, injected as a stylesheet rather than set as
59
+ * tokens (dsh ships no motion tokens).
60
+ *
61
+ * Durations are `0.001ms` rather than `0s` on purpose: a zero duration can stop
62
+ * `transitionend` / `animationend` from firing, and components that wait on
63
+ * those events would hang. A near-zero duration keeps every event firing while
64
+ * being visually instant — the same approach the `prefers-reduced-motion`
65
+ * convention uses.
66
+ */
67
+ export declare const MOTION_OFF_CSS: string;
package/lib/tokens.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The verified dsh design-token surface this plugin is allowed to write.
3
+ *
4
+ * WHY THIS FILE EXISTS
5
+ * --------------------
6
+ * An earlier version of this plugin invented its own variable names
7
+ * (`--accent`, `--border`, `--dsh-radius-medium`, `--dsh-transition-fast`).
8
+ * None of them exist in dsh, so nothing the panel did had any effect on the
9
+ * real UI — while the panel's own preview, which read the same invented
10
+ * fallbacks, changed color and made it look like it worked.
11
+ *
12
+ * Every name below was read out of the shipped dsh stylesheets
13
+ * (`@deepseek-ai/dsh-client-ui-theme/lib/client.js`), which define the palette
14
+ * on `body` and `body[data-ds-dark-theme]`. Anything not on this list must not
15
+ * be written by this plugin, and `tests/tokens.test.mjs` enforces that.
16
+ *
17
+ * Two consequences that are load-bearing:
18
+ *
19
+ * 1. The palette lives on `body`, not `:root`/`html`. A custom property set on
20
+ * `documentElement` is inherited INTO body, but body's own declaration wins
21
+ * — so this plugin must write to `document.body`, which `applyTheme` does.
22
+ * 2. dsh hardcodes `border-radius` per component (including many `50%` circles
23
+ * and `corner-shape: round`), and ships no motion tokens at all. Radius
24
+ * therefore has no faithful token target and is deliberately not offered;
25
+ * animation-reduction is done with a stylesheet instead (see `motionCss`).
26
+ *
27
+ * @module tokens
28
+ */
29
+ /**
30
+ * Token names confirmed present in dsh, kept here as data so the guard test can
31
+ * check every map this plugin builds against it.
32
+ */
33
+ export const VERIFIED_TOKENS = [
34
+ // Accent / business
35
+ '--dsw-alias-state-business-primary',
36
+ '--dsw-alias-state-business-tertiary',
37
+ '--dsw-alias-brand-primary',
38
+ '--dsw-alias-brand-text',
39
+ '--dsw-alias-link',
40
+ '--dsw-alias-interactive-bg-hover',
41
+ '--dsw-alias-interactive-bg-hover-accent',
42
+ '--dsw-alias-button-primary-fill',
43
+ '--dsw-alias-button-primary-hover',
44
+ // Typography
45
+ '--dsw-font-family',
46
+ '--dsw-font-mono',
47
+ '--dsw-font-xs-13',
48
+ '--dsw-font-xs-strong-13',
49
+ '--dsw-font-xxs-12',
50
+ '--dsw-font-xxxs-11',
51
+ '--dsw-font-s-14',
52
+ '--dsw-font-l-20',
53
+ '--dsh-content-font-size',
54
+ '--dsh-content-font-delta',
55
+ ];
56
+ /** Fast membership test used by the presets and the guard test. */
57
+ const VERIFIED = new Set(VERIFIED_TOKENS);
58
+ /** True when `name` is a token this plugin has verified against dsh. */
59
+ export function isVerifiedToken(name) {
60
+ return VERIFIED.has(name);
61
+ }
62
+ /** Turn `#rrggbb` into an `rgba()` tint at the given alpha. */
63
+ export function tint(hex, alpha) {
64
+ const value = hex.trim().replace(/^#/, '');
65
+ let r;
66
+ let g;
67
+ let b;
68
+ if (value.length === 3) {
69
+ r = parseInt(value[0] + value[0], 16);
70
+ g = parseInt(value[1] + value[1], 16);
71
+ b = parseInt(value[2] + value[2], 16);
72
+ }
73
+ else if (value.length === 6) {
74
+ r = parseInt(value.slice(0, 2), 16);
75
+ g = parseInt(value.slice(2, 4), 16);
76
+ b = parseInt(value.slice(4, 6), 16);
77
+ }
78
+ else {
79
+ return hex;
80
+ }
81
+ if ([r, g, b].some((n) => Number.isNaN(n)))
82
+ return hex;
83
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
84
+ }
85
+ /**
86
+ * The accent bundle: every token that should follow one accent color.
87
+ *
88
+ * Applying one color to all of these keeps the primary button, links, active
89
+ * nav item, focus accents and hover tint in step. Deriving the hover tint from
90
+ * the accent rather than using a fixed value is what makes a custom color look
91
+ * deliberate instead of half-applied.
92
+ */
93
+ export function accentTokens(hex) {
94
+ return {
95
+ '--dsw-alias-state-business-primary': hex,
96
+ '--dsw-alias-state-business-tertiary': tint(hex, 0.12),
97
+ '--dsw-alias-brand-primary': hex,
98
+ '--dsw-alias-brand-text': hex,
99
+ '--dsw-alias-link': hex,
100
+ '--dsw-alias-interactive-bg-hover-accent': tint(hex, 0.10),
101
+ '--dsw-alias-button-primary-fill': hex,
102
+ '--dsw-alias-button-primary-hover': hex,
103
+ };
104
+ }
105
+ /**
106
+ * Density → content font size.
107
+ *
108
+ * `--dsh-content-font-size` is the verified knob: dsh's own theme bootstrap
109
+ * writes it on `body`, and 22 shipped stylesheets read it, so changing it moves
110
+ * the whole content column.
111
+ */
112
+ export const DENSITY_TOKENS = {
113
+ compact: { '--dsh-content-font-size': '13px' },
114
+ comfortable: { '--dsh-content-font-size': '14px' },
115
+ spacious: { '--dsh-content-font-size': '15px' },
116
+ };
117
+ /** Font family → the two verified family tokens. */
118
+ export const FONT_TOKENS = {
119
+ system: {
120
+ '--dsw-font-family': "system-ui, -apple-system, 'Segoe UI', sans-serif",
121
+ '--dsw-font-mono': "'Cascadia Code', 'JetBrains Mono', Consolas, monospace",
122
+ },
123
+ mono: {
124
+ '--dsw-font-family': "'Cascadia Code', 'JetBrains Mono', Consolas, monospace",
125
+ '--dsw-font-mono': "'Cascadia Code', 'JetBrains Mono', Consolas, monospace",
126
+ },
127
+ serif: {
128
+ '--dsw-font-family': "Georgia, 'Times New Roman', serif",
129
+ '--dsw-font-mono': "'Cascadia Code', Consolas, monospace",
130
+ },
131
+ };
132
+ /**
133
+ * CSS that suppresses motion, injected as a stylesheet rather than set as
134
+ * tokens (dsh ships no motion tokens).
135
+ *
136
+ * Durations are `0.001ms` rather than `0s` on purpose: a zero duration can stop
137
+ * `transitionend` / `animationend` from firing, and components that wait on
138
+ * those events would hang. A near-zero duration keeps every event firing while
139
+ * being visually instant — the same approach the `prefers-reduced-motion`
140
+ * convention uses.
141
+ */
142
+ export const MOTION_OFF_CSS = `
143
+ *, *::before, *::after {
144
+ transition-duration: 0.001ms !important;
145
+ transition-delay: 0ms !important;
146
+ animation-duration: 0.001ms !important;
147
+ animation-delay: 0ms !important;
148
+ animation-iteration-count: 1 !important;
149
+ scroll-behavior: auto !important;
150
+ }
151
+ `.trim();
package/lib/types.d.ts CHANGED
@@ -8,25 +8,29 @@ export interface ThemePreset {
8
8
  id: string;
9
9
  name: string;
10
10
  description: string;
11
- /** CSS custom property value, applied to document.documentElement.style */
11
+ /** Accent tokens for light mode. */
12
12
  tokens: Record<string, string>;
13
+ /** Accent tokens for dark mode, applied when dsh is dark. */
14
+ darkTokens?: Record<string, string>;
13
15
  }
14
16
  /** User-selectable density preference. */
15
17
  export type Density = 'compact' | 'comfortable' | 'spacious';
16
- /** User-selectable border-radius preference. */
17
- export type Radius = 'sharp' | 'rounded' | 'soft';
18
18
  /** User-selectable font family preference. */
19
19
  export type FontFamily = 'system' | 'mono' | 'serif';
20
20
  /** Complete theme preferences persisted in localStorage. */
21
21
  export interface ThemePreferences {
22
22
  preset: string | null;
23
23
  accentColor: string | null;
24
+ /** Separate accent color for dark mode; null = use accentColor */
25
+ darkAccentColor: string | null;
24
26
  density: Density;
25
- radius: Radius;
26
27
  fontFamily: FontFamily;
28
+ animations: boolean;
27
29
  customCss: string;
28
30
  }
29
31
  /** Default preferences — nothing overridden, dsh's built-in theme wins. */
30
32
  export declare const DEFAULT_PREFERENCES: ThemePreferences;
31
33
  /** localStorage key for persisting preferences. */
32
34
  export declare const STORAGE_KEY = "dsh-theme-studio";
35
+ /** Element carrying the plugin's injected stylesheet id. */
36
+ export declare const STYLE_ELEMENT_ID = "dsh-theme-studio-overrides";
package/lib/types.js CHANGED
@@ -7,10 +7,13 @@
7
7
  export const DEFAULT_PREFERENCES = {
8
8
  preset: null,
9
9
  accentColor: null,
10
+ darkAccentColor: null,
10
11
  density: 'comfortable',
11
- radius: 'rounded',
12
12
  fontFamily: 'system',
13
+ animations: true,
13
14
  customCss: '',
14
15
  };
15
16
  /** localStorage key for persisting preferences. */
16
17
  export const STORAGE_KEY = 'dsh-theme-studio';
18
+ /** Element carrying the plugin's injected stylesheet id. */
19
+ export const STYLE_ELEMENT_ID = 'dsh-theme-studio-overrides';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-theme-studio",
3
- "version": "0.1.0",
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.",
3
+ "version": "0.3.0",
4
+ "description": "Customize the dsh UI theme: 12 accent presets, dark-mode aware accents with a contrast guard, density, font family, animation toggle, and raw design-token overrides.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",