Repository navigation
Releases: ruegg114514/dsh-colorlab
Release list
v1.1.9 — fix boxes-inside-boxes on cards
Fixed
Cards grew "boxes inside boxes". The deliverable-file and plan cards showed outlined, shadowed strips across their title row, description row — and even the grid container around the cards.
The card glass rule matched [class*="flL80G_file"] — a substring match. CSS Modules give every inner element of the same component a shared prefix, so the haze, border and shadow landed on each of those rows too:
| Token | What it is | Old *= |
New ~= |
|---|---|---|---|
flL80G_file |
card body | hit | hit |
flL80G_fileBody |
inner row | wrongly hit | — |
flL80G_fileName |
file name | wrongly hit | — |
flL80G_fileIcon |
icon | wrongly hit | — |
KuQXFq_cardTitle |
plan title row | wrongly hit | — |
KuQXFq_cardOpen |
open button | wrongly hit | — |
KuQXFq_cards |
the grid container! | wrongly hit | — |
The fix
*= → ~= (whole-word class match). CSS Modules give each element exactly one token, so flL80G_fileBody is no longer confused with flL80G_file.
bubble deliberately keeps *=: message bubbles carry per-package hashes in their token (cJsG2q_bubble, v1fjQa_bubble) that must not be hard-coded — and no bubbleXxx inner elements exist in the host, so there is no stacking risk.
Pattern worth naming
This is the third instance of the same mistake (the popover haze selector had the identical problem in v1.1.7): substring matching is unsafe against CSS Modules naming because inner elements share the owner's prefix. Wherever the exact token is known, use ~=.
Tests
13 assertions: each of the six prefixes verified to use ~= and not *=; bubble verified to keep *=. Mutation (reverting to *=) turns all 13 red.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.9
v1.1.8 — background brightness for wallpaper mode
Added
Background brightness now applies to wallpaper mode too. It was gradient-only before.
The two sources need different implementations but share one meaning:
| Source | Implementation | 50 = as-is | Extremes |
|---|---|---|---|
| Gradient | mixTowards() shifts the generated base colour |
middle | 0 pure black / 100 pure white (deliberate; the ambience layer fades with it) |
| Wallpaper | CSS brightness(), on the same filter chain as blur and saturate |
1.0 | 0 full black / 100 doubles |
| Setting | brightness() |
|---|---|
| 0 | 0.000 |
| 20 | 0.400 |
| 50 | 1.000 |
| 80 | 1.600 |
| 100 | 2.000 |
Two deliberate choices
- Wallpaper stops at doubling, not pure white. Pure white would erase the photo's detail. Gradient reaching pure white is intentional, so the two differ at the extreme.
- Contour mode is unaffected. Its line colour comes straight from the accent colour, so brightness would only distort it — the same reasoning that already excludes it from
saturate.
The settings row is now shared by both sources instead of being gated on gradient alone, and the hint explains how each behaves.
Tests
Four new assertions on the filter chain and conversion formula, plus one on the settings guard covering both sources.
Mutation-tested:
- making brightness a no-op → 3 assertions red
- restricting the settings row back to gradient → 2 red
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.8
v1.1.7 — nested haze down to an eighth
Changed
Nested haze reduced from a third to an eighth. v1.1.6 gave code blocks haze / 3, which combined to 10.5% — visibly denser than the 8% single-layer baseline. It is now haze / 8, combining to 8.9%, which matches the surrounding surfaces.
| Divisor | Child | Combined | |
|---|---|---|---|
| 1 | 8.00% | 15.36% | two full layers — the original bug |
| 3 | 2.67% | 10.45% | v1.1.6 — still too dense |
| 8 | 1.00% | 8.92% | matches the baseline |
What I got wrong twice
Both earlier attempts had an unclear target:
- v1.1.5 optimised for "don't stack too thick" and landed on zero — which left that surface with no haze at all, because a code block paints its own opaque fill and hides the parent's contribution.
- v1.1.6 optimised for "the haze should be visible" and overshot to 10.5%.
The right target is combined density close to the single-layer baseline. The child layer only needs enough to avoid reading as absent — not enough to be independently noticeable.
Tests
The bound is now two-sided: 0.005 ≤ haze < 0.02.
- below the lower bound → effectively invisible (the v1.1.5 mistake)
- above the upper bound → combined exceeds 9.8%, reads denser than everything else (the v1.1.6 mistake)
All three wrong versions are caught by mutation testing: zero, a third, and full.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.7
v1.1.6 — nested haze is a third, not zero
Fixed
Code blocks lost their frosted haze entirely. v1.1.5 removed the haze from nested surfaces to stop it stacking — but removing it is not the same as reducing it.
The haze is a white overlay, so two layers combine as 1-(1-a)(1-b):
| Parent | Child | Combined | Look |
|---|---|---|---|
| 8% | 8% | 15.4% | too heavy (the original bug) |
| 8% | 0% | 8% | child does not whiten at all — haze is gone |
| 8% | 2.7% | 10.2% | matches the surrounding surfaces |
Why zero does not work
A code block paints its own fairly opaque surface, which covers the parent's haze. Without a layer of its own, that surface genuinely has no haze — the parent's contribution is hidden underneath it.
The value: a third
Nested surfaces now get haze / 3. A third rather than half because a code block's own fill is one tier more opaque than a card's, so the haze carries less visual weight on it:
- full value → reads too white
- zero → reads as absent
- a third → matches the surrounding surfaces
| Constant | Haze | Used by |
|---|---|---|
glassSurface |
haze |
outermost — columns, cards, dialogs, popovers, composer |
glassSurfaceFlat |
haze / 3 |
nested — code blocks, pre, inline code |
Note
The name glassSurfaceFlat is now slightly misleading, since the surface it produces is no longer flat. It was kept to avoid touching every call site; the comment above it states what it actually produces.
Tests
The assertion changed from "code block has no haze" to "code block has haze, but clearly less than its parent":
codeHaze !== null— must not benone0 < codeHaze < 0.05— present, and faint
Mutation-tested in both directions:
- child set to 0 → 2 assertions red (reproduces the v1.1.5 mistake)
- child set to full value → 2 assertions red (reproduces the original bug)
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.6
v1.1.5 — stop stacking the frosted haze
Fixed
Code blocks looked noticeably whiter and duller than other glass surfaces.
The frosted haze is a translucent white gradient defined on the shared glassSurface constant, which every glass surface used — including nested ones. A code block sits inside a card or column, so the haze stacked:
card / column ← glassSurface (haze 8%)
└ code block ← glassSurface (haze 8%) ← stacked again
Combined density is 1 - (1-0.08)² ≈ 15.4%, nearly double the configured value. That is why code blocks read as whiter and more muted than everything around them.
The fix: split by nesting level
| Constant | Haze | Used by |
|---|---|---|
glassSurface |
yes | outermost surfaces — columns, cards, dialogs, popovers, composer |
glassSurfaceFlat |
no | nested surfaces — code blocks, pre, inline code |
Both keep the box-shadow and border. Those draw on the element's own edges and do not accumulate brightness, so stacking them is harmless.
Lowering the global haze value would have been the wrong fix — it exists to give the outermost layer its material feel, and thinning it everywhere would flatten single-layer surfaces.
How it was found
It is not visible in the source: the constant looks correct in isolation. Listing which selectors actually end up carrying the haze gradient in the generated stylesheet made the duplication obvious — the code-block rule and the card rule both had it.
Tests
- the code-block rule must not carry the white gradient, but must keep its shadow and border
- the outermost card rule must still carry the haze
Mutation-tested: switching the code block back to glassSurface turns 2 assertions red.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.5
v1.1.4 — pass detected popover classes through
Fixed
The turn-preview popover had no blur again. A one-line wiring bug introduced in v1.1.3.
buildGlassSheet gained a sixth parameter for the detected popover class names, but the real call site never passed it. The edit had been applied to an identically-worded string in the module's export block rather than to the function that actually builds the stylesheet.
The rule-generation logic was correct and its unit tests passed — the rule simply never received any class names at runtime, so it was never emitted. That is why the previous release's tests were green while the feature did nothing.
All call sites now pass the parameter, and a test asserts that every runtime call site includes it.
Also fixed
- Detection now also runs inside
syncGlassSheet, not onlysyncCardSheet.syncCardSheetruns early in startup, when the host may not have injected all of its stylesheets yet; detection would return nothing and there was no second attempt.syncGlassSheetnow retries — bounded at 60 attempts, after which the result is cached — so a late-arriving stylesheet still gets picked up. - The status object now exposes
popovers— the number of popover classes detected.0means detection has not succeeded yet, which makes this class of failure visible instead of silent.
Four releases, four different failures
| Version | What broke |
|---|---|
| v1.1.1 | selector too narrow — missed xpvNua_preview |
| v1.1.2 | selector too broad — hit JSON-tree colouring, diff went solid |
| v1.1.3 | detection correct, but the call site never passed the result |
| v1.1.4 | wiring fixed; detection also retries and is observable |
The recurring pattern: the unit-tested piece kept passing while the integration point silently dropped the data. The test added here asserts the call-site wiring directly rather than only the builder's behaviour.
Tests
Mutation-tested: removing the argument from the call site turns 2 assertions red.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.4
v1.1.3 — detect popovers at runtime
Fixed
Diff regions turned solid, and the popover text gained dark boxes around it. A regression from v1.1.2.
v1.1.2 widened the popover selector to [class*="preview"] so it would cover both class naming formats. But the host has eight class names containing "preview", and only two of them are popovers:
| Class | What it is | Has background? |
|---|---|---|
_preview_38jqx_52 |
hover file preview | yes — popover |
xpvNua_preview |
turn preview | yes — popover |
_preview_1alm6_221 |
JSON-tree syntax colouring | no — only color |
_previewProperty_1alm6_225 |
same | no |
_previewEllipsis_1alm6_229 |
same | no |
xpvNua_previewPrompt / previewResponse |
text inside the popover | no |
_floatingCell_6nhg2_156 |
dockkit 0×0 positioning cell | no |
The JSON-tree ones live inside diff regions, so painting them as surfaces put a dark box under every JSON value.
The fix: detect at runtime instead of matching names
The substring selector is gone. detectPopoverClasses() scans the stylesheets and accepts a class only if both conditions hold:
- the name matches a popover naming pattern (either
_preview_<hash>_<n>or<hash>_preview) - the rule declares a
background
A popover has to paint its own surface; an inner text block does not. That criterion follows the host implementation rather than a class-name list, so a new previewXxx class in a future host build cannot silently break it again.
Generated rules now target exact class names:
html[data-dsh-glass] ._preview_38jqx_52,
html[data-dsh-glass] .xpvNua_preview,
html[data-dsh-glass] ._float_6nhg2_156 { … }The contain override uses the same detected names. Also fixed the format-A regex — it was missing the leading underscore, so ._preview_38jqx_52 never matched and only format-B names were found.
Three iterations
| Version | Selector | Outcome |
|---|---|---|
| v1.1.1 | [class*="_preview_"] |
missed xpvNua_preview (no leading underscore) |
| v1.1.2 | [class*="preview"] |
matched JSON-tree colouring, diff went solid |
| v1.1.3 | runtime detection + exact names | both cases covered |
Substring matching on class names was the wrong tool: too narrow and it misses, too broad and it hits unrelated elements. The fix was to write the definition of "is a popover" as an executable condition instead of enumerating names.
Tests
Detection is now tested against a real host CSS fragment: three popovers must be found (covering both naming formats), seven non-popovers must be excluded.
Mutation-tested:
- removing the
backgroundcriterion → 2 assertions red (reproduces the diff bug) - dropping the leading underscore from the regex → 3 assertions red
- deleting the
containoverride → 3 assertions red
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.3
v1.1.2 — blur the turn-preview popover
Fixed
The turn-preview popover still was not blurred. v1.1.1 injected the right rule — verified in the page's stylesheet — but it had no effect on this popover, for two independent reasons.
1. Class name format
The rule matched [class*="_preview_"], which only covers CSS Modules hashed names like _preview_38jqx_52. This popover's class is xpvNua_preview — module name, then element name, no leading underscore — so the selector never matched it.
| Format | Example | Old selector |
|---|---|---|
| CSS Modules hash | _preview_38jqx_52 |
matches |
| module_element | xpvNua_preview |
misses |
Now uses [class*="preview"] to cover both, with :not([class*="dmt-"]) to exclude the plugin's own settings-page elements.
2. contain: layout blocked the backdrop sampling
Its parent nav carries contain: layout, which establishes a new containing block and confines backdrop-filter sampling to that box — and the box is 28px wide. The blur had nothing to sample.
Verified side by side (preview-contain-test.html):
| Group | Structure | Blurred? |
|---|---|---|
| A | parent has contain:layout |
no — flat translucent |
| B | parent without contain |
yes |
| C | position:fixed escaped |
yes |
A new rule clears contain on any ancestor that directly contains a popover, using :has(). The hashed class name cannot be hard-coded since it changes on every build. :has() is supported well before this Electron version; if it were unsupported the rule is ignored and the popover degrades to "translucent but not frosted", exactly as before.
Note on the previous release
v1.1.1 was verified by checking that the CSS rule reached the page — it did. That turned out not to be sufficient: contain makes backdrop-filter fail silently while background-color still applies, so the popover looked translucent but unfrosted and the injection check passed.
The first diagnostic script also missed it, because it filtered for position:fixed and this popover is position:absolute.
Tests
Six new assertions covering both halves of the fix. Mutation-tested:
- reverting to the
_preview_selector → 2 assertions red - deleting the
containoverride → 3 assertions red
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.2
v1.1.1 — glass the transient popovers
Fixed
Hover popovers were not glassed at all.
The file-change diff popover and the conversation-history popover — both position:fixed elements with no role attribute — fell outside the [role="dialog"] / [role="menu"] rules, so they rendered as flat opaque boxes over live text. The only code that touched them lived behind the Solid floats switch (off by default), and it pinned them fully opaque rather than glassing them.
They now follow the glass in two layers:
| State | Behaviour |
|---|---|
| Default | Same delta-alpha compositing and blur as dialogs and menus, with a slightly higher +0.03 readability compensation — these are transient and you read them immediately |
| Solid floats on | Still pins them opaque; its rule wins by source order |
The HoverCard bubble had the same problem in a different shape: its background is the hard-coded --dsw-hovercard-bg: #2C2C2E, which no token override can reach. It is now overridden at the variable itself with a translucent glass value, resolved through runtime class detection so the hashed class name keeps working across builds.
Why it took a separate fix
The three affected popovers are all position:fixed divs with no role, so the whole role-based rule set silently skipped them. Nothing errored — they just stayed opaque.
One constraint worth noting: _card_ is a generic local name the host also uses for message cards and plan cards, so the HoverCard fix could not use a substring selector without pinning ordinary content cards too. It targets the detected hash class directly instead.
Tests
Rewrote 8 assertions that encoded the old "write no rule by default" behaviour, and added 6 new ones:
- default tier must be
rgba(and carrybackdrop-filter:blur( - solid tier must be 6-digit hex with no
rgba( - solid tier must come after the glass tier (same specificity, order decides)
Mutation-tested: deleting the popover rule → 6 assertions red; deleting the HoverCard rule → 6 red; turning the glass tier opaque → 2 red.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.1
v1.1.0 — background ground rework, settings rebuilt
Background ground is now a compositor of its own
Three modes, every colour picked by you:
- Glow — 2–6 light sources, each with its own colour, strength and X/Y position, plus overall density (0–100%) and spread (40–140%).
- Bands — 2–6 colour strips, each its own colour, a cutting angle anywhere from 0° to 360°, and an edge-softness slider.
- Blend — bands underneath, glow on top.
Glow density ceiling raised from 70% to 100%. Gradient mode no longer applies the wallpaper veil, blur, saturation or upscale — those were designed for photo wallpapers and were eating roughly a third of the glow.
Settings page rebuilt: 11 groups → 5
The Glass group now keeps only two rows permanently visible (master switch and background source). Everything else folds into Common settings and Current background only — the latter renders just the parameters for the source you picked, and returns null (not display:none) for the other two.
All colour controls — syntax palette, prose roles, process rows, accent, interface surfaces, 9 prose slots, 18-slot palette — now live in a single Colour group.
Removed
Settings that existed but did nothing meaningful: apply scope, background variant, accent presets, prose brightness, and all three preview blocks. Plus dead code: glassGlow, glassAmbientAccent and its palette table, OPTIONS.glassBandDir, previewVars.
Fixed
- Wallpaper local-path input and fit dropdown had gone missing — a regression from earlier refactors. The field and its read path were intact, so nothing errored; you just could not fill it in.
- Light-source sliders overflowed the panel —
.dmt-slideris a fixed 132px and.dmt-sliderRowisflex:none. - Band-mode glow still looked like a hard cut — the glow branch was accidentally including the band layer.
- Gradient glow looked much weaker than the preview — the CSS was byte-identical; the difference was the veil and filter layers above it.
Tests
~50 new assertions, mutation-tested. Every new assertion was verified to go red when the behaviour it guards is broken. Three false-green assertions were caught and rewritten this way.
Full changelog: CHANGELOG.md
Install: dsh plugin add ruegg114514/dsh-colorlab
Pin this version: dsh plugin add ruegg114514/dsh-colorlab#v1.1.0

