Bloom Morandi theme: four variants (mist, cinnabar, petal, ripple) built on OKLCH so light and dark stay perceptually even, switchable from the top bar, with all eight foreground/background pairs measured at WCAG AA or better.
Install
# from npm (prebuilt)
dsh plugin --profile web add @kubor/dsh-bloom-theme
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:webkubor/dsh-bloom-theme
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
What Bloom is
Bloom started as a Typora theme. Its point was never "swap the colors" — it is a complete Morandi design language: low-saturation ambient gradients, cool-toned hairlines, long soft shadows, restrained radii and spacing.
This plugin ports that language to DSH — including the half of it that is easiest to miss:
The Morandi character does not live in
--accent. It lives in--accent-rgb.
Why Bloom
| Feature | Notes |
|---|---|
| Two-track palette | One track guarantees contrast, the other carries the mood — never mixed |
| A real texture layer | Ambient gradients, cool hairlines, paper-like shadows, Markdown detailing — not just recolored variables |
| OKLCH throughout | Perceptually uniform, so light/dark switching doesn't jump in brightness |
| WCAG AA | All 8 accent/background pairs measured at ≥ 4.5:1 |
| Zero dependencies | Pure client-side injection, nothing added to your runtime |
| Respects native controls | The switcher mounts into the DSH top bar instead of floating over it |
Palettes
Four palettes, each with a light and a dark version.
Switch from the top bar; your choice persists in localStorage:
Install
dsh plugin --profile web add @kubor/dsh-bloom-theme
Then add the package to bundles in ~/.dsh/profiles/web/package.json:
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"@kubor/dsh-bloom-theme"
]
}
}
}
Restart DSH — the theme dropdown appears in the top bar.
The plugin ships its own cordis.patch.yml and declares it via dsh.bundle, so once listed
in bundles it inserts itself into the boot graph — no need to hand-edit cordis.patch.yml.
If you'd rather not touch bundles, inserting it in ~/.dsh/profiles/web/cordis.patch.yml works too:
- insert:
- id: bloom-theme
name: '@kubor/dsh-bloom-theme'
Install from source: dsh plugin --profile web add github:webkubor/dsh-bloom-theme
Design: the two-track palette
Every Bloom palette carries two colors. The original root-mist.css says it outright:
/* --- Morandi Mist (Blue) - Deepened for better contrast --- */
--accent: oklch(50% 0.08 240); /* readable track: deliberately darkened, to pass contrast */
--accent-rgb: 146, 168, 179; /* mood track: the actual Morandi color — greyed, desaturated */
The two must not be mixed:
- Readable track → text, button fills, borders. It is the darkened version; painting large areas with it looks loud and muddy.
- Mood track → only for wide ambient gradients and glows at
rgba(morandi, 0.05~0.2). All 14 gradients in the original use it, and never the readable track.
Port only --accent — the natural assumption — and petal turns from lotus pink #e8859b
into fluorescent magenta #e63f9f. The hue is right; the Morandi character is gone.
| Palette | Mood track | Readable track (light / dark) | Light contrast |
|---|---|---|---|
mist |
#92a8b3 |
oklch(50%) / oklch(72%) |
5.28:1 |
cinnabar |
#d74b4b |
oklch(55%) / oklch(72%) |
4.87:1 |
petal |
#e8859b |
oklch(58%) / oklch(75%) |
4.55:1 |
ripple |
#5fa8b2 |
oklch(51%) / oklch(75%) |
4.61:1 |
The light readable track was calibrated by solving backwards from WCAG AA — and darkening it
actually brings it closer to Morandi, which is exactly what the original author did for mist.
The texture layer
Port the palette alone and you get "the same UI in different colors". In the original,
root-*.css (palette) is 89 lines while base-light/dark.css (texture) is 2,968 — that gap
is the whole point.
| Technique | Implementation |
|---|---|
| Ambient gradients | Four stacked Morandi glows on body, background-attachment: fixed |
| Cool hairlines | Sidebar rule, card outlines, tab underline, inset top highlight |
| Paper feel | Three shadow tiers plus an inset 0 1px 0 highlight |
| Markdown | Gradient heading rules, fading hr, accent-barred blockquotes, outlined code blocks |
| Sidebar | Ambient wash, cool hover states, accent marker on the active item |
Architecture
lib/index.js node half (cordis plugin) — empty; this is a pure client-side theme
lib/client.js browser half, all logic lives here
├─ PALETTE 4 palettes × two tracks
├─ bloomTokens() → --bloom-* tokens (single source of truth for the texture layer)
├─ mistLight/Dark() → mist fully takes over the DSH alias + specific variable system
├─ variantBlock() → the other 3 override only accent and background; greys inherit mist
├─ COMPONENT_CSS → texture layer (written once, adapts across 4 palettes × light/dark)
└─ SWITCHER_CSS → top-bar dropdown switcher
Development
npm run dev # watch lib/, deploy to the web profile, auto-reload the browser on save
npm run deploy # deploy once
The skin is injected client-side and its CSS is generated at runtime by client.js, so there
is no "hot-reload the CSS only" path — the script must re-run, which means the page must
reload. npm run dev handles it (press a to toggle, or set DSH_BLOOM_NO_AUTORELOAD=1).
FAQ
Make sure the package is listed in dsh.profile.bundles in ~/.dsh/profiles/web/package.json,
then restart the DSH service (reloading the page is not enough):
launchctl kickstart -k gui/$(id -u)/ai.deepseek.dsh # macOS LaunchAgent
The boot graph is resolved at process start; a page reload won't re-read the profile.
The skin is injected client-side and its CSS is generated at runtime by client.js — there is
no "hot-reload the CSS only" path. The script has to re-run, which means the page must reload
(Cmd+R). npm run dev does it for you on save.
Two usual causes:
- The package was renamed but stale copies remain under
~/.dsh/profiles/web/. Clean upnode_modules/<old-scope>/,cordis.patch.ymlandnode_modules/.package-map.jsontogether, then restart the service. - The client factory returned a bare
{}. DSH expects a function or an object with anapplymethod. That error surfaces in the browser and is unrelated to the ESM export style oflib/index.js.
Yes. The switcher only sets body[data-bloom-variant] and persists it to localStorage.
Set that attribute yourself in CSS, or trim VARIANTS down to a single entry.
Light/dark follows DSH's own appearance setting (Settings → Appearance). Each of the four palettes ships a light and a dark version and adapts automatically — no separate toggle.
Known limitations
<think>tags flash briefly while streaming. Mid-stream the tag and the content share a single text node; only after the response completes and markdown re-renders do they split into standalone paragraphs the rule can catch. The end state is correct. A real fix belongs in the LLM provider adapter — parsing the thinking into a reasoning field so DSH's nativeReasoningRowrenders it — which is outside a theme's job.- Relies on CSS Modules semantic class names. DSH class names look like
wSkVaW_root(<hash>_<semantic>); the hash changes between DSH builds while the semantic part is stable, so this plugin matches on[class*="_semantic"]. If a DSH release breaks the match, the effect degrades to flat colors — nothing misaligns or breaks. --dsw-alias-toast-bg/tooltip-bgare not overridden per palette, so all four inherit mist's blue-grey hue. In dark mode the value sits close to the background; this has not been verified against a real toast.- Web profile only. The tui / headless profiles don't render in a browser, so this has no effect.
Lessons learned
Full write-ups in DEV_NOTES.md — each with symptom → root cause → fix → takeaway. The ones worth reading first:
- A client factory must return an object with an
applymethod. Returning a bare{}white-screens the whole of DSH. That error surfaces in the browser and has nothing to do with the ESM export style oflib/index.js. - Foreground tokens can't be used as backgrounds or shadows. In dark mode the foreground
is near-white, so a
color-mix"shadow" renders as a white haze, and givingmarkdown-inline-codea text color yields a 1.2:1 pale-on-white block. - DSH uses CSS Modules — class names look like
wSkVaW_root. Match with[class*="_semanticName"], always scope todiv(or you'll hit SVG elements), and always count the matches (a bare_cardhits 30+ message cards). - Only outline the layer that has a solid background. Outlining an inner transparent element produces a box-in-a-box.
License
Author
@webkubor · See also: Bloom for Typora
Links
More in this category
Small-tailqwq/dsh-deep-whale#maid-atelier★ 1396
Whale-girl skin series for the DSH Web UI (maid-atelier).
d-dev0101/open-sea-skin★ 177
Realtime WebGPU ocean skin with quick controls for waves, daylight, glass opacity, and an automatic day cycle.
kingOfSoySauce/dsh-liang-skin★ 102
Adaptive reasoning slider skin that maps each model's available reasoning efforts onto a 0–30 visual intensity scale, with synchronized portraits, background, and interface colors.
GGBond2424648901/deep-whale-day-night-theme★ 88
Day/night whale-girl skin: a crystal workshop by day and a moon-tide observatory by night, with paired scenes, chibi companions, ornaments, and lightweight bubble and star ambience.
elysia395/dsh-wallpaper-engine★ 78
Wallpaper Engine backgrounds for the DSH web GUI: renders local Video and Web wallpapers behind the chat with an iOS-style liquid glass effect and adjustable blur, dim, border and glass sliders.
RevolutionLA/dsh-dream-skin★ 58
One-command skin plugin: 8 original themes, translucent wallpaper with opacity/blur, per-user accent, and shareable theme-pack import/export, favorites and surprise-me — purely native on DSH's token system.