@pure-ds/core 0.6.9 → 0.6.11

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 (90) hide show
  1. package/custom-elements.json +865 -35
  2. package/dist/types/pds.d.ts +31 -0
  3. package/dist/types/public/assets/js/pds-manager.d.ts +100 -2
  4. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  5. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  6. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  7. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts +8 -0
  8. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts.map +1 -0
  9. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts +1 -195
  10. package/dist/types/public/assets/pds/components/pds-live-edit.d.ts.map +1 -1
  11. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts +2 -0
  12. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts.map +1 -0
  13. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts +2 -0
  14. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts.map +1 -0
  15. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts +0 -2
  16. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  17. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts +20 -0
  18. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts.map +1 -1
  19. package/dist/types/public/assets/pds/components/pds-toaster.d.ts +1 -1
  20. package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -1
  21. package/dist/types/public/assets/pds/components/pds-treeview.d.ts +37 -0
  22. package/dist/types/public/assets/pds/components/pds-treeview.d.ts.map +1 -0
  23. package/dist/types/src/js/common/toast.d.ts +8 -0
  24. package/dist/types/src/js/common/toast.d.ts.map +1 -1
  25. package/dist/types/src/js/pds-core/pds-config.d.ts +1306 -13
  26. package/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -1
  27. package/dist/types/src/js/pds-core/pds-enhancers-meta.d.ts.map +1 -1
  28. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -1
  29. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  30. package/dist/types/src/js/pds-core/pds-live.d.ts +2 -1
  31. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  32. package/dist/types/src/js/pds-core/pds-ontology.d.ts.map +1 -1
  33. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts +1 -4
  34. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts.map +1 -1
  35. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts +66 -0
  36. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts.map +1 -0
  37. package/dist/types/src/js/pds-live-manager/import-contract.d.ts +15 -0
  38. package/dist/types/src/js/pds-live-manager/import-contract.d.ts.map +1 -0
  39. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts +32 -0
  40. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts.map +1 -0
  41. package/dist/types/src/js/pds-live-manager/import-service.d.ts +21 -0
  42. package/dist/types/src/js/pds-live-manager/import-service.d.ts.map +1 -0
  43. package/dist/types/src/js/pds-live-manager/template-service.d.ts +17 -0
  44. package/dist/types/src/js/pds-live-manager/template-service.d.ts.map +1 -0
  45. package/dist/types/src/js/pds-manager.d.ts +4 -0
  46. package/dist/types/src/js/pds.d.ts.map +1 -1
  47. package/package.json +7 -3
  48. package/packages/pds-cli/README.md +51 -0
  49. package/packages/pds-cli/bin/pds-import.js +176 -0
  50. package/packages/pds-cli/bin/pds-static.js +31 -1
  51. package/packages/pds-cli/bin/postinstall.mjs +17 -8
  52. package/public/assets/js/app.js +23 -147
  53. package/public/assets/js/pds-manager.js +481 -248
  54. package/public/assets/js/pds.js +16 -16
  55. package/public/assets/pds/components/pds-form.js +124 -27
  56. package/public/assets/pds/components/pds-live-converter.js +47 -0
  57. package/public/assets/pds/components/pds-live-edit.js +1626 -211
  58. package/public/assets/pds/components/pds-live-importer.js +772 -0
  59. package/public/assets/pds/components/pds-live-template-canvas.js +171 -0
  60. package/public/assets/pds/components/pds-omnibox.js +146 -20
  61. package/public/assets/pds/components/pds-scrollrow.js +56 -1
  62. package/public/assets/pds/components/pds-toaster.js +50 -5
  63. package/public/assets/pds/components/pds-treeview.js +972 -0
  64. package/public/assets/pds/custom-elements.json +865 -35
  65. package/public/assets/pds/pds-css-complete.json +7 -7
  66. package/public/assets/pds/pds.css-data.json +5 -35
  67. package/public/assets/pds/templates/commerce-scroll-explorer.html +115 -0
  68. package/public/assets/pds/templates/content-brand-showcase.html +110 -0
  69. package/public/assets/pds/templates/feedback-ops-dashboard.html +91 -0
  70. package/public/assets/pds/templates/release-readiness-radar.html +69 -0
  71. package/public/assets/pds/templates/support-command-center.html +92 -0
  72. package/public/assets/pds/templates/templates.json +53 -0
  73. package/public/assets/pds/templates/workspace-settings-lab.html +131 -0
  74. package/public/assets/pds/vscode-custom-data.json +54 -4
  75. package/readme.md +34 -0
  76. package/src/js/pds-core/pds-config.js +831 -40
  77. package/src/js/pds-core/pds-enhancers-meta.js +11 -0
  78. package/src/js/pds-core/pds-enhancers.js +259 -5
  79. package/src/js/pds-core/pds-generator.js +353 -52
  80. package/src/js/pds-core/pds-live.js +630 -15
  81. package/src/js/pds-core/pds-ontology.js +6 -0
  82. package/src/js/pds-core/pds-start-helpers.js +14 -6
  83. package/src/js/pds-live-manager/conversion-service.js +3136 -0
  84. package/src/js/pds-live-manager/import-contract.js +57 -0
  85. package/src/js/pds-live-manager/import-history-service.js +145 -0
  86. package/src/js/pds-live-manager/import-service.js +255 -0
  87. package/src/js/pds-live-manager/tailwind-conversion-rules.json +383 -0
  88. package/src/js/pds-live-manager/template-service.js +170 -0
  89. package/src/js/pds.d.ts +31 -0
  90. package/src/js/pds.js +71 -60
@@ -0,0 +1,972 @@
1
+ /**
2
+ * Accessible, form-associated treeview with nested UL output.
3
+ *
4
+ * @element pds-treeview
5
+ * @formAssociated
6
+ *
7
+ * @attr {string} name - Form field name.
8
+ * @attr {string} value - Selected node value.
9
+ * @attr {boolean} disabled - Disables interaction.
10
+ * @attr {boolean} required - Requires a selected node for form validity.
11
+ * @attr {boolean} display-only - Renders as browse-only tree without selection.
12
+ * @attr {boolean} expanded-all - Expands all branch nodes on load.
13
+ * @attr {string} src - Optional URL source for tree JSON.
14
+ *
15
+ * @property {object} settings - Data and behavior settings object.
16
+ * @property {object} options - Alias for settings.
17
+ * @property {Function} settings.getChildren - Optional async child loader invoked on first expand per node.
18
+ *
19
+ * @fires treeview-load Fired after root data has been loaded and indexed.
20
+ * @fires treeview-error Fired when root source loading fails.
21
+ * @fires node-toggle Fired when a branch node is expanded or collapsed.
22
+ * @fires node-select Fired when a node is selected.
23
+ * @fires node-load Fired when lazy children for an expanded node are loaded.
24
+ * @fires node-load-error Fired when lazy children loading fails for a node.
25
+ */
26
+
27
+ const LAYERS = ["tokens", "primitives", "components", "utilities"];
28
+ const ROOT_SELECTOR = ".tv-tree";
29
+
30
+ export class PdsTreeview extends HTMLElement {
31
+ static formAssociated = true;
32
+
33
+ static get observedAttributes() {
34
+ return ["name", "value", "disabled", "required", "display-only", "expanded-all", "src"];
35
+ }
36
+
37
+ #root;
38
+ #internals;
39
+ #settings = {};
40
+ #nodes = [];
41
+ #expandedIds = new Set();
42
+ #nodeById = new Map();
43
+ #parentById = new Map();
44
+ #selectedId = null;
45
+ #focusedId = null;
46
+ #defaultValue = "";
47
+ #loadToken = 0;
48
+ #childrenLoadPromises = new Map();
49
+
50
+ constructor() {
51
+ super();
52
+ this.#root = this.attachShadow({ mode: "open" });
53
+ this.#internals = this.attachInternals();
54
+ this.#renderShell();
55
+ void this.#adoptStyles();
56
+ }
57
+
58
+ connectedCallback() {
59
+ this.#defaultValue = this.getAttribute("value") || "";
60
+ this.#syncAttributes();
61
+ void this.refresh();
62
+ }
63
+
64
+ attributeChangedCallback(name, oldValue, newValue) {
65
+ if (oldValue === newValue) return;
66
+ if (name === "src") {
67
+ void this.refresh();
68
+ return;
69
+ }
70
+ this.#syncAttributes();
71
+ if (name === "value" && this.#nodes.length) {
72
+ this.selectByValue(newValue ?? "");
73
+ }
74
+ }
75
+
76
+ formAssociatedCallback() {}
77
+
78
+ formDisabledCallback(disabled) {
79
+ this.toggleAttribute("disabled", Boolean(disabled));
80
+ this.#syncAttributes();
81
+ }
82
+
83
+ formResetCallback() {
84
+ this.value = this.#defaultValue;
85
+ this.selectByValue(this.#defaultValue);
86
+ }
87
+
88
+ formStateRestoreCallback(state) {
89
+ this.value = state ?? "";
90
+ this.selectByValue(this.value);
91
+ }
92
+
93
+ get settings() {
94
+ return this.#settings;
95
+ }
96
+
97
+ set settings(value) {
98
+ this.#settings = value && typeof value === "object" ? value : {};
99
+ void this.refresh();
100
+ }
101
+
102
+ get options() {
103
+ return this.settings;
104
+ }
105
+
106
+ set options(value) {
107
+ this.settings = value;
108
+ }
109
+
110
+ get name() {
111
+ return this.getAttribute("name") || "";
112
+ }
113
+
114
+ set name(value) {
115
+ if (value == null || value === "") this.removeAttribute("name");
116
+ else this.setAttribute("name", value);
117
+ }
118
+
119
+ get value() {
120
+ return this.getAttribute("value") || "";
121
+ }
122
+
123
+ set value(value) {
124
+ const next = value == null ? "" : String(value);
125
+ if (next === "") this.removeAttribute("value");
126
+ else this.setAttribute("value", next);
127
+ }
128
+
129
+ get disabled() {
130
+ return this.hasAttribute("disabled");
131
+ }
132
+
133
+ set disabled(value) {
134
+ if (value) this.setAttribute("disabled", "");
135
+ else this.removeAttribute("disabled");
136
+ }
137
+
138
+ get required() {
139
+ return this.hasAttribute("required");
140
+ }
141
+
142
+ set required(value) {
143
+ if (value) this.setAttribute("required", "");
144
+ else this.removeAttribute("required");
145
+ }
146
+
147
+ get displayOnly() {
148
+ return this.hasAttribute("display-only");
149
+ }
150
+
151
+ set displayOnly(value) {
152
+ if (value) this.setAttribute("display-only", "");
153
+ else this.removeAttribute("display-only");
154
+ }
155
+
156
+ get expandedAll() {
157
+ return this.hasAttribute("expanded-all");
158
+ }
159
+
160
+ set expandedAll(value) {
161
+ if (value) this.setAttribute("expanded-all", "");
162
+ else this.removeAttribute("expanded-all");
163
+ }
164
+
165
+ get selectedNode() {
166
+ return this.#selectedId ? this.#nodeById.get(this.#selectedId) || null : null;
167
+ }
168
+
169
+ getSelectedNode() {
170
+ return this.selectedNode;
171
+ }
172
+
173
+ async refresh() {
174
+ const loadToken = ++this.#loadToken;
175
+ const host = this.#root.querySelector(".tv-host");
176
+ if (host) host.dataset.state = "loading";
177
+
178
+ try {
179
+ const sourceData = await this.#resolveSource();
180
+ if (loadToken !== this.#loadToken) return;
181
+ this.#ingestData(sourceData);
182
+ this.#renderTree();
183
+ this.#restoreSelection();
184
+ this.dispatchEvent(
185
+ new CustomEvent("treeview-load", {
186
+ detail: { nodes: this.#nodes },
187
+ bubbles: true,
188
+ composed: true,
189
+ }),
190
+ );
191
+ if (host) host.dataset.state = "ready";
192
+ } catch (error) {
193
+ if (loadToken !== this.#loadToken) return;
194
+ console.error("pds-treeview: failed to load data", error);
195
+ this.#nodes = [];
196
+ this.#nodeById.clear();
197
+ this.#parentById.clear();
198
+ this.#expandedIds.clear();
199
+ this.#selectedId = null;
200
+ this.#focusedId = null;
201
+ this.#renderTree();
202
+ if (host) host.dataset.state = "error";
203
+ this.dispatchEvent(
204
+ new CustomEvent("treeview-error", {
205
+ detail: { error },
206
+ bubbles: true,
207
+ composed: true,
208
+ }),
209
+ );
210
+ }
211
+ }
212
+
213
+ expandAll() {
214
+ for (const [id, node] of this.#nodeById.entries()) {
215
+ if (node.children?.length) this.#expandedIds.add(id);
216
+ }
217
+ this.#renderTree();
218
+ }
219
+
220
+ collapseAll() {
221
+ this.#expandedIds.clear();
222
+ this.#renderTree();
223
+ }
224
+
225
+ selectById(id) {
226
+ if (!id || !this.#nodeById.has(id)) return false;
227
+ this.#selectNode(id, { user: false, focus: true });
228
+ return true;
229
+ }
230
+
231
+ selectByValue(value) {
232
+ const normalized = value == null ? "" : String(value);
233
+ if (!normalized) {
234
+ this.#selectedId = null;
235
+ this.#syncValue();
236
+ this.#renderTree();
237
+ return true;
238
+ }
239
+ for (const [id, node] of this.#nodeById.entries()) {
240
+ if (String(node.value) === normalized) {
241
+ this.#expandAncestors(id);
242
+ this.#selectNode(id, { user: false, focus: false });
243
+ return true;
244
+ }
245
+ }
246
+ return false;
247
+ }
248
+
249
+ checkValidity() {
250
+ this.#syncValidity();
251
+ return this.#internals.checkValidity();
252
+ }
253
+
254
+ reportValidity() {
255
+ this.#syncValidity();
256
+ return this.#internals.reportValidity();
257
+ }
258
+
259
+ #renderShell() {
260
+ this.#root.innerHTML = `
261
+ <div class="tv-host" data-state="ready">
262
+ <ul class="tv-tree" role="tree" aria-label="Treeview"></ul>
263
+ </div>
264
+ `;
265
+
266
+ this.#root.addEventListener("click", (event) => {
267
+ const target = event.target;
268
+ if (!(target instanceof Element) || this.disabled) return;
269
+
270
+ const toggle = target.closest(".tv-toggle");
271
+ if (toggle) {
272
+ const id = toggle.getAttribute("data-node-id");
273
+ if (id) void this.#toggleNode(id, true);
274
+ return;
275
+ }
276
+
277
+ const row = target.closest(".tv-row");
278
+ if (!row) return;
279
+ const id = row.getAttribute("data-node-id");
280
+ if (!id) return;
281
+
282
+ if (event instanceof MouseEvent && event.detail === 2) {
283
+ void this.#toggleNode(id, true);
284
+ if (!this.displayOnly) {
285
+ this.#selectNode(id, { user: true, focus: true });
286
+ } else {
287
+ this.#focusRow(id);
288
+ }
289
+ return;
290
+ }
291
+
292
+ if (!this.displayOnly) {
293
+ this.#selectNode(id, { user: true, focus: true });
294
+ } else {
295
+ this.#focusRow(id);
296
+ }
297
+ });
298
+
299
+ this.#root.addEventListener("focusin", (event) => {
300
+ const target = event.target;
301
+ if (!(target instanceof Element)) return;
302
+ const row = target.closest(".tv-row");
303
+ if (!row) return;
304
+ const id = row.getAttribute("data-node-id");
305
+ if (!id) return;
306
+ this.#focusedId = id;
307
+ this.#syncTabStops();
308
+ });
309
+
310
+ this.#root.addEventListener("keydown", (event) => {
311
+ if (this.disabled) return;
312
+ const handled = this.#handleKeydown(event);
313
+ if (handled) {
314
+ event.preventDefault();
315
+ event.stopPropagation();
316
+ }
317
+ });
318
+ }
319
+
320
+ async #adoptStyles() {
321
+ const componentStyles = PDS.createStylesheet(/*css*/ `
322
+ @layer treeview {
323
+ :host {
324
+ display: block;
325
+ --tv-indent: var(--spacing-5);
326
+ --tv-toggle-size: 30px;
327
+ --tv-row-height: var(--tv-toggle-size);
328
+ }
329
+
330
+ .tv-host {
331
+ color: var(--color-text-primary);
332
+ }
333
+
334
+ .tv-tree,
335
+ .tv-group {
336
+ list-style: none;
337
+ margin: var(--spacing-0);
338
+ padding: var(--spacing-0);
339
+ }
340
+
341
+ .tv-group {
342
+ margin-inline-start: var(--tv-indent);
343
+ }
344
+
345
+ .tv-item {
346
+ margin: var(--spacing-0);
347
+ padding: var(--spacing-0);
348
+ }
349
+
350
+ .tv-row {
351
+ min-height: var(--tv-row-height);
352
+ display: grid;
353
+ grid-template-columns: var(--tv-toggle-size) auto 1fr;
354
+ align-items: center;
355
+ gap: var(--spacing-1);
356
+ border-radius: var(--radius-sm);
357
+ cursor: default;
358
+ outline: none;
359
+ }
360
+
361
+ .tv-row[aria-selected="true"] {
362
+ background: var(--color-surface-hover);
363
+ color: var(--color-text-primary);
364
+ }
365
+
366
+ .tv-row:focus-visible {
367
+ box-shadow: inset 0 0 0 1px var(--color-primary-500);
368
+ background: var(--color-surface-hover);
369
+ }
370
+
371
+ .tv-toggle,
372
+ .tv-toggle-gap {
373
+ width: var(--tv-toggle-size);
374
+ height: var(--tv-toggle-size);
375
+ display: inline-grid;
376
+ place-items: center;
377
+ font-weight: var(--font-weight-semibold);
378
+ }
379
+
380
+ .tv-toggle {
381
+ min-width: var(--tv-toggle-size);
382
+ min-height: var(--tv-toggle-size);
383
+ aspect-ratio: 1 / 1;
384
+ border: 1px solid transparent;
385
+ border-radius: var(--radius-sm);
386
+ background: transparent;
387
+ color: var(--color-text-muted);
388
+ padding: var(--spacing-0);
389
+ line-height: 1;
390
+ cursor: pointer;
391
+ }
392
+
393
+ .tv-toggle:hover {
394
+ border-color: var(--color-primary-500);
395
+ color: var(--color-text-primary);
396
+ }
397
+
398
+ :host([disabled]) .tv-row,
399
+ :host([disabled]) .tv-toggle,
400
+ .tv-host[data-state="loading"] .tv-row {
401
+ opacity: 0.65;
402
+ cursor: not-allowed;
403
+ pointer-events: none;
404
+ }
405
+
406
+ .tv-prefix {
407
+ display: inline-flex;
408
+ align-items: center;
409
+ justify-content: center;
410
+ width: var(--spacing-5);
411
+ height: var(--spacing-5);
412
+ color: var(--color-text-muted);
413
+ }
414
+
415
+ .tv-prefix img {
416
+ width: 100%;
417
+ height: 100%;
418
+ object-fit: cover;
419
+ border-radius: var(--radius-sm);
420
+ }
421
+
422
+ .tv-label {
423
+ min-width: 0;
424
+ white-space: nowrap;
425
+ overflow: hidden;
426
+ text-overflow: ellipsis;
427
+ }
428
+
429
+ .tv-label-link {
430
+ color: inherit;
431
+ text-decoration: none;
432
+ }
433
+
434
+ .tv-label-link:hover,
435
+ .tv-label-link:focus-visible {
436
+ text-decoration: underline;
437
+ }
438
+ }
439
+ `);
440
+
441
+ await PDS.adoptLayers(this.#root, LAYERS, [componentStyles]);
442
+ }
443
+
444
+ #syncAttributes() {
445
+ this.#syncValue();
446
+ this.#syncTabStops();
447
+ }
448
+
449
+ async #resolveSource() {
450
+ const options = this.#settings || {};
451
+ const getItems =
452
+ typeof options.getItems === "function" ? options.getItems : null;
453
+
454
+ let source =
455
+ options.source ??
456
+ options.items ??
457
+ options.data ??
458
+ this.getAttribute("src") ??
459
+ null;
460
+
461
+ if (!source && getItems) {
462
+ source = await getItems({ host: this, options, settings: options });
463
+ }
464
+
465
+ source = await this.#resolveDynamicSource(source, { options, settings: options, host: this }, options.fetch);
466
+
467
+ if (typeof options.transform === "function") {
468
+ source = await options.transform(source, { host: this, options, settings: options });
469
+ }
470
+
471
+ return source;
472
+ }
473
+
474
+ async #fetchJson(url, fetchOptions) {
475
+ const response = await fetch(url, fetchOptions || {});
476
+ if (!response.ok) {
477
+ throw new Error(`Failed to load tree data (${response.status})`);
478
+ }
479
+ return response.json();
480
+ }
481
+
482
+ async #resolveDynamicSource(source, context, fetchOptions) {
483
+ if (typeof source === "function") {
484
+ source = await source(context);
485
+ } else if (typeof source === "string") {
486
+ source = await this.#fetchJson(source, fetchOptions);
487
+ } else if (source && typeof source === "object" && !Array.isArray(source) && typeof source.url === "string") {
488
+ const response = await this.#fetchJson(source.url, source.fetch || fetchOptions);
489
+ source = typeof source.mapResponse === "function" ? source.mapResponse(response) : response;
490
+ }
491
+
492
+ return source;
493
+ }
494
+
495
+ #ingestData(source) {
496
+ const settings = this.#settings || {};
497
+ const list = Array.isArray(source) ? source : source ? [source] : [];
498
+ const roots = list.map((node, index) => this.#normalizeNode(node, [], index)).filter(Boolean);
499
+
500
+ this.#nodes = roots;
501
+ this.#reindexNodes();
502
+
503
+ if (this.expandedAll || settings.expandedAll) {
504
+ this.#expandedIds = new Set(
505
+ Array.from(this.#nodeById.values())
506
+ .filter((node) => node.children.length)
507
+ .map((node) => node.id),
508
+ );
509
+ } else {
510
+ this.#expandedIds.clear();
511
+ const defaultExpanded = settings.defaultExpanded ?? [];
512
+ for (const id of defaultExpanded) {
513
+ if (this.#nodeById.has(id)) this.#expandedIds.add(id);
514
+ }
515
+ }
516
+ }
517
+
518
+ #normalizeNode(rawNode, path, index) {
519
+ if (!rawNode || typeof rawNode !== "object") return null;
520
+
521
+ const rawId = rawNode.id ?? rawNode.value ?? rawNode.key;
522
+ const fallbackId = [...path, index + 1].join(".");
523
+ const id = String(rawId ?? fallbackId);
524
+
525
+ const text = String(rawNode.text ?? rawNode.label ?? rawNode.title ?? id);
526
+ const value = String(rawNode.value ?? rawNode.id ?? id);
527
+ const link = this.#sanitizeLink(rawNode.link ?? rawNode.href ?? null);
528
+ const icon = rawNode.icon ?? rawNode.prefixIcon ?? rawNode.prefix?.icon ?? null;
529
+ const image = rawNode.image ?? rawNode.prefixImage ?? rawNode.prefix?.image ?? null;
530
+ const childrenRaw =
531
+ rawNode.children ?? rawNode.subnodes ?? rawNode.nodes ?? rawNode.items ?? [];
532
+ const childArray = Array.isArray(childrenRaw) ? childrenRaw : [];
533
+ const nextPath = [...path, id];
534
+ const children = childArray
535
+ .map((child, childIndex) => this.#normalizeNode(child, nextPath, childIndex))
536
+ .filter(Boolean);
537
+ const explicitHasChildren = rawNode.hasChildren ?? rawNode.hasChildNodes ?? rawNode.branch;
538
+ const hasChildren =
539
+ typeof explicitHasChildren === "boolean"
540
+ ? explicitHasChildren || children.length > 0
541
+ : children.length > 0;
542
+ const explicitLoaded = rawNode.childrenLoaded;
543
+ const childrenLoaded =
544
+ typeof explicitLoaded === "boolean"
545
+ ? explicitLoaded
546
+ : childArray.length > 0 || !hasChildren;
547
+
548
+ return {
549
+ id,
550
+ text,
551
+ value,
552
+ link,
553
+ icon,
554
+ image,
555
+ data: rawNode,
556
+ hasChildren,
557
+ childrenLoaded,
558
+ loadingChildren: false,
559
+ children,
560
+ };
561
+ }
562
+
563
+ #reindexNodes() {
564
+ this.#nodeById.clear();
565
+ this.#parentById.clear();
566
+
567
+ const visit = (node, parentId) => {
568
+ this.#nodeById.set(node.id, node);
569
+ if (parentId) this.#parentById.set(node.id, parentId);
570
+ for (const child of node.children) visit(child, node.id);
571
+ };
572
+ for (const node of this.#nodes) visit(node, null);
573
+ }
574
+
575
+ #renderTree() {
576
+ const tree = this.#root.querySelector(ROOT_SELECTOR);
577
+ if (!tree) return;
578
+ tree.innerHTML = this.#renderNodes(this.#nodes, 1, this.#canRenderLinks());
579
+
580
+ if (!this.#focusedId) {
581
+ this.#focusedId = this.#selectedId || this.#firstVisibleId() || null;
582
+ }
583
+ this.#syncTabStops();
584
+ this.#syncValue();
585
+ }
586
+
587
+ #renderNodes(nodes, level, linksEnabled) {
588
+ if (!nodes.length) return "";
589
+
590
+ return nodes
591
+ .map((node) => {
592
+ const expanded = this.#expandedIds.has(node.id);
593
+ const hasChildren = Boolean(node.hasChildren);
594
+ const selected = this.#selectedId === node.id;
595
+ const toggleGlyph = node.loadingChildren ? "…" : expanded ? "−" : "+";
596
+ const toggle = hasChildren
597
+ ? `<button type="button" class="tv-toggle icon-only" data-node-id="${this.#escapeAttribute(node.id)}" aria-label="${expanded ? "Collapse" : "Expand"} ${this.#escapeAttribute(node.text)}" ${node.loadingChildren ? "disabled" : ""}>${toggleGlyph}</button>`
598
+ : `<span class="tv-toggle-gap" aria-hidden="true"></span>`;
599
+ const prefix = this.#renderPrefix(node);
600
+ const label = linksEnabled && node.link
601
+ ? `<a class="tv-label tv-label-link" href="${this.#escapeAttribute(node.link)}">${this.#escapeHtml(node.text)}</a>`
602
+ : `<span class="tv-label">${this.#escapeHtml(node.text)}</span>`;
603
+ const childGroup =
604
+ hasChildren && expanded
605
+ ? `<ul class="tv-group" role="group">${this.#renderNodes(node.children, level + 1, linksEnabled)}</ul>`
606
+ : "";
607
+
608
+ return `
609
+ <li class="tv-item" role="none">
610
+ <div
611
+ class="tv-row"
612
+ role="treeitem"
613
+ aria-level="${level}"
614
+ ${hasChildren ? `aria-expanded="${expanded ? "true" : "false"}"` : ""}
615
+ ${node.loadingChildren ? "aria-busy=\"true\"" : ""}
616
+ aria-selected="${selected ? "true" : "false"}"
617
+ data-node-id="${this.#escapeAttribute(node.id)}"
618
+ tabindex="-1"
619
+ >
620
+ ${toggle}
621
+ ${prefix}
622
+ ${label}
623
+ </div>
624
+ ${childGroup}
625
+ </li>
626
+ `;
627
+ })
628
+ .join("");
629
+ }
630
+
631
+ #canRenderLinks() {
632
+ return !this.closest("form");
633
+ }
634
+
635
+ #renderPrefix(node) {
636
+ if (node.image) {
637
+ return `<span class="tv-prefix"><img src="${this.#escapeAttribute(node.image)}" alt="" loading="lazy" /></span>`;
638
+ }
639
+ if (node.icon) {
640
+ return `<span class="tv-prefix"><pds-icon icon="${this.#escapeAttribute(node.icon)}"></pds-icon></span>`;
641
+ }
642
+ return `<span class="tv-prefix" aria-hidden="true"></span>`;
643
+ }
644
+
645
+ #handleKeydown(event) {
646
+ const key = event.key;
647
+ const visibleIds = this.#getVisibleIds();
648
+ if (!visibleIds.length) return false;
649
+
650
+ const activeId = this.#focusedId && visibleIds.includes(this.#focusedId)
651
+ ? this.#focusedId
652
+ : visibleIds[0];
653
+ const currentIndex = visibleIds.indexOf(activeId);
654
+ const currentNode = this.#nodeById.get(activeId);
655
+
656
+ if (!currentNode) return false;
657
+
658
+ if (key === "ArrowDown") {
659
+ const nextId = visibleIds[Math.min(visibleIds.length - 1, currentIndex + 1)];
660
+ this.#focusRow(nextId);
661
+ return true;
662
+ }
663
+
664
+ if (key === "ArrowUp") {
665
+ const prevId = visibleIds[Math.max(0, currentIndex - 1)];
666
+ this.#focusRow(prevId);
667
+ return true;
668
+ }
669
+
670
+ if (key === "Home") {
671
+ this.#focusRow(visibleIds[0]);
672
+ return true;
673
+ }
674
+
675
+ if (key === "End") {
676
+ this.#focusRow(visibleIds[visibleIds.length - 1]);
677
+ return true;
678
+ }
679
+
680
+ if (key === "ArrowRight") {
681
+ if (currentNode.hasChildren && !this.#expandedIds.has(activeId)) {
682
+ void this.#toggleNode(activeId, true);
683
+ return true;
684
+ }
685
+ if (currentNode.hasChildren && this.#expandedIds.has(activeId) && currentNode.children.length) {
686
+ this.#focusRow(currentNode.children[0].id);
687
+ return true;
688
+ }
689
+ return false;
690
+ }
691
+
692
+ if (key === "ArrowLeft") {
693
+ if (currentNode.hasChildren && this.#expandedIds.has(activeId)) {
694
+ void this.#toggleNode(activeId, true);
695
+ return true;
696
+ }
697
+ const parentId = this.#parentById.get(activeId);
698
+ if (parentId) {
699
+ this.#focusRow(parentId);
700
+ return true;
701
+ }
702
+ return false;
703
+ }
704
+
705
+ if (key === "Enter" || key === " ") {
706
+ if (!this.displayOnly) {
707
+ this.#selectNode(activeId, { user: true, focus: true });
708
+ } else if (currentNode.hasChildren) {
709
+ void this.#toggleNode(activeId, true);
710
+ }
711
+ return true;
712
+ }
713
+
714
+ return false;
715
+ }
716
+
717
+ async #toggleNode(id, user) {
718
+ const node = this.#nodeById.get(id);
719
+ if (!node || !node.hasChildren) return;
720
+
721
+ const nextExpanded = !this.#expandedIds.has(id);
722
+ if (nextExpanded) {
723
+ this.#expandedIds.add(id);
724
+ this.#renderTree();
725
+ this.#focusRow(id);
726
+ await this.#ensureNodeChildrenLoaded(id, { user: Boolean(user) });
727
+ } else {
728
+ this.#expandedIds.delete(id);
729
+ }
730
+ this.#renderTree();
731
+ this.#focusRow(id);
732
+
733
+ this.dispatchEvent(
734
+ new CustomEvent("node-toggle", {
735
+ detail: { node, expanded: nextExpanded, user: Boolean(user) },
736
+ bubbles: true,
737
+ composed: true,
738
+ }),
739
+ );
740
+ }
741
+
742
+ async #ensureNodeChildrenLoaded(id, { user } = {}) {
743
+ const node = this.#nodeById.get(id);
744
+ if (!node || node.childrenLoaded || node.loadingChildren || !node.hasChildren) return;
745
+
746
+ if (this.#childrenLoadPromises.has(id)) {
747
+ await this.#childrenLoadPromises.get(id);
748
+ return;
749
+ }
750
+
751
+ const task = (async () => {
752
+ node.loadingChildren = true;
753
+ this.#renderTree();
754
+ this.#focusRow(id);
755
+
756
+ try {
757
+ const childSource = await this.#resolveNodeChildren(node);
758
+ const childList = Array.isArray(childSource)
759
+ ? childSource
760
+ : childSource
761
+ ? [childSource]
762
+ : [];
763
+ node.children = childList
764
+ .map((child, childIndex) => this.#normalizeNode(child, [node.id], childIndex))
765
+ .filter(Boolean);
766
+ node.childrenLoaded = true;
767
+ node.hasChildren = node.children.length > 0;
768
+ this.#reindexNodes();
769
+
770
+ this.dispatchEvent(
771
+ new CustomEvent("node-load", {
772
+ detail: { node, children: node.children, user: Boolean(user) },
773
+ bubbles: true,
774
+ composed: true,
775
+ }),
776
+ );
777
+ } catch (error) {
778
+ node.childrenLoaded = false;
779
+ console.error("pds-treeview: failed to load child nodes", error);
780
+ this.dispatchEvent(
781
+ new CustomEvent("node-load-error", {
782
+ detail: { node, error, user: Boolean(user) },
783
+ bubbles: true,
784
+ composed: true,
785
+ }),
786
+ );
787
+ } finally {
788
+ node.loadingChildren = false;
789
+ }
790
+ })();
791
+
792
+ this.#childrenLoadPromises.set(id, task);
793
+ try {
794
+ await task;
795
+ } finally {
796
+ this.#childrenLoadPromises.delete(id);
797
+ }
798
+ }
799
+
800
+ async #resolveNodeChildren(node) {
801
+ const options = this.#settings || {};
802
+ const getChildren = typeof options.getChildren === "function" ? options.getChildren : null;
803
+ let source = null;
804
+
805
+ if (getChildren) {
806
+ source = await getChildren({
807
+ host: this,
808
+ node,
809
+ nodeId: node.id,
810
+ options,
811
+ settings: options,
812
+ });
813
+ } else {
814
+ const raw = node.data || {};
815
+ source = raw.childrenSource ?? raw.childrenSrc ?? raw.childrenUrl ?? null;
816
+ }
817
+
818
+ source = await this.#resolveDynamicSource(
819
+ source,
820
+ { host: this, node, nodeId: node.id, options, settings: options },
821
+ options.fetch,
822
+ );
823
+
824
+ if (typeof options.transformChildren === "function") {
825
+ source = await options.transformChildren(source, {
826
+ host: this,
827
+ node,
828
+ nodeId: node.id,
829
+ options,
830
+ settings: options,
831
+ });
832
+ }
833
+
834
+ return source;
835
+ }
836
+
837
+ #selectNode(id, { user, focus }) {
838
+ const node = this.#nodeById.get(id);
839
+ if (!node) return;
840
+
841
+ this.#selectedId = id;
842
+ this.#expandAncestors(id);
843
+ this.#renderTree();
844
+ if (focus) this.#focusRow(id);
845
+ this.#syncValue();
846
+
847
+ this.dispatchEvent(
848
+ new CustomEvent("node-select", {
849
+ detail: { node, value: node.value, user: Boolean(user) },
850
+ bubbles: true,
851
+ composed: true,
852
+ }),
853
+ );
854
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
855
+ this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
856
+
857
+ if (user && typeof this.#settings?.onSelect === "function") {
858
+ this.#settings.onSelect(node, this);
859
+ }
860
+ }
861
+
862
+ #restoreSelection() {
863
+ const preferred = this.getAttribute("value") || this.#defaultValue || "";
864
+ if (preferred) {
865
+ if (!this.selectByValue(preferred)) {
866
+ this.#selectedId = null;
867
+ }
868
+ } else if (this.#selectedId && this.#nodeById.has(this.#selectedId)) {
869
+ this.#expandAncestors(this.#selectedId);
870
+ this.#renderTree();
871
+ } else {
872
+ this.#selectedId = null;
873
+ this.#syncValue();
874
+ }
875
+ }
876
+
877
+ #expandAncestors(id) {
878
+ let current = this.#parentById.get(id);
879
+ while (current) {
880
+ this.#expandedIds.add(current);
881
+ current = this.#parentById.get(current);
882
+ }
883
+ }
884
+
885
+ #syncValue() {
886
+ const selected = this.selectedNode;
887
+ const nextValue = this.displayOnly ? "" : selected?.value ? String(selected.value) : "";
888
+
889
+ if (nextValue) {
890
+ if (this.getAttribute("value") !== nextValue) {
891
+ this.setAttribute("value", nextValue);
892
+ }
893
+ this.#internals.setFormValue(nextValue);
894
+ } else {
895
+ if (this.hasAttribute("value")) this.removeAttribute("value");
896
+ this.#internals.setFormValue("");
897
+ }
898
+
899
+ this.#syncValidity();
900
+ }
901
+
902
+ #syncValidity() {
903
+ if (this.required && !this.displayOnly && !this.selectedNode) {
904
+ this.#internals.setValidity(
905
+ { valueMissing: true },
906
+ "Please select a node.",
907
+ this,
908
+ );
909
+ return;
910
+ }
911
+ this.#internals.setValidity({});
912
+ }
913
+
914
+ #focusRow(id) {
915
+ if (!id) return;
916
+ this.#focusedId = id;
917
+ this.#syncTabStops();
918
+ const row = this.#root.querySelector(`.tv-row[data-node-id="${CSS.escape(id)}"]`);
919
+ row?.focus();
920
+ }
921
+
922
+ #syncTabStops() {
923
+ const rows = this.#root.querySelectorAll(".tv-row");
924
+ if (!rows.length) return;
925
+
926
+ const fallbackId = this.#selectedId || this.#firstVisibleId();
927
+ if (!this.#focusedId || !this.#root.querySelector(`.tv-row[data-node-id="${CSS.escape(this.#focusedId)}"]`)) {
928
+ this.#focusedId = fallbackId || null;
929
+ }
930
+
931
+ for (const row of rows) {
932
+ const id = row.getAttribute("data-node-id");
933
+ row.setAttribute("tabindex", id === this.#focusedId ? "0" : "-1");
934
+ }
935
+ }
936
+
937
+ #firstVisibleId() {
938
+ const first = this.#root.querySelector(".tv-row");
939
+ return first?.getAttribute("data-node-id") || null;
940
+ }
941
+
942
+ #getVisibleIds() {
943
+ return Array.from(this.#root.querySelectorAll(".tv-row"))
944
+ .map((row) => row.getAttribute("data-node-id"))
945
+ .filter(Boolean);
946
+ }
947
+
948
+ #sanitizeLink(link) {
949
+ if (!link) return null;
950
+ const value = String(link).trim();
951
+ if (!value) return null;
952
+ if (/^javascript\s*:/i.test(value)) return null;
953
+ return value;
954
+ }
955
+
956
+ #escapeHtml(value) {
957
+ return String(value)
958
+ .replaceAll("&", "&amp;")
959
+ .replaceAll("<", "&lt;")
960
+ .replaceAll(">", "&gt;")
961
+ .replaceAll('"', "&quot;")
962
+ .replaceAll("'", "&#39;");
963
+ }
964
+
965
+ #escapeAttribute(value) {
966
+ return this.#escapeHtml(value);
967
+ }
968
+ }
969
+
970
+ if (!customElements.get("pds-treeview")) {
971
+ customElements.define("pds-treeview", PdsTreeview);
972
+ }