@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,648 @@
1
+ const STAR_PATH =
2
+ "M12 .9l3.1 6.3 7 1-5 4.9 1.2 6.9L12 16.8 5.7 20l1.2-6.9-5-4.9 7-1L12 .9z";
3
+
4
+ /**
5
+ * Star-based rating input that participates in native HTML forms.
6
+ *
7
+ * @element pds-rating
8
+ * @formAssociated
9
+ *
10
+ * @attr {number} max - Maximum rating value and rendered star count (minimum 1, default 5)
11
+ * @attr {number} value - Current rating value (snapped to 0.5 increments)
12
+ * @attr {boolean} disabled - Disables keyboard/pointer interaction and form submission value
13
+ * @attr {boolean} required - Marks the rating as required for form validation
14
+ * @attr {boolean} readonly - Makes the rating non-editable while still focusable
15
+ * @attr {string} name - Form field name used when submitting the rating
16
+ * @attr {string} color - Optional active star color (CSS color value)
17
+ *
18
+ * @property {number} min - Minimum rating value (always 0)
19
+ * @property {number} max - Maximum rating value and rendered star count
20
+ * @property {number} step - Step size used for keyboard and pointer input (always 0.5)
21
+ * @property {number} value - Current rating value
22
+ * @property {string} name - Form field name
23
+ * @property {boolean} disabled - Disabled state
24
+ * @property {boolean} required - Required state
25
+ * @property {boolean} readOnly - Read-only state
26
+ * @property {string} color - Active star color override
27
+ * @property {HTMLFormElement|null} form - Associated form element
28
+ * @property {string} type - Form control type (`range`)
29
+ * @property {ValidityState} validity - Current validity state
30
+ * @property {string} validationMessage - Current validation message
31
+ * @property {boolean} willValidate - Whether the element is eligible for validation
32
+ *
33
+ * @fires input - Fired when the value changes via pointer or keyboard interaction
34
+ * @fires change - Fired after input when the value commit completes
35
+ */
36
+ class PdsRating extends HTMLElement {
37
+ static formAssociated = true;
38
+
39
+ static observedAttributes = ["max", "value", "disabled", "required", "readonly", "name", "color"];
40
+
41
+ #internals;
42
+ #input;
43
+ #interactive;
44
+ #svg;
45
+ #fillRect;
46
+ #onInput;
47
+ #onChange;
48
+ #onPointerDown;
49
+ #onPointerMove;
50
+ #onPointerUp;
51
+ #onKeyDown;
52
+ #observer;
53
+ #pointerActive = false;
54
+
55
+ /**
56
+ * Creates a form-associated rating control with a shadow DOM star strip renderer.
57
+ */
58
+ constructor() {
59
+ super();
60
+ this.#internals = this.attachInternals();
61
+ this.attachShadow({ mode: "open", delegatesFocus: true });
62
+
63
+ this.#onInput = () => {
64
+ if (this.readOnly && this.#input) {
65
+ this.#input.value = String(this.getAttribute("value") ?? 0);
66
+ return;
67
+ }
68
+ this.#syncFromInput();
69
+ };
70
+ this.#onChange = () => {
71
+ if (this.readOnly && this.#input) {
72
+ this.#input.value = String(this.getAttribute("value") ?? 0);
73
+ return;
74
+ }
75
+ this.#syncFromInput();
76
+ };
77
+
78
+ this.#onPointerDown = (event) => {
79
+ if (this.disabled || this.readOnly) return;
80
+ this.#pointerActive = true;
81
+ this.#interactive?.setPointerCapture?.(event.pointerId);
82
+ this.#setValueFromClientX(event.clientX);
83
+ event.preventDefault();
84
+ };
85
+
86
+ this.#onPointerMove = (event) => {
87
+ if (!this.#pointerActive || this.disabled || this.readOnly) return;
88
+ this.#setValueFromClientX(event.clientX);
89
+ event.preventDefault();
90
+ };
91
+
92
+ this.#onPointerUp = (event) => {
93
+ if (!this.#pointerActive) return;
94
+ this.#pointerActive = false;
95
+ this.#interactive?.releasePointerCapture?.(event.pointerId);
96
+ this.#dispatchInputAndChange();
97
+ };
98
+
99
+ this.#onKeyDown = (event) => {
100
+ if (this.disabled || this.readOnly) return;
101
+
102
+ const step = this.step;
103
+ let next = this.value;
104
+
105
+ switch (event.key) {
106
+ case "ArrowLeft":
107
+ case "ArrowDown":
108
+ next -= step;
109
+ break;
110
+ case "ArrowRight":
111
+ case "ArrowUp":
112
+ next += step;
113
+ break;
114
+ case "Home":
115
+ next = this.min;
116
+ break;
117
+ case "End":
118
+ next = this.max;
119
+ break;
120
+ default:
121
+ return;
122
+ }
123
+
124
+ event.preventDefault();
125
+ this.value = next;
126
+ this.#dispatchInputAndChange();
127
+ };
128
+
129
+ this.shadowRoot.innerHTML = /* html */ `
130
+ <style>
131
+ :host {
132
+ --rating-star-size: var(--spacing-lg);
133
+ --rating-star-gap: var(--spacing-2xs);
134
+ --rating-star-off: var(--color-text-muted);
135
+ --rating-star-on: var(--color-warning-500);
136
+ --rating-focus: var(--color-primary-500);
137
+ --rating-fill-pct: 0%;
138
+ display: inline-block;
139
+ inline-size: fit-content;
140
+ max-inline-size: 100%;
141
+ touch-action: none;
142
+ }
143
+
144
+ .wrap {
145
+ position: relative;
146
+ display: inline-grid;
147
+ gap: var(--spacing-2xs);
148
+ align-items: center;
149
+ justify-items: start;
150
+ min-block-size: calc(var(--rating-star-size) + var(--spacing-xs));
151
+ }
152
+
153
+ .interactive {
154
+ display: inline-grid;
155
+ place-items: center start;
156
+ inline-size: 100%;
157
+ cursor: pointer;
158
+ user-select: none;
159
+ -webkit-tap-highlight-color: transparent;
160
+ touch-action: pan-x;
161
+ }
162
+
163
+ :host([disabled]) .interactive,
164
+ :host([readonly]) .interactive {
165
+ cursor: default;
166
+ }
167
+
168
+ :host([disabled]) {
169
+ opacity: 0.5;
170
+ }
171
+
172
+ .stars {
173
+ display: block;
174
+ inline-size: auto;
175
+ block-size: var(--rating-star-size);
176
+ overflow: visible;
177
+ transition: transform 220ms ease, opacity 220ms ease;
178
+ }
179
+
180
+ :host([data-in-view="false"]) .stars {
181
+ opacity: 0;
182
+ transform: translateY(var(--spacing-xs)) scale(0.98);
183
+ }
184
+
185
+ :host([data-in-view="true"]) .stars {
186
+ opacity: 1;
187
+ transform: translateY(0) scale(1);
188
+ }
189
+
190
+ input[type="range"] {
191
+ position: absolute !important;
192
+ inset-inline-start: 0 !important;
193
+ inset-block-start: 0 !important;
194
+ inline-size: 1px !important;
195
+ block-size: 1px !important;
196
+ min-inline-size: 1px !important;
197
+ min-block-size: 1px !important;
198
+ margin: -1px !important;
199
+ border: 0 !important;
200
+ padding: 0 !important;
201
+ opacity: 0 !important;
202
+ overflow: hidden !important;
203
+ clip: rect(0 0 0 0) !important;
204
+ clip-path: inset(50%) !important;
205
+ white-space: nowrap !important;
206
+ appearance: none !important;
207
+ -webkit-appearance: none !important;
208
+ pointer-events: none !important;
209
+ }
210
+
211
+ :host(:focus-within) .interactive {
212
+ outline: 2px solid var(--rating-focus);
213
+ outline-offset: var(--spacing-3xs);
214
+ border-radius: var(--radius-sm);
215
+ }
216
+
217
+ @media (prefers-reduced-motion: reduce) {
218
+ .stars {
219
+ transition: none;
220
+ }
221
+ }
222
+ </style>
223
+
224
+ <div class="wrap" part="control">
225
+ <div class="interactive" part="interactive" aria-hidden="true">
226
+ <svg class="stars" part="stars" focusable="false" aria-hidden="true"></svg>
227
+ </div>
228
+ <input class="native-input" type="range" />
229
+ </div>
230
+ `;
231
+
232
+ this.#interactive = this.shadowRoot.querySelector(".interactive");
233
+ this.#svg = this.shadowRoot.querySelector(".stars");
234
+ }
235
+
236
+ /**
237
+ * Initializes DOM wiring, syncing, and in-view animation observation.
238
+ * @returns {void}
239
+ */
240
+ connectedCallback() {
241
+ this.setAttribute("data-in-view", "false");
242
+ this.#wireInput();
243
+ this.#renderSvg();
244
+ this.#syncFromInput();
245
+ this.#observeInView();
246
+
247
+ this.#interactive?.addEventListener("pointerdown", this.#onPointerDown);
248
+ this.#interactive?.addEventListener("pointermove", this.#onPointerMove);
249
+ this.#interactive?.addEventListener("pointerup", this.#onPointerUp);
250
+ this.#interactive?.addEventListener("pointercancel", this.#onPointerUp);
251
+
252
+ this.addEventListener("keydown", this.#onKeyDown);
253
+ }
254
+
255
+ /**
256
+ * Cleans up listeners and observers.
257
+ * @returns {void}
258
+ */
259
+ disconnectedCallback() {
260
+ this.#teardownInput();
261
+ this.#observer?.disconnect();
262
+
263
+ this.#interactive?.removeEventListener("pointerdown", this.#onPointerDown);
264
+ this.#interactive?.removeEventListener("pointermove", this.#onPointerMove);
265
+ this.#interactive?.removeEventListener("pointerup", this.#onPointerUp);
266
+ this.#interactive?.removeEventListener("pointercancel", this.#onPointerUp);
267
+
268
+ this.removeEventListener("keydown", this.#onKeyDown);
269
+ }
270
+
271
+ /**
272
+ * Reacts to reflected attribute changes and keeps internal state in sync.
273
+ * @param {string} name
274
+ * @param {string|null} oldValue
275
+ * @param {string|null} newValue
276
+ * @returns {void}
277
+ */
278
+ attributeChangedCallback(name, oldValue, newValue) {
279
+ if (oldValue === newValue) return;
280
+
281
+ if (name === "max") {
282
+ if (this.#input) this.#input.max = String(this.max);
283
+ this.#renderSvg();
284
+ this.#syncFromInput();
285
+ return;
286
+ }
287
+
288
+ if (name === "value") {
289
+ if (this.#input && this.#input.value !== String(this.value)) {
290
+ this.#input.value = String(this.value);
291
+ }
292
+ this.#syncFromInput();
293
+ return;
294
+ }
295
+
296
+ if (name === "disabled") {
297
+ if (this.#input) this.#input.disabled = this.disabled;
298
+ this.#syncFromInput();
299
+ return;
300
+ }
301
+
302
+ if (name === "required") {
303
+ if (this.#input) this.#input.required = this.required;
304
+ this.#syncFromInput();
305
+ return;
306
+ }
307
+
308
+ if (name === "readonly") {
309
+ if (this.#input) this.#input.readOnly = this.readOnly;
310
+ this.#syncFromInput();
311
+ return;
312
+ }
313
+
314
+ if (name === "name" && this.#input) {
315
+ this.#input.name = this.name;
316
+ this.#syncFromInput();
317
+ }
318
+
319
+ if (name === "color") {
320
+ this.#syncFromInput();
321
+ }
322
+ }
323
+
324
+ /** @returns {number} */
325
+ get min() {
326
+ return 0;
327
+ }
328
+
329
+ /** @returns {number} */
330
+ get max() {
331
+ const raw = Number(this.getAttribute("max") ?? 5);
332
+ if (!Number.isFinite(raw)) return 5;
333
+ return Math.max(1, Math.round(raw));
334
+ }
335
+
336
+ /** @param {number|string} value */
337
+ set max(value) {
338
+ this.setAttribute("max", String(value));
339
+ }
340
+
341
+ /** @returns {number} */
342
+ get step() {
343
+ return 0.5;
344
+ }
345
+
346
+ /** @returns {number} */
347
+ get value() {
348
+ const raw = Number(this.getAttribute("value") ?? this.#input?.value ?? 0);
349
+ if (!Number.isFinite(raw)) return 0;
350
+ return this.#clamp(raw);
351
+ }
352
+
353
+ /** @param {number|string} next */
354
+ set value(next) {
355
+ const clamped = this.#clamp(Number(next));
356
+ this.setAttribute("value", String(clamped));
357
+ if (this.#input) this.#input.value = String(clamped);
358
+ this.#syncFromInput();
359
+ }
360
+
361
+ /** @returns {string} */
362
+ get name() {
363
+ return this.getAttribute("name") ?? "";
364
+ }
365
+
366
+ /** @param {string|null|undefined} next */
367
+ set name(next) {
368
+ if (next == null || next === "") {
369
+ this.removeAttribute("name");
370
+ return;
371
+ }
372
+ this.setAttribute("name", String(next));
373
+ }
374
+
375
+ /** @returns {HTMLFormElement|null} */
376
+ get form() {
377
+ return this.#internals.form;
378
+ }
379
+
380
+ /** @returns {string} */
381
+ get color() {
382
+ return this.getAttribute("color") ?? "";
383
+ }
384
+
385
+ /** @param {string|null|undefined} next */
386
+ set color(next) {
387
+ if (next == null || String(next).trim() === "") {
388
+ this.removeAttribute("color");
389
+ return;
390
+ }
391
+ this.setAttribute("color", String(next));
392
+ }
393
+
394
+ /** @returns {string} */
395
+ get type() {
396
+ return "range";
397
+ }
398
+
399
+ /** @returns {ValidityState} */
400
+ get validity() {
401
+ return this.#internals.validity;
402
+ }
403
+
404
+ /** @returns {string} */
405
+ get validationMessage() {
406
+ return this.#internals.validationMessage;
407
+ }
408
+
409
+ /** @returns {boolean} */
410
+ get willValidate() {
411
+ return this.#internals.willValidate;
412
+ }
413
+
414
+ /** @returns {boolean} */
415
+ get disabled() {
416
+ return this.hasAttribute("disabled");
417
+ }
418
+
419
+ /** @param {boolean} next */
420
+ set disabled(next) {
421
+ this.toggleAttribute("disabled", Boolean(next));
422
+ }
423
+
424
+ /** @returns {boolean} */
425
+ get required() {
426
+ return this.hasAttribute("required");
427
+ }
428
+
429
+ /** @param {boolean} next */
430
+ set required(next) {
431
+ this.toggleAttribute("required", Boolean(next));
432
+ }
433
+
434
+ /** @returns {boolean} */
435
+ get readOnly() {
436
+ return this.hasAttribute("readonly");
437
+ }
438
+
439
+ /** @param {boolean} next */
440
+ set readOnly(next) {
441
+ this.toggleAttribute("readonly", Boolean(next));
442
+ }
443
+
444
+ /**
445
+ * Runs constraint validation.
446
+ * @returns {boolean}
447
+ */
448
+ checkValidity() {
449
+ return this.#internals.checkValidity();
450
+ }
451
+
452
+ /**
453
+ * Runs constraint validation and reports the result to the user agent.
454
+ * @returns {boolean}
455
+ */
456
+ reportValidity() {
457
+ return this.#internals.reportValidity();
458
+ }
459
+
460
+ /**
461
+ * Restores default value during parent form reset.
462
+ * @returns {void}
463
+ */
464
+ formResetCallback() {
465
+ if (!this.#input) return;
466
+ const resetValue = this.#input.defaultValue ? Number(this.#input.defaultValue) : 0;
467
+ this.value = this.#clamp(resetValue);
468
+ }
469
+
470
+ /**
471
+ * Restores state from browser session history/form restore.
472
+ * @param {string|File|FormData|null} state
473
+ * @returns {void}
474
+ */
475
+ formStateRestoreCallback(state) {
476
+ const restored = Number(state);
477
+ if (Number.isFinite(restored)) {
478
+ this.value = restored;
479
+ }
480
+ }
481
+
482
+ #wireInput() {
483
+ this.#input = this.shadowRoot?.querySelector(".native-input");
484
+ if (!this.#input) return;
485
+
486
+ this.#input.type = "range";
487
+ this.#input.min = "0";
488
+ this.#input.max = String(this.max);
489
+ this.#input.step = "0.5";
490
+ this.#input.setAttribute("data-enhanced-range", "1");
491
+ this.#input.tabIndex = -1;
492
+
493
+ this.#input.value = String(this.value);
494
+
495
+ if (this.disabled) this.#input.disabled = true;
496
+ if (this.required) this.#input.required = true;
497
+ if (this.readOnly) this.#input.readOnly = true;
498
+
499
+ this.#input.name = this.name;
500
+
501
+ if (!this.#input.hasAttribute("aria-label") && !this.#input.hasAttribute("aria-labelledby")) {
502
+ this.#input.setAttribute("aria-label", "Rating");
503
+ }
504
+
505
+ if (!this.hasAttribute("role")) {
506
+ this.setAttribute("role", "slider");
507
+ }
508
+
509
+ this.#input.addEventListener("input", this.#onInput);
510
+ this.#input.addEventListener("change", this.#onChange);
511
+
512
+ this.tabIndex = this.disabled ? -1 : 0;
513
+ }
514
+
515
+ #teardownInput() {
516
+ if (!this.#input) return;
517
+ this.#input.removeEventListener("input", this.#onInput);
518
+ this.#input.removeEventListener("change", this.#onChange);
519
+ }
520
+
521
+ #syncFromInput() {
522
+ if (!this.#input) return;
523
+
524
+ const current = this.#clamp(Number(this.#input.value));
525
+ this.#input.value = String(current);
526
+
527
+ if (this.getAttribute("value") !== String(current)) {
528
+ this.setAttribute("value", String(current));
529
+ }
530
+
531
+ const max = this.max;
532
+ const percentage = max > 0 ? (current / max) * 100 : 0;
533
+ this.style.setProperty("--rating-fill-pct", `${Math.max(0, Math.min(100, percentage))}%`);
534
+
535
+ if (this.color.trim()) {
536
+ this.style.setProperty("--rating-star-on", this.color.trim());
537
+ } else {
538
+ this.style.removeProperty("--rating-star-on");
539
+ }
540
+
541
+ this.#input.setAttribute("aria-valuetext", `${current} out of ${max} stars`);
542
+ this.setAttribute("aria-label", this.#input.getAttribute("aria-label") || "Rating");
543
+ this.setAttribute("aria-disabled", String(this.disabled));
544
+ this.setAttribute("aria-readonly", String(this.readOnly));
545
+ this.setAttribute("aria-valuemin", "0");
546
+ this.setAttribute("aria-valuemax", String(max));
547
+ this.setAttribute("aria-valuenow", String(current));
548
+
549
+ if (this.#fillRect) {
550
+ this.#fillRect.setAttribute("width", String((current / max) * this.#starStripWidth(max)));
551
+ }
552
+
553
+ const valueForForm = this.disabled ? null : String(current);
554
+ this.#internals.setFormValue(valueForForm, valueForForm);
555
+ this.#internals.setValidity(this.#input.validity, this.#input.validationMessage, this.#input);
556
+
557
+ this.tabIndex = this.disabled ? -1 : 0;
558
+ }
559
+
560
+ #renderSvg() {
561
+ const max = this.max;
562
+ const starSize = 24;
563
+ const gap = 4;
564
+ const width = this.#starStripWidth(max, starSize, gap);
565
+ const height = starSize;
566
+ const maskId = `stars-mask-${crypto.randomUUID()}`;
567
+
568
+ const stars = Array.from({ length: max }, (_, index) => {
569
+ const x = index * (starSize + gap);
570
+ return `<path d="${STAR_PATH}" transform="translate(${x},0)" />`;
571
+ }).join("");
572
+
573
+ this.#svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
574
+ this.#svg.setAttribute("width", String(width));
575
+ this.#svg.setAttribute("height", String(height));
576
+ this.#svg.innerHTML = `
577
+ <style>
578
+ #rating-fill {
579
+ transition: width 180ms ease-out;
580
+ }
581
+ </style>
582
+ <defs>
583
+ <mask id="${maskId}" maskUnits="userSpaceOnUse" x="0" y="0" width="${width}" height="${height}">
584
+ <rect x="0" y="0" width="${width}" height="${height}" fill="black"></rect>
585
+ <g fill="white">${stars}</g>
586
+ </mask>
587
+ </defs>
588
+ <rect x="0" y="0" width="${width}" height="${height}" fill="var(--rating-star-off)" mask="url(#${maskId})"></rect>
589
+ <rect id="rating-fill" x="0" y="0" width="0" height="${height}" fill="var(--rating-star-on)" mask="url(#${maskId})"></rect>
590
+ `;
591
+
592
+ this.#fillRect = this.#svg.querySelector("#rating-fill");
593
+ }
594
+
595
+ #starStripWidth(max, starSize = 24, gap = 4) {
596
+ return max * starSize + Math.max(0, max - 1) * gap;
597
+ }
598
+
599
+ #setValueFromClientX(clientX) {
600
+ const rect = this.#interactive?.getBoundingClientRect();
601
+ if (!rect || !rect.width) return;
602
+
603
+ const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
604
+ const ratio = x / rect.width;
605
+ const raw = this.min + ratio * (this.max - this.min);
606
+ const snapped = Math.round(raw / this.step) * this.step;
607
+
608
+ this.value = snapped;
609
+ }
610
+
611
+ #dispatchInputAndChange() {
612
+ if (!this.#input) return;
613
+
614
+ const inputEvent = new Event("input", { bubbles: true, composed: true });
615
+ const changeEvent = new Event("change", { bubbles: true, composed: true });
616
+
617
+ this.#input.dispatchEvent(inputEvent);
618
+ this.#input.dispatchEvent(changeEvent);
619
+
620
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
621
+ this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
622
+ }
623
+
624
+ #clamp(value) {
625
+ if (!Number.isFinite(value)) return 0;
626
+ const bounded = Math.max(this.min, Math.min(this.max, value));
627
+ return Math.round(bounded / this.step) * this.step;
628
+ }
629
+
630
+ #observeInView() {
631
+ this.#observer?.disconnect();
632
+ this.#observer = new IntersectionObserver(
633
+ (entries) => {
634
+ const [entry] = entries;
635
+ if (entry?.isIntersecting) {
636
+ this.setAttribute("data-in-view", "true");
637
+ this.#observer?.disconnect();
638
+ }
639
+ },
640
+ { threshold: 0.2 }
641
+ );
642
+ this.#observer.observe(this);
643
+ }
644
+ }
645
+
646
+ if (!customElements.get("pds-rating")) {
647
+ customElements.define("pds-rating", PdsRating);
648
+ }