@pure-ds/core 0.7.25 → 0.7.27

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 (41) hide show
  1. package/.cursorrules +12 -1
  2. package/.github/copilot-instructions.md +12 -1
  3. package/custom-elements.json +1099 -74
  4. package/dist/types/public/assets/js/pds-ask.d.ts +2 -1
  5. package/dist/types/public/assets/js/pds-ask.d.ts.map +1 -1
  6. package/dist/types/public/assets/js/pds-autocomplete.d.ts +25 -36
  7. package/dist/types/public/assets/js/pds-autocomplete.d.ts.map +1 -1
  8. package/dist/types/public/assets/js/pds-enhancers.d.ts +4 -4
  9. package/dist/types/public/assets/js/pds-enhancers.d.ts.map +1 -1
  10. package/dist/types/public/assets/js/pds-manager.d.ts +444 -159
  11. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  12. package/dist/types/public/assets/js/pds-toast.d.ts +7 -6
  13. package/dist/types/public/assets/js/pds-toast.d.ts.map +1 -1
  14. package/dist/types/public/assets/js/pds.d.ts +4 -3
  15. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  16. package/dist/types/public/assets/pds/components/pds-daterange.d.ts +2 -0
  17. package/dist/types/public/assets/pds/components/pds-daterange.d.ts.map +1 -0
  18. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  19. package/dist/types/public/assets/pds/components/pds-rating.d.ts +120 -0
  20. package/dist/types/public/assets/pds/components/pds-rating.d.ts.map +1 -0
  21. package/dist/types/public/assets/pds/components/pds-tags.d.ts +2 -0
  22. package/dist/types/public/assets/pds/components/pds-tags.d.ts.map +1 -0
  23. package/dist/types/src/js/common/ask.d.ts.map +1 -1
  24. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/public/assets/js/app.js +1 -1
  27. package/public/assets/js/pds-ask.js +6 -6
  28. package/public/assets/js/pds-manager.js +104 -29
  29. package/public/assets/pds/components/pds-calendar.js +91 -159
  30. package/public/assets/pds/components/pds-daterange.js +683 -0
  31. package/public/assets/pds/components/pds-form.js +123 -21
  32. package/public/assets/pds/components/pds-rating.js +648 -0
  33. package/public/assets/pds/components/pds-tags.js +802 -0
  34. package/public/assets/pds/core/pds-ask.js +6 -6
  35. package/public/assets/pds/core/pds-manager.js +104 -29
  36. package/public/assets/pds/custom-elements.json +1099 -74
  37. package/public/assets/pds/pds-css-complete.json +7 -2
  38. package/public/assets/pds/pds.css-data.json +4 -4
  39. package/public/assets/pds/vscode-custom-data.json +97 -0
  40. package/src/js/pds-core/pds-generator.js +103 -28
  41. package/src/js/pds-core/pds-ontology.js +2 -2
@@ -0,0 +1,802 @@
1
+ import { PDS } from "#pds";
2
+
3
+ /**
4
+ * Multi-select tags control built on top of pds-omnibox.
5
+ *
6
+ * @element pds-tags
7
+ * @formAssociated
8
+ *
9
+ * @attr {string} name - Form field name used for submitted values
10
+ * @attr {string} placeholder - Search placeholder for the omnibox
11
+ * @attr {string} value - Comma-separated selected item ids
12
+ * @attr {string} options - JSON array of options (string[] or object[])
13
+ * @attr {string} settings - JSON omnibox settings (optional)
14
+ * @attr {boolean} disabled - Disables interaction
15
+ * @attr {boolean} required - Requires at least one selected value
16
+ *
17
+ * @property {Array<string|Object>} options - Option source for omnibox suggestions
18
+ * @property {Object} settings - Omnibox-compatible settings source
19
+ * @property {string[]} value - Selected option ids
20
+ */
21
+ class PdsTags extends HTMLElement {
22
+ static formAssociated = true;
23
+
24
+ static observedAttributes = [
25
+ "name",
26
+ "placeholder",
27
+ "value",
28
+ "options",
29
+ "settings",
30
+ "disabled",
31
+ "required",
32
+ ];
33
+
34
+ #internals;
35
+ #root;
36
+ #omnibox;
37
+ #chips;
38
+ #empty;
39
+ #defaultValue = "";
40
+ #selectedIds = new Set();
41
+ #selectedItems = new Map();
42
+ #items = [];
43
+ #sourceSettings = null;
44
+ #stylesAdopted = false;
45
+ #syncingValueAttribute = false;
46
+ #settingsInitialized = false;
47
+ #pendingFocusId = null;
48
+
49
+ constructor() {
50
+ super();
51
+ this.#debug("constructor");
52
+ this.#internals = this.attachInternals();
53
+ this.#root = this.attachShadow({ mode: "open", delegatesFocus: true });
54
+ this.#render();
55
+ }
56
+
57
+ async connectedCallback() {
58
+ this.#debug("connectedCallback:start", {
59
+ hasOptionsAttr: this.hasAttribute("options"),
60
+ hasValueAttr: this.hasAttribute("value"),
61
+ });
62
+ this.#upgradeProperty("options");
63
+ this.#upgradeProperty("value");
64
+ this.#upgradeProperty("name");
65
+ this.#upgradeProperty("placeholder");
66
+ this.#upgradeProperty("settings");
67
+ this.#upgradeProperty("disabled");
68
+ this.#upgradeProperty("required");
69
+
70
+ await this.#adoptStyles();
71
+ this.#hydrateFromAttributes();
72
+ this.#defaultValue = this.hasAttribute("value")
73
+ ? (this.getAttribute("value") || "")
74
+ : Array.from(this.#selectedIds).join(",");
75
+ this.#debug("connectedCallback:defaultValue", { defaultValue: this.#defaultValue });
76
+ await this.#ensureOmnibox();
77
+ this.#debug("connectedCallback:end", {
78
+ itemsCount: this.#items.length,
79
+ selected: Array.from(this.#selectedIds),
80
+ settingsInitialized: this.#settingsInitialized,
81
+ });
82
+ }
83
+
84
+ #upgradeProperty(propertyName) {
85
+ this.#debug("upgradeProperty:check", { propertyName, hasOwn: Object.prototype.hasOwnProperty.call(this, propertyName) });
86
+ if (!Object.prototype.hasOwnProperty.call(this, propertyName)) return;
87
+ const value = this[propertyName];
88
+ this.#debug("upgradeProperty:apply", { propertyName, value });
89
+ delete this[propertyName];
90
+ this[propertyName] = value;
91
+ }
92
+
93
+ async #adoptStyles() {
94
+ if (this.#stylesAdopted) return;
95
+ const componentStyles = PDS.createStylesheet(/* css */ `
96
+ :host {
97
+ display: block;
98
+ }
99
+
100
+ .chips {
101
+ display: flex;
102
+ flex-wrap: wrap;
103
+ align-items: center;
104
+ gap: var(--spacing-2);
105
+ min-block-size: var(--spacing-6);
106
+
107
+ &:empty {
108
+ display: none;
109
+ }
110
+ }
111
+
112
+ .empty {
113
+ color: var(--color-text-muted);
114
+ font-size: var(--font-size-sm);
115
+ }
116
+
117
+ .badge button {
118
+ min-height: unset;
119
+ }
120
+ `);
121
+
122
+ await PDS.adoptLayers(
123
+ this.#root,
124
+ ["tokens", "primitives", "components", "utilities"],
125
+ [componentStyles],
126
+ );
127
+
128
+ this.#stylesAdopted = true;
129
+ }
130
+
131
+ attributeChangedCallback(name, oldValue, newValue) {
132
+ this.#debug("attributeChangedCallback", { name, oldValue, newValue });
133
+ if (oldValue === newValue) return;
134
+
135
+ if (name === "value") {
136
+ if (this.#syncingValueAttribute) return;
137
+ this.#selectedIds = new Set(this.#parseValueList(newValue));
138
+ this.#renderChips();
139
+ this.#syncFormValue();
140
+ return;
141
+ }
142
+
143
+ if (name === "options") {
144
+ this.#items = this.#normalizeItems(this.#parseOptionsAttribute(newValue));
145
+ this.#syncSelectedItemsFromItems();
146
+ this.#reconcileSelection();
147
+ this.#renderChips();
148
+ if (!this.#sourceSettings) this.#settingsInitialized = false;
149
+ this.#applyOmniboxSettings();
150
+ this.#syncFormValue();
151
+ return;
152
+ }
153
+
154
+ if (name === "settings") {
155
+ this.#sourceSettings = this.#parseSettingsAttribute(newValue);
156
+ this.#settingsInitialized = false;
157
+ this.#applyOmniboxSettings();
158
+ return;
159
+ }
160
+
161
+ this.#syncAttributes();
162
+ this.#applyOmniboxSettings();
163
+ this.#syncFormValue();
164
+ }
165
+
166
+ get name() {
167
+ return this.getAttribute("name") || "";
168
+ }
169
+
170
+ set name(value) {
171
+ this.#debug("set name", { value });
172
+ if (value == null || value === "") this.removeAttribute("name");
173
+ else this.setAttribute("name", value);
174
+ }
175
+
176
+ get placeholder() {
177
+ return this.getAttribute("placeholder") || "Search tags...";
178
+ }
179
+
180
+ set placeholder(value) {
181
+ this.#debug("set placeholder", { value });
182
+ if (value == null || value === "") this.removeAttribute("placeholder");
183
+ else this.setAttribute("placeholder", value);
184
+ }
185
+
186
+ get options() {
187
+ return this.#items.slice();
188
+ }
189
+
190
+ set options(value) {
191
+ this.#debug("set options", {
192
+ receivedType: value == null ? String(value) : Array.isArray(value) ? "array" : typeof value,
193
+ receivedLength: Array.isArray(value) ? value.length : undefined,
194
+ });
195
+ if (value == null) return;
196
+ const nextItems = this.#normalizeItems(Array.isArray(value) ? value : []);
197
+ this.#items = nextItems;
198
+ this.#syncSelectedItemsFromItems();
199
+ this.#debug("set options:normalized", { itemsCount: this.#items.length });
200
+ this.#reconcileSelection();
201
+ this.#renderChips();
202
+ if (!this.#sourceSettings) this.#settingsInitialized = false;
203
+ this.#applyOmniboxSettings();
204
+ this.#syncFormValue();
205
+ }
206
+
207
+ get settings() {
208
+ return this.#sourceSettings;
209
+ }
210
+
211
+ set settings(value) {
212
+ this.#debug("set settings", {
213
+ receivedType: value == null ? String(value) : typeof value,
214
+ hasCategories: Boolean(value?.categories),
215
+ });
216
+ this.#sourceSettings = value && typeof value === "object" ? value : null;
217
+ this.#settingsInitialized = false;
218
+ this.#applyOmniboxSettings();
219
+ }
220
+
221
+ get value() {
222
+ return Array.from(this.#selectedIds);
223
+ }
224
+
225
+ set value(value) {
226
+ this.#debug("set value", { value });
227
+ const incoming = Array.isArray(value)
228
+ ? value.map((item) => String(item))
229
+ : this.#parseValueList(String(value ?? ""));
230
+ this.#selectedIds = new Set(incoming);
231
+ this.#syncSelectedItemsFromItems();
232
+ this.#debug("set value:parsed", { selected: Array.from(this.#selectedIds) });
233
+ this.#reconcileSelection();
234
+ this.#renderChips();
235
+ this.#applyOmniboxSettings();
236
+ this.#syncFormValue();
237
+ }
238
+
239
+ get disabled() {
240
+ return this.hasAttribute("disabled");
241
+ }
242
+
243
+ set disabled(value) {
244
+ if (value) this.setAttribute("disabled", "");
245
+ else this.removeAttribute("disabled");
246
+ }
247
+
248
+ get required() {
249
+ return this.hasAttribute("required");
250
+ }
251
+
252
+ set required(value) {
253
+ if (value) this.setAttribute("required", "");
254
+ else this.removeAttribute("required");
255
+ }
256
+
257
+ formResetCallback() {
258
+ this.#debug("formResetCallback", { defaultValue: this.#defaultValue });
259
+ this.value = this.#defaultValue;
260
+ }
261
+
262
+ formStateRestoreCallback(state) {
263
+ this.#debug("formStateRestoreCallback", { state });
264
+ this.value = state ?? "";
265
+ }
266
+
267
+ formDisabledCallback(disabled) {
268
+ this.#debug("formDisabledCallback", { disabled });
269
+ if (disabled) this.setAttribute("disabled", "");
270
+ else this.removeAttribute("disabled");
271
+ this.#syncAttributes();
272
+ }
273
+
274
+ checkValidity() {
275
+ return this.#internals.checkValidity();
276
+ }
277
+
278
+ reportValidity() {
279
+ return this.#internals.reportValidity();
280
+ }
281
+
282
+ async #ensureOmnibox() {
283
+ this.#debug("ensureOmnibox:start", { hasDefinition: Boolean(customElements.get("pds-omnibox")) });
284
+ if (!customElements.get("pds-omnibox")) {
285
+ const moduleUrl = new URL("./pds-omnibox.js", import.meta.url).href;
286
+ this.#debug("ensureOmnibox:import", { moduleUrl });
287
+ await import(moduleUrl);
288
+ }
289
+
290
+ await customElements.whenDefined("pds-omnibox");
291
+ this.#debug("ensureOmnibox:defined");
292
+
293
+ if (!this.#omnibox) {
294
+ this.#omnibox = this.#root.querySelector("pds-omnibox");
295
+ this.#debug("ensureOmnibox:query", { found: Boolean(this.#omnibox) });
296
+ }
297
+
298
+ this.#syncAttributes();
299
+ this.#applyOmniboxSettings();
300
+ this.#renderChips();
301
+ this.#syncFormValue();
302
+ this.#debug("ensureOmnibox:end", {
303
+ itemsCount: this.#items.length,
304
+ selected: Array.from(this.#selectedIds),
305
+ settingsInitialized: this.#settingsInitialized,
306
+ });
307
+ }
308
+
309
+ #render() {
310
+ this.#root.innerHTML = /* html */ `
311
+ <div class="stack-sm" part="wrap">
312
+ <div class="chips" part="chips" aria-live="polite"></div>
313
+ <span class="empty" part="empty">No tags selected.</span>
314
+ <pds-omnibox part="omnibox"></pds-omnibox>
315
+ </div>
316
+ `;
317
+
318
+ this.#chips = this.#root.querySelector(".chips");
319
+ this.#empty = this.#root.querySelector(".empty");
320
+ this.#omnibox = this.#root.querySelector("pds-omnibox");
321
+ }
322
+
323
+ #hydrateFromAttributes() {
324
+ this.#debug("hydrateFromAttributes:start", {
325
+ hasOptionsAttr: this.hasAttribute("options"),
326
+ hasValueAttr: this.hasAttribute("value"),
327
+ });
328
+ if (this.hasAttribute("options")) {
329
+ this.#items = this.#normalizeItems(
330
+ this.#parseOptionsAttribute(this.getAttribute("options")),
331
+ );
332
+ this.#syncSelectedItemsFromItems();
333
+ this.#debug("hydrateFromAttributes:options", { itemsCount: this.#items.length });
334
+ }
335
+
336
+ if (this.hasAttribute("settings")) {
337
+ this.#sourceSettings = this.#parseSettingsAttribute(this.getAttribute("settings"));
338
+ this.#debug("hydrateFromAttributes:settings", {
339
+ hasSettings: Boolean(this.#sourceSettings),
340
+ hasCategories: Boolean(this.#sourceSettings?.categories),
341
+ });
342
+ }
343
+
344
+ if (this.hasAttribute("value")) {
345
+ this.#selectedIds = new Set(this.#parseValueList(this.getAttribute("value")));
346
+ this.#debug("hydrateFromAttributes:value", { selected: Array.from(this.#selectedIds) });
347
+ }
348
+
349
+ this.#reconcileSelection();
350
+ this.#syncAttributes();
351
+ this.#renderChips();
352
+ this.#syncFormValue();
353
+ this.#debug("hydrateFromAttributes:end", {
354
+ itemsCount: this.#items.length,
355
+ selected: Array.from(this.#selectedIds),
356
+ });
357
+ }
358
+
359
+ #syncAttributes() {
360
+ this.#debug("syncAttributes:start", {
361
+ hasOmnibox: Boolean(this.#omnibox),
362
+ defined: Boolean(customElements.get("pds-omnibox")),
363
+ placeholder: this.placeholder,
364
+ disabled: this.disabled,
365
+ });
366
+ if (!this.#omnibox) return;
367
+ if (!customElements.get("pds-omnibox")) return;
368
+ this.#omnibox.placeholder = this.placeholder;
369
+ this.#omnibox.disabled = this.disabled;
370
+ this.#omnibox.required = false;
371
+ this.#omnibox.value = "";
372
+ this.#debug("syncAttributes:end");
373
+ }
374
+
375
+ #applyOmniboxSettings() {
376
+ this.#debug("applyOmniboxSettings:start", {
377
+ hasOmnibox: Boolean(this.#omnibox),
378
+ defined: Boolean(customElements.get("pds-omnibox")),
379
+ settingsInitialized: this.#settingsInitialized,
380
+ itemsCount: this.#items.length,
381
+ });
382
+ if (!this.#omnibox) return;
383
+ if (!customElements.get("pds-omnibox")) return;
384
+ if (this.#settingsInitialized) return;
385
+ const sourceSettings = this.#resolveSourceSettings();
386
+ if (!sourceSettings) return;
387
+ const nextSettings = this.#buildSettings(sourceSettings);
388
+ if (!nextSettings || !nextSettings.categories || Object.keys(nextSettings.categories).length === 0) return;
389
+ this.#omnibox.settings = nextSettings;
390
+ this.#settingsInitialized = true;
391
+ this.#debug("applyOmniboxSettings:applied", {
392
+ categoryCount: Object.keys(nextSettings.categories).length,
393
+ categoryNames: Object.keys(nextSettings.categories),
394
+ });
395
+ }
396
+
397
+ #buildSettings(sourceSettings) {
398
+ this.#debug("buildSettings:start", {
399
+ hasCategories: Boolean(sourceSettings?.categories),
400
+ categoryCount: sourceSettings?.categories
401
+ ? Object.keys(sourceSettings.categories).length
402
+ : 0,
403
+ });
404
+
405
+ if (!sourceSettings || typeof sourceSettings !== "object") return null;
406
+ const sourceCategories = sourceSettings.categories;
407
+ if (!sourceCategories || typeof sourceCategories !== "object") return null;
408
+
409
+ const categories = {};
410
+ for (const [categoryName, categoryConfig] of Object.entries(sourceCategories)) {
411
+ if (!categoryConfig || typeof categoryConfig !== "object") continue;
412
+
413
+ const sourceGetItems = categoryConfig.getItems;
414
+ const sourceAction = categoryConfig.action;
415
+
416
+ categories[categoryName] = {
417
+ ...categoryConfig,
418
+ getItems: async (options) => {
419
+ const liveSearch = String(this.#omnibox?.value || "");
420
+ const forwardedOptions = {
421
+ ...(options && typeof options === "object" ? options : {}),
422
+ search: liveSearch,
423
+ };
424
+
425
+ const incoming = typeof sourceGetItems === "function"
426
+ ? await sourceGetItems(forwardedOptions)
427
+ : [];
428
+
429
+ const normalized = this.#normalizeResultItems(incoming);
430
+ for (const item of normalized) {
431
+ this.#selectedItems.set(item.id, item);
432
+ }
433
+
434
+ const selected = this.#selectedIds;
435
+ const filtered = normalized.filter((item) => !selected.has(item.id));
436
+
437
+ this.#debug("settings.getItems", {
438
+ categoryName,
439
+ liveSearch,
440
+ incomingCount: normalized.length,
441
+ selected: Array.from(selected),
442
+ returnedCount: filtered.length,
443
+ returnedIds: filtered.map((item) => item.id),
444
+ });
445
+
446
+ return filtered;
447
+ },
448
+ action: (item) => {
449
+ const normalized = this.#normalizeResultItem(item);
450
+ const id = normalized.id;
451
+ this.#debug("settings.action", { categoryName, item: normalized, id });
452
+ if (!id) return;
453
+ this.#toggleSelection(id, normalized);
454
+ if (typeof sourceAction === "function") sourceAction(item);
455
+ },
456
+ };
457
+ }
458
+
459
+ this.#debug("buildSettings:end", {
460
+ categoryNames: Object.keys(categories),
461
+ categoryCount: Object.keys(categories).length,
462
+ });
463
+
464
+ const { categories: _ignoredCategories, ...sourceSettingsWithoutCategories } = sourceSettings;
465
+
466
+ return {
467
+ ...sourceSettingsWithoutCategories,
468
+ categories,
469
+ hideCategory: true,
470
+ itemGrid: "0 1fr 0",
471
+ iconHandler: () => "",
472
+ };
473
+ }
474
+
475
+ #matchesQuery(item, query) {
476
+ if (!query) return true;
477
+ return (
478
+ item.text.toLowerCase().includes(query) ||
479
+ item.id.toLowerCase().includes(query) ||
480
+ (item.description && item.description.toLowerCase().includes(query))
481
+ );
482
+ }
483
+
484
+ #toggleSelection(id, item = null) {
485
+ this.#debug("toggleSelection:start", {
486
+ id,
487
+ disabled: this.disabled,
488
+ selectedBefore: Array.from(this.#selectedIds),
489
+ });
490
+ if (this.disabled || !id) return;
491
+ const key = String(id);
492
+ if (this.#selectedIds.has(key)) {
493
+ this.#selectedIds.delete(key);
494
+ this.#selectedItems.delete(key);
495
+ if (this.#pendingFocusId === key) this.#pendingFocusId = null;
496
+ } else {
497
+ this.#selectedIds.add(key);
498
+ if (item && item.id) this.#selectedItems.set(key, item);
499
+ this.#pendingFocusId = key;
500
+ }
501
+
502
+ this.#renderChips();
503
+ this.#syncFormValue();
504
+ this.#resetOmniboxSearchState("toggleSelection");
505
+ this.#debug("toggleSelection:end", { selectedAfter: Array.from(this.#selectedIds) });
506
+
507
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
508
+ this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
509
+ }
510
+
511
+ #renderChips() {
512
+ if (!this.#chips || !this.#empty) return;
513
+
514
+ const selectedItems = Array.from(this.#selectedIds).map((id) => {
515
+ const fromCache = this.#selectedItems.get(id);
516
+ if (fromCache) return fromCache;
517
+ const fromItems = this.#items.find((item) => item.id === id);
518
+ if (fromItems) return fromItems;
519
+ return { id, text: id, description: "", icon: "", category: "Options" };
520
+ });
521
+ this.#debug("renderChips", {
522
+ selectedCount: selectedItems.length,
523
+ selectedIds: selectedItems.map((item) => item.id),
524
+ });
525
+
526
+ this.#chips.textContent = "";
527
+ for (const item of selectedItems) {
528
+ const chip = document.createElement("button");
529
+ chip.type = "button";
530
+ chip.className = "btn-secondary btn-sm";
531
+ chip.part = "chip";
532
+ chip.dataset.tagId = item.id;
533
+ chip.innerHTML = /* html */ `${item.text} <pds-icon icon="x" size="xs" aria-hidden="true"></pds-icon>`;
534
+ chip.disabled = this.disabled;
535
+ chip.setAttribute("aria-label", `Remove ${item.text}`);
536
+ chip.addEventListener("click", () => {
537
+ this.#toggleSelection(item.id);
538
+ });
539
+
540
+ this.#chips.appendChild(chip);
541
+ }
542
+
543
+ this.#empty.hidden = selectedItems.length > 0;
544
+
545
+ if (this.#pendingFocusId) {
546
+ const focusSelector = `button[data-tag-id="${CSS.escape(this.#pendingFocusId)}"]`;
547
+ const focusTarget = this.#chips.querySelector(focusSelector);
548
+ this.#pendingFocusId = null;
549
+ if (focusTarget && !focusTarget.disabled) {
550
+ queueMicrotask(() => focusTarget.focus());
551
+ }
552
+ }
553
+ }
554
+
555
+ #syncFormValue() {
556
+ const selected = Array.from(this.#selectedIds);
557
+ const serialized = selected.join(",");
558
+ this.#debug("syncFormValue:start", {
559
+ selected,
560
+ serialized,
561
+ required: this.required,
562
+ name: this.name,
563
+ });
564
+
565
+ this.#syncingValueAttribute = true;
566
+ try {
567
+ if (serialized) {
568
+ if (this.getAttribute("value") !== serialized) {
569
+ this.setAttribute("value", serialized);
570
+ }
571
+ } else if (this.hasAttribute("value")) {
572
+ this.removeAttribute("value");
573
+ }
574
+ } finally {
575
+ this.#syncingValueAttribute = false;
576
+ }
577
+
578
+ if (this.name && selected.length > 0) {
579
+ const formData = new FormData();
580
+ for (const value of selected) {
581
+ formData.append(this.name, value);
582
+ }
583
+ this.#internals.setFormValue(formData, serialized);
584
+ } else {
585
+ this.#internals.setFormValue(serialized || "", serialized || "");
586
+ }
587
+
588
+ if (this.required && selected.length === 0) {
589
+ this.#debug("syncFormValue:invalid", { reason: "valueMissing" });
590
+ this.#internals.setValidity(
591
+ { valueMissing: true },
592
+ "Please select at least one option.",
593
+ this.#omnibox || this,
594
+ );
595
+ return;
596
+ }
597
+
598
+ this.#internals.setValidity({});
599
+ this.#debug("syncFormValue:valid");
600
+ }
601
+
602
+ #parseOptionsAttribute(value) {
603
+ this.#debug("parseOptionsAttribute:start", { hasValue: Boolean(value), rawType: typeof value });
604
+ if (!value) return [];
605
+ try {
606
+ const parsed = JSON.parse(value);
607
+ this.#debug("parseOptionsAttribute:parsed", { isArray: Array.isArray(parsed), length: Array.isArray(parsed) ? parsed.length : undefined });
608
+ return Array.isArray(parsed) ? parsed : [];
609
+ } catch {
610
+ this.#debug("parseOptionsAttribute:error", { value });
611
+ return [];
612
+ }
613
+ }
614
+
615
+ #parseSettingsAttribute(value) {
616
+ this.#debug("parseSettingsAttribute:start", { hasValue: Boolean(value), rawType: typeof value });
617
+ if (!value) return null;
618
+ try {
619
+ const parsed = JSON.parse(value);
620
+ const valid = parsed && typeof parsed === "object" ? parsed : null;
621
+ this.#debug("parseSettingsAttribute:parsed", {
622
+ valid: Boolean(valid),
623
+ hasCategories: Boolean(valid?.categories),
624
+ });
625
+ return valid;
626
+ } catch {
627
+ this.#debug("parseSettingsAttribute:error", { value });
628
+ return null;
629
+ }
630
+ }
631
+
632
+ #normalizeItems(list) {
633
+ this.#debug("normalizeItems:start", { isArray: Array.isArray(list), length: Array.isArray(list) ? list.length : undefined });
634
+ const normalized = [];
635
+
636
+ for (const raw of list) {
637
+ if (typeof raw === "string") {
638
+ normalized.push({
639
+ id: raw,
640
+ text: raw,
641
+ description: "",
642
+ icon: "",
643
+ category: "Options",
644
+ });
645
+ continue;
646
+ }
647
+
648
+ if (!raw || typeof raw !== "object") continue;
649
+
650
+ const id = String(raw.id ?? raw.value ?? raw.text ?? "").trim();
651
+ if (!id) continue;
652
+
653
+ const text = String(raw.text ?? raw.label ?? id);
654
+ normalized.push({
655
+ id,
656
+ text,
657
+ description: raw.description ? String(raw.description) : "",
658
+ icon: raw.icon ? String(raw.icon) : "",
659
+ category: this.#normalizeCategory(raw.category),
660
+ });
661
+ }
662
+
663
+ this.#debug("normalizeItems:end", { length: normalized.length, ids: normalized.map((item) => item.id) });
664
+ return normalized;
665
+ }
666
+
667
+ #normalizeResultItem(raw) {
668
+ if (typeof raw === "string") {
669
+ const id = String(raw);
670
+ return { id, text: id };
671
+ }
672
+
673
+ if (!raw || typeof raw !== "object") return { id: "", text: "" };
674
+
675
+ const id = String(raw.id ?? raw.value ?? raw.text ?? "").trim();
676
+ const text = String(raw.text ?? raw.label ?? id);
677
+ return {
678
+ ...raw,
679
+ id,
680
+ text,
681
+ };
682
+ }
683
+
684
+ #normalizeResultItems(list) {
685
+ if (!Array.isArray(list)) return [];
686
+ return list
687
+ .map((item) => this.#normalizeResultItem(item))
688
+ .filter((item) => item.id);
689
+ }
690
+
691
+ #parseValueList(value) {
692
+ const raw = String(value ?? "").trim();
693
+ this.#debug("parseValueList", { value, raw });
694
+ if (!raw) return [];
695
+ return raw
696
+ .split(",")
697
+ .map((item) => item.trim())
698
+ .filter(Boolean);
699
+ }
700
+
701
+ #normalizeCategory(value) {
702
+ const normalized = String(value ?? "").trim();
703
+ this.#debug("normalizeCategory", { value, normalized });
704
+ if (!normalized) return "Options";
705
+ const lower = normalized.toLowerCase();
706
+ if (lower === "null" || lower === "undefined") return "Options";
707
+ return normalized;
708
+ }
709
+
710
+ #reconcileSelection() {
711
+ this.#debug("reconcileSelection:start", { selectedBefore: Array.from(this.#selectedIds) });
712
+ if (!this.#selectedIds.size) return;
713
+ const available = new Set(this.#items.map((item) => item.id));
714
+ this.#selectedIds = new Set(
715
+ Array.from(this.#selectedIds).filter((id) => available.has(id)),
716
+ );
717
+ for (const id of Array.from(this.#selectedItems.keys())) {
718
+ if (!this.#selectedIds.has(id)) this.#selectedItems.delete(id);
719
+ }
720
+ this.#debug("reconcileSelection:end", { selectedAfter: Array.from(this.#selectedIds) });
721
+ }
722
+
723
+ #resolveSourceSettings() {
724
+ if (this.#sourceSettings) return this.#sourceSettings;
725
+ if (!Array.isArray(this.#items) || this.#items.length === 0) return null;
726
+ return this.#buildOptionsSettings();
727
+ }
728
+
729
+ #buildOptionsSettings() {
730
+ const categoryMap = new Map();
731
+ for (const item of this.#items) {
732
+ const categoryName = this.#normalizeCategory(item.category);
733
+ if (!categoryMap.has(categoryName)) categoryMap.set(categoryName, []);
734
+ categoryMap.get(categoryName).push(item);
735
+ }
736
+
737
+ const categories = {};
738
+ for (const [categoryName, entries] of categoryMap.entries()) {
739
+ categories[categoryName] = {
740
+ trigger: () => true,
741
+ getItems: async (options) => {
742
+ const query = String(options?.search || "").trim().toLowerCase();
743
+ return entries
744
+ .filter((item) => this.#matchesQuery(item, query))
745
+ .map((item) => ({
746
+ id: item.id,
747
+ text: item.text,
748
+ icon: item.icon,
749
+ description: item.description,
750
+ }));
751
+ },
752
+ };
753
+ }
754
+
755
+ return {
756
+ hideCategory: true,
757
+ itemGrid: "0 1fr 0",
758
+ iconHandler: () => "",
759
+ categories,
760
+ };
761
+ }
762
+
763
+ #syncSelectedItemsFromItems() {
764
+ if (!this.#selectedIds.size || !this.#items.length) return;
765
+ for (const item of this.#items) {
766
+ if (this.#selectedIds.has(item.id)) {
767
+ this.#selectedItems.set(item.id, item);
768
+ }
769
+ }
770
+ }
771
+
772
+ #resetOmniboxSearchState(reason = "unknown") {
773
+ if (!this.#omnibox) return;
774
+
775
+ this.#omnibox.value = "";
776
+
777
+ const input = this.#omnibox.shadowRoot?.querySelector("input");
778
+ if (!input) {
779
+ this.#debug("resetOmniboxSearchState:no-input", { reason });
780
+ return;
781
+ }
782
+
783
+ if (input.value !== "") input.value = "";
784
+
785
+ const autoComplete = input._autoComplete;
786
+ autoComplete?.controller?.().clear?.(`pds-tags:${reason}`);
787
+
788
+ this.#debug("resetOmniboxSearchState", {
789
+ reason,
790
+ inputValue: input.value,
791
+ hasAutoComplete: Boolean(autoComplete),
792
+ });
793
+ }
794
+
795
+ #debug(message, details) {
796
+ return;
797
+ }
798
+ }
799
+
800
+ if (!customElements.get("pds-tags")) {
801
+ customElements.define("pds-tags", PdsTags);
802
+ }