@pure-ds/core 0.7.19 → 0.7.20

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 +24 -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 +67 -16
  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,9 +48,9 @@ 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
55
 
56
56
  static instances = new Set();
@@ -199,6 +199,8 @@ export class SvgIcon extends HTMLElement {
199
199
  // Determine if we should use sprite or fallback
200
200
  let useFallback = this.hasAttribute('no-sprite') || !this.spriteAvailable();
201
201
 
202
+ const externalIconBasePath = SvgIcon.normalizeExternalIconPath();
203
+
202
204
  let effectiveHref = spriteHref ? `${spriteHref}#${icon}` : `#${icon}`;
203
205
  let inlineSymbolContent = null;
204
206
  let inlineSymbolViewBox = null;
@@ -241,7 +243,8 @@ export class SvgIcon extends HTMLElement {
241
243
  // IMPORTANT: Don't try external icons while sprite is still loading
242
244
  const hasFallback = SvgIcon.#fallbackIcons.hasOwnProperty(icon);
243
245
  if (spriteIconNotFound && !hasFallback && !spriteStillLoading) {
244
- const cached = SvgIcon.externalIconCache.get(icon);
246
+ const cacheKey = SvgIcon.getExternalIconCacheKey(icon, externalIconBasePath);
247
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
245
248
  if (cached) {
246
249
  if (cached.loaded && cached.content) {
247
250
  useExternalIcon = true;
@@ -252,7 +255,7 @@ export class SvgIcon extends HTMLElement {
252
255
  }
253
256
  } else {
254
257
  // Trigger async fetch - will re-render when complete
255
- SvgIcon.fetchExternalIcon(icon);
258
+ SvgIcon.fetchExternalIcon(icon, externalIconBasePath);
256
259
  useFallback = true;
257
260
  }
258
261
  }
@@ -306,7 +309,8 @@ export class SvgIcon extends HTMLElement {
306
309
 
307
310
  const hasFallbackLocal = SvgIcon.#fallbackIcons.hasOwnProperty(iconName);
308
311
  if (spriteIconNotFoundLocal && !hasFallbackLocal && !spriteStillLoadingLocal) {
309
- const cached = SvgIcon.externalIconCache.get(iconName);
312
+ const cacheKey = SvgIcon.getExternalIconCacheKey(iconName, externalIconBasePath);
313
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
310
314
  if (cached) {
311
315
  if (cached.loaded && cached.content) {
312
316
  useExternalIconLocal = true;
@@ -315,7 +319,7 @@ export class SvgIcon extends HTMLElement {
315
319
  useFallbackLocal = true;
316
320
  }
317
321
  } else {
318
- SvgIcon.fetchExternalIcon(iconName);
322
+ SvgIcon.fetchExternalIcon(iconName, externalIconBasePath);
319
323
  useFallbackLocal = true;
320
324
  }
321
325
  }
@@ -601,29 +605,76 @@ export class SvgIcon extends HTMLElement {
601
605
  return '/assets/img/icons/';
602
606
  }
603
607
 
608
+ /**
609
+ * Normalize the external icon path to make cache keys and URL joining stable.
610
+ * @private
611
+ * @param {string} [basePath]
612
+ * @returns {string}
613
+ */
614
+ static normalizeExternalIconPath(basePath) {
615
+ const rawBasePath = typeof basePath === 'string' ? basePath : SvgIcon.getExternalIconPath();
616
+ const trimmed = (rawBasePath || '/assets/img/icons/').trim();
617
+ if (!trimmed) {
618
+ return '/assets/img/icons/';
619
+ }
620
+ return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
621
+ }
622
+
623
+ /**
624
+ * Build a deterministic cache key for external icon content.
625
+ * @private
626
+ * @param {string} iconName
627
+ * @param {string} basePath
628
+ * @returns {string}
629
+ */
630
+ static getExternalIconCacheKey(iconName, basePath) {
631
+ return `${SvgIcon.normalizeExternalIconPath(basePath)}::${iconName}`;
632
+ }
633
+
634
+ /**
635
+ * Resolve an external icon URL from icon name + base path.
636
+ * @private
637
+ * @param {string} iconName
638
+ * @param {string} basePath
639
+ * @returns {string}
640
+ */
641
+ static getExternalIconURL(iconName, basePath) {
642
+ const normalizedBasePath = SvgIcon.normalizeExternalIconPath(basePath);
643
+ try {
644
+ if (typeof window !== 'undefined' && window.location?.href) {
645
+ return new URL(`${iconName}.svg`, normalizedBasePath).href;
646
+ }
647
+ } catch (error) {
648
+ // Ignore URL construction errors and fall back to string concatenation.
649
+ }
650
+ return `${normalizedBasePath}${iconName}.svg`;
651
+ }
652
+
604
653
  /**
605
654
  * Fetch an external SVG icon and cache it
606
655
  * @param {string} iconName - The icon name (without .svg extension)
607
656
  * @returns {Promise<boolean>} True if successfully fetched
608
657
  */
609
- static async fetchExternalIcon(iconName) {
658
+ static async fetchExternalIcon(iconName, basePath) {
610
659
  if (!iconName || typeof document === 'undefined') {
611
660
  return false;
612
661
  }
613
662
 
663
+ const normalizedBasePath = SvgIcon.normalizeExternalIconPath(basePath);
664
+ const cacheKey = SvgIcon.getExternalIconCacheKey(iconName, normalizedBasePath);
665
+
614
666
  // Check if already cached
615
- const cached = SvgIcon.externalIconCache.get(iconName);
667
+ const cached = SvgIcon.externalIconCache.get(cacheKey);
616
668
  if (cached) {
617
669
  return cached.loaded;
618
670
  }
619
671
 
620
672
  // Check if fetch is already in progress
621
- if (SvgIcon.externalIconPromises.has(iconName)) {
622
- return SvgIcon.externalIconPromises.get(iconName);
673
+ if (SvgIcon.externalIconPromises.has(cacheKey)) {
674
+ return SvgIcon.externalIconPromises.get(cacheKey);
623
675
  }
624
676
 
625
- const basePath = SvgIcon.getExternalIconPath();
626
- const iconUrl = `${basePath}${iconName}.svg`;
677
+ const iconUrl = SvgIcon.getExternalIconURL(iconName, normalizedBasePath);
627
678
 
628
679
  const promise = fetch(iconUrl)
629
680
  .then(async (response) => {
@@ -649,7 +700,7 @@ export class SvgIcon extends HTMLElement {
649
700
  .map((node) => serializer.serializeToString(node))
650
701
  .join('');
651
702
 
652
- SvgIcon.externalIconCache.set(iconName, {
703
+ SvgIcon.externalIconCache.set(cacheKey, {
653
704
  loaded: true,
654
705
  error: false,
655
706
  content,
@@ -665,7 +716,7 @@ export class SvgIcon extends HTMLElement {
665
716
  if (!error.message?.includes('404')) {
666
717
  console.debug('[pds-icon] External icon not found:', iconName, error.message);
667
718
  }
668
- SvgIcon.externalIconCache.set(iconName, {
719
+ SvgIcon.externalIconCache.set(cacheKey, {
669
720
  loaded: false,
670
721
  error: true,
671
722
  content: null,
@@ -676,10 +727,10 @@ export class SvgIcon extends HTMLElement {
676
727
  return false;
677
728
  })
678
729
  .finally(() => {
679
- SvgIcon.externalIconPromises.delete(iconName);
730
+ SvgIcon.externalIconPromises.delete(cacheKey);
680
731
  });
681
732
 
682
- SvgIcon.externalIconPromises.set(iconName, promise);
733
+ SvgIcon.externalIconPromises.set(cacheKey, promise);
683
734
  return promise;
684
735
  }
685
736
 
@@ -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");