@pure-ds/storybook 0.4.5 → 0.4.7

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,8 +1,9 @@
1
- import React, { useState, useCallback } from 'react';
1
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
2
2
  import { useChannel } from '@storybook/manager-api';
3
3
  import { IconButton } from '@storybook/components';
4
4
  import { EVENTS } from './constants.js';
5
5
  import { styled } from '@storybook/theming';
6
+ import { loadShiki, escapeHtml } from '../../shiki.js';
6
7
 
7
8
  const Container = styled.div`
8
9
  position: relative;
@@ -21,22 +22,16 @@ const CodeBlock = styled.pre`
21
22
  color: ${props => props.theme.color.defaultText};
22
23
  background: transparent;
23
24
  tab-size: 2;
24
-
25
- .html-token-tag {
26
- color: #e06c75;
27
- }
28
-
29
- .html-token-attr {
30
- color: #d19a66;
31
- }
32
-
33
- .html-token-value {
34
- color: #98c379;
25
+
26
+ /* Shiki generates its own pre/code, style the inner content */
27
+ .shiki {
28
+ background: transparent !important;
29
+ margin: 0;
30
+ padding: 0;
35
31
  }
36
-
37
- .html-token-comment {
38
- color: #5c6370;
39
- font-style: italic;
32
+
33
+ .shiki code {
34
+ background: transparent;
40
35
  }
41
36
  `;
42
37
 
@@ -115,6 +110,50 @@ const CheckIcon = () => (
115
110
  export const Panel = ({ active }) => {
116
111
  const [source, setSource] = useState({ markup: '', jsonForms: [] });
117
112
  const [copied, setCopied] = useState(false);
113
+ const [highlightedMarkup, setHighlightedMarkup] = useState('');
114
+ const shikiRef = useRef(null);
115
+
116
+ // Load Shiki once on mount
117
+ useEffect(() => {
118
+ loadShiki().then(highlighter => {
119
+ shikiRef.current = highlighter;
120
+ });
121
+ }, []);
122
+
123
+ // Highlight markup when source changes
124
+ useEffect(() => {
125
+ if (!source.markup) {
126
+ setHighlightedMarkup('');
127
+ return;
128
+ }
129
+
130
+ const highlighter = shikiRef.current;
131
+ if (highlighter) {
132
+ try {
133
+ const html = highlighter.codeToHtml(source.markup, {
134
+ lang: 'html',
135
+ theme: 'github-dark'
136
+ });
137
+ setHighlightedMarkup(html);
138
+ } catch (err) {
139
+ setHighlightedMarkup(`<pre><code>${escapeHtml(source.markup)}</code></pre>`);
140
+ }
141
+ } else {
142
+ // Fallback while Shiki loads
143
+ setHighlightedMarkup(`<pre><code>${escapeHtml(source.markup)}</code></pre>`);
144
+ // Retry when Shiki becomes available
145
+ loadShiki().then(hl => {
146
+ if (hl && source.markup) {
147
+ try {
148
+ const html = hl.codeToHtml(source.markup, { lang: 'html', theme: 'github-dark' });
149
+ setHighlightedMarkup(html);
150
+ } catch (err) {
151
+ // Keep fallback
152
+ }
153
+ }
154
+ });
155
+ }
156
+ }, [source.markup]);
118
157
 
119
158
  useChannel({
120
159
  [EVENTS.UPDATE_HTML]: (payload) => {
@@ -162,94 +201,6 @@ export const Panel = ({ active }) => {
162
201
  }
163
202
  }, [source.markup]);
164
203
 
165
- const highlightHTML = useCallback((code) => {
166
- if (!code) return '';
167
-
168
- let result = '';
169
- let i = 0;
170
-
171
- const escapeHtml = (text) => {
172
- return text
173
- .replace(/&/g, '&amp;')
174
- .replace(/</g, '&lt;')
175
- .replace(/>/g, '&gt;');
176
- };
177
-
178
- while (i < code.length) {
179
- // Handle HTML comments
180
- if (code.substr(i, 4) === '<!--') {
181
- const end = code.indexOf('-->', i);
182
- if (end !== -1) {
183
- const comment = code.substring(i, end + 3);
184
- result += `<span class="html-token-comment">${escapeHtml(comment)}</span>`;
185
- i = end + 3;
186
- continue;
187
- }
188
- }
189
-
190
- // Handle tags
191
- if (code[i] === '<') {
192
- const tagEnd = code.indexOf('>', i);
193
- if (tagEnd !== -1) {
194
- const tagContent = code.substring(i + 1, tagEnd);
195
- result += '&lt;';
196
-
197
- // Check if it's a closing tag
198
- if (tagContent[0] === '/') {
199
- result += '/';
200
- const tagName = tagContent.substring(1).split(/[\s>]/)[0];
201
- result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
202
- } else {
203
- // Parse tag name and attributes
204
- const parts = tagContent.match(/^([\w-]+)([\s\S]*?)(\/?)?$/);
205
- if (parts) {
206
- const [, tagName, attrsStr, slash] = parts;
207
- result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
208
-
209
- // Parse attributes
210
- if (attrsStr.trim()) {
211
- const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
212
- let match;
213
- let lastIndex = 0;
214
-
215
- while ((match = attrRegex.exec(attrsStr)) !== null) {
216
- result += escapeHtml(attrsStr.substring(lastIndex, match.index));
217
-
218
- const [fullMatch, attrName, doubleQuoted, singleQuoted, unquoted] = match;
219
- result += `<span class="html-token-attr">${escapeHtml(attrName)}</span>`;
220
-
221
- if (doubleQuoted !== undefined) {
222
- result += `=<span class="html-token-value">"${escapeHtml(doubleQuoted)}"</span>`;
223
- } else if (singleQuoted !== undefined) {
224
- result += `=<span class="html-token-value">'${escapeHtml(singleQuoted)}'</span>`;
225
- } else if (unquoted !== undefined) {
226
- result += `=<span class="html-token-value">${escapeHtml(unquoted)}</span>`;
227
- }
228
-
229
- lastIndex = match.index + fullMatch.length;
230
- }
231
-
232
- result += escapeHtml(attrsStr.substring(lastIndex));
233
- }
234
-
235
- if (slash) result += '/';
236
- }
237
- }
238
-
239
- result += '&gt;';
240
- i = tagEnd + 1;
241
- continue;
242
- }
243
- }
244
-
245
- // Regular text
246
- result += escapeHtml(code[i]);
247
- i++;
248
- }
249
-
250
- return result;
251
- }, []);
252
-
253
204
  const hasMarkup = Boolean(source.markup);
254
205
  const hasJsonForms = source.jsonForms.length > 0;
255
206
 
@@ -269,9 +220,7 @@ export const Panel = ({ active }) => {
269
220
  {hasMarkup && (
270
221
  <SectionWrapper>
271
222
  <SectionHeading>Markup</SectionHeading>
272
- <CodeBlock>
273
- <code dangerouslySetInnerHTML={{ __html: highlightHTML(source.markup) }} />
274
- </CodeBlock>
223
+ <CodeBlock dangerouslySetInnerHTML={{ __html: highlightedMarkup }} />
275
224
  </SectionWrapper>
276
225
  )}
277
226
 
@@ -0,0 +1,44 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { useChannel } from '@storybook/manager-api';
3
+ import { IconButton } from '@storybook/components';
4
+ import { EVENTS } from './constants.js';
5
+
6
+ export const SearchTool = () => {
7
+ const [isOpen, setIsOpen] = useState(false);
8
+
9
+ const channel = useChannel({
10
+ [EVENTS.QUERY_EXECUTED + '_RESPONSE']: (data) => {
11
+ console.log('Query response received:', data);
12
+ }
13
+ });
14
+
15
+ const toggleSearch = useCallback(() => {
16
+ const newState = !isOpen;
17
+ console.log('Toggle search - current isOpen:', isOpen, 'newState:', newState);
18
+
19
+ if (newState) {
20
+ // Emit event to open search in preview
21
+ const searchQuery = prompt('Enter search query (e.g., "primary color", "spacing", "button"):');
22
+ if (searchQuery) {
23
+ console.log('Executing search query:', searchQuery);
24
+ channel.emit(EVENTS.QUERY_EXECUTED, { query: searchQuery });
25
+ }
26
+ }
27
+
28
+ setIsOpen(newState);
29
+ }, [isOpen, channel]);
30
+
31
+ return (
32
+ <IconButton
33
+ key="search-tool"
34
+ active={isOpen}
35
+ title="Quick Search PDS"
36
+ onClick={toggleSearch}
37
+ >
38
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
39
+ <circle cx="11" cy="11" r="8" />
40
+ <path d="m21 21-4.35-4.35" />
41
+ </svg>
42
+ </IconButton>
43
+ );
44
+ };
@@ -33,59 +33,33 @@
33
33
  padding: var(--spacing-4, 1rem);
34
34
  }
35
35
 
36
- .html-source-pre {
36
+ /* Shiki-generated code blocks */
37
+ .html-source-content pre {
37
38
  margin: 0;
38
39
  padding: var(--spacing-4, 1rem);
39
40
  border-radius: var(--radius-md, 4px);
40
41
  overflow-x: auto;
41
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
42
42
  font-size: 0.875rem;
43
43
  line-height: 1.6;
44
44
  tab-size: 2;
45
45
  }
46
46
 
47
- /* Syntax highlighting colors - work in both light and dark modes */
48
- :root {
49
- --code-bg: #282c34;
50
- --code-text: #abb2bf;
51
- --code-tag: #e06c75;
52
- --code-attr: #d19a66;
53
- --code-value: #98c379;
54
- --code-comment: #5c6370;
55
- }
56
-
57
- [data-theme="dark"] {
58
- --code-bg: #1e1e1e;
59
- --code-text: #d4d4d4;
60
- --code-tag: #569cd6;
61
- --code-attr: #9cdcfe;
62
- --code-value: #ce9178;
63
- --code-comment: #6a9955;
47
+ .html-source-content pre code {
48
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
64
49
  }
65
50
 
51
+ /* Fallback pre styling (before Shiki loads) */
66
52
  .html-source-pre {
67
- background: var(--code-bg);
68
- }
69
-
70
- .html-source-code {
71
- color: var(--code-text);
72
- }
73
-
74
- .html-token-tag {
75
- color: var(--code-tag);
76
- }
77
-
78
- .html-token-attr {
79
- color: var(--code-attr);
80
- }
81
-
82
- .html-token-value {
83
- color: var(--code-value);
84
- }
85
-
86
- .html-token-comment {
87
- color: var(--code-comment);
88
- font-style: italic;
53
+ margin: 0;
54
+ padding: var(--spacing-4, 1rem);
55
+ border-radius: var(--radius-md, 4px);
56
+ overflow-x: auto;
57
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
58
+ font-size: 0.875rem;
59
+ line-height: 1.6;
60
+ tab-size: 2;
61
+ background: var(--color-surface-raised, #282c34);
62
+ color: var(--color-text-primary, #abb2bf);
89
63
  }
90
64
 
91
65
  pds-config-form {
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * HTML Preview and Copy Utility for Storybook
3
3
  * Provides HTML source viewing and copy functionality for all stories
4
- * Uses Storybook's built-in source code display in Docs mode
4
+ * Uses Shiki for syntax highlighting
5
5
  */
6
6
 
7
7
  import { render as litRender } from 'lit';
8
+ import { highlight, getCurrentTheme, escapeHtml, preloadShiki } from './shiki.js';
9
+
10
+ // Pre-load Shiki in the background
11
+ preloadShiki();
8
12
 
9
13
  /**
10
14
  * Format HTML string with proper indentation
@@ -145,96 +149,14 @@ export const withHTMLSource = (storyFn, context) => {
145
149
  }
146
150
 
147
151
  const codeEl = sourceSection.querySelector('.html-source-code');
148
- if (codeEl && html) {
149
- // Simple syntax highlighter that properly tokenizes HTML
150
- const highlightHTML = (code) => {
151
- let result = '';
152
- let i = 0;
153
-
154
- while (i < code.length) {
155
- // Handle HTML comments
156
- if (code.substr(i, 4) === '<!--') {
157
- const end = code.indexOf('-->', i);
158
- if (end !== -1) {
159
- const comment = code.substring(i, end + 3);
160
- result += `<span class="html-token-comment">${escapeHtml(comment)}</span>`;
161
- i = end + 3;
162
- continue;
163
- }
164
- }
165
-
166
- // Handle tags
167
- if (code[i] === '<') {
168
- const tagEnd = code.indexOf('>', i);
169
- if (tagEnd !== -1) {
170
- const tagContent = code.substring(i + 1, tagEnd);
171
- result += '&lt;';
172
-
173
- // Check if it's a closing tag
174
- if (tagContent[0] === '/') {
175
- result += '/';
176
- const tagName = tagContent.substring(1).split(/[\s>]/)[0];
177
- result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
178
- } else {
179
- // Parse tag name and attributes
180
- const parts = tagContent.match(/^([\w-]+)([\s\S]*?)(\/?)?$/);
181
- if (parts) {
182
- const [, tagName, attrsStr, slash] = parts;
183
- result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
184
-
185
- // Parse attributes
186
- if (attrsStr.trim()) {
187
- const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
188
- let match;
189
- let lastIndex = 0;
190
-
191
- while ((match = attrRegex.exec(attrsStr)) !== null) {
192
- // Add whitespace before attribute
193
- result += escapeHtml(attrsStr.substring(lastIndex, match.index));
194
-
195
- const [fullMatch, attrName, doubleQuoted, singleQuoted, unquoted] = match;
196
- result += `<span class="html-token-attr">${escapeHtml(attrName)}</span>`;
197
-
198
- if (doubleQuoted !== undefined) {
199
- result += `=<span class="html-token-value">"${escapeHtml(doubleQuoted)}"</span>`;
200
- } else if (singleQuoted !== undefined) {
201
- result += `=<span class="html-token-value">'${escapeHtml(singleQuoted)}'</span>`;
202
- } else if (unquoted !== undefined) {
203
- result += `=<span class="html-token-value">${escapeHtml(unquoted)}</span>`;
204
- }
205
-
206
- lastIndex = match.index + fullMatch.length;
207
- }
208
-
209
- result += escapeHtml(attrsStr.substring(lastIndex));
210
- }
211
-
212
- if (slash) result += '/';
213
- }
214
- }
215
-
216
- result += '&gt;';
217
- i = tagEnd + 1;
218
- continue;
219
- }
220
- }
221
-
222
- // Regular text
223
- result += escapeHtml(code[i]);
224
- i++;
225
- }
226
-
227
- return result;
228
- };
229
-
230
- const escapeHtml = (text) => {
231
- return text
232
- .replace(/&/g, '&amp;')
233
- .replace(/</g, '&lt;')
234
- .replace(/>/g, '&gt;');
235
- };
152
+ const preEl = sourceSection.querySelector('.html-source-pre');
153
+ if (preEl && html) {
154
+ // Use Shiki for syntax highlighting
155
+ const theme = getCurrentTheme();
156
+ const highlighted = await highlight(html, 'html', theme);
236
157
 
237
- codeEl.innerHTML = highlightHTML(html);
158
+ // Shiki returns complete <pre><code>...</code></pre>, replace the whole pre element
159
+ preEl.outerHTML = highlighted;
238
160
  }
239
161
 
240
162
  // Setup copy button
@@ -32,7 +32,8 @@ async function loadOntology() {
32
32
  if (referenceData) return referenceData;
33
33
 
34
34
  try {
35
- const response = await fetch('/pds-data/pds-reference.json');
35
+ // Use relative path so it works both in dev and when deployed to a subdirectory
36
+ const response = await fetch('./pds-data/pds-reference.json');
36
37
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
37
38
  referenceData = await response.json();
38
39
  buildIndices();
@@ -287,7 +287,7 @@ const withGlobalsHandler = (story, context) => {
287
287
  const DEFAULT_STORY_TAGS = new Set(['dev', 'test', 'story', 'stories', 'autodocs', 'example', 'examples']);
288
288
 
289
289
  const TAG_SYNONYMS = new Map([
290
- ['padding', 'spacing'],
290
+ ['padding', 'spacing', 'space'],
291
291
  ['gap', 'spacing'],
292
292
  ['grid', 'layout'],
293
293
  ['flex', 'layout'],
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared Shiki syntax highlighter for PDS Storybook
3
+ * Provides consistent code highlighting across all stories and previews
4
+ *
5
+ * Re-exports from stories/utils/shiki.js to maintain a single source of truth
6
+ */
7
+
8
+ export {
9
+ loadShiki,
10
+ highlight,
11
+ getCurrentTheme,
12
+ escapeHtml,
13
+ preloadShiki
14
+ } from '../stories/utils/shiki.js';
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2025-12-24T10:33:42.738Z",
2
+ "generatedAt": "2025-12-30T11:12:24.605Z",
3
3
  "sources": {
4
4
  "customElements": "custom-elements.json",
5
5
  "ontology": "src\\js\\pds-core\\pds-ontology.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure-ds/storybook",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Storybook showcase for Pure Design System with live configuration",
5
5
  "type": "module",
6
6
  "private": false,
@@ -35,7 +35,7 @@
35
35
  "storybook:build": "npm run build-storybook"
36
36
  },
37
37
  "peerDependencies": {
38
- "@pure-ds/core": "^0.4.5"
38
+ "@pure-ds/core": "^0.4.7"
39
39
  },
40
40
  "dependencies": {
41
41
  "@custom-elements-manifest/analyzer": "^0.11.0",
@@ -3684,7 +3684,7 @@ export const pdsConfig = ${JSON.stringify(this.config,null,2)};
3684
3684
  `,setTimeout(()=>{p.innerHTML=`
3685
3685
  <pds-icon icon="clipboard" size="sm"></pds-icon>
3686
3686
  Copy HTML
3687
- `},2e3)})})},100)}async loadShiki(){if(this.#a)return this.#a;if(this.#e){for(;this.#e;)await new Promise(r=>setTimeout(r,100));return this.#a}this.#e=!0;try{let r=await import("https://esm.sh/shiki@1.0.0");return this.#a=await r.getHighlighter({themes:["dark-plus"],langs:["html"]}),this.#a}catch(r){return console.error("Failed to load Shiki:",r),null}finally{this.#e=!1}}async highlightWithShiki(r){let e=await this.loadShiki();if(!e)return this.escapeHTML(r);try{let a=e.codeToHtml(r,{lang:"html",theme:"dark-plus"}).match(/<code[^>]*>([\s\S]*)<\/code>/);return a?a[1]:this.escapeHTML(r)}catch(t){return console.error("Shiki highlighting failed:",t),this.escapeHTML(r)}}escapeHTML(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}async getShowdownConverter(){if(this._showdown)return this._showdown;let r=await this.loadShowdownFromCDN();return!r||!r.Converter?null:(this._showdown=new r.Converter({ghCompatibleHeaderId:!0,tables:!0,strikethrough:!0,tasklists:!0}),this._showdown)}async loadShowdownFromCDN(){if(typeof window<"u"&&window.showdown)return window.showdown;if(this._showdownLoading){for(;this._showdownLoading;)await new Promise(e=>setTimeout(e,50));return window.showdown||null}this._showdownLoading=!0;let r=["https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js","https://unpkg.com/showdown@2.1.0/dist/showdown.min.js"];for(let e of r)try{if(await this._injectScript(e,"showdown"),window.showdown)return this._showdownLoading=!1,window.showdown}catch{}return this._showdownLoading=!1,null}_injectScript(r,e){return new Promise((t,a)=>{if(document.querySelector(`script[data-lib="${e}"][src="${r}"]`)){setTimeout(t,0);return}let o=document.createElement("script");o.src=r,o.async=!0,o.defer=!0,o.dataset.lib=e||"lib",o.onload=()=>t(),o.onerror=()=>{o.remove(),a(new Error(`Failed to load script: ${r}`))},document.head.appendChild(o)})}scrollToRelevantSection(r){console.log("\u{1F3AF} Scrolling to section for field:",r);let e=r.startsWith("/")?r.slice(1):r;console.log(" Normalized path:",e);let t={"colors/primary":"color-system","colors/secondary":"color-system","colors/accent":"color-system","colors/background":"color-system","colors/success":"color-system","colors/warning":"color-system","colors/danger":"color-system","colors/info":"color-system","typography/":"typography","spatialRhythm/":"spacing","layers/":"surfaces-shadows","shape/":"buttons","behavior/transitionSpeed":"interactive-states","behavior/":"interactive-states","components/forms":"forms","components/alerts":"alerts","components/badges":"badges","components/tables":"tables","components/toasts":"toasts","components/modals":"modals","components/tabStrip":"tabs","icons/":"icons"},a=null;for(let[o,i]of Object.entries(t))if(e.startsWith(o)){a=i,console.log(` \u2713 Matched pattern "${o}" \u2192 section "${i}"`);break}if(a){let o=this.querySelector(`[data-section="${a}"]`);console.log(` Searching for section: [data-section="${a}"]`,o?"\u2713 Found":"\u2717 Not found"),o?(o.scrollIntoView({behavior:"smooth",block:"start"}),o.style.transition="background-color 0.3s ease",o.style.backgroundColor="var(--color-primary-50)",setTimeout(()=>{o.style.backgroundColor=""},1500),console.log(" \u2713 Scrolled and highlighted section")):console.warn(` \u2717 Section [data-section="${a}"] not found in DOM`)}else console.warn(` \u2717 No section mapping found for field: ${r}`)}renderDisabledSection(r,e){return S`
3687
+ `},2e3)})})},100)}async loadShiki(){if(this.#a)return this.#a;if(this.#e){for(;this.#e;)await new Promise(r=>setTimeout(r,50));return this.#a}this.#e=!0;try{let r=await import("https://esm.sh/shiki@1.0.0");return this.#a=await r.getHighlighter({themes:["github-dark","github-light"],langs:["html","css","javascript","json"]}),this.#a}catch(r){return console.error("Failed to load Shiki:",r),null}finally{this.#e=!1}}async highlightWithShiki(r,e="html"){let t=await this.loadShiki();if(!t)return this.escapeHTML(r);try{let o=document.documentElement.getAttribute("data-theme")==="dark"?"github-dark":"github-light",s=t.codeToHtml(r,{lang:e,theme:o}).match(/<code[^>]*>([\s\S]*)<\/code>/);return s?s[1]:this.escapeHTML(r)}catch(a){return console.error("Shiki highlighting failed:",a),this.escapeHTML(r)}}escapeHTML(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}async getShowdownConverter(){if(this._showdown)return this._showdown;let r=await this.loadShowdownFromCDN();return!r||!r.Converter?null:(this._showdown=new r.Converter({ghCompatibleHeaderId:!0,tables:!0,strikethrough:!0,tasklists:!0}),this._showdown)}async loadShowdownFromCDN(){if(typeof window<"u"&&window.showdown)return window.showdown;if(this._showdownLoading){for(;this._showdownLoading;)await new Promise(e=>setTimeout(e,50));return window.showdown||null}this._showdownLoading=!0;let r=["https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js","https://unpkg.com/showdown@2.1.0/dist/showdown.min.js"];for(let e of r)try{if(await this._injectScript(e,"showdown"),window.showdown)return this._showdownLoading=!1,window.showdown}catch{}return this._showdownLoading=!1,null}_injectScript(r,e){return new Promise((t,a)=>{if(document.querySelector(`script[data-lib="${e}"][src="${r}"]`)){setTimeout(t,0);return}let o=document.createElement("script");o.src=r,o.async=!0,o.defer=!0,o.dataset.lib=e||"lib",o.onload=()=>t(),o.onerror=()=>{o.remove(),a(new Error(`Failed to load script: ${r}`))},document.head.appendChild(o)})}scrollToRelevantSection(r){console.log("\u{1F3AF} Scrolling to section for field:",r);let e=r.startsWith("/")?r.slice(1):r;console.log(" Normalized path:",e);let t={"colors/primary":"color-system","colors/secondary":"color-system","colors/accent":"color-system","colors/background":"color-system","colors/success":"color-system","colors/warning":"color-system","colors/danger":"color-system","colors/info":"color-system","typography/":"typography","spatialRhythm/":"spacing","layers/":"surfaces-shadows","shape/":"buttons","behavior/transitionSpeed":"interactive-states","behavior/":"interactive-states","components/forms":"forms","components/alerts":"alerts","components/badges":"badges","components/tables":"tables","components/toasts":"toasts","components/modals":"modals","components/tabStrip":"tabs","icons/":"icons"},a=null;for(let[o,i]of Object.entries(t))if(e.startsWith(o)){a=i,console.log(` \u2713 Matched pattern "${o}" \u2192 section "${i}"`);break}if(a){let o=this.querySelector(`[data-section="${a}"]`);console.log(` Searching for section: [data-section="${a}"]`,o?"\u2713 Found":"\u2717 Not found"),o?(o.scrollIntoView({behavior:"smooth",block:"start"}),o.style.transition="background-color 0.3s ease",o.style.backgroundColor="var(--color-primary-50)",setTimeout(()=>{o.style.backgroundColor=""},1500),console.log(" \u2713 Scrolled and highlighted section")):console.warn(` \u2717 Section [data-section="${a}"] not found in DOM`)}else console.warn(` \u2717 No section mapping found for field: ${r}`)}renderDisabledSection(r,e){return S`
3688
3688
  <section class="showcase-section disabled">
3689
3689
  <h2>${r}</h2>
3690
3690
  <p class="disabled-message">${e}</p>
@@ -53,7 +53,7 @@ const DEFAULT_OPTIONS = {
53
53
  *
54
54
  * 5. Completely custom actions (hides default buttons):
55
55
  * <pds-jsonform .jsonSchema=${schema} hide-actions>
56
- * <div slot="actions" style="display: flex; gap: 1rem;">
56
+ * <div slot="actions" class="flex gap-md">
57
57
  * <button type="submit" class="btn btn-primary">Custom Submit</button>
58
58
  * <button type="button" class="btn">Custom Action</button>
59
59
  * </div>
@@ -416,10 +416,7 @@ export class SchemaForm extends LitElement {
416
416
  render() {
417
417
  const tree = this.#compiled;
418
418
  if (!tree)
419
- return html`<div
420
- class="pds-jsonform-error"
421
- style="color: red; padding: 1rem; border: 1px solid red; background: #fee;"
422
- >
419
+ return html`<div class="alert alert-error">
423
420
  <p>Failed to generate form schema.</p>
424
421
  <pre>${JSON.stringify(this.#data, null, 2)}</pre>
425
422
  </div>`;
@@ -431,6 +428,7 @@ export class SchemaForm extends LitElement {
431
428
  : "post";
432
429
  return html`
433
430
  <form
431
+ ?data-required=${this.hasAttribute("data-required")}
434
432
  method=${m}
435
433
  action=${this.action ?? nothing}
436
434
  @submit=${this.#onSubmit}
@@ -501,9 +499,8 @@ export class SchemaForm extends LitElement {
501
499
  // Check for surface wrapping
502
500
  const surface = ui?.["ui:surface"] || pathOptions.surface;
503
501
 
504
- // Build layout classes and inline styles
502
+ // Build layout classes using PDS utilities
505
503
  const layoutClasses = [];
506
- let layoutStyle = "";
507
504
  const layoutOptions = ui?.["ui:layoutOptions"] || {};
508
505
 
509
506
  if (layout === "flex") {
@@ -511,16 +508,8 @@ export class SchemaForm extends LitElement {
511
508
  if (layoutOptions.wrap) layoutClasses.push("flex-wrap");
512
509
  if (layoutOptions.direction === "column") layoutClasses.push("flex-col");
513
510
  if (layoutOptions.gap) {
514
- // Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
515
- if (
516
- layoutOptions.gap.startsWith("var(") ||
517
- layoutOptions.gap.includes("px") ||
518
- layoutOptions.gap.includes("rem")
519
- ) {
520
- layoutStyle += `gap: ${layoutOptions.gap};`;
521
- } else {
522
- layoutClasses.push(`gap-${layoutOptions.gap}`);
523
- }
511
+ // Use PDS gap utility classes (xs, sm, md, lg, xl, 0)
512
+ layoutClasses.push(`gap-${layoutOptions.gap}`);
524
513
  }
525
514
  } else if (layout === "grid") {
526
515
  layoutClasses.push("grid");
@@ -532,16 +521,8 @@ export class SchemaForm extends LitElement {
532
521
  layoutClasses.push(`grid-cols-${cols}`);
533
522
  }
534
523
  if (layoutOptions.gap) {
535
- // Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
536
- if (
537
- layoutOptions.gap.startsWith("var(") ||
538
- layoutOptions.gap.includes("px") ||
539
- layoutOptions.gap.includes("rem")
540
- ) {
541
- layoutStyle += `gap: ${layoutOptions.gap};`;
542
- } else {
543
- layoutClasses.push(`gap-${layoutOptions.gap}`);
544
- }
524
+ // Use PDS gap utility classes (xs, sm, md, lg, xl, 0)
525
+ layoutClasses.push(`gap-${layoutOptions.gap}`);
545
526
  }
546
527
  }
547
528
 
@@ -553,7 +534,6 @@ export class SchemaForm extends LitElement {
553
534
  <fieldset
554
535
  data-path=${node.path}
555
536
  class=${ifDefined(fieldsetClass)}
556
- style=${ifDefined(layoutStyle || undefined)}
557
537
  >
558
538
  ${!this.hideLegend && !context.hideLegend
559
539
  ? html`<legend>${legend}</legend>`
@@ -1612,7 +1592,7 @@ export class SchemaForm extends LitElement {
1612
1592
  richtextOpts.placeholder || attrs.placeholder
1613
1593
  )}
1614
1594
  .value=${value ?? ""}
1615
- toolbar=${ifDefined(richtextOpts.toolbar)}
1595
+ ?toolbar=${richtextOpts.toolbar}
1616
1596
  ?required=${!!attrs.required}
1617
1597
  ?submit-on-enter=${richtextOpts.submitOnEnter ?? false}
1618
1598
  spellcheck=${richtextOpts.spellcheck ?? true ? "true" : "false"}
@@ -733,12 +733,7 @@ html[data-theme="dark"] {
733
733
  overflow-x: hidden;
734
734
  -webkit-overflow-scrolling: touch;
735
735
  }
736
-
737
- :where(body.drawer-open) {
738
- /* overflow: hidden; */
739
- /* scrollbar-gutter: stable; */
740
- }
741
-
736
+
742
737
  /* Button primitives */
743
738
  :where(button) {
744
739
  all: unset;
@@ -1459,6 +1454,7 @@ input[type="range"]:active::-moz-range-thumb {
1459
1454
  }
1460
1455
 
1461
1456
  input[type="color"] {
1457
+ appearance: none;
1462
1458
  -webkit-appearance: none;
1463
1459
  padding: 0;
1464
1460
  width: 3rem;