Bundle
dsh-theme-studio
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.
- Source
- hj01857655
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-theme-studio
[](https://www.npmjs.com/package/dsh-theme-studio)
[](https://www.npmjs.com/package/dsh-theme-studio)
[](https://github.com/hj01857655/dsh-theme-studio/actions/workflows/ci.yml)
[](LICENSE)
[](package.json)
[](https://github.com/hj01857655/dsh-theme-studio/stargazers)
[](https://github.com/topics/dsh-plugin)
A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin that customizes the UI theme: accent presets, a custom accent color, content density, font family, an animation toggle, and raw design-token overrides.
## What it can and cannot change
This plugin only writes design tokens that **actually exist in dsh**, verified by reading the shipped stylesheets (`@deepseek-ai/dsh-client-ui-theme/lib/client.js`). Every token it writes is listed in `src/tokens.ts` and enforced by `tests/tokens.test.mjs`.
Changes go through **`ctx.theme.overrideTokens()`** — dsh's own theme service — not direct DOM writes. See [Mechanism](#mechanism) for what that buys.
That constraint has two visible consequences:
- **No border-radius control.** dsh hardcodes `border-radius` per component — including `50%` circles and `corner-shape: round` — and ships no radius token. There is no faithful way to offer this, so it is not offered.
- **The animation toggle is a stylesheet, not a token.** dsh ships no motion tokens, so disabling animations injects a rule compressing transition and animation durations to `0.001ms` rather than `0s` — a zero duration can stop `transitionend` / `animationend` from firing and hang components that wait on them. The sheet is owned by this plugin's effect scope, so unloading removes it.
## Features
- **12 presets**: Ocean, Forest, Sunset, Monochrome, Nord, Dracula, Gruvbox, Solarized, Tokyo Night, Catppuccin, Rosé, Ember
- **Dark-aware presets** — a preset whose dark accent differs from its light one adapts automatically when dsh enters dark mode
- **Custom accent color**, plus a separate dark-mode accent
- **Contrast guard** — a dark-mode accent below a WCAG luminance floor is lightened in steps, and the panel reports that it was adjusted
- **Density**: Compact / Comfortable / Spacious — writes dsh's own content font
size through `ctx.theme.setFontSize()`, so it agrees with the official
Appearance control instead of shadowing it
- **Font family**: System / Monospace / Serif
- **Animation toggle**
- **Custom CSS** — override any `--dsw-*` or `--dsh-*` token
- **Import / export** themes as validated JSON. A theme exported by an older
version carries a `density` field; it is ignored on import, since the font size
is host state now
- **Live preview** rendering the real tokens, showing the accent value currently in effect
## Install
```bash
dsh plugin add dsh-theme-studio
```
From source:
```bash
git clone https://github.com/hj01857655/dsh-theme-studio.git
cd dsh-theme-studio
npm install && npm run build
dsh plugin add link:.
```
## How it works
The plugin registers a `settings.section` slot (order 47). Preferences persist in `localStorage`.
### Mechanism
Every theme change goes through dsh's theme service:
```ts
export const inject = ['slots', 'locale', 'theme']
…
ctx.theme.overrideTokens('dsh-theme-studio', overrides)
```
`overrideTokens` stacks a partial token layer over the active theme. The service handles the rest:
- **Layers compose in order, last writer wins per token.** Two theme plugins can coexist without either silently clobbering the other's unrelated tokens.
- **Every value is a `{ light, dark }` pair.** A single value would go illegible on the other color scheme, so the API rejects one. `src/apply.ts` builds the pairs; `tests/apply.test.mjs` asserts the shape of every value it can emit.
- **The layer is disposable.** Unloading the plugin removes exactly its layer, restoring whatever the host had underneath — including a font size the user chose in the official Appearance row.
The layer is keyed by source (`dsh-theme-studio`), and re-publishing replaces it wholesale, so a shrinking override set can never leave a stale token behind.
An earlier version wrote custom properties straight onto `document.body` with `style.setProperty`. That happened to work — dsh's presenter only retracts variables it wrote itself — but it bypassed the stacking order, the paired values and the dispose story, and made every reset the plugin's own responsibility.
### Density drives the official font-size setting
`--dsh-content-font-size` is the axis the official Appearance row owns, and an
override layer sits *on top of* the theme snapshot. Overriding that token
therefore does not change the setting — it hides it, leaving the official stepper
displaying a number the UI no longer rendered.
So Density does not use an override at all. It calls
`ctx.theme.setFontSize()`, the same entry point the official control uses, and
reads back through `getTheme().fontSize`:
- The official stepper keeps showing (and setting) the same value.
- Change the size there and the matching preset highlights here; pick 15px or
17px and nothing is highlighted, because no preset claims those.
- `--dsh-content-font-size` never appears in this plugin's override layer. The
custom-CSS box can still set it, because that is you asking, not the plugin
deciding.
- Reset restores the size the host had before you first changed it here.
An earlier version treated the size as this plugin's own state, which produced a
control that silently disabled the official one on install (0.5.0) and then, once
the default was made inert, one that still shadowed it whenever it was used
(0.5.1).
### The preview is not a mock
It renders with the same variable names the application uses, so a wrong token looks wrong in the preview too, rather than being masked by a hardcoded fallback color.
### One validator for every way preferences get in
Preferences arrive three ways — `localStorage`, an imported JSON file, and a
hand-edited version of either. All three pass through `normalizePreferences()`,
which is the only place that validates: unknown enum values fall back to the
inert default, a `density: "comfortable"` written by 0.5.0 migrates to
`"default"`, malformed colors become `null`, and non-string/non-boolean fields
drop back to defaults. `parseTheme` intentionally keeps no whitelist of its own,
so the two entry points cannot drift apart.
## License
MIT
Install
dsh plugin --profile web add github:hj01857655/dsh-theme-studio#bd11f1d179d61f87e5fb991e83ae36fb23ae5c7f
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-theme-studio from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.