@incursa/ui-kit 1.0.1 → 1.2.0

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 (45) hide show
  1. package/AI-AGENT-INSTRUCTIONS.md +41 -28
  2. package/LLMS.txt +55 -41
  3. package/README.md +181 -68
  4. package/dist/inc-design-language.css +413 -239
  5. package/dist/inc-design-language.css.map +1 -1
  6. package/dist/inc-design-language.js +520 -0
  7. package/dist/inc-design-language.min.css +1 -1
  8. package/dist/inc-design-language.min.css.map +1 -1
  9. package/dist/web-components/README.md +92 -0
  10. package/dist/web-components/RUNTIME-NOTES.md +40 -0
  11. package/dist/web-components/base-element.js +193 -0
  12. package/dist/web-components/components/feedback.js +1074 -0
  13. package/dist/web-components/components/forms.js +979 -0
  14. package/dist/web-components/components/layout.js +408 -0
  15. package/dist/web-components/components/navigation.js +854 -0
  16. package/dist/web-components/components/overlays.js +634 -0
  17. package/dist/web-components/controllers/focus.js +101 -0
  18. package/dist/web-components/controllers/overlay.js +128 -0
  19. package/dist/web-components/controllers/selection.js +145 -0
  20. package/dist/web-components/controllers/theme.js +173 -0
  21. package/dist/web-components/index.js +886 -0
  22. package/dist/web-components/package.json +3 -0
  23. package/dist/web-components/registry.js +74 -0
  24. package/dist/web-components/shared.js +186 -0
  25. package/dist/web-components/style.css +6 -0
  26. package/package.json +11 -2
  27. package/src/inc-design-language.js +520 -0
  28. package/src/inc-design-language.scss +434 -246
  29. package/src/web-components/README.md +92 -0
  30. package/src/web-components/RUNTIME-NOTES.md +40 -0
  31. package/src/web-components/base-element.js +193 -0
  32. package/src/web-components/components/feedback.js +1074 -0
  33. package/src/web-components/components/forms.js +979 -0
  34. package/src/web-components/components/layout.js +408 -0
  35. package/src/web-components/components/navigation.js +854 -0
  36. package/src/web-components/components/overlays.js +634 -0
  37. package/src/web-components/controllers/focus.js +101 -0
  38. package/src/web-components/controllers/overlay.js +128 -0
  39. package/src/web-components/controllers/selection.js +145 -0
  40. package/src/web-components/controllers/theme.js +173 -0
  41. package/src/web-components/index.js +886 -0
  42. package/src/web-components/package.json +3 -0
  43. package/src/web-components/registry.js +74 -0
  44. package/src/web-components/shared.js +186 -0
  45. package/src/web-components/style.css +6 -0
@@ -0,0 +1,1074 @@
1
+ const THEME_MODES = ["light", "dark", "system"];
2
+ const DEFAULT_THEME_STORAGE_KEY = "inc-theme-mode";
3
+ const HostElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
4
+ const themeSubscribers = new Set();
5
+
6
+ let themeRuntimeInitialized = false;
7
+ let themeMode = "system";
8
+ let themeResolved = "light";
9
+ let themeStorageKey = DEFAULT_THEME_STORAGE_KEY;
10
+ let themeMediaQuery = null;
11
+ let themeStorageListenerBound = false;
12
+ let themeMediaListenerBound = false;
13
+
14
+ function isThemeMode(value) {
15
+ return THEME_MODES.includes(value);
16
+ }
17
+
18
+ function toBooleanAttribute(value) {
19
+ if (value === null || value === undefined) {
20
+ return false;
21
+ }
22
+
23
+ if (value === "" || value === "true") {
24
+ return true;
25
+ }
26
+
27
+ return value !== "false";
28
+ }
29
+
30
+ function toPositiveInt(value) {
31
+ const parsed = Number.parseInt(value || "", 10);
32
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
33
+ }
34
+
35
+ function getSystemTheme() {
36
+ if (!window.matchMedia) {
37
+ return "light";
38
+ }
39
+
40
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
41
+ }
42
+
43
+ function resolveTheme(mode) {
44
+ return mode === "system" ? getSystemTheme() : mode;
45
+ }
46
+
47
+ function getRootThemeMode() {
48
+ const root = document.documentElement;
49
+
50
+ return root.getAttribute("data-inc-theme-mode")
51
+ || root.dataset.incThemeMode
52
+ || root.getAttribute("data-bs-theme")
53
+ || "system";
54
+ }
55
+
56
+ function getStoredThemeMode(storageKey = DEFAULT_THEME_STORAGE_KEY) {
57
+ try {
58
+ const stored = window.localStorage.getItem(storageKey);
59
+ return isThemeMode(stored) ? stored : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function persistThemeMode(mode, storageKey = DEFAULT_THEME_STORAGE_KEY) {
66
+ try {
67
+ if (mode === "system") {
68
+ window.localStorage.removeItem(storageKey);
69
+ return;
70
+ }
71
+
72
+ window.localStorage.setItem(storageKey, mode);
73
+ } catch {
74
+ // Ignore storage failures in restricted/private modes.
75
+ }
76
+ }
77
+
78
+ function applyThemeMode(mode, options = {}) {
79
+ const nextMode = isThemeMode(mode) ? mode : "system";
80
+ const resolved = resolveTheme(nextMode);
81
+ const root = document.documentElement;
82
+ const storageKey = options.storageKey || themeStorageKey || DEFAULT_THEME_STORAGE_KEY;
83
+
84
+ themeMode = nextMode;
85
+ themeResolved = resolved;
86
+ themeStorageKey = storageKey;
87
+
88
+ root.setAttribute("data-inc-theme-mode", nextMode);
89
+ root.setAttribute("data-bs-theme", resolved);
90
+ root.style.colorScheme = resolved;
91
+ root.dataset.incThemeModeState = nextMode;
92
+ root.dataset.incThemeResolved = resolved;
93
+
94
+ if (options.persist !== false) {
95
+ persistThemeMode(nextMode, storageKey);
96
+ }
97
+
98
+ if (options.dispatch !== false) {
99
+ const event = new CustomEvent("inc-theme-change", {
100
+ bubbles: true,
101
+ composed: true,
102
+ detail: {
103
+ mode: nextMode,
104
+ resolved,
105
+ },
106
+ });
107
+ root.dispatchEvent(event);
108
+ }
109
+
110
+ themeSubscribers.forEach((notify) => {
111
+ try {
112
+ notify({ mode: nextMode, resolved });
113
+ } catch {
114
+ // Isolate subscriber failures.
115
+ }
116
+ });
117
+
118
+ return { mode: nextMode, resolved };
119
+ }
120
+
121
+ function initializeThemeRuntime(storageKey = DEFAULT_THEME_STORAGE_KEY) {
122
+ themeStorageKey = storageKey || DEFAULT_THEME_STORAGE_KEY;
123
+
124
+ if (!themeRuntimeInitialized) {
125
+ themeRuntimeInitialized = true;
126
+
127
+ const initialMode = getStoredThemeMode(themeStorageKey) || getRootThemeMode();
128
+ applyThemeMode(initialMode, {
129
+ dispatch: false,
130
+ persist: false,
131
+ storageKey: themeStorageKey,
132
+ });
133
+
134
+ if (!themeStorageListenerBound) {
135
+ themeStorageListenerBound = true;
136
+ window.addEventListener("storage", (event) => {
137
+ if (event.key !== themeStorageKey) {
138
+ return;
139
+ }
140
+
141
+ const storedMode = getStoredThemeMode(themeStorageKey) || getRootThemeMode();
142
+ applyThemeMode(storedMode, {
143
+ dispatch: false,
144
+ persist: false,
145
+ storageKey: themeStorageKey,
146
+ });
147
+ });
148
+ }
149
+
150
+ if (!themeMediaListenerBound && window.matchMedia) {
151
+ themeMediaListenerBound = true;
152
+ themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
153
+
154
+ const onMediaChange = () => {
155
+ if (themeMode === "system") {
156
+ applyThemeMode("system", {
157
+ dispatch: true,
158
+ persist: false,
159
+ storageKey: themeStorageKey,
160
+ });
161
+ }
162
+ };
163
+
164
+ if (typeof themeMediaQuery.addEventListener === "function") {
165
+ themeMediaQuery.addEventListener("change", onMediaChange);
166
+ } else if (typeof themeMediaQuery.addListener === "function") {
167
+ themeMediaQuery.addListener(onMediaChange);
168
+ }
169
+ }
170
+ }
171
+
172
+ return {
173
+ mode: themeMode,
174
+ resolved: themeResolved,
175
+ };
176
+ }
177
+
178
+ function subscribeThemeState(handler) {
179
+ themeSubscribers.add(handler);
180
+ handler({ mode: themeMode, resolved: themeResolved });
181
+ return () => themeSubscribers.delete(handler);
182
+ }
183
+
184
+ function formatRemaining(totalSeconds) {
185
+ if (totalSeconds < 60) {
186
+ return `${totalSeconds}s`;
187
+ }
188
+
189
+ const minutes = Math.floor(totalSeconds / 60);
190
+ const seconds = totalSeconds % 60;
191
+ return `${minutes}m ${seconds}s`;
192
+ }
193
+
194
+ export class IncStatePanel extends HostElement {
195
+ static observedAttributes = ["tone", "variant", "title", "body", "status", "open"];
196
+
197
+ #fallback = null;
198
+ #appliedVariantClass = "";
199
+
200
+ connectedCallback() {
201
+ this.classList.add("inc-state-panel");
202
+ this.setAttribute("part", "panel");
203
+ this.#ensureFallback();
204
+ this.#syncFromAttributes();
205
+ this.#dispatchSlotChange();
206
+ }
207
+
208
+ attributeChangedCallback() {
209
+ if (!this.isConnected) {
210
+ return;
211
+ }
212
+
213
+ this.#syncFromAttributes();
214
+ }
215
+
216
+ #ensureFallback() {
217
+ if (this.childElementCount > 0) {
218
+ this.#fallback = null;
219
+ return;
220
+ }
221
+
222
+ const head = document.createElement("div");
223
+ const icon = document.createElement("span");
224
+ const title = document.createElement("h2");
225
+ const body = document.createElement("p");
226
+ const actions = document.createElement("div");
227
+
228
+ head.className = "inc-state-panel__head";
229
+ icon.className = "inc-state-panel__icon";
230
+ title.className = "inc-state-panel__title";
231
+ body.className = "inc-state-panel__body";
232
+ actions.className = "inc-state-panel__actions";
233
+
234
+ icon.setAttribute("part", "icon");
235
+ title.setAttribute("part", "title");
236
+ body.setAttribute("part", "body");
237
+ actions.setAttribute("part", "actions");
238
+ head.append(icon, title);
239
+ this.append(head, body, actions);
240
+
241
+ this.#fallback = { icon, title, body, actions };
242
+ }
243
+
244
+ #syncFromAttributes() {
245
+ const nextVariant = this.getAttribute("variant") || this.getAttribute("tone") || "";
246
+
247
+ if (this.#appliedVariantClass) {
248
+ this.classList.remove(this.#appliedVariantClass);
249
+ this.#appliedVariantClass = "";
250
+ }
251
+
252
+ if (nextVariant) {
253
+ this.#appliedVariantClass = `inc-state-panel--${nextVariant}`;
254
+ this.classList.add(this.#appliedVariantClass);
255
+ }
256
+
257
+ const isOpen = this.getAttribute("open") === null
258
+ ? true
259
+ : toBooleanAttribute(this.getAttribute("open"));
260
+
261
+ this.toggleAttribute("hidden", !isOpen);
262
+ this.setAttribute("aria-hidden", isOpen ? "false" : "true");
263
+
264
+ if (!this.#fallback) {
265
+ return;
266
+ }
267
+
268
+ const title = this.getAttribute("title") || "";
269
+ const body = this.getAttribute("body") || "";
270
+ const status = this.getAttribute("status") || "";
271
+
272
+ this.#fallback.title.textContent = title;
273
+ this.#fallback.body.textContent = body;
274
+ this.#fallback.icon.textContent = status;
275
+ this.#fallback.icon.hidden = !status;
276
+ this.#fallback.actions.hidden = true;
277
+ }
278
+
279
+ #dispatchSlotChange() {
280
+ this.dispatchEvent(new Event("slotchange", { bubbles: true, composed: true }));
281
+ }
282
+ }
283
+
284
+ export class IncLiveRegion extends HostElement {
285
+ static observedAttributes = ["politeness", "atomic", "busy"];
286
+
287
+ #announceNode = null;
288
+
289
+ connectedCallback() {
290
+ this.classList.add("inc-live-region");
291
+ this.setAttribute("part", "region");
292
+ this.#ensureNode();
293
+ this.#syncA11y();
294
+ }
295
+
296
+ attributeChangedCallback() {
297
+ if (!this.isConnected) {
298
+ return;
299
+ }
300
+
301
+ this.#syncA11y();
302
+ }
303
+
304
+ announce(message) {
305
+ this.#ensureNode();
306
+ const text = message == null ? "" : String(message);
307
+ this.#announceNode.textContent = "";
308
+
309
+ const apply = () => {
310
+ this.#announceNode.textContent = text;
311
+ };
312
+
313
+ if (window.requestAnimationFrame) {
314
+ window.requestAnimationFrame(apply);
315
+ return;
316
+ }
317
+
318
+ window.setTimeout(apply, 0);
319
+ }
320
+
321
+ #ensureNode() {
322
+ if (this.#announceNode) {
323
+ return;
324
+ }
325
+
326
+ this.#announceNode = document.createElement("span");
327
+ this.#announceNode.className = "inc-live-region__message";
328
+ this.#announceNode.setAttribute("part", "region");
329
+
330
+ if (!this.firstElementChild) {
331
+ this.append(this.#announceNode);
332
+ return;
333
+ }
334
+
335
+ const existing = this.querySelector(".inc-live-region__message");
336
+
337
+ if (existing instanceof HTMLElement) {
338
+ this.#announceNode = existing;
339
+ return;
340
+ }
341
+
342
+ this.append(this.#announceNode);
343
+ }
344
+
345
+ #syncA11y() {
346
+ const politeness = this.getAttribute("politeness") || "polite";
347
+ const isAtomic = this.getAttribute("atomic") === null
348
+ ? true
349
+ : toBooleanAttribute(this.getAttribute("atomic"));
350
+ const isBusy = toBooleanAttribute(this.getAttribute("busy"));
351
+
352
+ this.setAttribute("role", politeness === "assertive" ? "alert" : "status");
353
+ this.setAttribute("aria-live", politeness);
354
+ this.setAttribute("aria-atomic", isAtomic ? "true" : "false");
355
+ this.setAttribute("aria-busy", isBusy ? "true" : "false");
356
+ }
357
+ }
358
+
359
+ export class IncAutoRefresh extends HostElement {
360
+ static observedAttributes = [
361
+ "seconds",
362
+ "label",
363
+ "loading-label",
364
+ "paused-label",
365
+ "pause-action-label",
366
+ "resume-action-label",
367
+ "paused",
368
+ ];
369
+
370
+ #parts = null;
371
+ #timeoutId = 0;
372
+ #visibilityHandler = null;
373
+ #isPaused = false;
374
+ #isLoading = false;
375
+ #deadline = 0;
376
+ #remainingMs = 0;
377
+
378
+ connectedCallback() {
379
+ this.classList.add("inc-auto-refresh");
380
+ this.#ensureMarkup();
381
+ this.#bindHandlers();
382
+ this.#start();
383
+ }
384
+
385
+ disconnectedCallback() {
386
+ this.#stop();
387
+ if (this.#visibilityHandler) {
388
+ document.removeEventListener("visibilitychange", this.#visibilityHandler);
389
+ this.#visibilityHandler = null;
390
+ }
391
+ }
392
+
393
+ attributeChangedCallback(name) {
394
+ if (!this.isConnected || !this.#parts) {
395
+ return;
396
+ }
397
+
398
+ if (name === "paused") {
399
+ if (toBooleanAttribute(this.getAttribute("paused"))) {
400
+ this.pause();
401
+ } else {
402
+ this.resume();
403
+ }
404
+ return;
405
+ }
406
+
407
+ if (name === "seconds") {
408
+ this.#start();
409
+ return;
410
+ }
411
+
412
+ this.#render();
413
+ }
414
+
415
+ pause() {
416
+ if (this.#isLoading || this.#isPaused) {
417
+ return;
418
+ }
419
+
420
+ this.#isPaused = true;
421
+ this.#remainingMs = Math.max(this.#deadline - Date.now(), 0);
422
+ this.#stop();
423
+ this.setAttribute("paused", "");
424
+ this.#render();
425
+ this.dispatchEvent(new CustomEvent("pause", { bubbles: true, composed: true }));
426
+ this.#emitStateChange("paused");
427
+ }
428
+
429
+ resume() {
430
+ if (this.#isLoading || !this.#isPaused) {
431
+ return;
432
+ }
433
+
434
+ this.#isPaused = false;
435
+ this.removeAttribute("paused");
436
+ this.#deadline = Date.now() + Math.max(this.#remainingMs, 1000);
437
+ this.#remainingMs = 0;
438
+ this.#scheduleTick();
439
+ this.dispatchEvent(new CustomEvent("resume", { bubbles: true, composed: true }));
440
+ this.#emitStateChange("running");
441
+ }
442
+
443
+ toggle() {
444
+ if (this.#isPaused) {
445
+ this.resume();
446
+ return;
447
+ }
448
+
449
+ this.pause();
450
+ }
451
+
452
+ refresh() {
453
+ if (this.#isLoading) {
454
+ return;
455
+ }
456
+
457
+ this.#isLoading = true;
458
+ this.#stop();
459
+ this.#render();
460
+ this.#emitStateChange("loading");
461
+
462
+ const refreshEvent = new CustomEvent("refresh", {
463
+ bubbles: true,
464
+ composed: true,
465
+ cancelable: true,
466
+ detail: this.#buildState(),
467
+ });
468
+
469
+ this.dispatchEvent(refreshEvent);
470
+ if (!refreshEvent.defaultPrevented) {
471
+ const deferToPaint = window.requestAnimationFrame
472
+ ? window.requestAnimationFrame.bind(window)
473
+ : (callback) => window.setTimeout(callback, 16);
474
+
475
+ deferToPaint(() => {
476
+ window.setTimeout(() => {
477
+ window.location.reload();
478
+ }, 120);
479
+ });
480
+ }
481
+ }
482
+
483
+ #bindHandlers() {
484
+ if (!this.#parts?.toggle) {
485
+ return;
486
+ }
487
+
488
+ if (!this.#parts.toggle.dataset.incWcBound) {
489
+ this.#parts.toggle.dataset.incWcBound = "true";
490
+ this.#parts.toggle.addEventListener("click", (event) => {
491
+ event.preventDefault();
492
+ this.toggle();
493
+ });
494
+ }
495
+
496
+ if (!this.#visibilityHandler) {
497
+ this.#visibilityHandler = () => {
498
+ if (document.hidden || this.#isPaused || this.#isLoading) {
499
+ return;
500
+ }
501
+
502
+ if ((this.#deadline - Date.now()) <= 0) {
503
+ this.refresh();
504
+ return;
505
+ }
506
+
507
+ this.#scheduleTick();
508
+ };
509
+ document.addEventListener("visibilitychange", this.#visibilityHandler);
510
+ }
511
+ }
512
+
513
+ #ensureMarkup() {
514
+ if (this.querySelector(".inc-auto-refresh__countdown")) {
515
+ this.#parts = this.#getParts();
516
+ return;
517
+ }
518
+
519
+ this.innerHTML = `
520
+ <span class="inc-auto-refresh__countdown" part="countdown">
521
+ <span class="inc-auto-refresh__label" part="label"></span>
522
+ <span class="inc-auto-refresh__value" part="value"></span>
523
+ </span>
524
+ <span class="inc-auto-refresh__status" part="status" hidden>
525
+ <span class="inc-auto-refresh__status-text"></span>
526
+ </span>
527
+ <button type="button" class="inc-auto-refresh__toggle inc-btn inc-btn--outline-secondary inc-btn--micro" part="toggle">
528
+ <span class="inc-auto-refresh__toggle-text"></span>
529
+ </button>
530
+ `.trim();
531
+
532
+ this.#parts = this.#getParts();
533
+ }
534
+
535
+ #getParts() {
536
+ return {
537
+ countdown: this.querySelector(".inc-auto-refresh__countdown"),
538
+ label: this.querySelector(".inc-auto-refresh__label"),
539
+ value: this.querySelector(".inc-auto-refresh__value"),
540
+ status: this.querySelector(".inc-auto-refresh__status"),
541
+ statusText: this.querySelector(".inc-auto-refresh__status-text"),
542
+ toggle: this.querySelector(".inc-auto-refresh__toggle"),
543
+ toggleText: this.querySelector(".inc-auto-refresh__toggle-text"),
544
+ };
545
+ }
546
+
547
+ #start() {
548
+ const refreshSeconds = toPositiveInt(this.getAttribute("seconds"));
549
+
550
+ this.#stop();
551
+ this.#isLoading = false;
552
+ this.#isPaused = toBooleanAttribute(this.getAttribute("paused"));
553
+
554
+ if (!refreshSeconds) {
555
+ this.#render();
556
+ return;
557
+ }
558
+
559
+ this.#deadline = Date.now() + (refreshSeconds * 1000);
560
+ this.#remainingMs = refreshSeconds * 1000;
561
+
562
+ if (this.#isPaused) {
563
+ this.#render();
564
+ return;
565
+ }
566
+
567
+ this.#scheduleTick();
568
+ }
569
+
570
+ #scheduleTick() {
571
+ if (this.#isPaused || this.#isLoading) {
572
+ return;
573
+ }
574
+
575
+ this.#stop();
576
+
577
+ const remainingMs = this.#deadline - Date.now();
578
+
579
+ if (remainingMs <= 0) {
580
+ this.refresh();
581
+ return;
582
+ }
583
+
584
+ const remainingSeconds = Math.ceil(remainingMs / 1000);
585
+ this.#remainingMs = remainingMs;
586
+ this.#renderCountdown(remainingSeconds);
587
+
588
+ this.dispatchEvent(new CustomEvent("tick", {
589
+ bubbles: true,
590
+ composed: true,
591
+ detail: this.#buildState(),
592
+ }));
593
+
594
+ const nextDelay = remainingMs % 1000 || 1000;
595
+ this.#timeoutId = window.setTimeout(() => {
596
+ this.#scheduleTick();
597
+ }, nextDelay);
598
+ }
599
+
600
+ #render() {
601
+ if (this.#isLoading) {
602
+ this.#renderLoading();
603
+ return;
604
+ }
605
+
606
+ const fallbackSeconds = Math.max(1, Math.ceil(this.#remainingMs / 1000));
607
+ if (this.#isPaused) {
608
+ this.#renderPaused(fallbackSeconds);
609
+ return;
610
+ }
611
+
612
+ this.#renderCountdown(fallbackSeconds);
613
+ }
614
+
615
+ #renderCountdown(seconds) {
616
+ const label = this.getAttribute("label") || "Refresh in";
617
+ if (this.#parts.label) {
618
+ this.#parts.label.textContent = label;
619
+ }
620
+
621
+ if (this.#parts.value) {
622
+ this.#parts.value.textContent = formatRemaining(seconds);
623
+ }
624
+
625
+ this.classList.remove("is-paused");
626
+ this.classList.remove("is-loading");
627
+ this.setAttribute("aria-busy", "false");
628
+
629
+ if (this.#parts.countdown) {
630
+ this.#parts.countdown.hidden = false;
631
+ }
632
+
633
+ if (this.#parts.status) {
634
+ this.#parts.status.hidden = true;
635
+ }
636
+
637
+ this.#updateToggle();
638
+ }
639
+
640
+ #renderPaused(seconds) {
641
+ const label = this.getAttribute("paused-label") || "Paused at";
642
+ if (this.#parts.label) {
643
+ this.#parts.label.textContent = label;
644
+ }
645
+
646
+ if (this.#parts.value) {
647
+ this.#parts.value.textContent = formatRemaining(seconds);
648
+ }
649
+
650
+ this.classList.add("is-paused");
651
+ this.classList.remove("is-loading");
652
+ this.setAttribute("aria-busy", "false");
653
+
654
+ if (this.#parts.countdown) {
655
+ this.#parts.countdown.hidden = false;
656
+ }
657
+
658
+ if (this.#parts.status) {
659
+ this.#parts.status.hidden = true;
660
+ }
661
+
662
+ this.#updateToggle();
663
+ }
664
+
665
+ #renderLoading() {
666
+ const loadingLabel = this.getAttribute("loading-label") || "Refreshing";
667
+
668
+ this.classList.remove("is-paused");
669
+ this.classList.add("is-loading");
670
+ this.setAttribute("aria-busy", "true");
671
+
672
+ if (this.#parts.countdown) {
673
+ this.#parts.countdown.hidden = true;
674
+ }
675
+
676
+ if (this.#parts.statusText) {
677
+ this.#parts.statusText.textContent = loadingLabel;
678
+ }
679
+
680
+ if (this.#parts.status) {
681
+ this.#parts.status.hidden = false;
682
+ }
683
+
684
+ this.#updateToggle();
685
+ }
686
+
687
+ #updateToggle() {
688
+ if (!(this.#parts.toggle instanceof HTMLElement)) {
689
+ return;
690
+ }
691
+
692
+ const pauseLabel = this.getAttribute("pause-action-label") || "Pause";
693
+ const resumeLabel = this.getAttribute("resume-action-label") || "Resume";
694
+ const actionLabel = this.#isPaused ? resumeLabel : pauseLabel;
695
+
696
+ this.#parts.toggle.disabled = this.#isLoading;
697
+ this.#parts.toggle.setAttribute("aria-pressed", this.#isPaused ? "true" : "false");
698
+ this.#parts.toggle.setAttribute("aria-label", actionLabel);
699
+
700
+ if (this.#parts.toggleText) {
701
+ this.#parts.toggleText.textContent = actionLabel;
702
+ }
703
+ }
704
+
705
+ #stop() {
706
+ if (this.#timeoutId) {
707
+ window.clearTimeout(this.#timeoutId);
708
+ this.#timeoutId = 0;
709
+ }
710
+ }
711
+
712
+ #buildState() {
713
+ return {
714
+ paused: this.#isPaused,
715
+ loading: this.#isLoading,
716
+ remainingSeconds: Math.max(0, Math.ceil(this.#remainingMs / 1000)),
717
+ };
718
+ }
719
+
720
+ #emitStateChange(status) {
721
+ this.dispatchEvent(new CustomEvent("statechange", {
722
+ bubbles: true,
723
+ composed: true,
724
+ detail: {
725
+ status,
726
+ ...this.#buildState(),
727
+ },
728
+ }));
729
+ }
730
+ }
731
+
732
+ export class IncThemeSwitcher extends HostElement {
733
+ static observedAttributes = ["mode", "variant", "block", "label", "menu-label", "heading", "storage-key"];
734
+
735
+ #details = null;
736
+ #summary = null;
737
+ #status = null;
738
+ #panel = null;
739
+ #bound = false;
740
+ #unsubscribe = null;
741
+ #ignoreModeReflection = false;
742
+
743
+ connectedCallback() {
744
+ initializeThemeRuntime(this.storageKey);
745
+ this.#ensureMarkup();
746
+ this.#applyVisualConfig();
747
+ this.#bindHandlers();
748
+ this.#subscribeTheme();
749
+ this.#syncModeFromAttribute();
750
+ }
751
+
752
+ disconnectedCallback() {
753
+ if (this.#unsubscribe) {
754
+ this.#unsubscribe();
755
+ this.#unsubscribe = null;
756
+ }
757
+ }
758
+
759
+ attributeChangedCallback(name) {
760
+ if (!this.isConnected) {
761
+ return;
762
+ }
763
+
764
+ if (name === "storage-key") {
765
+ initializeThemeRuntime(this.storageKey);
766
+ return;
767
+ }
768
+
769
+ if (name === "mode" && !this.#ignoreModeReflection) {
770
+ this.setMode(this.getAttribute("mode") || "system");
771
+ return;
772
+ }
773
+
774
+ this.#applyVisualConfig();
775
+ }
776
+
777
+ get storageKey() {
778
+ return this.getAttribute("storage-key") || DEFAULT_THEME_STORAGE_KEY;
779
+ }
780
+
781
+ getMode() {
782
+ return themeMode;
783
+ }
784
+
785
+ getResolvedTheme() {
786
+ return themeResolved;
787
+ }
788
+
789
+ setMode(mode) {
790
+ initializeThemeRuntime(this.storageKey);
791
+ const next = isThemeMode(mode) ? mode : "system";
792
+ applyThemeMode(next, {
793
+ dispatch: true,
794
+ persist: true,
795
+ storageKey: this.storageKey,
796
+ });
797
+ }
798
+
799
+ cycleMode() {
800
+ const index = THEME_MODES.indexOf(themeMode);
801
+ const nextMode = THEME_MODES[(index + 1) % THEME_MODES.length];
802
+ this.setMode(nextMode);
803
+ }
804
+
805
+ #ensureMarkup() {
806
+ this.classList.add("inc-theme-switcher-host");
807
+ this.#details = this.querySelector("details.inc-theme-switcher");
808
+
809
+ if (!(this.#details instanceof HTMLDetailsElement)) {
810
+ this.innerHTML = `
811
+ <details class="inc-native-menu inc-theme-switcher">
812
+ <summary class="inc-native-menu__summary inc-theme-switcher__summary" part="summary">
813
+ <span class="inc-theme-switcher__meta">
814
+ <span class="inc-theme-switcher__label" part="label"></span>
815
+ <span class="inc-theme-switcher__status" part="status"></span>
816
+ </span>
817
+ </summary>
818
+ <div class="inc-native-menu__panel inc-theme-switcher__panel" role="menu" part="panel">
819
+ <div class="inc-native-menu__header"></div>
820
+ </div>
821
+ </details>
822
+ `.trim();
823
+ this.#details = this.querySelector("details.inc-theme-switcher");
824
+ }
825
+
826
+ this.#summary = this.#details?.querySelector("summary");
827
+ this.#status = this.#details?.querySelector(".inc-theme-switcher__status");
828
+ this.#panel = this.#details?.querySelector(".inc-theme-switcher__panel");
829
+
830
+ if (!this.#panel) {
831
+ return;
832
+ }
833
+
834
+ const header = this.#panel.querySelector(".inc-native-menu__header") || document.createElement("div");
835
+ header.classList.add("inc-native-menu__header");
836
+ header.textContent = this.getAttribute("heading") || "Choose appearance";
837
+
838
+ if (!header.parentElement) {
839
+ this.#panel.append(header);
840
+ }
841
+
842
+ const existingOptions = this.#panel.querySelectorAll("[data-inc-theme-mode]");
843
+ if (!existingOptions.length) {
844
+ THEME_MODES.forEach((mode) => {
845
+ const option = document.createElement("button");
846
+ const body = document.createElement("span");
847
+ const label = document.createElement("span");
848
+ const detail = document.createElement("span");
849
+
850
+ option.type = "button";
851
+ option.className = "inc-theme-switcher__option";
852
+ option.dataset.incThemeMode = mode;
853
+ option.setAttribute("data-inc-theme-mode", mode);
854
+ option.setAttribute("role", "menuitemradio");
855
+ option.setAttribute("part", "option");
856
+
857
+ body.className = "inc-theme-switcher__option-body";
858
+ body.setAttribute("part", "option-body");
859
+ label.className = "inc-theme-switcher__option-label";
860
+ label.setAttribute("part", "option-label");
861
+ detail.className = "inc-theme-switcher__option-detail";
862
+ detail.setAttribute("part", "option-detail");
863
+
864
+ label.textContent = mode.charAt(0).toUpperCase() + mode.slice(1);
865
+ detail.textContent = mode === "system"
866
+ ? "Match the device preference automatically."
867
+ : `Use the ${mode} application palette.`;
868
+
869
+ body.append(label, detail);
870
+ option.append(body);
871
+ this.#panel.append(option);
872
+ });
873
+ }
874
+ }
875
+
876
+ #applyVisualConfig() {
877
+ if (!(this.#details instanceof HTMLDetailsElement)) {
878
+ return;
879
+ }
880
+
881
+ const label = this.getAttribute("label") || "Theme";
882
+ const menuLabel = this.getAttribute("menu-label") || "Theme";
883
+ const heading = this.getAttribute("heading") || "Choose appearance";
884
+ const isBlock = toBooleanAttribute(this.getAttribute("block"));
885
+ const variant = this.getAttribute("variant");
886
+
887
+ this.#details.classList.remove("inc-native-menu--navbar", "inc-native-menu--block");
888
+ if (variant === "navbar") {
889
+ this.#details.classList.add("inc-native-menu--navbar");
890
+ }
891
+
892
+ if (isBlock) {
893
+ this.#details.classList.add("inc-native-menu--block");
894
+ }
895
+
896
+ const labelNode = this.#details.querySelector(".inc-theme-switcher__label");
897
+ const headerNode = this.#details.querySelector(".inc-native-menu__header");
898
+ if (labelNode) {
899
+ labelNode.textContent = label;
900
+ }
901
+
902
+ if (this.#panel) {
903
+ this.#panel.setAttribute("aria-label", menuLabel);
904
+ }
905
+
906
+ if (headerNode) {
907
+ headerNode.textContent = heading;
908
+ }
909
+ }
910
+
911
+ #bindHandlers() {
912
+ if (this.#bound || !this.#details) {
913
+ return;
914
+ }
915
+
916
+ this.#bound = true;
917
+
918
+ this.#details.addEventListener("click", (event) => {
919
+ const control = event.target.closest("[data-inc-theme-mode]");
920
+ if (!control) {
921
+ return;
922
+ }
923
+
924
+ event.preventDefault();
925
+ const mode = control.getAttribute("data-inc-theme-mode");
926
+ this.setMode(mode);
927
+
928
+ this.#details.open = false;
929
+ if (this.#summary) {
930
+ this.#summary.focus();
931
+ }
932
+ });
933
+
934
+ this.#summary?.addEventListener("keydown", (event) => {
935
+ if (event.key !== "Enter" && event.key !== " ") {
936
+ return;
937
+ }
938
+
939
+ event.preventDefault();
940
+ this.#details.open = !this.#details.open;
941
+ if (!this.#details.open && this.#summary) {
942
+ this.#summary.focus();
943
+ }
944
+ });
945
+
946
+ this.#details.addEventListener("keydown", (event) => {
947
+ const control = event.target.closest("[data-inc-theme-mode]");
948
+
949
+ if (event.key === "Escape" && this.#details.open) {
950
+ this.#details.open = false;
951
+ if (this.#summary) {
952
+ this.#summary.focus();
953
+ }
954
+ return;
955
+ }
956
+
957
+ if (!control || !this.#panel) {
958
+ return;
959
+ }
960
+
961
+ const options = Array.from(this.#panel.querySelectorAll("[data-inc-theme-mode]"));
962
+ if (!options.length) {
963
+ return;
964
+ }
965
+
966
+ const index = options.indexOf(control);
967
+ if (index < 0) {
968
+ return;
969
+ }
970
+
971
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
972
+ event.preventDefault();
973
+ options[(index + 1) % options.length]?.focus();
974
+ return;
975
+ }
976
+
977
+ if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
978
+ event.preventDefault();
979
+ options[(index - 1 + options.length) % options.length]?.focus();
980
+ return;
981
+ }
982
+
983
+ if (event.key === "Home") {
984
+ event.preventDefault();
985
+ options[0]?.focus();
986
+ return;
987
+ }
988
+
989
+ if (event.key === "End") {
990
+ event.preventDefault();
991
+ options[options.length - 1]?.focus();
992
+ }
993
+ });
994
+ }
995
+
996
+ #subscribeTheme() {
997
+ if (this.#unsubscribe) {
998
+ this.#unsubscribe();
999
+ }
1000
+
1001
+ this.#unsubscribe = subscribeThemeState((state) => {
1002
+ this.#syncUI(state.mode, state.resolved);
1003
+ });
1004
+ }
1005
+
1006
+ #syncModeFromAttribute() {
1007
+ const declared = this.getAttribute("mode");
1008
+ if (!declared) {
1009
+ return;
1010
+ }
1011
+
1012
+ this.setMode(declared);
1013
+ }
1014
+
1015
+ #syncUI(mode, resolved) {
1016
+ if (this.#status) {
1017
+ const label = mode === "system"
1018
+ ? `System (${resolved.charAt(0).toUpperCase()}${resolved.slice(1)})`
1019
+ : `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
1020
+ this.#status.textContent = label;
1021
+ }
1022
+
1023
+ if (this.#panel) {
1024
+ const options = this.#panel.querySelectorAll("[data-inc-theme-mode]");
1025
+ options.forEach((option) => {
1026
+ const optionMode = option.getAttribute("data-inc-theme-mode");
1027
+ const isSelected = optionMode === mode;
1028
+ option.classList.toggle("is-selected", isSelected);
1029
+ option.setAttribute("aria-checked", isSelected ? "true" : "false");
1030
+ option.setAttribute("aria-pressed", isSelected ? "true" : "false");
1031
+ });
1032
+ }
1033
+
1034
+ this.dataset.incThemeModeState = mode;
1035
+ this.dataset.incThemeResolved = resolved;
1036
+
1037
+ this.#ignoreModeReflection = true;
1038
+ this.setAttribute("mode", mode);
1039
+ this.#ignoreModeReflection = false;
1040
+ }
1041
+ }
1042
+
1043
+ export const feedbackDefinitions = [
1044
+ ["inc-state-panel", IncStatePanel],
1045
+ ["inc-live-region", IncLiveRegion],
1046
+ ["inc-auto-refresh", IncAutoRefresh],
1047
+ ["inc-theme-switcher", IncThemeSwitcher],
1048
+ ];
1049
+
1050
+ export function defineFeedbackComponents(definer = typeof customElements !== "undefined" ? customElements : null) {
1051
+ if (!definer || typeof definer.get !== "function" || typeof definer.define !== "function") {
1052
+ return;
1053
+ }
1054
+
1055
+ feedbackDefinitions.forEach(([tagName, ctor]) => {
1056
+ if (!definer.get(tagName)) {
1057
+ definer.define(tagName, ctor);
1058
+ }
1059
+ });
1060
+ }
1061
+
1062
+ if (typeof globalThis !== "undefined") {
1063
+ const namespace = globalThis.IncWebComponents || (globalThis.IncWebComponents = {});
1064
+ namespace.feedback = Object.assign({}, namespace.feedback, {
1065
+ defineFeedbackComponents,
1066
+ feedbackDefinitions,
1067
+ components: {
1068
+ IncStatePanel,
1069
+ IncLiveRegion,
1070
+ IncAutoRefresh,
1071
+ IncThemeSwitcher,
1072
+ },
1073
+ });
1074
+ }