@pure-ds/storybook 0.7.25 → 0.7.26

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 (68) hide show
  1. package/.storybook/preview.js +5 -4
  2. package/dist/pds-reference.json +2266 -1017
  3. package/package.json +2 -2
  4. package/public/assets/js/app.js +1 -1
  5. package/public/assets/js/pds-ask.js +6 -6
  6. package/public/assets/js/pds-manager.js +104 -29
  7. package/public/assets/pds/components/pds-calendar.js +91 -159
  8. package/public/assets/pds/components/pds-daterange.js +683 -0
  9. package/public/assets/pds/components/pds-form.js +123 -21
  10. package/public/assets/pds/components/pds-rating.js +648 -0
  11. package/public/assets/pds/components/pds-tags.js +802 -0
  12. package/public/assets/pds/core/pds-ask.js +6 -6
  13. package/public/assets/pds/core/pds-manager.js +104 -29
  14. package/public/assets/pds/custom-elements.json +1099 -74
  15. package/public/assets/pds/pds-css-complete.json +7 -2
  16. package/public/assets/pds/pds-runtime-config.json +1 -1
  17. package/public/assets/pds/pds.css-data.json +4 -4
  18. package/public/assets/pds/styles/pds-components.css +96 -24
  19. package/public/assets/pds/styles/pds-components.css.js +192 -48
  20. package/public/assets/pds/styles/pds-primitives.css +6 -3
  21. package/public/assets/pds/styles/pds-primitives.css.js +12 -6
  22. package/public/assets/pds/styles/pds-styles.css +104 -29
  23. package/public/assets/pds/styles/pds-styles.css.js +208 -58
  24. package/public/assets/pds/styles/pds-utilities.css +2 -2
  25. package/public/assets/pds/styles/pds-utilities.css.js +4 -4
  26. package/public/assets/pds/vscode-custom-data.json +97 -0
  27. package/src/js/common/ask.js +122 -12
  28. package/src/js/pds-core/pds-generator.js +103 -28
  29. package/src/js/pds-core/pds-ontology.js +2 -2
  30. package/stories/components/PdsCalendar.stories.js +650 -168
  31. package/stories/components/PdsDaterange.stories.js +85 -0
  32. package/stories/components/PdsForm.Basics.stories.js +16 -0
  33. package/stories/components/PdsForm.ConditionalAndCalculated.stories.js +16 -0
  34. package/stories/components/PdsForm.CustomContent.stories.js +10 -0
  35. package/stories/components/PdsForm.Dialogs.stories.js +7 -0
  36. package/stories/components/PdsForm.Layout.stories.js +16 -0
  37. package/stories/components/PdsForm.SelectionAndArrays.stories.js +12 -0
  38. package/stories/components/PdsForm.stories.js +179 -219
  39. package/stories/components/PdsIcon.stories.js +17 -7
  40. package/stories/components/PdsOmnibox.stories.js +1 -60
  41. package/stories/components/PdsRating.stories.js +126 -0
  42. package/stories/components/PdsTags.stories.js +224 -0
  43. package/stories/components/PdsToaster.stories.js +1 -1
  44. package/stories/components/omnibox-countries-api-settings.js +63 -0
  45. package/stories/foundations/Colors.stories.js +6 -1
  46. package/stories/foundations/HTMLDefaults.stories.js +6 -6
  47. package/stories/foundations/Icons.stories.js +4 -4
  48. package/stories/foundations/MeshGradients.stories.js +13 -3
  49. package/stories/foundations/SmartSurfaces.stories.js +43 -31
  50. package/stories/foundations/Typography.stories.js +30 -108
  51. package/stories/foundations/ZIndex.stories.js +3 -3
  52. package/stories/layout/LayoutOverview.stories.js +3 -3
  53. package/stories/layout/LayoutSystem.stories.js +24 -18
  54. package/stories/patterns/BorderEffects.stories.js +28 -16
  55. package/stories/patterns/InteractiveStates.stories.js +39 -24
  56. package/stories/patterns/Utilities.stories.js +45 -23
  57. package/stories/primitives/ArticleLayout.stories.js +176 -0
  58. package/stories/primitives/Badges.stories.js +24 -8
  59. package/stories/primitives/Buttons.stories.js +29 -9
  60. package/stories/primitives/Callouts.stories.js +8 -7
  61. package/stories/primitives/Cards.stories.js +4 -1
  62. package/stories/primitives/FormElements.stories.js +8 -2
  63. package/stories/primitives/HtmlFormElements.stories.js +8 -2
  64. package/stories/primitives/HtmlFormGroups.stories.js +5 -2
  65. package/stories/primitives/Media.stories.js +44 -30
  66. package/stories/primitives/Tables.stories.js +25 -7
  67. package/stories/utils/PdsAsk.stories.js +17 -5
  68. package/stories/utils/PdsObjectApi.stories.js +16 -4
@@ -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
+ }