@machfivetechchicago/machvive-webmcp-ai 5.6.0 → 5.7.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/README.md CHANGED
@@ -29,6 +29,8 @@ elements with Shadow DOM that work anywhere `customElements` does.
29
29
  | WebMCP analytics | `<machvive-webmcp-analytics>` | Captures every tool call for listing, editing, export, replay, and dataLayer |
30
30
  | Lorum Ipsum | `<machvive-lorum-ipsum>` | Placeholder copy that projects slotted content |
31
31
 
32
+ <img width="1098" height="894" alt="m5t_machvive_ai_webmcp" src="https://github.com/user-attachments/assets/a1433ff0-f8a9-46fb-ad59-735aed155ff8" />
33
+
32
34
  ## ⚡ Integration with Vite (Vanilla JS)
33
35
 
34
36
  This package is optimized for modern ESM environments like Vite. You can import individual components or the entire library at once.
@@ -206,6 +208,52 @@ Each call then pushes `{ event: 'webmcp_tool_call', webmcp_tool, webmcp_status,
206
208
  webmcp_duration_ms, webmcp_params }`. Without the attribute, push individual entries
207
209
  on demand with `callLog.pushToDataLayer(id)` or the per-entry button in the UI.
208
210
 
211
+ ## 🌓 Theming
212
+
213
+ The UI components follow the viewer's OS preference automatically — no
214
+ configuration, no flash of the wrong palette. Set `theme` to override:
215
+
216
+ ```html
217
+ <machvive-webmcp-inspect></machvive-webmcp-inspect> <!-- follows the OS -->
218
+ <machvive-webmcp-analytics theme="dark"></machvive-webmcp-analytics>
219
+ <machvive-webmcp-inspect theme="light"></machvive-webmcp-inspect>
220
+ ```
221
+
222
+ `theme` is also a reflected property, so `el.theme = 'dark'` and
223
+ `el.theme = null` (back to following the OS) work from script.
224
+
225
+ ### Overriding the palette
226
+
227
+ Colors are CSS custom properties on the host. Custom properties inherit *through*
228
+ shadow boundaries where ordinary styles do not, so you can restyle the components
229
+ from your own stylesheet without `::part` or `!important`:
230
+
231
+ ```css
232
+ machvive-webmcp-inspect,
233
+ machvive-webmcp-analytics {
234
+ --mv-accent: #7c3aed;
235
+ --mv-bg: #ffffff;
236
+ --mv-fg: #111827;
237
+ --mv-border: #e5e7eb;
238
+ }
239
+ ```
240
+
241
+ Every token has both a light and a dark value; overriding one replaces it in both
242
+ themes unless you scope your override inside your own media query.
243
+
244
+ | Token | Role |
245
+ | --- | --- |
246
+ | `--mv-fg` / `--mv-muted` / `--mv-faint` | Text: primary, secondary, hints |
247
+ | `--mv-bg` / `--mv-surface` / `--mv-input-bg` | Panel, raised areas, form fields |
248
+ | `--mv-border` / `--mv-border-soft` / `--mv-control-border` | Outlines, dividers, controls |
249
+ | `--mv-hover` / `--mv-selected` | Interactive states |
250
+ | `--mv-accent` / `--mv-accent-fg` | Primary button |
251
+ | `--mv-ok-bg` / `--mv-ok-fg` / `--mv-err-bg` / `--mv-err-fg` | Status badges |
252
+ | `--mv-danger` / `--mv-danger-border` | Destructive actions |
253
+
254
+ Every combination of OS preference and `theme` is verified to meet WCAG AA
255
+ (≥ 4.5:1) across the full UI. If you override tokens, re-check your own contrast.
256
+
209
257
  ## TypeScript
210
258
 
211
259
  Declarations ship with the package — no `@types/*` needed. Importing a component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machfivetechchicago/machvive-webmcp-ai",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "description": "I don't want to use your product's agent; I want my agent to be able to use your product.",
5
5
  "keywords": [
6
6
  "WebMCP",
@@ -1,5 +1,8 @@
1
1
  /** Placeholder-copy element. Projects light DOM through a slot. */
2
- export class MachviveLorumIpsum extends HTMLElement {}
2
+ export class MachviveLorumIpsum extends HTMLElement {
3
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
4
+ theme: 'light' | 'dark' | null;
5
+ }
3
6
 
4
7
  declare global {
5
8
  interface HTMLElementTagNameMap {
@@ -4,10 +4,26 @@ export class MachviveLorumIpsum extends HTMLElement {
4
4
  this.attachShadow({ mode: 'open' });
5
5
  }
6
6
 
7
+
8
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
9
+ get theme() {
10
+ return this.getAttribute('theme');
11
+ }
12
+
13
+ set theme(value) {
14
+ if (value == null) this.removeAttribute('theme');
15
+ else this.setAttribute('theme', value);
16
+ }
17
+
7
18
  connectedCallback() {
8
19
  this.shadowRoot.innerHTML = `
9
20
  <style>
10
21
  :host { display: block; font-family: sans-serif; color: #333; }
22
+ @media (prefers-color-scheme: dark) {
23
+ :host(:not([theme="light"])) { color: #e8eaed; }
24
+ }
25
+ :host([theme="dark"]) { color: #e8eaed; }
26
+ :host([theme="light"]) { color: #333; }
11
27
  </style>
12
28
  <div>
13
29
  <slot>Lorem ipsum dolor sit amet...</slot>
@@ -68,6 +68,8 @@ export function installAnalytics(log?: CallLog): boolean;
68
68
 
69
69
  export class MachviveWebmcpAnalytics extends HTMLElement {
70
70
  readonly log: CallLog;
71
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
72
+ theme: 'light' | 'dark' | null;
71
73
  }
72
74
 
73
75
  declare global {
@@ -13,6 +13,7 @@
13
13
  // exactly what capturing every call requires — finds no registry to instrument
14
14
  // and silently captures nothing.
15
15
  import '../machvive-webmcp-polyfill/machvive-webmcp-polyfill.js';
16
+ import { THEME_CSS } from '../shared/theme.js';
16
17
 
17
18
  export const CALL_EVENT = 'machvive-webmcp-call';
18
19
  export const DB_NAME = 'machvive-webmcp';
@@ -368,33 +369,45 @@ export function installAnalytics(log = callLog) {
368
369
  }
369
370
 
370
371
  const STYLES = `
371
- :host { display: block; font: 13px/1.5 system-ui, sans-serif; color: #1a1a1a; }
372
+ ${THEME_CSS}
373
+
374
+ /* Painting the background is not optional: this component sets its own text
375
+ colour per theme, so it cannot rely on inheriting a compatible surface from
376
+ whatever page embeds it. */
377
+ :host { display: block; font: 13px/1.5 system-ui, sans-serif;
378
+ color: var(--mv-fg); background: var(--mv-bg); }
372
379
  :host([hidden]) { display: none; }
373
380
  .bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
374
381
  .count { font-weight: 600; margin-right: auto; }
375
- button { font: inherit; padding: 3px 9px; border: 1px solid #ccc; border-radius: 4px;
376
- background: #fff; cursor: pointer; }
377
- button:hover { background: #f2f2f2; }
378
- button.danger { color: #a01; border-color: #d8a0a0; }
379
- ol { list-style: none; margin: 0; padding: 0; border: 1px solid #e2e2e2; border-radius: 6px;
380
- max-height: 380px; overflow-y: auto; }
381
- li { border-bottom: 1px solid #eee; }
382
+ /* color is required, not decorative: form controls do not inherit it, so
383
+ without this the UA picks one per theme and white-on-white can result. */
384
+ button { font: inherit; color: var(--mv-fg); padding: 3px 9px;
385
+ border: 1px solid var(--mv-control-border); border-radius: 4px;
386
+ background: var(--mv-bg); cursor: pointer; }
387
+ button:hover { background: var(--mv-hover); }
388
+ button.danger { color: var(--mv-danger); border-color: var(--mv-danger-border); }
389
+ ol { list-style: none; margin: 0; padding: 0; border: 1px solid var(--mv-border);
390
+ border-radius: 6px; max-height: 380px; overflow-y: auto; background: var(--mv-bg); }
391
+ li { border-bottom: 1px solid var(--mv-border-soft); }
382
392
  li:last-child { border-bottom: 0; }
383
393
  .row { display: flex; gap: 8px; align-items: center; padding: 6px 10px; cursor: pointer; }
384
- .row:hover { background: #fafafa; }
394
+ .row:hover { background: var(--mv-hover); }
385
395
  .tool { font-family: ui-monospace, monospace; font-weight: 600; }
386
396
  .status { font-size: 11px; padding: 1px 6px; border-radius: 10px; }
387
- .status.ok { background: #e6f4ea; color: #137333; }
388
- .status.error { background: #fce8e6; color: #c5221f; }
389
- .ms { color: #777; font-size: 11px; margin-left: auto; }
390
- .detail { padding: 8px 10px; background: #fbfbfb; border-top: 1px solid #eee; }
391
- .detail label { display: block; font-size: 11px; color: #666; margin: 6px 0 2px; }
397
+ .status.ok { background: var(--mv-ok-bg); color: var(--mv-ok-fg); }
398
+ .status.error { background: var(--mv-err-bg); color: var(--mv-err-fg); }
399
+ .ms { color: var(--mv-muted); font-size: 11px; margin-left: auto; }
400
+ .detail { padding: 8px 10px; background: var(--mv-surface);
401
+ border-top: 1px solid var(--mv-border-soft); }
402
+ .detail label { display: block; font-size: 11px; color: var(--mv-muted); margin: 6px 0 2px; }
392
403
  textarea { width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace;
393
- font-size: 12px; border: 1px solid #ddd; border-radius: 4px; padding: 5px; }
394
- pre { margin: 0; padding: 6px; background: #fff; border: 1px solid #eee; border-radius: 4px;
404
+ font-size: 12px; color: var(--mv-fg); background: var(--mv-input-bg);
405
+ border: 1px solid var(--mv-control-border); border-radius: 4px; padding: 5px; }
406
+ pre { margin: 0; padding: 6px; color: var(--mv-fg); background: var(--mv-bg);
407
+ border: 1px solid var(--mv-border-soft); border-radius: 4px;
395
408
  font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
396
- .empty { padding: 20px; text-align: center; color: #888; }
397
- .err { color: #c5221f; }
409
+ .empty { padding: 20px; text-align: center; color: var(--mv-faint); }
410
+ .err { color: var(--mv-err-fg); }
398
411
  `;
399
412
 
400
413
  export class MachviveWebmcpAnalytics extends HTMLElement {
@@ -403,7 +416,7 @@ export class MachviveWebmcpAnalytics extends HTMLElement {
403
416
  #onChange = () => this.#render();
404
417
 
405
418
  static get observedAttributes() {
406
- return ['datalayer'];
419
+ return ['datalayer', 'theme'];
407
420
  }
408
421
 
409
422
  constructor() {
@@ -426,6 +439,17 @@ export class MachviveWebmcpAnalytics extends HTMLElement {
426
439
  globalThis.window.removeEventListener(CALL_EVENT, this.#maybeAutoPush);
427
440
  }
428
441
 
442
+
443
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
444
+ get theme() {
445
+ return this.getAttribute('theme');
446
+ }
447
+
448
+ set theme(value) {
449
+ if (value == null) this.removeAttribute('theme');
450
+ else this.setAttribute('theme', value);
451
+ }
452
+
429
453
  /** The shared call log, for page code that wants direct access. */
430
454
  get log() {
431
455
  return this.#log;
@@ -2,7 +2,11 @@
2
2
  * Lists registered WebMCP tools, builds a form from each tool's inputSchema,
3
3
  * and executes it. Inline by default; `floating` docks it as an overlay.
4
4
  */
5
+ export type MachviveTheme = 'light' | 'dark' | null;
6
+
5
7
  export class MachviveWebmcpInspect extends HTMLElement {
8
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
9
+ theme: MachviveTheme;
6
10
  /** Opens the panel (floating mode only). */
7
11
  show(): void;
8
12
  /** Closes the panel (floating mode only). */
@@ -9,6 +9,7 @@
9
9
  * invocation (`callTool`) are additions the bare spec does not provide.
10
10
  */
11
11
  import { TOOLS_CHANGED_EVENT } from '../machvive-webmcp-polyfill/machvive-webmcp-polyfill.js';
12
+ import { THEME_CSS } from '../shared/theme.js';
12
13
 
13
14
  /** Reads a form control back as the JSON type its schema calls for. */
14
15
  function readControl(input, schema = {}) {
@@ -31,58 +32,78 @@ function readControl(input, schema = {}) {
31
32
  }
32
33
 
33
34
  const STYLES = `
34
- :host { display: block; font: 13px/1.5 system-ui, sans-serif; color: #1a1a1a; }
35
+ ${THEME_CSS}
36
+
37
+ /* See analytics: a component that themes its own text must paint its own
38
+ surface rather than assume the embedding page supplies a matching one. */
39
+ :host { display: block; font: 13px/1.5 system-ui, sans-serif;
40
+ color: var(--mv-fg); background: var(--mv-bg); }
35
41
  :host([hidden]) { display: none; }
36
42
 
37
43
  /* Floating mode docks the panel without disturbing page layout. */
44
+ /* Floating mode is a detached panel: the .panel and .fab paint themselves, so
45
+ the host must stay transparent or it draws a block over the page. */
38
46
  :host([floating]) { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000;
39
- display: block; width: auto; }
47
+ display: block; width: auto; background: transparent; }
40
48
  :host([floating]) .panel { display: none; width: min(420px, calc(100vw - 32px));
41
49
  max-height: min(70vh, 560px); overflow: auto;
42
- box-shadow: 0 8px 28px rgba(0,0,0,.18); background: #fff; }
50
+ box-shadow: 0 8px 28px var(--mv-shadow); background: var(--mv-bg); }
43
51
  :host([floating][open]) .panel { display: block; }
44
52
  :host([floating]) .fab { display: inline-flex; }
45
53
  .fab { display: none; align-items: center; gap: 6px; margin-top: 8px; float: right;
46
- padding: 7px 13px; border-radius: 999px; border: 1px solid #ccc; background: #fff;
47
- cursor: pointer; font: inherit; box-shadow: 0 2px 8px rgba(0,0,0,.14); }
54
+ padding: 7px 13px; border-radius: 999px; border: 1px solid var(--mv-control-border);
55
+ background: var(--mv-bg); color: var(--mv-fg); cursor: pointer; font: inherit;
56
+ box-shadow: 0 2px 8px var(--mv-shadow); }
48
57
 
49
- .panel { border: 1px solid #e2e2e2; border-radius: 8px; overflow: hidden; }
58
+ .panel { border: 1px solid var(--mv-border); border-radius: 8px; overflow: hidden;
59
+ background: var(--mv-bg); }
50
60
  header { display: flex; align-items: center; gap: 8px; padding: 7px 10px;
51
- background: #fafafa; border-bottom: 1px solid #eee; font-weight: 600; }
61
+ background: var(--mv-surface); color: var(--mv-fg);
62
+ border-bottom: 1px solid var(--mv-border-soft); font-weight: 600; }
52
63
  header .close { margin-left: auto; border: 0; background: none; cursor: pointer;
53
- font-size: 16px; line-height: 1; color: #666; }
64
+ font-size: 16px; line-height: 1; color: var(--mv-muted); }
54
65
  :host(:not([floating])) header .close { display: none; }
55
66
 
56
67
  .body { display: flex; min-height: 150px; }
57
68
  @media (max-width: 520px) { .body { flex-direction: column; } }
58
69
 
59
- .tools { flex: 0 0 150px; border-right: 1px solid #eee; overflow-y: auto; }
60
- @media (max-width: 520px) { .tools { flex: none; border-right: 0; border-bottom: 1px solid #eee; } }
70
+ .tools { flex: 0 1 auto; min-width: 120px; max-width: 200px;
71
+ border-right: 1px solid var(--mv-border-soft); overflow-y: auto; }
72
+ @media (max-width: 520px) { .tools { flex: none; max-width: none; border-right: 0;
73
+ border-bottom: 1px solid var(--mv-border-soft); } }
74
+ /* Tool names are arbitrary identifiers; long ones must wrap inside the column
75
+ rather than spill over the divider. */
61
76
  .tools button { display: block; width: 100%; text-align: left; padding: 6px 10px;
62
- border: 0; background: none; cursor: pointer; font: inherit;
63
- font-family: ui-monospace, monospace; border-bottom: 1px solid #f4f4f4; }
64
- .tools button:hover { background: #f6f6f6; }
65
- .tools button[aria-current="true"] { background: #e8f0fe; font-weight: 600; }
77
+ border: 0; background: none; color: var(--mv-fg); cursor: pointer;
78
+ font: inherit; font-family: ui-monospace, monospace; font-size: 12px;
79
+ overflow-wrap: anywhere; border-bottom: 1px solid var(--mv-border-soft); }
80
+ .tools button:hover { background: var(--mv-hover); }
81
+ .tools button[aria-current="true"] { background: var(--mv-selected); font-weight: 600; }
66
82
 
67
83
  .form { flex: 1; padding: 10px; min-width: 0; }
68
- .desc { color: #666; margin: 0 0 8px; }
84
+ .desc { color: var(--mv-muted); margin: 0 0 8px; }
69
85
  label { display: block; margin-bottom: 7px; }
70
86
  .name { font-family: ui-monospace, monospace; font-size: 12px; }
71
- .req { color: #c5221f; }
72
- .hint { color: #888; font-size: 11px; }
87
+ .req { color: var(--mv-err-fg); }
88
+ .hint { color: var(--mv-faint); font-size: 11px; }
89
+ /* color/background are required, not decorative: form controls do not inherit
90
+ them, so without these the UA picks per-theme defaults and text can render
91
+ white on white. */
73
92
  input, select, textarea { width: 100%; box-sizing: border-box; font: inherit; font-size: 12px;
74
- padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px; }
93
+ color: var(--mv-fg); background: var(--mv-input-bg); padding: 4px 6px;
94
+ border: 1px solid var(--mv-control-border); border-radius: 4px; }
75
95
  input[type="checkbox"] { width: auto; }
76
96
  textarea { font-family: ui-monospace, monospace; }
77
- .run { margin-top: 4px; padding: 5px 14px; border: 1px solid #1a73e8; border-radius: 4px;
78
- background: #1a73e8; color: #fff; cursor: pointer; font: inherit; }
97
+ .run { margin-top: 4px; padding: 5px 14px; border: 1px solid var(--mv-accent);
98
+ border-radius: 4px; background: var(--mv-accent); color: var(--mv-accent-fg);
99
+ cursor: pointer; font: inherit; }
79
100
  .run:disabled { opacity: .6; cursor: default; }
80
- pre { margin: 8px 0 0; padding: 7px; background: #fafafa; border: 1px solid #eee;
81
- border-radius: 4px; font-size: 12px; white-space: pre-wrap; word-break: break-word;
82
- max-height: 180px; overflow: auto; }
83
- pre.error { background: #fce8e6; border-color: #f5c6c2; color: #c5221f; }
84
- .field-error { color: #c5221f; font-size: 11px; }
85
- .empty { padding: 20px; text-align: center; color: #888; }
101
+ pre { margin: 8px 0 0; padding: 7px; color: var(--mv-fg); background: var(--mv-surface);
102
+ border: 1px solid var(--mv-border-soft); border-radius: 4px; font-size: 12px;
103
+ white-space: pre-wrap; word-break: break-word; max-height: 180px; overflow: auto; }
104
+ pre.error { background: var(--mv-err-bg); border-color: var(--mv-err-border); color: var(--mv-err-fg); }
105
+ .field-error { color: var(--mv-err-fg); font-size: 11px; }
106
+ .empty { padding: 20px; text-align: center; color: var(--mv-faint); }
86
107
  `;
87
108
 
88
109
  export class MachviveWebmcpInspect extends HTMLElement {
@@ -91,7 +112,7 @@ export class MachviveWebmcpInspect extends HTMLElement {
91
112
  #onToolsChanged = () => this.#render();
92
113
 
93
114
  static get observedAttributes() {
94
- return ['floating', 'open'];
115
+ return ['floating', 'open', 'theme'];
95
116
  }
96
117
 
97
118
  constructor() {
@@ -113,6 +134,17 @@ export class MachviveWebmcpInspect extends HTMLElement {
113
134
  if (this.shadowRoot?.getElementById('root')) this.#render();
114
135
  }
115
136
 
137
+
138
+ /** Forces a palette regardless of the OS preference. null follows the OS. */
139
+ get theme() {
140
+ return this.getAttribute('theme');
141
+ }
142
+
143
+ set theme(value) {
144
+ if (value == null) this.removeAttribute('theme');
145
+ else this.setAttribute('theme', value);
146
+ }
147
+
116
148
  /** Opens the panel (floating mode only). */
117
149
  show() {
118
150
  this.setAttribute('open', '');
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared design tokens for the WebMCP UI components.
3
+ *
4
+ * Tokens are plain custom properties on :host, so a consumer can override any of
5
+ * them from outside the shadow DOM — custom properties inherit through shadow
6
+ * boundaries where ordinary styles do not.
7
+ *
8
+ * Resolution order, matching how a component should behave:
9
+ * 1. light palette on :host (the default)
10
+ * 2. dark palette when the OS asks for dark (unless theme="light" opts out)
11
+ * 3. dark palette when theme="dark" (explicit, wins over the OS)
12
+ */
13
+
14
+ const LIGHT = `
15
+ --mv-fg: #1a1a1a;
16
+ --mv-muted: #666;
17
+ --mv-faint: #6e6e6e;
18
+ --mv-bg: #fff;
19
+ --mv-surface: #fafafa;
20
+ --mv-input-bg: #fff;
21
+ --mv-border: #e2e2e2;
22
+ --mv-border-soft: #eee;
23
+ --mv-control-border: #ccc;
24
+ --mv-hover: #f2f2f2;
25
+ --mv-selected: #e8f0fe;
26
+ --mv-accent: #1565c0;
27
+ --mv-accent-fg: #fff;
28
+ --mv-ok-bg: #e6f4ea;
29
+ --mv-ok-fg: #0f6b2e;
30
+ --mv-err-bg: #fce8e6;
31
+ --mv-err-fg: #b3261e;
32
+ --mv-err-border: #f5c6c2;
33
+ --mv-danger: #a4161a;
34
+ --mv-danger-border: #d8a0a0;
35
+ --mv-shadow: rgba(0, 0, 0, .18);
36
+ `;
37
+
38
+ const DARK = `
39
+ --mv-fg: #e8eaed;
40
+ --mv-muted: #9aa0a6;
41
+ --mv-faint: #8b9096;
42
+ --mv-bg: #1f2125;
43
+ --mv-surface: #282b30;
44
+ --mv-input-bg: #16181b;
45
+ --mv-border: #3c4046;
46
+ --mv-border-soft: #33363b;
47
+ --mv-control-border: #4a4f56;
48
+ --mv-hover: #32363c;
49
+ --mv-selected: #1e3a5f;
50
+ --mv-accent: #5b9bf8;
51
+ --mv-accent-fg: #0b1220;
52
+ --mv-ok-bg: #16351f;
53
+ --mv-ok-fg: #7ee2a0;
54
+ --mv-err-bg: #3f1d1c;
55
+ --mv-err-fg: #ff9d97;
56
+ --mv-err-border: #5c2c2a;
57
+ --mv-danger: #ff9d97;
58
+ --mv-danger-border: #5c2c2a;
59
+ --mv-shadow: rgba(0, 0, 0, .5);
60
+ `;
61
+
62
+ /**
63
+ * CSS declaring both palettes. Prepend to a component's stylesheet.
64
+ *
65
+ * `color-scheme: light dark` lets UA-rendered parts (scrollbars, select popups,
66
+ * checkboxes, focus rings) follow the same theme as the tokens — without it the
67
+ * browser would paint light controls onto a dark panel.
68
+ */
69
+ export const THEME_CSS = `
70
+ :host {
71
+ color-scheme: light dark;
72
+ ${LIGHT} }
73
+
74
+ @media (prefers-color-scheme: dark) {
75
+ :host(:not([theme="light"])) {
76
+ ${DARK} }
77
+ }
78
+
79
+ /* Explicit choice beats the OS preference, in both directions. */
80
+ :host([theme="dark"]) {
81
+ ${DARK} }
82
+
83
+ :host([theme="light"]) {
84
+ color-scheme: light;
85
+ ${LIGHT} }
86
+ `;