@incursa/ui-kit 1.6.1 → 1.7.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.
@@ -24,6 +24,10 @@ const BUTTON_VARIANTS = new Set([
24
24
  "outline-info",
25
25
  ]);
26
26
  const BUTTON_SIZES = new Set(["sm", "lg", "micro"]);
27
+ const ALERT_DEFAULT_ROLE_BY_TONE = new Map([
28
+ ["info", "status"],
29
+ ["secondary", "status"],
30
+ ]);
27
31
 
28
32
  const HostElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
29
33
 
@@ -35,6 +39,11 @@ function toBoolean(value, fallback = false) {
35
39
  return !FALSE_TOKENS.has(String(value).toLowerCase());
36
40
  }
37
41
 
42
+ function toPositiveInt(value) {
43
+ const parsed = Number.parseInt(String(value ?? "").trim(), 10);
44
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
45
+ }
46
+
38
47
  function emit(host, type, detail = {}, options = {}) {
39
48
  return host.dispatchEvent(new CustomEvent(type, {
40
49
  detail,
@@ -292,7 +301,6 @@ export class IncCloseButtonElement extends IncElement {
292
301
  }
293
302
 
294
303
  connectedCallback() {
295
- addClass(this, "inc-close-button");
296
304
  this.sync();
297
305
  }
298
306
 
@@ -303,7 +311,7 @@ export class IncCloseButtonElement extends IncElement {
303
311
  }
304
312
 
305
313
  sync() {
306
- addClass(this, "inc-close-button");
314
+ this.classList.remove("inc-close-button", "inc-close-button--white");
307
315
  this.setAttribute("part", "close-button");
308
316
  const control = this.ensureControl();
309
317
  const variant = normalizeToken(this.getAttribute("variant"));
@@ -316,9 +324,7 @@ export class IncCloseButtonElement extends IncElement {
316
324
 
317
325
  control.type = "button";
318
326
  control.setAttribute("aria-label", this.getAttribute("label") || "Close");
319
- if (!control.childNodes.length) {
320
- control.textContent = "×";
321
- }
327
+ control.textContent = "";
322
328
  }
323
329
 
324
330
  ensureControl() {
@@ -344,7 +350,7 @@ export class IncCloseButtonElement extends IncElement {
344
350
 
345
351
  export class IncAlertElement extends IncElement {
346
352
  static get observedAttributes() {
347
- return ["tone", "variant", "dismissible", "dismiss-label"];
353
+ return ["tone", "variant", "dismissible", "dismiss-label", "timeout"];
348
354
  }
349
355
 
350
356
  connectedCallback() {
@@ -354,6 +360,7 @@ export class IncAlertElement extends IncElement {
354
360
  }
355
361
 
356
362
  disconnectedCallback() {
363
+ this.stopDismissTimer();
357
364
  if (this._boundClick) {
358
365
  this.removeEventListener("click", this._boundClick);
359
366
  }
@@ -377,7 +384,7 @@ export class IncAlertElement extends IncElement {
377
384
  }
378
385
 
379
386
  event.preventDefault();
380
- this.hide();
387
+ this.dismiss("manual");
381
388
  };
382
389
 
383
390
  this.addEventListener("click", this._boundClick);
@@ -400,12 +407,25 @@ export class IncAlertElement extends IncElement {
400
407
  }
401
408
 
402
409
  if (!this.hasAttribute("role")) {
403
- this.setAttribute("role", resolvedTone === "info" || resolvedTone === "secondary" ? "status" : "alert");
410
+ this.setAttribute("role", ALERT_DEFAULT_ROLE_BY_TONE.get(resolvedTone) || "alert");
404
411
  }
405
412
  if (!this.hasAttribute("aria-live")) {
406
413
  this.setAttribute("aria-live", this.getAttribute("role") === "alert" ? "assertive" : "polite");
407
414
  }
408
415
  this.setAttribute("aria-atomic", "true");
416
+
417
+ const timeoutMs = toPositiveInt(this.getAttribute("timeout"));
418
+ if (timeoutMs) {
419
+ this.ensureProgressBar();
420
+ if (!this.hidden && this.getAttribute("aria-hidden") !== "true") {
421
+ this.startDismissTimer(timeoutMs);
422
+ } else {
423
+ this.stopDismissTimer();
424
+ }
425
+ } else {
426
+ this.stopDismissTimer();
427
+ this.removeProgressBar();
428
+ }
409
429
  }
410
430
 
411
431
  ensureDismissButton() {
@@ -420,9 +440,7 @@ export class IncAlertElement extends IncElement {
420
440
  button.className = "inc-close-button";
421
441
  button.setAttribute("part", "dismiss");
422
442
  button.setAttribute("aria-label", this.getAttribute("dismiss-label") || "Dismiss alert");
423
- if (!button.childNodes.length) {
424
- button.textContent = "×";
425
- }
443
+ button.textContent = "";
426
444
  return button;
427
445
  }
428
446
 
@@ -430,10 +448,68 @@ export class IncAlertElement extends IncElement {
430
448
  this.querySelectorAll(":scope > [data-inc-alert-dismiss]").forEach((node) => node.remove());
431
449
  }
432
450
 
433
- hide() {
451
+ ensureProgressBar() {
452
+ let progress = this.querySelector(":scope > .inc-alert__progress");
453
+ if (!progress) {
454
+ progress = document.createElement("div");
455
+ progress.className = "inc-alert__progress";
456
+ progress.setAttribute("part", "progress");
457
+ progress.setAttribute("aria-hidden", "true");
458
+ this.append(progress);
459
+ }
460
+
461
+ return progress;
462
+ }
463
+
464
+ removeProgressBar() {
465
+ this.querySelectorAll(":scope > .inc-alert__progress").forEach((node) => node.remove());
466
+ }
467
+
468
+ startDismissTimer(timeoutMs) {
469
+ const progress = this.ensureProgressBar();
470
+ this.stopDismissTimer();
471
+ this._dismissTimeoutMs = timeoutMs;
472
+ this._dismissStartedAt = performance.now();
473
+
474
+ const tick = (now) => {
475
+ if (this.hidden || this.getAttribute("aria-hidden") === "true") {
476
+ this.stopDismissTimer();
477
+ return;
478
+ }
479
+
480
+ const elapsed = Math.max(0, now - this._dismissStartedAt);
481
+ const remaining = Math.max(0, timeoutMs - elapsed);
482
+ const ratio = timeoutMs > 0 ? remaining / timeoutMs : 0;
483
+ progress.style.transform = `scaleX(${ratio})`;
484
+
485
+ if (remaining <= 0) {
486
+ this.dismiss("timeout");
487
+ return;
488
+ }
489
+
490
+ this._dismissFrame = window.requestAnimationFrame(tick);
491
+ };
492
+
493
+ progress.style.transform = "scaleX(1)";
494
+ this._dismissFrame = window.requestAnimationFrame(tick);
495
+ }
496
+
497
+ stopDismissTimer() {
498
+ if (this._dismissFrame) {
499
+ window.cancelAnimationFrame(this._dismissFrame);
500
+ this._dismissFrame = 0;
501
+ }
502
+ }
503
+
504
+ dismiss(reason = "manual") {
505
+ this.hide(reason);
506
+ }
507
+
508
+ hide(reason = "manual") {
509
+ this.stopDismissTimer();
434
510
  this.hidden = true;
435
511
  this.setAttribute("aria-hidden", "true");
436
- this.emit("dismiss", { hidden: true });
512
+ this.emit("dismiss", { hidden: true, reason });
437
513
  }
438
514
  }
439
515
 
@@ -2,6 +2,14 @@ const THEME_MODES = ["light", "dark", "system"];
2
2
  const DEFAULT_THEME_STORAGE_KEY = "inc-theme-mode";
3
3
  const BADGE_TONES = new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
4
4
  const SPINNER_VARIANTS = new Set(["border", "grow"]);
5
+ const AUTO_REFRESH_PAUSE_ICON = `
6
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
7
+ <path d="M4 3h3v10H4zM9 3h3v10H9z"></path>
8
+ </svg>`.trim();
9
+ const AUTO_REFRESH_PLAY_ICON = `
10
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
11
+ <path d="M4 3.5v9l8-4.5-8-4.5z"></path>
12
+ </svg>`.trim();
5
13
  const HostElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
6
14
  const themeSubscribers = new Set();
7
15
 
@@ -677,6 +685,10 @@ export class IncAutoRefresh extends HostElement {
677
685
  }
678
686
 
679
687
  this.innerHTML = `
688
+ <button type="button" class="inc-auto-refresh__toggle inc-btn inc-btn--outline-secondary inc-btn--micro" part="toggle">
689
+ <span class="inc-auto-refresh__toggle-icon" aria-hidden="true"></span>
690
+ <span class="inc-auto-refresh__toggle-text"></span>
691
+ </button>
680
692
  <span class="inc-auto-refresh__countdown" part="countdown">
681
693
  <span class="inc-auto-refresh__label" part="label"></span>
682
694
  <span class="inc-auto-refresh__value" part="value"></span>
@@ -684,10 +696,6 @@ export class IncAutoRefresh extends HostElement {
684
696
  <span class="inc-auto-refresh__status" part="status" hidden>
685
697
  <span class="inc-auto-refresh__status-text"></span>
686
698
  </span>
687
- <button type="button" class="inc-auto-refresh__toggle inc-btn inc-btn--outline-secondary inc-btn--micro" part="toggle">
688
- <span class="inc-auto-refresh__toggle-icon" aria-hidden="true"></span>
689
- <span class="inc-auto-refresh__toggle-text"></span>
690
- </button>
691
699
  `.trim();
692
700
 
693
701
  this.#parts = this.#getParts();
@@ -701,6 +709,7 @@ export class IncAutoRefresh extends HostElement {
701
709
  status: this.querySelector(".inc-auto-refresh__status"),
702
710
  statusText: this.querySelector(".inc-auto-refresh__status-text"),
703
711
  toggle: this.querySelector(".inc-auto-refresh__toggle"),
712
+ toggleIcon: this.querySelector(".inc-auto-refresh__toggle-icon"),
704
713
  toggleText: this.querySelector(".inc-auto-refresh__toggle-text"),
705
714
  };
706
715
  }
@@ -861,6 +870,10 @@ export class IncAutoRefresh extends HostElement {
861
870
  if (this.#parts.toggleText) {
862
871
  this.#parts.toggleText.textContent = actionLabel;
863
872
  }
873
+
874
+ if (this.#parts.toggleIcon instanceof HTMLElement) {
875
+ this.#parts.toggleIcon.innerHTML = this.#isPaused ? AUTO_REFRESH_PLAY_ICON : AUTO_REFRESH_PAUSE_ICON;
876
+ }
864
877
  }
865
878
 
866
879
  #stop() {
@@ -1989,6 +1989,14 @@ var THEME_MODES = ["light", "dark", "system"];
1989
1989
  var DEFAULT_THEME_STORAGE_KEY = "inc-theme-mode";
1990
1990
  var BADGE_TONES = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1991
1991
  var SPINNER_VARIANTS = /* @__PURE__ */ new Set(["border", "grow"]);
1992
+ var AUTO_REFRESH_PAUSE_ICON = `
1993
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
1994
+ <path d="M4 3h3v10H4zM9 3h3v10H9z"></path>
1995
+ </svg>`.trim();
1996
+ var AUTO_REFRESH_PLAY_ICON = `
1997
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
1998
+ <path d="M4 3.5v9l8-4.5-8-4.5z"></path>
1999
+ </svg>`.trim();
1992
2000
  var HostElement3 = typeof HTMLElement === "undefined" ? class {
1993
2001
  } : HTMLElement;
1994
2002
  var themeSubscribers = /* @__PURE__ */ new Set();
@@ -2525,6 +2533,10 @@ var IncAutoRefresh = class extends HostElement3 {
2525
2533
  return;
2526
2534
  }
2527
2535
  this.innerHTML = `
2536
+ <button type="button" class="inc-auto-refresh__toggle inc-btn inc-btn--outline-secondary inc-btn--micro" part="toggle">
2537
+ <span class="inc-auto-refresh__toggle-icon" aria-hidden="true"></span>
2538
+ <span class="inc-auto-refresh__toggle-text"></span>
2539
+ </button>
2528
2540
  <span class="inc-auto-refresh__countdown" part="countdown">
2529
2541
  <span class="inc-auto-refresh__label" part="label"></span>
2530
2542
  <span class="inc-auto-refresh__value" part="value"></span>
@@ -2532,10 +2544,6 @@ var IncAutoRefresh = class extends HostElement3 {
2532
2544
  <span class="inc-auto-refresh__status" part="status" hidden>
2533
2545
  <span class="inc-auto-refresh__status-text"></span>
2534
2546
  </span>
2535
- <button type="button" class="inc-auto-refresh__toggle inc-btn inc-btn--outline-secondary inc-btn--micro" part="toggle">
2536
- <span class="inc-auto-refresh__toggle-icon" aria-hidden="true"></span>
2537
- <span class="inc-auto-refresh__toggle-text"></span>
2538
- </button>
2539
2547
  `.trim();
2540
2548
  this.#parts = this.#getParts();
2541
2549
  }
@@ -2547,6 +2555,7 @@ var IncAutoRefresh = class extends HostElement3 {
2547
2555
  status: this.querySelector(".inc-auto-refresh__status"),
2548
2556
  statusText: this.querySelector(".inc-auto-refresh__status-text"),
2549
2557
  toggle: this.querySelector(".inc-auto-refresh__toggle"),
2558
+ toggleIcon: this.querySelector(".inc-auto-refresh__toggle-icon"),
2550
2559
  toggleText: this.querySelector(".inc-auto-refresh__toggle-text")
2551
2560
  };
2552
2561
  }
@@ -2669,6 +2678,9 @@ var IncAutoRefresh = class extends HostElement3 {
2669
2678
  if (this.#parts.toggleText) {
2670
2679
  this.#parts.toggleText.textContent = actionLabel;
2671
2680
  }
2681
+ if (this.#parts.toggleIcon instanceof HTMLElement) {
2682
+ this.#parts.toggleIcon.innerHTML = this.#isPaused ? AUTO_REFRESH_PLAY_ICON : AUTO_REFRESH_PAUSE_ICON;
2683
+ }
2672
2684
  }
2673
2685
  #stop() {
2674
2686
  if (this.#timeoutId) {
@@ -3028,6 +3040,10 @@ var BUTTON_VARIANTS = /* @__PURE__ */ new Set([
3028
3040
  "outline-info"
3029
3041
  ]);
3030
3042
  var BUTTON_SIZES = /* @__PURE__ */ new Set(["sm", "lg", "micro"]);
3043
+ var ALERT_DEFAULT_ROLE_BY_TONE = /* @__PURE__ */ new Map([
3044
+ ["info", "status"],
3045
+ ["secondary", "status"]
3046
+ ]);
3031
3047
  var HostElement4 = typeof HTMLElement === "undefined" ? class {
3032
3048
  } : HTMLElement;
3033
3049
  function toBoolean(value, fallback = false) {
@@ -3036,6 +3052,10 @@ function toBoolean(value, fallback = false) {
3036
3052
  }
3037
3053
  return !FALSE_TOKENS.has(String(value).toLowerCase());
3038
3054
  }
3055
+ function toPositiveInt2(value) {
3056
+ const parsed = Number.parseInt(String(value ?? "").trim(), 10);
3057
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
3058
+ }
3039
3059
  function emit2(host, type, detail = {}, options = {}) {
3040
3060
  return host.dispatchEvent(new CustomEvent(type, {
3041
3061
  detail,
@@ -3251,7 +3271,6 @@ var IncCloseButtonElement = class extends IncElement {
3251
3271
  return ["label", "variant"];
3252
3272
  }
3253
3273
  connectedCallback() {
3254
- addClass(this, "inc-close-button");
3255
3274
  this.sync();
3256
3275
  }
3257
3276
  attributeChangedCallback() {
@@ -3260,7 +3279,7 @@ var IncCloseButtonElement = class extends IncElement {
3260
3279
  }
3261
3280
  }
3262
3281
  sync() {
3263
- addClass(this, "inc-close-button");
3282
+ this.classList.remove("inc-close-button", "inc-close-button--white");
3264
3283
  this.setAttribute("part", "close-button");
3265
3284
  const control = this.ensureControl();
3266
3285
  const variant = normalizeToken2(this.getAttribute("variant"));
@@ -3271,9 +3290,7 @@ var IncCloseButtonElement = class extends IncElement {
3271
3290
  }
3272
3291
  control.type = "button";
3273
3292
  control.setAttribute("aria-label", this.getAttribute("label") || "Close");
3274
- if (!control.childNodes.length) {
3275
- control.textContent = "\xD7";
3276
- }
3293
+ control.textContent = "";
3277
3294
  }
3278
3295
  ensureControl() {
3279
3296
  const existing = this._control || this.querySelector(":scope > button.inc-close-button");
@@ -3296,7 +3313,7 @@ var IncCloseButtonElement = class extends IncElement {
3296
3313
  };
3297
3314
  var IncAlertElement = class extends IncElement {
3298
3315
  static get observedAttributes() {
3299
- return ["tone", "variant", "dismissible", "dismiss-label"];
3316
+ return ["tone", "variant", "dismissible", "dismiss-label", "timeout"];
3300
3317
  }
3301
3318
  connectedCallback() {
3302
3319
  addClass(this, "inc-alert");
@@ -3304,6 +3321,7 @@ var IncAlertElement = class extends IncElement {
3304
3321
  this.sync();
3305
3322
  }
3306
3323
  disconnectedCallback() {
3324
+ this.stopDismissTimer();
3307
3325
  if (this._boundClick) {
3308
3326
  this.removeEventListener("click", this._boundClick);
3309
3327
  }
@@ -3323,7 +3341,7 @@ var IncAlertElement = class extends IncElement {
3323
3341
  return;
3324
3342
  }
3325
3343
  event.preventDefault();
3326
- this.hide();
3344
+ this.dismiss("manual");
3327
3345
  };
3328
3346
  this.addEventListener("click", this._boundClick);
3329
3347
  }
@@ -3341,12 +3359,24 @@ var IncAlertElement = class extends IncElement {
3341
3359
  this.removeDismissButton();
3342
3360
  }
3343
3361
  if (!this.hasAttribute("role")) {
3344
- this.setAttribute("role", resolvedTone === "info" || resolvedTone === "secondary" ? "status" : "alert");
3362
+ this.setAttribute("role", ALERT_DEFAULT_ROLE_BY_TONE.get(resolvedTone) || "alert");
3345
3363
  }
3346
3364
  if (!this.hasAttribute("aria-live")) {
3347
3365
  this.setAttribute("aria-live", this.getAttribute("role") === "alert" ? "assertive" : "polite");
3348
3366
  }
3349
3367
  this.setAttribute("aria-atomic", "true");
3368
+ const timeoutMs = toPositiveInt2(this.getAttribute("timeout"));
3369
+ if (timeoutMs) {
3370
+ this.ensureProgressBar();
3371
+ if (!this.hidden && this.getAttribute("aria-hidden") !== "true") {
3372
+ this.startDismissTimer(timeoutMs);
3373
+ } else {
3374
+ this.stopDismissTimer();
3375
+ }
3376
+ } else {
3377
+ this.stopDismissTimer();
3378
+ this.removeProgressBar();
3379
+ }
3350
3380
  }
3351
3381
  ensureDismissButton() {
3352
3382
  let button = this.querySelector(":scope > [data-inc-alert-dismiss]");
@@ -3359,18 +3389,63 @@ var IncAlertElement = class extends IncElement {
3359
3389
  button.className = "inc-close-button";
3360
3390
  button.setAttribute("part", "dismiss");
3361
3391
  button.setAttribute("aria-label", this.getAttribute("dismiss-label") || "Dismiss alert");
3362
- if (!button.childNodes.length) {
3363
- button.textContent = "\xD7";
3364
- }
3392
+ button.textContent = "";
3365
3393
  return button;
3366
3394
  }
3367
3395
  removeDismissButton() {
3368
3396
  this.querySelectorAll(":scope > [data-inc-alert-dismiss]").forEach((node) => node.remove());
3369
3397
  }
3370
- hide() {
3398
+ ensureProgressBar() {
3399
+ let progress = this.querySelector(":scope > .inc-alert__progress");
3400
+ if (!progress) {
3401
+ progress = document.createElement("div");
3402
+ progress.className = "inc-alert__progress";
3403
+ progress.setAttribute("part", "progress");
3404
+ progress.setAttribute("aria-hidden", "true");
3405
+ this.append(progress);
3406
+ }
3407
+ return progress;
3408
+ }
3409
+ removeProgressBar() {
3410
+ this.querySelectorAll(":scope > .inc-alert__progress").forEach((node) => node.remove());
3411
+ }
3412
+ startDismissTimer(timeoutMs) {
3413
+ const progress = this.ensureProgressBar();
3414
+ this.stopDismissTimer();
3415
+ this._dismissTimeoutMs = timeoutMs;
3416
+ this._dismissStartedAt = performance.now();
3417
+ const tick = (now) => {
3418
+ if (this.hidden || this.getAttribute("aria-hidden") === "true") {
3419
+ this.stopDismissTimer();
3420
+ return;
3421
+ }
3422
+ const elapsed = Math.max(0, now - this._dismissStartedAt);
3423
+ const remaining = Math.max(0, timeoutMs - elapsed);
3424
+ const ratio = timeoutMs > 0 ? remaining / timeoutMs : 0;
3425
+ progress.style.transform = `scaleX(${ratio})`;
3426
+ if (remaining <= 0) {
3427
+ this.dismiss("timeout");
3428
+ return;
3429
+ }
3430
+ this._dismissFrame = window.requestAnimationFrame(tick);
3431
+ };
3432
+ progress.style.transform = "scaleX(1)";
3433
+ this._dismissFrame = window.requestAnimationFrame(tick);
3434
+ }
3435
+ stopDismissTimer() {
3436
+ if (this._dismissFrame) {
3437
+ window.cancelAnimationFrame(this._dismissFrame);
3438
+ this._dismissFrame = 0;
3439
+ }
3440
+ }
3441
+ dismiss(reason = "manual") {
3442
+ this.hide(reason);
3443
+ }
3444
+ hide(reason = "manual") {
3445
+ this.stopDismissTimer();
3371
3446
  this.hidden = true;
3372
3447
  this.setAttribute("aria-hidden", "true");
3373
- this.emit("dismiss", { hidden: true });
3448
+ this.emit("dismiss", { hidden: true, reason });
3374
3449
  }
3375
3450
  };
3376
3451
  var IncEmptyStateElement = class extends IncElement {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incursa/ui-kit",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "private": false,
5
5
  "description": "Reusable UI kit for data-heavy business applications.",
6
6
  "keywords": [