@pure-ds/storybook 0.5.30 → 0.5.31

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.
@@ -1,5 +1,5 @@
1
1
  import { addons } from '@storybook/preview-api';
2
- import { SELECT_STORY } from '@storybook/core-events';
2
+ import { SELECT_STORY, UPDATE_GLOBALS } from '@storybook/core-events';
3
3
  import React from 'react';
4
4
  import { Title, Subtitle, Description as DocsDescription, Controls } from '@storybook/blocks';
5
5
  import { PDS } from '@pds-src/js/pds.js';
@@ -121,6 +121,61 @@ PDS.addEventListener('pds:ready', (event) => {
121
121
  */
122
122
  // Track current view mode globally for other code to check
123
123
  let currentViewMode = 'story';
124
+ let toolbarTheme = null;
125
+
126
+ const setToolbarThemeValue = (value) => {
127
+ if (!value) return;
128
+ toolbarTheme = value;
129
+ };
130
+
131
+ const emitThemeGlobals = (value) => {
132
+ if (!value || value === toolbarTheme) return;
133
+
134
+ const channel = addons?.getChannel?.();
135
+ if (!channel) return;
136
+
137
+ toolbarTheme = value;
138
+ channel.emit(UPDATE_GLOBALS, {
139
+ globals: { theme: value }
140
+ });
141
+ };
142
+
143
+ const initThemeSync = (() => {
144
+ let initialized = false;
145
+ return () => {
146
+ if (initialized || typeof window === 'undefined') return;
147
+ initialized = true;
148
+
149
+ PDS.addEventListener('pds:theme:changed', (event) => {
150
+ emitThemeGlobals(event?.detail?.theme ?? PDS.theme);
151
+ });
152
+
153
+ const watchDomTheme = () => {
154
+ const targets = [document.body, document.documentElement].filter(Boolean);
155
+ if (!targets.length) return;
156
+
157
+ const syncFromDom = () => {
158
+ const nextTheme = document.body?.getAttribute('data-theme')
159
+ || document.documentElement?.getAttribute('data-theme');
160
+ emitThemeGlobals(nextTheme);
161
+ };
162
+
163
+ const observer = new MutationObserver(syncFromDom);
164
+ targets.forEach((target) => {
165
+ observer.observe(target, { attributes: true, attributeFilter: ['data-theme'] });
166
+ });
167
+ syncFromDom();
168
+ };
169
+
170
+ if (!document.body && document.readyState === 'loading') {
171
+ document.addEventListener('DOMContentLoaded', watchDomTheme, { once: true });
172
+ } else {
173
+ watchDomTheme();
174
+ }
175
+ };
176
+ })();
177
+
178
+ initThemeSync();
124
179
 
125
180
  const withPDS = (story, context) => {
126
181
  currentViewMode = context.viewMode;
@@ -318,6 +373,9 @@ const withGlobalsHandler = (story, context) => {
318
373
  document.body.setAttribute('data-theme', globals.theme);
319
374
  PDS.theme = globals.theme;
320
375
  }
376
+ if (globals?.theme) {
377
+ setToolbarThemeValue(globals.theme);
378
+ }
321
379
 
322
380
  return story();
323
381
  };
@@ -1452,6 +1510,7 @@ if (typeof window !== 'undefined') {
1452
1510
  PDS.theme = globals.theme;
1453
1511
 
1454
1512
  console.log('✅ Theme applied:', globals.theme);
1513
+ setToolbarThemeValue(globals.theme);
1455
1514
  }
1456
1515
 
1457
1516
  if (globals?.preset) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-01-22T15:56:09.074Z",
2
+ "generatedAt": "2026-01-23T07:20:42.584Z",
3
3
  "sources": {
4
4
  "customElements": "custom-elements.json",
5
5
  "ontology": "src\\js\\pds-core\\pds-ontology.js",
@@ -1928,6 +1928,90 @@
1928
1928
  ],
1929
1929
  "notes": []
1930
1930
  },
1931
+ "pds-theme": {
1932
+ "tag": "pds-theme",
1933
+ "className": "PdsTheme",
1934
+ "displayName": "Pds Theme",
1935
+ "storyTitle": "Components/Pds Theme",
1936
+ "category": "Components",
1937
+ "description": null,
1938
+ "docsDescription": null,
1939
+ "pdsTags": [
1940
+ "appearance",
1941
+ "autodocs",
1942
+ "pds-theme",
1943
+ "switcher",
1944
+ "theme",
1945
+ "toggle"
1946
+ ],
1947
+ "ontology": null,
1948
+ "stories": [],
1949
+ "sourceModule": "public/assets/pds/components/pds-theme.js",
1950
+ "superclass": "HTMLElement",
1951
+ "attributes": [
1952
+ {
1953
+ "name": "label",
1954
+ "description": null,
1955
+ "type": null,
1956
+ "default": null,
1957
+ "fieldName": null
1958
+ }
1959
+ ],
1960
+ "properties": [
1961
+ {
1962
+ "name": "label",
1963
+ "attribute": "label",
1964
+ "description": "Gets the legend/aria-label text to display.",
1965
+ "type": null,
1966
+ "default": null,
1967
+ "reflects": false,
1968
+ "privacy": "public"
1969
+ }
1970
+ ],
1971
+ "methods": [
1972
+ {
1973
+ "name": "attributeChangedCallback",
1974
+ "description": null,
1975
+ "parameters": [
1976
+ {
1977
+ "name": "name",
1978
+ "type": "any",
1979
+ "description": null,
1980
+ "optional": false
1981
+ },
1982
+ {
1983
+ "name": "oldValue",
1984
+ "type": "any",
1985
+ "description": null,
1986
+ "optional": false
1987
+ },
1988
+ {
1989
+ "name": "newValue",
1990
+ "type": "any",
1991
+ "description": null,
1992
+ "optional": false
1993
+ }
1994
+ ],
1995
+ "return": "void"
1996
+ },
1997
+ {
1998
+ "name": "connectedCallback",
1999
+ "description": null,
2000
+ "parameters": [],
2001
+ "return": "void"
2002
+ },
2003
+ {
2004
+ "name": "disconnectedCallback",
2005
+ "description": null,
2006
+ "parameters": [],
2007
+ "return": "void"
2008
+ }
2009
+ ],
2010
+ "events": [],
2011
+ "slots": [],
2012
+ "cssParts": [],
2013
+ "notes": []
2014
+ },
1931
2015
  "pds-toaster": {
1932
2016
  "tag": "pds-toaster",
1933
2017
  "className": "AppToaster",
@@ -4949,6 +5033,33 @@
4949
5033
  "packages\\pds-storybook\\stories\\components\\PdsTabstrip.stories.js"
4950
5034
  ]
4951
5035
  },
5036
+ "pds-theme": {
5037
+ "slug": "pds-theme",
5038
+ "storyTitle": "Components/Pds Theme",
5039
+ "category": "Components",
5040
+ "name": "Pds Theme",
5041
+ "description": null,
5042
+ "tags": [
5043
+ "appearance",
5044
+ "autodocs",
5045
+ "pds-theme",
5046
+ "switcher",
5047
+ "theme",
5048
+ "toggle"
5049
+ ],
5050
+ "pdsParameters": {
5051
+ "tags": [
5052
+ "theme",
5053
+ "appearance",
5054
+ "toggle",
5055
+ "pds-theme"
5056
+ ]
5057
+ },
5058
+ "stories": [],
5059
+ "files": [
5060
+ "packages\\pds-storybook\\stories\\components\\PdsTheme\\PdsTheme.stories.js"
5061
+ ]
5062
+ },
4952
5063
  "pds-toaster": {
4953
5064
  "slug": "pds-toaster",
4954
5065
  "storyTitle": "Components/Pds Toaster",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure-ds/storybook",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
4
4
  "description": "Storybook showcase for Pure Design System with live configuration",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,7 +38,7 @@
38
38
  "pds:build-icons": "pds-build-icons"
39
39
  },
40
40
  "peerDependencies": {
41
- "@pure-ds/core": "^0.5.30"
41
+ "@pure-ds/core": "^0.5.31"
42
42
  },
43
43
  "dependencies": {
44
44
  "@custom-elements-manifest/analyzer": "^0.11.0",
@@ -0,0 +1,170 @@
1
+ /**
2
+ * `<pds-theme>` exposes a zero-config theme toggle that updates `PDS.theme` and
3
+ * keeps its UI in sync with programmatic theme changes.
4
+ *
5
+ * @element pds-theme
6
+ * @attr {string} label - Optional legend text (defaults to "Theme").
7
+ */
8
+ const THEME_OPTIONS = [
9
+ { value: "system", label: "System", icon: "moon-stars" },
10
+ { value: "light", label: "Light", icon: "sun" },
11
+ { value: "dark", label: "Dark", icon: "moon" },
12
+ ];
13
+
14
+ const DEFAULT_LABEL = "Theme";
15
+ const LAYERS = ["tokens", "primitives", "components", "utilities"];
16
+
17
+ class PdsTheme extends HTMLElement {
18
+ static get observedAttributes() {
19
+ return ["label"];
20
+ }
21
+
22
+ #observer;
23
+ #listening = false;
24
+
25
+ constructor() {
26
+ super();
27
+ this.attachShadow({ mode: "open" });
28
+ }
29
+
30
+ connectedCallback() {
31
+ if (!this.shadowRoot.hasChildNodes()) {
32
+ void this.#setup();
33
+ } else {
34
+ this.#attachObserver();
35
+ this.#syncLegend();
36
+ this.#syncCheckedState();
37
+ }
38
+ }
39
+
40
+ disconnectedCallback() {
41
+ this.#teardownObserver();
42
+ }
43
+
44
+ attributeChangedCallback(name, oldValue, newValue) {
45
+ if (oldValue === newValue) return;
46
+ if (name === "label") {
47
+ this.#syncLegend();
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Gets the legend/aria-label text to display.
53
+ * @returns {string}
54
+ */
55
+ get label() {
56
+ return this.getAttribute("label")?.trim() || DEFAULT_LABEL;
57
+ }
58
+
59
+ set label(value) {
60
+ if (value == null || value === "") {
61
+ this.removeAttribute("label");
62
+ } else {
63
+ this.setAttribute("label", value);
64
+ }
65
+ }
66
+
67
+ async #setup() {
68
+ const componentStyles = PDS.createStylesheet(`
69
+ :host {
70
+ display: block;
71
+ }
72
+
73
+ label span {
74
+ display: inline-flex;
75
+ gap: var(--spacing-xs, 0.35rem);
76
+ align-items: center;
77
+ }
78
+ `);
79
+
80
+ await PDS.adoptLayers(this.shadowRoot, LAYERS, [componentStyles]);
81
+
82
+ this.shadowRoot.innerHTML = this.#template();
83
+
84
+ if (!this.#listening) {
85
+ this.shadowRoot.addEventListener("change", this.#handleChange);
86
+ this.#listening = true;
87
+ }
88
+
89
+ this.#attachObserver();
90
+ this.#syncLegend();
91
+ this.#syncCheckedState();
92
+ }
93
+
94
+ #template() {
95
+ const optionsMarkup = THEME_OPTIONS.map(
96
+ (option) => /*html*/`
97
+ <label part="option">
98
+ <input type="radio" name="theme" value="${option.value}" />
99
+ <span>
100
+ <pds-icon icon="${option.icon}" size="sm"></pds-icon>
101
+ ${option.label}
102
+ </span>
103
+ </label>`,
104
+ ).join("");
105
+
106
+ return /*html*/`
107
+ <form part="form">
108
+ <fieldset part="fieldset" role="radiogroup" aria-label="${DEFAULT_LABEL}" class="buttons">
109
+ <legend part="legend">${DEFAULT_LABEL}</legend>
110
+ ${optionsMarkup}
111
+ </fieldset>
112
+ </form>`;
113
+ }
114
+
115
+ #handleChange = (event) => {
116
+ const target = event.target;
117
+ if (!(target instanceof HTMLInputElement) || target.name !== "theme") {
118
+ return;
119
+ }
120
+
121
+ const { value } = target;
122
+ if (!THEME_OPTIONS.some((option) => option.value === value)) {
123
+ return;
124
+ }
125
+
126
+ if (PDS.theme !== value) {
127
+ PDS.theme = value;
128
+ }
129
+
130
+ };
131
+
132
+ #attachObserver() {
133
+ if (this.#observer) return;
134
+
135
+ this.#observer = new MutationObserver(() => {
136
+ this.#syncCheckedState();
137
+ });
138
+
139
+ this.#observer.observe(document.documentElement, {
140
+ attributes: true,
141
+ attributeFilter: ["data-theme"],
142
+ });
143
+ }
144
+
145
+ #teardownObserver() {
146
+ if (!this.#observer) return;
147
+ this.#observer.disconnect();
148
+ this.#observer = undefined;
149
+ }
150
+
151
+ #syncLegend() {
152
+ const legend = this.shadowRoot.querySelector("legend");
153
+ const fieldset = this.shadowRoot.querySelector("fieldset");
154
+ if (legend) legend.textContent = this.label;
155
+ if (fieldset) fieldset.setAttribute("aria-label", this.label);
156
+ }
157
+
158
+ #syncCheckedState() {
159
+ const currentTheme = PDS.theme || "system";
160
+ this.shadowRoot
161
+ .querySelectorAll('input[type="radio"]')
162
+ .forEach((radio) => {
163
+ radio.checked = radio.value === currentTheme;
164
+ });
165
+ }
166
+ }
167
+
168
+ if (!customElements.get("pds-theme")) {
169
+ customElements.define("pds-theme", PdsTheme);
170
+ }
@@ -775,6 +775,16 @@
775
775
  }
776
776
  ]
777
777
  },
778
+ {
779
+ "name": "pds-theme",
780
+ "description": "PdsTheme component",
781
+ "attributes": [
782
+ {
783
+ "name": "label",
784
+ "description": ""
785
+ }
786
+ ]
787
+ },
778
788
  {
779
789
  "name": "pds-toaster",
780
790
  "description": "AppToaster component"
@@ -0,0 +1,68 @@
1
+ import { html } from 'lit';
2
+ import { createComponentDocsPage } from '../../reference/reference-docs.js';
3
+
4
+ const componentDescription = `The \`<pds-theme>\` component lets users switch between **system**, **light**, and **dark** modes.
5
+ It updates \`PDS.theme\`, stays in sync with programmatic changes, and emits \`pds:theme:changed\` so bootstrap code can react (for example, to show a toast).
6
+
7
+ ---
8
+
9
+ ## Quick Reference
10
+
11
+ | Attribute | Type | Default | Description |
12
+ |-----------|------|---------|-------------|
13
+ | \`label\` | string | \`"Theme"\` | Custom legend + aria-label |
14
+
15
+ Listen for global theme updates with \`PDS.addEventListener('pds:theme:changed', handler)\` when other UI needs to respond.
16
+ `;
17
+
18
+ const docsParameters = {
19
+ description: {
20
+ component: componentDescription
21
+ },
22
+ page: createComponentDocsPage('pds-theme')
23
+ };
24
+
25
+ const renderThemeToggle = (args) => html`
26
+ <pds-theme
27
+ label=${args.label || ''}
28
+ ></pds-theme>
29
+ `;
30
+
31
+ export default {
32
+ title: 'Components/Pds Theme',
33
+ tags: ['autodocs', 'theme', 'appearance', 'pds-theme', 'switcher'],
34
+ parameters: {
35
+ pds: {
36
+ tags: ['theme', 'appearance', 'toggle', 'pds-theme']
37
+ },
38
+ docs: docsParameters
39
+ },
40
+ argTypes: {
41
+ label: {
42
+ control: 'text',
43
+ description: 'Legend + aria-label text'
44
+ }
45
+ }
46
+ };
47
+
48
+ export const Playground = {
49
+ name: 'Interactive Playground',
50
+ args: {
51
+ label: 'Theme'
52
+ },
53
+ parameters: {
54
+ docs: {
55
+ description: {
56
+ story: 'Use the controls to adjust the component label.'
57
+ }
58
+ }
59
+ },
60
+ render: (args) => html`
61
+ <div class="stack-md">
62
+ <p class="text-muted">The toggle updates <code>PDS.theme</code> so the rest of your UI stays in sync.</p>
63
+ ${renderThemeToggle(args)}
64
+ </div>
65
+ `
66
+ };
67
+
68
+
@@ -21,7 +21,6 @@ What you get (key files):
21
21
  - `pds.config.js` — your design system config
22
22
  - `src/js/app.js` — bootstraps PDS and mounts the app
23
23
  - `public/assets/my/my-home.js` — your first web component
24
- - `public/assets/my/my-theme.js` — simple dark/light mode switcher
25
24
  - `public/index.html` — app shell
26
25
 
27
26
  **Next edits:**