@cas-smartdesign/design-tokens 3.0.3 → 3.0.4

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.
@@ -12,18 +12,29 @@
12
12
  box-sizing: border-box;
13
13
  }
14
14
 
15
+ html {
16
+ height: 100%;
17
+ }
18
+
15
19
  body {
16
20
  font: var(--text-body);
17
21
  color: var(--color-fg-default);
18
22
  background-color: var(--color-bg-default);
19
-
20
- padding: var(--spacing-x16);
23
+ /* The page itself does not scroll — the sidebar and the markdown content are
24
+ individual scroll containers, so anchor navigation only scrolls the content. */
25
+ height: 100%;
26
+ margin: 0;
27
+ overflow: hidden;
21
28
  }
22
29
 
23
30
  .markdown-body {
24
31
  box-sizing: border-box;
25
32
  padding-bottom: var(--spacing-x16);
26
33
 
34
+ overflow-y: auto;
35
+ min-width: 0;
36
+ scroll-behavior: smooth;
37
+
27
38
  color: inherit !important;
28
39
  background-color: inherit !important;
29
40
 
@@ -37,6 +48,7 @@ body {
37
48
  & svg {
38
49
  float: none;
39
50
  margin: 0;
51
+ max-width: 100%;
40
52
  }
41
53
  }
42
54
 
@@ -141,40 +153,6 @@ body {
141
153
  .specimen-meta code {
142
154
  white-space: pre;
143
155
  }
144
- .theme-container {
145
- background-color: var(--color-bg-default);
146
- top: var(--spacing-x16);
147
- right: var(--spacing-x16);
148
- width: auto;
149
- padding: var(--spacing-x4);
150
- overflow: hidden;
151
- border-radius: var(--radius-m);
152
- }
153
- .theme-toggle-container {
154
- display: flex;
155
- justify-content: center;
156
- gap: var(--spacing-x4);
157
- }
158
-
159
- .theme-toggle {
160
- appearance: none;
161
- border-radius: var(--radius-s);
162
- min-width: 80px;
163
- height: 36px;
164
- color: var(--color-fg-default);
165
- border: 1px solid var(--color-border-action-neutral-default);
166
- padding: var(--spacing-x2) var(--spacing-x4);
167
- background: var(--color-bg-action-neutral-default);
168
- }
169
-
170
- .theme-toggle:focus,
171
- .theme-toggle:hover {
172
- background: var(--color-bg-action-neutral-default-hover);
173
- }
174
- .theme-toggle:active {
175
- background: var(--color-bg-action-neutral-default-pressed);
176
- }
177
-
178
156
  .hidden {
179
157
  display: none;
180
158
  }
@@ -220,6 +198,7 @@ body {
220
198
  position: relative;
221
199
  display: flex;
222
200
  flex-direction: row;
201
+ height: 100%;
223
202
  & > * {
224
203
  flex: 1;
225
204
  }
@@ -227,6 +206,7 @@ body {
227
206
 
228
207
  .sidebar {
229
208
  flex: 0 0 auto;
209
+ overflow-y: auto;
230
210
  padding-right: var(--spacing-x8);
231
211
  margin-right: var(--spacing-x16);
232
212
  border-right: 1px solid var(--color-border-default);
@@ -1394,16 +1374,7 @@ body {
1394
1374
  </head>
1395
1375
  <body sd-color-scheme="light dark">
1396
1376
  <div class="content-wrapper">
1397
- <div class="sidebar">
1398
- <div class="theme-container">
1399
- <div class="theme-toggle-container">
1400
- <button id="light-theme-toggle" class="theme-toggle">☀️ light</button>
1401
- <button id="dark-theme-toggle" class="theme-toggle">🌙 dark</button>
1402
- <button id="auto-theme-toggle" class="theme-toggle">🌓 auto</button>
1403
- </div>
1404
- </div>
1405
- <ul class="nav-bar"><li class="level-2"><a href="#token-structure">Token structure</a></li><li class="level-3"><a href="#base-tokens">Base tokens</a></li><li class="level-3"><a href="#semantic-tokens">Semantic tokens</a></li><li class="level-3"><a href="#themes">Themes</a></li><li class="level-2"><a href="#semantic-tokens---color">Semantic tokens - Color</a></li><li class="level-3"><a href="#choose-the-type-(-where?-)">Choose the Type ( Where? )</a></li><li class="level-4"><a href="#fg">fg</a></li><li class="level-4"><a href="#bg">bg</a></li><li class="level-4"><a href="#border">border</a></li><li class="level-4"><a href="#rainbow">rainbow</a></li><li class="level-3"><a href="#choose-the-role-(-why?-)">Choose the Role ( Why? )</a></li><li class="level-4"><a href="#empty">empty</a></li><li class="level-4"><a href="#accent">accent</a></li><li class="level-4"><a href="#danger">danger</a></li><li class="level-4"><a href="#success">success</a></li><li class="level-4"><a href="#warning">warning</a></li><li class="level-4"><a href="#assistive">assistive</a></li><li class="level-4"><a href="#action">action</a></li><li class="level-4"><a href="#selection">selection</a></li><li class="level-4"><a href="#interaction">interaction</a></li><li class="level-3"><a href="#choose-the-prominence-(-how-important?-)">Choose the Prominence ( How important? )</a></li><li class="level-4"><a href="#oncolor">onColor</a></li><li class="level-4"><a href="#translucent-+-ontranslucent">Translucent + onTranslucent</a></li><li class="level-4"><a href="#stronger">stronger</a></li><li class="level-4"><a href="#strong">strong</a></li><li class="level-4"><a href="#default">default</a></li><li class="level-4"><a href="#weak">weak</a></li><li class="level-4"><a href="#weaker">weaker</a></li><li class="level-4"><a href="#brand">brand</a></li><li class="level-4"><a href="#accent">accent</a></li><li class="level-4"><a href="#neutral">neutral</a></li><li class="level-4"><a href="#dangerdefault">dangerDefault</a></li><li class="level-3"><a href="#choose-the-state-(-when?-)">Choose the State ( When? )</a></li><li class="level-4"><a href="#empty">empty</a></li><li class="level-4"><a href="#hover">hover</a></li><li class="level-4"><a href="#pressed">pressed</a></li><li class="level-4"><a href="#disabled">disabled</a></li><li class="level-4"><a href="#focus">focus</a></li><li class="level-4"><a href="#oncolor">onColor</a></li><li class="level-2"><a href="#semantic-tokens---typography">Semantic tokens - Typography</a></li><li class="level-3"><a href="#displaylarge">displayLarge</a></li><li class="level-3"><a href="#displaymedium">displayMedium</a></li><li class="level-3"><a href="#displaysmall">displaySmall</a></li><li class="level-3"><a href="#titlelarge">titleLarge</a></li><li class="level-3"><a href="#titlemedium">titleMedium</a></li><li class="level-3"><a href="#titlesmall">titleSmall</a></li><li class="level-3"><a href="#subtitle">subtitle</a></li><li class="level-3"><a href="#body">body</a></li><li class="level-3"><a href="#description">description</a></li><li class="level-3"><a href="#caption">caption</a></li><li class="level-2"><a href="#semantic-tokens---typography-variants">Semantic tokens - Typography variants</a></li><li class="level-3"><a href="#highlighted">highlighted</a></li><li class="level-3"><a href="#strong">strong</a></li><li class="level-3"><a href="#default">default</a></li><li class="level-3"><a href="#sub">sub</a></li><li class="level-2"><a href="#semantic-tokens---corner-radius">Semantic Tokens - Corner Radius</a></li><li class="level-3"><a href="#radius/none">radius/none</a></li><li class="level-3"><a href="#radius/xs">radius/xs</a></li><li class="level-3"><a href="#radius/s">radius/s</a></li><li class="level-3"><a href="#radius/m">radius/m</a></li><li class="level-3"><a href="#radius/xl">radius/xl</a></li><li class="level-3"><a href="#radius/round">radius/round</a></li><li class="level-2"><a href="#semantic-tokens---effects">Semantic Tokens - Effects</a></li><li class="level-3"><a href="#shadow-strong">shadow-strong</a></li><li class="level-3"><a href="#shadow-default">shadow-default</a></li><li class="level-3"><a href="#shadow-weak">shadow-weak</a></li><li class="level-2"><a href="#semantic-tokens---opacity">Semantic Tokens - Opacity</a></li><li class="level-2"><a href="#semantic-tokens---spacing">Semantic Tokens - Spacing</a></li><li class="level-2"><a href="#examples">Examples</a></li></ul>
1406
- </div>
1377
+ <div class="sidebar"><ul class="nav-bar"><li class="level-2"><a href="#token-structure">Token structure</a></li><li class="level-3"><a href="#base-tokens">Base tokens</a></li><li class="level-3"><a href="#semantic-tokens">Semantic tokens</a></li><li class="level-3"><a href="#themes">Themes</a></li><li class="level-2"><a href="#semantic-tokens---color">Semantic tokens - Color</a></li><li class="level-3"><a href="#choose-the-type-(-where?-)">Choose the Type ( Where? )</a></li><li class="level-4"><a href="#fg">fg</a></li><li class="level-4"><a href="#bg">bg</a></li><li class="level-4"><a href="#border">border</a></li><li class="level-4"><a href="#rainbow">rainbow</a></li><li class="level-3"><a href="#choose-the-role-(-why?-)">Choose the Role ( Why? )</a></li><li class="level-4"><a href="#empty">empty</a></li><li class="level-4"><a href="#accent">accent</a></li><li class="level-4"><a href="#danger">danger</a></li><li class="level-4"><a href="#success">success</a></li><li class="level-4"><a href="#warning">warning</a></li><li class="level-4"><a href="#assistive">assistive</a></li><li class="level-4"><a href="#action">action</a></li><li class="level-4"><a href="#selection">selection</a></li><li class="level-4"><a href="#interaction">interaction</a></li><li class="level-3"><a href="#choose-the-prominence-(-how-important?-)">Choose the Prominence ( How important? )</a></li><li class="level-4"><a href="#oncolor">onColor</a></li><li class="level-4"><a href="#translucent-+-ontranslucent">Translucent + onTranslucent</a></li><li class="level-4"><a href="#stronger">stronger</a></li><li class="level-4"><a href="#strong">strong</a></li><li class="level-4"><a href="#default">default</a></li><li class="level-4"><a href="#weak">weak</a></li><li class="level-4"><a href="#weaker">weaker</a></li><li class="level-4"><a href="#brand">brand</a></li><li class="level-4"><a href="#accent">accent</a></li><li class="level-4"><a href="#neutral">neutral</a></li><li class="level-4"><a href="#dangerdefault">dangerDefault</a></li><li class="level-3"><a href="#choose-the-state-(-when?-)">Choose the State ( When? )</a></li><li class="level-4"><a href="#empty">empty</a></li><li class="level-4"><a href="#hover">hover</a></li><li class="level-4"><a href="#pressed">pressed</a></li><li class="level-4"><a href="#disabled">disabled</a></li><li class="level-4"><a href="#focus">focus</a></li><li class="level-4"><a href="#oncolor">onColor</a></li><li class="level-2"><a href="#semantic-tokens---typography">Semantic tokens - Typography</a></li><li class="level-3"><a href="#displaylarge">displayLarge</a></li><li class="level-3"><a href="#displaymedium">displayMedium</a></li><li class="level-3"><a href="#displaysmall">displaySmall</a></li><li class="level-3"><a href="#titlelarge">titleLarge</a></li><li class="level-3"><a href="#titlemedium">titleMedium</a></li><li class="level-3"><a href="#titlesmall">titleSmall</a></li><li class="level-3"><a href="#subtitle">subtitle</a></li><li class="level-3"><a href="#body">body</a></li><li class="level-3"><a href="#description">description</a></li><li class="level-3"><a href="#caption">caption</a></li><li class="level-2"><a href="#semantic-tokens---typography-variants">Semantic tokens - Typography variants</a></li><li class="level-3"><a href="#highlighted">highlighted</a></li><li class="level-3"><a href="#strong">strong</a></li><li class="level-3"><a href="#default">default</a></li><li class="level-3"><a href="#sub">sub</a></li><li class="level-2"><a href="#semantic-tokens---corner-radius">Semantic Tokens - Corner Radius</a></li><li class="level-3"><a href="#radius/none">radius/none</a></li><li class="level-3"><a href="#radius/xs">radius/xs</a></li><li class="level-3"><a href="#radius/s">radius/s</a></li><li class="level-3"><a href="#radius/m">radius/m</a></li><li class="level-3"><a href="#radius/xl">radius/xl</a></li><li class="level-3"><a href="#radius/round">radius/round</a></li><li class="level-2"><a href="#semantic-tokens---effects">Semantic Tokens - Effects</a></li><li class="level-3"><a href="#shadow-strong">shadow-strong</a></li><li class="level-3"><a href="#shadow-default">shadow-default</a></li><li class="level-3"><a href="#shadow-weak">shadow-weak</a></li><li class="level-2"><a href="#semantic-tokens---opacity">Semantic Tokens - Opacity</a></li><li class="level-2"><a href="#semantic-tokens---spacing">Semantic Tokens - Spacing</a></li><li class="level-2"><a href="#examples">Examples</a></li></ul></div>
1407
1378
  <div class="markdown-body">
1408
1379
  <div id="markdown-container"><h1 id="@cas-smartdesign/design-tokens">@cas-smartdesign/design-tokens</h1>
1409
1380
  <p>A shared package providing design tokens to create SmartDesign like components and apps.</p>
@@ -6410,62 +6381,155 @@ These sizes are proportional to one another.</p>
6410
6381
  </svg>
6411
6382
  </p>
6412
6383
  </div>
6384
+ <!-- Fallback for token categories without a readme section — inside the
6385
+ scroll container, as the page itself does not scroll. -->
6386
+ <div id="token-container"></div>
6413
6387
  </div>
6414
6388
  </div>
6415
6389
 
6416
- <div id="token-container"></div>
6390
+ <script type="module">
6391
+ /**
6392
+ * Light / dark / auto switcher for the SmartDesign color-scheme protocol.
6393
+ *
6394
+ * Importing this module applies the persisted preference and renders a fixed switcher in
6395
+ * the top-right corner of the page:
6396
+ *
6397
+ * - the `sd-color-scheme` attribute on <body> holds the preference
6398
+ * ("light" | "dark" | "light dark" for auto) — the selectors of the generated token CSS
6399
+ * key off it (see build.mts),
6400
+ * - the `prefers-dark` class on <body> mirrors the OS preference,
6401
+ * - the preference is persisted under the "sd-color-scheme" localStorage key.
6402
+ *
6403
+ * Embedding: when the page runs inside an iframe no UI is rendered — the host page owns
6404
+ * the switcher and controls the embedded page by posting
6405
+ * `{ type: "sd-color-scheme", preference }` messages. Every applied preference change is
6406
+ * announced as a "sd-color-scheme-change" CustomEvent on window, which host pages (e.g.
6407
+ * the aggregated documentation index) use to forward the preference to embedded pages.
6408
+ *
6409
+ * Shipped as a hand-written, dependency-free ES module so it can be used without any
6410
+ * build step (plain <script type="module">, inlining, or bundlers alike).
6411
+ *
6412
+ * Styling: importing this module does NOT load the token CSS — that is the page's own
6413
+ * concern (the ./css export). The switcher styles therefore use the token custom
6414
+ * properties with light-dark() fallbacks, so the switcher stays usable on pages that do
6415
+ * not load the tokens (e.g. the aggregated documentation index).
6416
+ */
6417
+
6418
+ const STORAGE_KEY = "sd-color-scheme";
6419
+ const MESSAGE_TYPE = "sd-color-scheme";
6420
+ const CHANGE_EVENT = "sd-color-scheme-change";
6421
+ const COLOR_SCHEMES = [
6422
+ ["light", "☀️ light"],
6423
+ ["dark", "🌙 dark"],
6424
+ ["light dark", "🌓 auto"],
6425
+ ];
6426
+
6427
+ /* The light-dark() fallbacks mirror the light/dark values of the referenced tokens
6428
+ (resolved from dist/{light,dark}/tokens.css) so pages without the token CSS render
6429
+ identically to pages with it — keep them in sync when the tokens change. */
6430
+ const switcherStyle = `
6431
+ .theme-switcher {
6432
+ position: fixed;
6433
+ top: 16px;
6434
+ right: 16px;
6435
+ z-index: 100;
6436
+ display: flex;
6437
+ gap: 4px;
6438
+ padding: 4px;
6439
+ border-radius: 8px;
6440
+ border: 1px solid var(--color-border-weak, light-dark(#ededed, #303030));
6441
+ background-color: var(--color-bg-weak, light-dark(#f1f1f1, #141414));
6442
+
6443
+ button {
6444
+ appearance: none;
6445
+ font-family: inherit;
6446
+ font-size: 13px;
6447
+ color: var(--color-fg-default, light-dark(#111111, #ffffff));
6448
+ border: 1px solid transparent;
6449
+ border-radius: 6px;
6450
+ padding: 4px 10px;
6451
+ background-color: transparent;
6452
+ cursor: pointer;
6453
+ }
6417
6454
 
6418
- <script>
6419
- const container = document.body;
6420
- const lightToggle = document.getElementById("light-theme-toggle");
6421
- const darkToggle = document.getElementById("dark-theme-toggle");
6422
- const autoToggle = document.getElementById("auto-theme-toggle");
6423
-
6424
- const persistPreference = (preference) => {
6425
- try {
6426
- localStorage.setItem("sd-color-scheme", preference ?? "light dark");
6427
- } catch {
6428
- console.warn("Failed to persist theme preference to localstorage");
6429
- }
6430
- };
6431
-
6432
- const readPreference = () => {
6433
- try {
6434
- return localStorage.getItem("sd-color-scheme") ?? "light dark";
6435
- } catch (error) {
6436
- return "light dark";
6437
- }
6438
- };
6439
- const setPreference = (scheme) => {
6440
- container.setAttribute("sd-color-scheme", scheme);
6441
- persistPreference(scheme);
6442
- };
6443
-
6444
- const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
6445
- const onColorSchemePreferenceChange = (dark) => {
6446
- document.body.classList.toggle("prefers-dark", dark);
6447
- };
6448
- prefersDark.addEventListener("change", (event) => {
6449
- onColorSchemePreferenceChange(event.matches);
6450
- });
6451
- onColorSchemePreferenceChange(prefersDark.matches);
6455
+ button:hover {
6456
+ background-color: var(--color-bg-action-neutral-default-hover, light-dark(#c6c6c6, #565656));
6457
+ }
6452
6458
 
6453
- lightToggle.addEventListener("click", () => {
6454
- setPreference("light");
6455
- });
6456
- darkToggle.addEventListener("click", () => {
6457
- setPreference("dark");
6458
- });
6459
- autoToggle.addEventListener("click", () => {
6460
- setPreference("light dark");
6461
- });
6462
- container.querySelectorAll(".accordion-button").forEach((el) => {
6463
- el.addEventListener("click", (e) => {
6459
+ button[aria-pressed="true"] {
6460
+ border-color: var(--color-border-action-neutral-default, #999999);
6461
+ background-color: var(--color-bg-action-neutral-default, light-dark(#d4d4d4, #414141));
6462
+ }
6463
+ }
6464
+ `;
6465
+
6466
+ function initializeThemeSwitcher() {
6467
+ const validSchemes = COLOR_SCHEMES.map(([scheme]) => scheme);
6468
+ const embedded = window.self !== window.top;
6469
+
6470
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
6471
+ const applyOsPreference = () => document.body.classList.toggle("prefers-dark", prefersDark.matches);
6472
+ prefersDark.addEventListener("change", applyOsPreference);
6473
+ applyOsPreference();
6474
+
6475
+ const buttons = new Map();
6476
+ const setPreference = (preference) => {
6477
+ document.body.setAttribute("sd-color-scheme", preference);
6478
+ buttons.forEach((button, scheme) => button.setAttribute("aria-pressed", String(scheme === preference)));
6479
+ try {
6480
+ localStorage.setItem(STORAGE_KEY, preference);
6481
+ } catch {
6482
+ // storage unavailable (e.g. private mode) — the preference just doesn't persist
6483
+ }
6484
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: preference }));
6485
+ };
6486
+
6487
+ // Host pages control embedded pages through messages.
6488
+ window.addEventListener("message", (event) => {
6489
+ if (event.data?.type === MESSAGE_TYPE && validSchemes.includes(event.data.preference)) {
6490
+ setPreference(event.data.preference);
6491
+ }
6492
+ });
6493
+
6494
+ if (!embedded) {
6495
+ const styles = document.createElement("style");
6496
+ styles.textContent = switcherStyle;
6497
+ document.head.appendChild(styles);
6498
+
6499
+ const switcher = document.createElement("div");
6500
+ switcher.className = "theme-switcher";
6501
+ switcher.role = "group";
6502
+ switcher.ariaLabel = "Color scheme";
6503
+ for (const [scheme, label] of COLOR_SCHEMES) {
6504
+ const button = document.createElement("button");
6505
+ button.type = "button";
6506
+ button.innerText = label;
6507
+ button.addEventListener("click", () => setPreference(scheme));
6508
+ buttons.set(scheme, button);
6509
+ switcher.appendChild(button);
6510
+ }
6511
+ document.body.appendChild(switcher);
6512
+ }
6513
+
6514
+ let storedPreference = null;
6515
+ try {
6516
+ storedPreference = localStorage.getItem(STORAGE_KEY);
6517
+ } catch {
6518
+ // storage unavailable — fall back to auto
6519
+ }
6520
+ setPreference(validSchemes.includes(storedPreference) ? storedPreference : "light dark");
6521
+ }
6522
+
6523
+ initializeThemeSwitcher();
6524
+
6525
+ </script>
6526
+ <script>
6527
+ document.body.querySelectorAll(".accordion-button").forEach((el) => {
6528
+ el.addEventListener("click", () => {
6464
6529
  el.nextElementSibling?.classList.toggle("hidden");
6465
6530
  el.classList.toggle("active");
6466
6531
  });
6467
6532
  });
6468
- setPreference(readPreference());
6469
6533
  </script>
6470
6534
  </body>
6471
6535
  </html>
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@cas-smartdesign/design-tokens",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Design tokens for SmartDesign",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "files": [
7
7
  "dist",
8
+ "theme-switcher.mjs",
9
+ "theme-switcher.d.ts",
8
10
  "npm-third-party-licenses.json"
9
11
  ],
10
12
  "type": "module",
11
13
  "exports": {
14
+ "./theme-switcher": {
15
+ "types": "./theme-switcher.d.ts",
16
+ "default": "./theme-switcher.mjs"
17
+ },
12
18
  "./css": {
13
19
  "types": "./dist/tokens.d.ts",
14
20
  "default": "./dist/tokens.css"
@@ -40,7 +46,7 @@
40
46
  "@types/node": "^25.5.2",
41
47
  "marked": "^18.0.0",
42
48
  "style-dictionary": "^5.4.0",
43
- "@cas-smartdesign/license-generator": "^1.10.0"
49
+ "@cas-smartdesign/license-generator": "^1.10.2"
44
50
  },
45
51
  "scripts": {
46
52
  "version": "pnpm version",
@@ -0,0 +1,3 @@
1
+ // Side-effect only module: applies the persisted sd-color-scheme preference and renders
2
+ // the theme switcher (unless the page is embedded). See theme-switcher.mjs.
3
+ export {};
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Light / dark / auto switcher for the SmartDesign color-scheme protocol.
3
+ *
4
+ * Importing this module applies the persisted preference and renders a fixed switcher in
5
+ * the top-right corner of the page:
6
+ *
7
+ * - the `sd-color-scheme` attribute on <body> holds the preference
8
+ * ("light" | "dark" | "light dark" for auto) — the selectors of the generated token CSS
9
+ * key off it (see build.mts),
10
+ * - the `prefers-dark` class on <body> mirrors the OS preference,
11
+ * - the preference is persisted under the "sd-color-scheme" localStorage key.
12
+ *
13
+ * Embedding: when the page runs inside an iframe no UI is rendered — the host page owns
14
+ * the switcher and controls the embedded page by posting
15
+ * `{ type: "sd-color-scheme", preference }` messages. Every applied preference change is
16
+ * announced as a "sd-color-scheme-change" CustomEvent on window, which host pages (e.g.
17
+ * the aggregated documentation index) use to forward the preference to embedded pages.
18
+ *
19
+ * Shipped as a hand-written, dependency-free ES module so it can be used without any
20
+ * build step (plain <script type="module">, inlining, or bundlers alike).
21
+ *
22
+ * Styling: importing this module does NOT load the token CSS — that is the page's own
23
+ * concern (the ./css export). The switcher styles therefore use the token custom
24
+ * properties with light-dark() fallbacks, so the switcher stays usable on pages that do
25
+ * not load the tokens (e.g. the aggregated documentation index).
26
+ */
27
+
28
+ const STORAGE_KEY = "sd-color-scheme";
29
+ const MESSAGE_TYPE = "sd-color-scheme";
30
+ const CHANGE_EVENT = "sd-color-scheme-change";
31
+ const COLOR_SCHEMES = [
32
+ ["light", "☀️ light"],
33
+ ["dark", "🌙 dark"],
34
+ ["light dark", "🌓 auto"],
35
+ ];
36
+
37
+ /* The light-dark() fallbacks mirror the light/dark values of the referenced tokens
38
+ (resolved from dist/{light,dark}/tokens.css) so pages without the token CSS render
39
+ identically to pages with it — keep them in sync when the tokens change. */
40
+ const switcherStyle = `
41
+ .theme-switcher {
42
+ position: fixed;
43
+ top: 16px;
44
+ right: 16px;
45
+ z-index: 100;
46
+ display: flex;
47
+ gap: 4px;
48
+ padding: 4px;
49
+ border-radius: 8px;
50
+ border: 1px solid var(--color-border-weak, light-dark(#ededed, #303030));
51
+ background-color: var(--color-bg-weak, light-dark(#f1f1f1, #141414));
52
+
53
+ button {
54
+ appearance: none;
55
+ font-family: inherit;
56
+ font-size: 13px;
57
+ color: var(--color-fg-default, light-dark(#111111, #ffffff));
58
+ border: 1px solid transparent;
59
+ border-radius: 6px;
60
+ padding: 4px 10px;
61
+ background-color: transparent;
62
+ cursor: pointer;
63
+ }
64
+
65
+ button:hover {
66
+ background-color: var(--color-bg-action-neutral-default-hover, light-dark(#c6c6c6, #565656));
67
+ }
68
+
69
+ button[aria-pressed="true"] {
70
+ border-color: var(--color-border-action-neutral-default, #999999);
71
+ background-color: var(--color-bg-action-neutral-default, light-dark(#d4d4d4, #414141));
72
+ }
73
+ }
74
+ `;
75
+
76
+ function initializeThemeSwitcher() {
77
+ const validSchemes = COLOR_SCHEMES.map(([scheme]) => scheme);
78
+ const embedded = window.self !== window.top;
79
+
80
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
81
+ const applyOsPreference = () => document.body.classList.toggle("prefers-dark", prefersDark.matches);
82
+ prefersDark.addEventListener("change", applyOsPreference);
83
+ applyOsPreference();
84
+
85
+ const buttons = new Map();
86
+ const setPreference = (preference) => {
87
+ document.body.setAttribute("sd-color-scheme", preference);
88
+ buttons.forEach((button, scheme) => button.setAttribute("aria-pressed", String(scheme === preference)));
89
+ try {
90
+ localStorage.setItem(STORAGE_KEY, preference);
91
+ } catch {
92
+ // storage unavailable (e.g. private mode) — the preference just doesn't persist
93
+ }
94
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: preference }));
95
+ };
96
+
97
+ // Host pages control embedded pages through messages.
98
+ window.addEventListener("message", (event) => {
99
+ if (event.data?.type === MESSAGE_TYPE && validSchemes.includes(event.data.preference)) {
100
+ setPreference(event.data.preference);
101
+ }
102
+ });
103
+
104
+ if (!embedded) {
105
+ const styles = document.createElement("style");
106
+ styles.textContent = switcherStyle;
107
+ document.head.appendChild(styles);
108
+
109
+ const switcher = document.createElement("div");
110
+ switcher.className = "theme-switcher";
111
+ switcher.role = "group";
112
+ switcher.ariaLabel = "Color scheme";
113
+ for (const [scheme, label] of COLOR_SCHEMES) {
114
+ const button = document.createElement("button");
115
+ button.type = "button";
116
+ button.innerText = label;
117
+ button.addEventListener("click", () => setPreference(scheme));
118
+ buttons.set(scheme, button);
119
+ switcher.appendChild(button);
120
+ }
121
+ document.body.appendChild(switcher);
122
+ }
123
+
124
+ let storedPreference = null;
125
+ try {
126
+ storedPreference = localStorage.getItem(STORAGE_KEY);
127
+ } catch {
128
+ // storage unavailable — fall back to auto
129
+ }
130
+ setPreference(validSchemes.includes(storedPreference) ? storedPreference : "light dark");
131
+ }
132
+
133
+ initializeThemeSwitcher();