@pure-ds/core 0.7.19 → 0.7.21

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.
Files changed (33) hide show
  1. package/.cursorrules +10 -0
  2. package/.github/copilot-instructions.md +10 -0
  3. package/custom-elements.json +232 -25
  4. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  5. package/dist/types/public/assets/pds/components/pds-code.d.ts +19 -0
  6. package/dist/types/public/assets/pds/components/pds-code.d.ts.map +1 -0
  7. package/dist/types/public/assets/pds/components/pds-icon.d.ts +31 -1
  8. package/dist/types/public/assets/pds/components/pds-icon.d.ts.map +1 -1
  9. package/dist/types/public/assets/pds/components/pds-treeview.d.ts +271 -16
  10. package/dist/types/public/assets/pds/components/pds-treeview.d.ts.map +1 -1
  11. package/dist/types/src/js/components/pds-code.d.ts +19 -0
  12. package/dist/types/src/js/components/pds-code.d.ts.map +1 -0
  13. package/dist/types/src/js/external/shiki.d.ts +3 -0
  14. package/dist/types/src/js/external/shiki.d.ts.map +1 -0
  15. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  16. package/package.json +1 -1
  17. package/packages/pds-cli/bin/templates/bootstrap/public/assets/my/my-home.js +1 -1
  18. package/public/assets/js/app.js +1 -1
  19. package/public/assets/js/pds-manager.js +138 -148
  20. package/public/assets/pds/components/pds-calendar.js +504 -16
  21. package/public/assets/pds/components/pds-code.js +203 -0
  22. package/public/assets/pds/components/pds-icon.js +102 -27
  23. package/public/assets/pds/components/pds-live-importer.js +2 -2
  24. package/public/assets/pds/components/pds-scrollrow.js +27 -2
  25. package/public/assets/pds/components/pds-treeview.js +185 -0
  26. package/public/assets/pds/core/pds-manager.js +138 -148
  27. package/public/assets/pds/custom-elements.json +263 -18
  28. package/public/assets/pds/external/shiki.js +32 -0
  29. package/public/assets/pds/pds-css-complete.json +1 -1
  30. package/public/assets/pds/templates/feedback-ops-dashboard.html +1 -1
  31. package/public/assets/pds/templates/release-readiness-radar.html +2 -2
  32. package/public/assets/pds/templates/support-command-center.html +1 -1
  33. package/src/js/pds-core/pds-generator.js +142 -152
@@ -0,0 +1,203 @@
1
+ const SHIKI_CDN_URL = 'https://esm.sh/shiki@1.0.0';
2
+
3
+ const SHIKI_LANGS = ['html', 'css', 'javascript', 'typescript', 'json', 'bash', 'shell'];
4
+ const SHIKI_THEMES = ['github-dark', 'github-light'];
5
+
6
+ let shikiModulePromise = null;
7
+ let highlighterPromise = null;
8
+
9
+ const escapeHtml = (text) => {
10
+ if (text == null) return '';
11
+ const value = typeof text === 'string' ? text : String(text);
12
+ return value
13
+ .replace(/&/g, '&')
14
+ .replace(/</g, '&lt;')
15
+ .replace(/>/g, '&gt;')
16
+ .replace(/\"/g, '&quot;')
17
+ .replace(/'/g, '&#039;');
18
+ };
19
+
20
+ const resolveTheme = (preferredTheme) => {
21
+ if (preferredTheme === 'github-dark' || preferredTheme === 'github-light') {
22
+ return preferredTheme;
23
+ }
24
+
25
+ const docTheme = document.documentElement.getAttribute('data-theme')
26
+ || document.body?.getAttribute('data-theme');
27
+
28
+ if (docTheme === 'dark') return 'github-dark';
29
+ if (docTheme === 'light') return 'github-light';
30
+
31
+ const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches;
32
+ return prefersDark ? 'github-dark' : 'github-light';
33
+ };
34
+
35
+ const resolveLang = (lang) => {
36
+ const next = String(lang || 'html').trim().toLowerCase();
37
+ if (next === 'js') return 'javascript';
38
+ if (next === 'ts') return 'typescript';
39
+ if (next === 'sh') return 'shell';
40
+ return next;
41
+ };
42
+
43
+ async function loadShikiModule() {
44
+ if (shikiModulePromise) return shikiModulePromise;
45
+
46
+ shikiModulePromise = (async () => {
47
+ try {
48
+ return await import('#shiki');
49
+ } catch {
50
+ return import(SHIKI_CDN_URL);
51
+ }
52
+ })();
53
+
54
+ return shikiModulePromise;
55
+ }
56
+
57
+ async function loadHighlighter() {
58
+ if (highlighterPromise) return highlighterPromise;
59
+
60
+ highlighterPromise = (async () => {
61
+ try {
62
+ const shiki = await loadShikiModule();
63
+ if (typeof shiki?.getHighlighter !== 'function') return null;
64
+ return shiki.getHighlighter({ themes: SHIKI_THEMES, langs: SHIKI_LANGS });
65
+ } catch {
66
+ return null;
67
+ }
68
+ })();
69
+
70
+ return highlighterPromise;
71
+ }
72
+
73
+ export class PDSCode extends HTMLElement {
74
+ static get observedAttributes() {
75
+ return ['code', 'lang', 'theme'];
76
+ }
77
+
78
+ constructor() {
79
+ super();
80
+ this._code = null;
81
+ this._capturedInitialText = false;
82
+ this._renderToken = 0;
83
+ this._textObserver = null;
84
+ }
85
+
86
+ connectedCallback() {
87
+ this.captureInitialText();
88
+ this.startTextObserver();
89
+ this.render();
90
+
91
+ queueMicrotask(() => {
92
+ if (!this.isConnected || this.hasAttribute('code')) return;
93
+ if (this.ensureCodeFromContent()) {
94
+ this.render();
95
+ }
96
+ });
97
+ }
98
+
99
+ disconnectedCallback() {
100
+ this.stopTextObserver();
101
+ }
102
+
103
+ attributeChangedCallback() {
104
+ this.render();
105
+ }
106
+
107
+ set code(value) {
108
+ this._code = value == null ? '' : String(value);
109
+ this.render();
110
+ }
111
+
112
+ get code() {
113
+ if (this._code != null) return this._code;
114
+ if (this.hasAttribute('code')) return this.getAttribute('code') || '';
115
+ return '';
116
+ }
117
+
118
+ setCode(value, { lang, theme } = {}) {
119
+ this.code = value;
120
+ if (lang) this.setAttribute('lang', lang);
121
+ if (theme) this.setAttribute('theme', theme);
122
+ }
123
+
124
+ captureInitialText() {
125
+ if (this._capturedInitialText || this._code != null || this.hasAttribute('code')) {
126
+ return;
127
+ }
128
+
129
+ this._code = this.textContent || '';
130
+ this._capturedInitialText = true;
131
+ }
132
+
133
+ ensureCodeFromContent() {
134
+ if (this.hasAttribute('code')) return false;
135
+ if (this._code && this._code.trim()) return false;
136
+
137
+ const text = this.textContent || '';
138
+ if (!text.trim()) return false;
139
+
140
+ this._code = text;
141
+ this._capturedInitialText = true;
142
+ return true;
143
+ }
144
+
145
+ startTextObserver() {
146
+ if (this._textObserver || this.hasAttribute('code')) return;
147
+
148
+ this._textObserver = new MutationObserver(() => {
149
+ if (!this.isConnected || this.hasAttribute('code')) return;
150
+ if (this.ensureCodeFromContent()) {
151
+ this.render();
152
+ }
153
+ });
154
+
155
+ this._textObserver.observe(this, {
156
+ childList: true,
157
+ characterData: true,
158
+ subtree: true,
159
+ });
160
+ }
161
+
162
+ stopTextObserver() {
163
+ if (!this._textObserver) return;
164
+ this._textObserver.disconnect();
165
+ this._textObserver = null;
166
+ }
167
+
168
+ async render() {
169
+ const renderToken = ++this._renderToken;
170
+ if (this.ensureCodeFromContent()) {
171
+ this.stopTextObserver();
172
+ }
173
+
174
+ const sourceCode = this.code;
175
+ const lang = resolveLang(this.getAttribute('lang') || 'html');
176
+ const theme = resolveTheme(this.getAttribute('theme'));
177
+
178
+ if (!sourceCode) {
179
+ return;
180
+ }
181
+
182
+ const highlighter = await loadHighlighter();
183
+
184
+ if (renderToken !== this._renderToken) {
185
+ return;
186
+ }
187
+
188
+ if (highlighter) {
189
+ try {
190
+ this.innerHTML = highlighter.codeToHtml(sourceCode, { lang, theme });
191
+ return;
192
+ } catch {
193
+ // Fall through to escaped pre/code fallback
194
+ }
195
+ }
196
+
197
+ this.innerHTML = `<pre><code>${escapeHtml(sourceCode)}</code></pre>`;
198
+ }
199
+ }
200
+
201
+ if (!customElements.get('pds-code')) {
202
+ customElements.define('pds-code', PDSCode);
203
+ }
@@ -48,10 +48,12 @@ export class SvgIcon extends HTMLElement {
48
48
  static spritePromises = new Map();
49
49
  static inlineSprites = new Map();
50
50
 
51
- // Cache for externally fetched SVG icons (icon name -> { content, viewBox, loaded, error })
51
+ // Cache for externally fetched SVG icons (path-aware key -> { content, viewBox, loaded, error })
52
52
  static externalIconCache = new Map();
53
- // Promises for in-flight external icon fetches
53
+ // Promises for in-flight external icon fetches (path-aware key)
54
54
  static externalIconPromises = new Map();
55
+ // Tracks base paths already logged for debug diagnostics
56
+ static externalPathDebugLogged = new Set();
55
57
 
56
58
  static instances = new Set();
57
59
 
@@ -199,6 +201,8 @@ export class SvgIcon extends HTMLElement {
199
201
  // Determine if we should use sprite or fallback
200
202
  let useFallback = this.hasAttribute('no-sprite') || !this.spriteAvailable();
201
203
 
204
+ const externalIconBasePath = SvgIcon.normalizeExternalIconPath();
205
+
202
206
  let effectiveHref = spriteHref ? `${spriteHref}#${icon}` : `#${icon}`;
203
207
  let inlineSymbolContent = null;
204
208
  let inlineSymbolViewBox = null;
@@ -241,7 +245,8 @@ export class SvgIcon extends HTMLElement {
241
245
  // IMPORTANT: Don't try external icons while sprite is still loading
242
246
  const hasFallback = SvgIcon.#fallbackIcons.hasOwnProperty(icon);
243
247
  if (spriteIconNotFound && !hasFallback && !spriteStillLoading) {
244
- const cached = SvgIcon.externalIconCache.get(icon);
248
+ const cacheKey = SvgIcon.getExternalIconCacheKey(icon, externalIconBasePath);
249
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
245
250
  if (cached) {
246
251
  if (cached.loaded && cached.content) {
247
252
  useExternalIcon = true;
@@ -252,7 +257,7 @@ export class SvgIcon extends HTMLElement {
252
257
  }
253
258
  } else {
254
259
  // Trigger async fetch - will re-render when complete
255
- SvgIcon.fetchExternalIcon(icon);
260
+ SvgIcon.fetchExternalIcon(icon, externalIconBasePath);
256
261
  useFallback = true;
257
262
  }
258
263
  }
@@ -306,7 +311,8 @@ export class SvgIcon extends HTMLElement {
306
311
 
307
312
  const hasFallbackLocal = SvgIcon.#fallbackIcons.hasOwnProperty(iconName);
308
313
  if (spriteIconNotFoundLocal && !hasFallbackLocal && !spriteStillLoadingLocal) {
309
- const cached = SvgIcon.externalIconCache.get(iconName);
314
+ const cacheKey = SvgIcon.getExternalIconCacheKey(iconName, externalIconBasePath);
315
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
310
316
  if (cached) {
311
317
  if (cached.loaded && cached.content) {
312
318
  useExternalIconLocal = true;
@@ -315,7 +321,7 @@ export class SvgIcon extends HTMLElement {
315
321
  useFallbackLocal = true;
316
322
  }
317
323
  } else {
318
- SvgIcon.fetchExternalIcon(iconName);
324
+ SvgIcon.fetchExternalIcon(iconName, externalIconBasePath);
319
325
  useFallbackLocal = true;
320
326
  }
321
327
  }
@@ -582,17 +588,20 @@ export class SvgIcon extends HTMLElement {
582
588
  */
583
589
  static getExternalIconPath() {
584
590
  try {
585
- // Try to get from PDS.compiled.tokens.icons.externalPath (live mode)
586
- if (PDS?.compiled?.tokens?.icons?.externalPath) {
587
- return PDS.compiled.tokens.icons.externalPath;
588
- }
589
- // Fallback: check compiled.config.design.icons.externalPath
590
- if (PDS?.compiled?.config?.design?.icons?.externalPath) {
591
- return PDS.compiled.config.design.icons.externalPath;
592
- }
593
- // Fallback: check currentConfig
594
- if (PDS?.currentConfig?.design?.icons?.externalPath) {
595
- return PDS.currentConfig.design.icons.externalPath;
591
+ const candidates = [
592
+ PDS?.compiled?.tokens?.icons?.externalPath,
593
+ PDS?.compiled?.config?.design?.icons?.externalPath,
594
+ PDS?.compiled?.config?.icons?.externalPath,
595
+ PDS?.compiled?.options?.design?.icons?.externalPath,
596
+ PDS?.compiled?.options?.icons?.externalPath,
597
+ PDS?.currentConfig?.design?.icons?.externalPath,
598
+ PDS?.currentConfig?.icons?.externalPath,
599
+ ];
600
+
601
+ for (const value of candidates) {
602
+ if (typeof value === 'string' && value.trim()) {
603
+ return value;
604
+ }
596
605
  }
597
606
  } catch (e) {
598
607
  // Ignore errors accessing config
@@ -601,29 +610,95 @@ export class SvgIcon extends HTMLElement {
601
610
  return '/assets/img/icons/';
602
611
  }
603
612
 
613
+ /**
614
+ * Normalize the external icon path to make cache keys and URL joining stable.
615
+ * @private
616
+ * @param {string} [basePath]
617
+ * @returns {string}
618
+ */
619
+ static normalizeExternalIconPath(basePath) {
620
+ const rawBasePath = typeof basePath === 'string' ? basePath : SvgIcon.getExternalIconPath();
621
+ const trimmed = (rawBasePath || '/assets/img/icons/').trim();
622
+ if (!trimmed) {
623
+ return '/assets/img/icons/';
624
+ }
625
+ return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
626
+ }
627
+
628
+ /**
629
+ * Build a deterministic cache key for external icon content.
630
+ * @private
631
+ * @param {string} iconName
632
+ * @param {string} basePath
633
+ * @returns {string}
634
+ */
635
+ static getExternalIconCacheKey(iconName, basePath) {
636
+ return `${SvgIcon.normalizeExternalIconPath(basePath)}::${iconName}`;
637
+ }
638
+
639
+ /**
640
+ * Resolve an external icon URL from icon name + base path.
641
+ * @private
642
+ * @param {string} iconName
643
+ * @param {string} basePath
644
+ * @returns {string}
645
+ */
646
+ static getExternalIconURL(iconName, basePath) {
647
+ const normalizedBasePath = SvgIcon.normalizeExternalIconPath(basePath);
648
+ try {
649
+ if (typeof window !== 'undefined' && window.location?.href) {
650
+ return new URL(`${iconName}.svg`, normalizedBasePath).href;
651
+ }
652
+ } catch (error) {
653
+ // Ignore URL construction errors and fall back to string concatenation.
654
+ }
655
+ return `${normalizedBasePath}${iconName}.svg`;
656
+ }
657
+
658
+ /**
659
+ * Whether verbose icon diagnostics should be logged.
660
+ * @private
661
+ * @returns {boolean}
662
+ */
663
+ static isDebugLoggingEnabled() {
664
+ return Boolean(
665
+ PDS?.debug === true ||
666
+ PDS?.compiled?.config?.design?.debug === true ||
667
+ PDS?.compiled?.options?.design?.debug === true ||
668
+ PDS?.currentConfig?.design?.debug === true
669
+ );
670
+ }
671
+
604
672
  /**
605
673
  * Fetch an external SVG icon and cache it
606
674
  * @param {string} iconName - The icon name (without .svg extension)
607
675
  * @returns {Promise<boolean>} True if successfully fetched
608
676
  */
609
- static async fetchExternalIcon(iconName) {
677
+ static async fetchExternalIcon(iconName, basePath) {
610
678
  if (!iconName || typeof document === 'undefined') {
611
679
  return false;
612
680
  }
613
681
 
682
+ const normalizedBasePath = SvgIcon.normalizeExternalIconPath(basePath);
683
+ const cacheKey = SvgIcon.getExternalIconCacheKey(iconName, normalizedBasePath);
684
+
614
685
  // Check if already cached
615
- const cached = SvgIcon.externalIconCache.get(iconName);
686
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
616
687
  if (cached) {
617
688
  return cached.loaded;
618
689
  }
619
690
 
620
691
  // Check if fetch is already in progress
621
- if (SvgIcon.externalIconPromises.has(iconName)) {
622
- return SvgIcon.externalIconPromises.get(iconName);
692
+ if (SvgIcon.externalIconPromises.has(cacheKey)) {
693
+ return SvgIcon.externalIconPromises.get(cacheKey);
623
694
  }
624
695
 
625
- const basePath = SvgIcon.getExternalIconPath();
626
- const iconUrl = `${basePath}${iconName}.svg`;
696
+ const iconUrl = SvgIcon.getExternalIconURL(iconName, normalizedBasePath);
697
+
698
+ if (SvgIcon.isDebugLoggingEnabled() && !SvgIcon.externalPathDebugLogged.has(normalizedBasePath)) {
699
+ SvgIcon.externalPathDebugLogged.add(normalizedBasePath);
700
+ console.debug('[pds-icon] Resolved external icon base path:', normalizedBasePath, 'example URL:', iconUrl);
701
+ }
627
702
 
628
703
  const promise = fetch(iconUrl)
629
704
  .then(async (response) => {
@@ -649,7 +724,7 @@ export class SvgIcon extends HTMLElement {
649
724
  .map((node) => serializer.serializeToString(node))
650
725
  .join('');
651
726
 
652
- SvgIcon.externalIconCache.set(iconName, {
727
+ SvgIcon.externalIconCache.set(cacheKey, {
653
728
  loaded: true,
654
729
  error: false,
655
730
  content,
@@ -665,7 +740,7 @@ export class SvgIcon extends HTMLElement {
665
740
  if (!error.message?.includes('404')) {
666
741
  console.debug('[pds-icon] External icon not found:', iconName, error.message);
667
742
  }
668
- SvgIcon.externalIconCache.set(iconName, {
743
+ SvgIcon.externalIconCache.set(cacheKey, {
669
744
  loaded: false,
670
745
  error: true,
671
746
  content: null,
@@ -676,10 +751,10 @@ export class SvgIcon extends HTMLElement {
676
751
  return false;
677
752
  })
678
753
  .finally(() => {
679
- SvgIcon.externalIconPromises.delete(iconName);
754
+ SvgIcon.externalIconPromises.delete(cacheKey);
680
755
  });
681
756
 
682
- SvgIcon.externalIconPromises.set(iconName, promise);
757
+ SvgIcon.externalIconPromises.set(cacheKey, promise);
683
758
  return promise;
684
759
  }
685
760
 
@@ -262,12 +262,12 @@ class PdsLiveImporter extends HTMLElement {
262
262
  ? entry.unknownTailwindTokens
263
263
  : [];
264
264
  const notesHtml = notes.length
265
- ? `<ul class="stack-xs">${notes
265
+ ? `<ul>${notes
266
266
  .map((note) => `<li>${escapeHtml(note)}</li>`)
267
267
  .join("")}</ul>`
268
268
  : "none";
269
269
  const issuesHtml = issueCount
270
- ? `<ul class="stack-xs">${entry.issues
270
+ ? `<ul>${entry.issues
271
271
  .map(
272
272
  (issue) =>
273
273
  `<li><strong>${escapeHtml(issue?.severity || "info")}</strong>: ${escapeHtml(issue?.message || "")}</li>`
@@ -348,8 +348,33 @@ class PdsScrollrow extends HTMLElement {
348
348
  #updateControls() {
349
349
  const el = this.#viewport;
350
350
  if (!el) return;
351
- const atStart = el.scrollLeft <= 2;
352
- const atEnd = Math.ceil(el.scrollLeft + el.clientWidth) >= el.scrollWidth - 2;
351
+
352
+ const computed = getComputedStyle(el);
353
+ const padStart = parseFloat(computed.paddingInlineStart) || 0;
354
+ const padEnd = parseFloat(computed.paddingInlineEnd) || 0;
355
+
356
+ const maxScroll = Math.max(0, el.scrollWidth - el.clientWidth);
357
+ const dir = computed.direction;
358
+ let position = el.scrollLeft;
359
+
360
+ if (dir === "rtl") {
361
+ position = position < 0 ? -position : maxScroll - position;
362
+ }
363
+
364
+ position = Math.min(maxScroll, Math.max(0, position));
365
+
366
+ const baseThreshold = 2;
367
+ const startThreshold = Math.max(baseThreshold, Math.ceil(padStart) + 1);
368
+ const endThreshold = Math.max(baseThreshold, Math.ceil(padEnd) + 1);
369
+
370
+ let atStart = position <= startThreshold;
371
+ let atEnd = maxScroll - position <= endThreshold;
372
+
373
+ if (maxScroll <= Math.max(startThreshold, endThreshold)) {
374
+ atStart = true;
375
+ atEnd = true;
376
+ }
377
+
353
378
  this.classList.toggle("can-scroll-left", !atStart);
354
379
  this.classList.toggle("can-scroll-right", !atEnd);
355
380
  const buttons = this.shadowRoot.querySelectorAll(".control button");