-
Notifications
You must be signed in to change notification settings - Fork 0
Theming
One copy of the palette, in
src/wc/machvive-chat-syncopation-services/theme.js. Every component interpolates
THEME_CSS at the top of its own stylesheet.
Custom properties inherit through shadow boundaries where ordinary styles do
not, so you restyle everything by setting tokens once. No ::part surgery, no
!important.
machvive-chat-syncopation {
--mcs-accent: #7b2d8e;
--mcs-accent-fg: #ffffff;
--mcs-user-bg: #f3e8f7;
--mcs-radius: 4px;
--mcs-font: 'Inter', system-ui, sans-serif;
--mcs-height: 40rem;
}Or globally:
:root {
--mcs-accent: #7b2d8e;
}| Token | Light | Dark | Used for |
|---|---|---|---|
--mcs-fg |
#1a1a1a |
#e8eaed |
Body text |
--mcs-muted |
#6e6e6e |
#9aa0a6 |
Timestamps, notes, system turns |
--mcs-bg |
#ffffff |
#1f2125 |
Component surface |
--mcs-surface |
#f6f7f9 |
#282b30 |
Headers, the CLI, inset areas |
--mcs-user-bg |
#e8f0fe |
#1e3a5f |
The user's bubbles |
--mcs-assistant-bg |
#f3f4f6 |
#2b2f36 |
Reply bubbles |
--mcs-border |
#e2e4e8 |
#3c4046 |
Rules and outlines |
--mcs-accent |
#1565c0 |
#5b9bf8 |
Send button, focus ring, CLI sigil |
--mcs-accent-fg |
#ffffff |
#0b1220 |
Text on the accent |
--mcs-danger |
#a4161a |
#ff9d97 |
Error turns, delete controls |
--mcs-radius |
12px |
— | Corner rounding |
--mcs-font |
system UI stack | — | Body font |
--mcs-mono |
system mono stack | — | CLI and inspector |
Layout tokens: --mcs-height (container, 32rem),
--mcs-inspector-height (14rem), --mcs-history-height (14rem).
Follows prefers-color-scheme with no configuration. Override per element:
<machvive-chat-syncopation theme="dark">
<machvive-chat-syncopation theme="light">An explicit choice wins in either direction — theme="dark" is dark on a light OS,
and theme="light" is light on a dark one.
:host { /* light tokens */ }
@media (prefers-color-scheme: dark) {
:host(:not([theme="light"])) { /* dark tokens */ }
}
:host([theme="dark"]) { /* dark tokens, again */ }
:host([theme="light"]) { /* light tokens, again */ }Three things are doing work here, and a test asserts the ordering:
- The
:not([theme="light"])guard inside the media query is what lets a page force light on a dark OS. -
:host([theme="dark"])comes after the media query. At equal specificity later wins, so declaring it earlier would make an explicit dark choice lose in a light OS — silently, and only for some users. -
color-scheme: light darkmust stay declared, or the browser paints light scrollbars, select popups and checkboxes onto a dark panel.
If you add a component or restyle one, these are not style preferences. Each one is a bug that reached production in this family of packages.
A second copy of the palette drifts from the first, and the drift is invisible until
someone switches themes. A test strips THEME_CSS from each component and fails on
any remaining --mcs-* definition.
Controls do not inherit color from :host. Without one, the UA supplies a
per-theme default — which is how every button in the sibling package rendered white
text on a white background, at 1:1 contrast, for everyone using dark mode. It
passed 145 tests. CONTROL_CSS exists for this and sets color and background on
button, input, textarea, select.
Analytics in the sibling package set a light color under a dark theme with no
background. Inside a light page its text rendered at 1.21:1 — light on light.
If a :host block sets color, it sets background too. A test enforces exactly
that pairing.
A token defined only in the light block inherits the light value under dark, which is rule 3 arriving by a different road. A test compares the two token sets.
jsdom has no layout engine and no computed style, so the test suite is structurally blind to contrast, overflow and stacking. Two real bugs shipped past a green suite in the sibling package for this reason.
Measure it, in a real browser, across all six combinations (OS light/dark ×
theme unset/light/dark). That audit is what caught --mv-faint: #888 sitting at
3.54:1 on white — below the WCAG AA threshold of 4.5:1, and already released.
A minimal harness:
import { chromium } from 'playwright';
// The cached Playwright Chromium is a version behind on some machines and fails
// to launch; use the installed Chrome.
const browser = await chromium.launch({ channel: 'chrome' });
for (const colorScheme of ['light', 'dark']) {
for (const theme of [null, 'light', 'dark']) {
const page = await browser.newPage({ colorScheme });
await page.goto('http://localhost:4173/');
if (theme) await page.evaluate((t) => {
document.querySelector('machvive-chat-syncopation').setAttribute('theme', t);
}, theme);
const ratio = await page.evaluate(() => {
const el = document.querySelector('machvive-chat-syncopation-canvas')
.shadowRoot.querySelector('.bubble');
const style = getComputedStyle(el);
return contrast(style.color, style.backgroundColor); // your WCAG helper
});
console.log(colorScheme, theme ?? 'auto', ratio.toFixed(2));
await page.close();
}
}prefers-reduced-motion: reduce disables the streaming caret animation and smooth
scrolling. If you add motion, honour it — a test fails on any animation: or
transition: in a component that has no prefers-reduced-motion block.
Focus uses :focus-visible with a 2px --mcs-accent outline and an offset, because
the UA default ring disappears against a dark surface.
Next: Data Agency.