@incursa/ui-kit 1.7.0 → 1.8.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/NOTICE +8 -0
  2. package/README.md +16 -0
  3. package/dist/icons/index.js +371 -0
  4. package/dist/icons/package.json +3 -0
  5. package/dist/inc-design-language.css +34 -1
  6. package/dist/inc-design-language.css.map +1 -1
  7. package/dist/inc-design-language.js +1558 -1462
  8. package/dist/inc-design-language.min.css +1 -1
  9. package/dist/inc-design-language.min.css.map +1 -1
  10. package/dist/mcp/components/cards.json +3 -3
  11. package/dist/mcp/components/metrics.json +3 -3
  12. package/dist/mcp/components/states.json +3 -3
  13. package/dist/mcp/examples/data-grid-advanced.json +2 -2
  14. package/dist/mcp/examples/demo.json +2 -2
  15. package/dist/mcp/examples/overlay-workflows.json +2 -2
  16. package/dist/mcp/examples/reference.json +2 -2
  17. package/dist/mcp/examples/states.json +2 -2
  18. package/dist/mcp/examples/web-components.json +2 -2
  19. package/dist/mcp/guides/latest.json +2 -2
  20. package/dist/mcp/guides/package-metadata.json +2 -2
  21. package/dist/mcp/guides/update.json +2 -2
  22. package/dist/mcp/install.json +1 -1
  23. package/dist/mcp/patterns/data-grid-advanced.json +2 -2
  24. package/dist/mcp/patterns/demo.json +2 -2
  25. package/dist/mcp/patterns/overlay-workflows.json +2 -2
  26. package/dist/mcp/patterns/reference.json +2 -2
  27. package/dist/mcp/patterns/states.json +2 -2
  28. package/dist/mcp/patterns/web-components.json +2 -2
  29. package/dist/mcp/resources.json +77 -74
  30. package/dist/mcp/search-index.json +21 -21
  31. package/dist/mcp/update.json +2 -2
  32. package/dist/mcp/worker.mjs +164 -281
  33. package/dist/mcp/worker.mjs.map +2 -2
  34. package/dist/web-components/README.md +4 -0
  35. package/dist/web-components/components/actions.js +149 -2
  36. package/dist/web-components/components/feedback.js +63 -12
  37. package/dist/web-components/index.js +501 -14
  38. package/package.json +10 -3
  39. package/src/icons/index.js +229 -0
  40. package/src/icons/package.json +3 -0
  41. package/src/inc-design-language.js +12 -11
  42. package/src/inc-design-language.scss +38 -1
  43. package/src/web-components/README.md +4 -0
  44. package/src/web-components/components/actions.js +149 -2
  45. package/src/web-components/components/feedback.js +63 -12
@@ -8,9 +8,11 @@ The CSS-first [`inc-*`](../../reference.html) class surface remains the canonica
8
8
 
9
9
  - Package entrypoint: `@incursa/ui-kit/web-components`
10
10
  - Style entrypoint: `@incursa/ui-kit/web-components/style.css`
11
+ - Icon entrypoint: `@incursa/ui-kit/icons`
11
12
  - Built output: `dist/web-components/`
12
13
  - Package export: `./web-components` resolves to `dist/web-components/index.js`
13
14
  - Package export: `./web-components/style.css` resolves to `dist/web-components/style.css`
15
+ - Package export: `./icons` resolves to `dist/icons/index.js`
14
16
  - Module boundary: `src/web-components/package.json` sets this subtree to `type: module`
15
17
 
16
18
  Load these entrypoints only when the consuming app wants the custom elements and their default look. CSS-only consumers should not pay for the runtime.
@@ -45,6 +47,8 @@ The runtime auto-defines the shipped elements on load. If a consumer needs expli
45
47
  The `IncElement` base class, including reflected attribute/property wiring and slot helpers.
46
48
  - [`registry.js`](registry.js)
47
49
  Idempotent registration helpers and the `IncWebComponents.registry` namespace.
50
+ - [`../icons/index.js`](../icons/index.js)
51
+ Semantic Incursa icon names, default Lucide-backed rendering, and the global renderer override used by component fallbacks.
48
52
  - [`components/dom-helpers.js`](components/dom-helpers.js)
49
53
  Shared DOM helpers used by the action and collection modules.
50
54
  - [`controllers/focus.js`](controllers/focus.js)
@@ -1,3 +1,7 @@
1
+ import {
2
+ normalizeIconName,
3
+ replaceIconContents,
4
+ } from "../../icons/index.js";
1
5
  import {
2
6
  addClass,
3
7
  ensureNode,
@@ -28,6 +32,14 @@ const ALERT_DEFAULT_ROLE_BY_TONE = new Map([
28
32
  ["info", "status"],
29
33
  ["secondary", "status"],
30
34
  ]);
35
+ const ALERT_ICON_BY_TONE = new Map([
36
+ ["success", "success"],
37
+ ["danger", "error"],
38
+ ["warning", "warning"],
39
+ ["info", "info"],
40
+ ["secondary", "info"],
41
+ ["primary", "info"],
42
+ ]);
31
43
 
32
44
  const HostElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
33
45
 
@@ -53,6 +65,29 @@ function emit(host, type, detail = {}, options = {}) {
53
65
  }));
54
66
  }
55
67
 
68
+ function getDirectIconSlot(host) {
69
+ return Array.from(host.children || []).find((node) => (
70
+ node instanceof HTMLElement
71
+ && node.getAttribute("slot") === "icon"
72
+ )) || null;
73
+ }
74
+
75
+ function hasConsumerIcon(container) {
76
+ return Array.from(container.children || []).some((node) => (
77
+ node instanceof HTMLElement
78
+ && !node.hasAttribute("data-inc-generated-icon")
79
+ ));
80
+ }
81
+
82
+ function renderDecorativeIcon(container, name, options = {}) {
83
+ replaceIconContents(container, name, {
84
+ className: "inc-icon",
85
+ decorative: true,
86
+ size: options.size || 16,
87
+ });
88
+ container.hidden = false;
89
+ }
90
+
56
91
  class IncElement extends HostElement {
57
92
  emit(type, detail = {}, options = {}) {
58
93
  return emit(this, type, detail, options);
@@ -61,7 +96,7 @@ class IncElement extends HostElement {
61
96
 
62
97
  export class IncButtonElement extends IncElement {
63
98
  static get observedAttributes() {
64
- return ["tone", "variant", "size", "loading", "href", "type", "disabled", "label", "target", "rel", "download"];
99
+ return ["tone", "variant", "size", "loading", "href", "type", "disabled", "label", "target", "rel", "download", "icon"];
65
100
  }
66
101
 
67
102
  connectedCallback() {
@@ -166,6 +201,8 @@ export class IncButtonElement extends IncElement {
166
201
  this.removeLoadingSpinner(control);
167
202
  }
168
203
 
204
+ this.syncIcon(control);
205
+
169
206
  const label = this.getAttribute("label");
170
207
  if (label) {
171
208
  control.setAttribute("aria-label", label);
@@ -220,6 +257,53 @@ export class IncButtonElement extends IncElement {
220
257
 
221
258
  control.querySelectorAll(":scope > [data-inc-button-spinner]").forEach((node) => node.remove());
222
259
  }
260
+
261
+ syncIcon(control) {
262
+ if (!(control instanceof HTMLElement)) {
263
+ return;
264
+ }
265
+
266
+ const explicitIcon = normalizeIconName(this.getAttribute("icon"));
267
+ const inferredIcon = this.getAttribute("download") != null
268
+ ? "download"
269
+ : this.getAttribute("target") === "_blank"
270
+ ? "external-link"
271
+ : "";
272
+ const iconName = explicitIcon || inferredIcon;
273
+ let icon = control.querySelector(":scope > [data-inc-button-icon]");
274
+ const slotted = getDirectIconSlot(control);
275
+
276
+ if (!icon && (iconName || slotted)) {
277
+ icon = document.createElement("span");
278
+ icon.className = "inc-btn__icon";
279
+ icon.setAttribute("data-inc-button-icon", "true");
280
+ icon.setAttribute("aria-hidden", "true");
281
+ control.prepend(icon);
282
+ }
283
+
284
+ if (!(icon instanceof HTMLElement)) {
285
+ return;
286
+ }
287
+
288
+ if (slotted) {
289
+ slotted.removeAttribute("slot");
290
+ icon.replaceChildren(slotted);
291
+ icon.hidden = false;
292
+ return;
293
+ }
294
+
295
+ if (hasConsumerIcon(icon)) {
296
+ icon.hidden = false;
297
+ return;
298
+ }
299
+
300
+ if (iconName && iconName !== "none") {
301
+ renderDecorativeIcon(icon, iconName, { size: 16 });
302
+ return;
303
+ }
304
+
305
+ icon.remove();
306
+ }
223
307
  }
224
308
 
225
309
  export class IncButtonGroupElement extends IncElement {
@@ -350,7 +434,7 @@ export class IncCloseButtonElement extends IncElement {
350
434
 
351
435
  export class IncAlertElement extends IncElement {
352
436
  static get observedAttributes() {
353
- return ["tone", "variant", "dismissible", "dismiss-label", "timeout"];
437
+ return ["tone", "variant", "dismissible", "dismiss-label", "timeout", "icon"];
354
438
  }
355
439
 
356
440
  connectedCallback() {
@@ -398,6 +482,7 @@ export class IncAlertElement extends IncElement {
398
482
  const tone = normalizeToken(this.getAttribute("tone") || this.getAttribute("variant")) || "info";
399
483
  const resolvedTone = BADGE_TONES.has(tone) ? tone : "info";
400
484
  this.classList.add(`inc-alert--${resolvedTone}`);
485
+ this.syncIcon(resolvedTone);
401
486
 
402
487
  if (toBoolean(this.getAttribute("dismissible"))) {
403
488
  this.classList.add("inc-alert--dismissible");
@@ -465,6 +550,44 @@ export class IncAlertElement extends IncElement {
465
550
  this.querySelectorAll(":scope > .inc-alert__progress").forEach((node) => node.remove());
466
551
  }
467
552
 
553
+ syncIcon(tone) {
554
+ const explicitIcon = normalizeIconName(this.getAttribute("icon"));
555
+ const iconName = explicitIcon || ALERT_ICON_BY_TONE.get(tone) || "info";
556
+ let icon = this.querySelector(":scope > .inc-alert__icon");
557
+ const slotted = getDirectIconSlot(this);
558
+
559
+ if (!icon && (iconName !== "none" || slotted)) {
560
+ icon = document.createElement("span");
561
+ icon.className = "inc-alert__icon";
562
+ icon.setAttribute("part", "icon");
563
+ icon.setAttribute("aria-hidden", "true");
564
+ this.prepend(icon);
565
+ }
566
+
567
+ if (!(icon instanceof HTMLElement)) {
568
+ return;
569
+ }
570
+
571
+ if (slotted) {
572
+ slotted.removeAttribute("slot");
573
+ icon.replaceChildren(slotted);
574
+ icon.hidden = false;
575
+ return;
576
+ }
577
+
578
+ if (hasConsumerIcon(icon)) {
579
+ icon.hidden = false;
580
+ return;
581
+ }
582
+
583
+ if (iconName === "none") {
584
+ icon.remove();
585
+ return;
586
+ }
587
+
588
+ renderDecorativeIcon(icon, iconName, { size: 18 });
589
+ }
590
+
468
591
  startDismissTimer(timeoutMs) {
469
592
  const progress = this.ensureProgressBar();
470
593
  this.stopDismissTimer();
@@ -514,11 +637,21 @@ export class IncAlertElement extends IncElement {
514
637
  }
515
638
 
516
639
  export class IncEmptyStateElement extends IncElement {
640
+ static get observedAttributes() {
641
+ return ["icon"];
642
+ }
643
+
517
644
  connectedCallback() {
518
645
  addClass(this, "inc-empty-state");
519
646
  this.sync();
520
647
  }
521
648
 
649
+ attributeChangedCallback() {
650
+ if (this.isConnected) {
651
+ this.sync();
652
+ }
653
+ }
654
+
522
655
  sync() {
523
656
  addClass(this, "inc-empty-state");
524
657
  this.setAttribute("part", "empty-state content icon body actions");
@@ -572,6 +705,20 @@ export class IncEmptyStateElement extends IncElement {
572
705
 
573
706
  body.append(node);
574
707
  });
708
+
709
+ if (hasConsumerIcon(icon)) {
710
+ icon.hidden = false;
711
+ return;
712
+ }
713
+
714
+ const iconName = normalizeIconName(this.getAttribute("icon")) || "empty";
715
+ if (iconName === "none") {
716
+ icon.replaceChildren();
717
+ icon.hidden = true;
718
+ return;
719
+ }
720
+
721
+ renderDecorativeIcon(icon, iconName, { size: 34 });
575
722
  }
576
723
  }
577
724
 
@@ -1,15 +1,30 @@
1
+ import {
2
+ incIconNames,
3
+ normalizeIconName,
4
+ replaceIconContents,
5
+ } from "../../icons/index.js";
6
+
1
7
  const THEME_MODES = ["light", "dark", "system"];
2
8
  const DEFAULT_THEME_STORAGE_KEY = "inc-theme-mode";
3
9
  const BADGE_TONES = new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
4
10
  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();
11
+ const ICON_NAME_SET = new Set(incIconNames);
12
+ const STATE_ICON_BY_VARIANT = new Map([
13
+ ["empty", "empty"],
14
+ ["results", "no-results"],
15
+ ["loading", "loading"],
16
+ ["error", "error"],
17
+ ["danger", "error"],
18
+ ["warning", "warning"],
19
+ ["success", "success"],
20
+ ["info", "info"],
21
+ ]);
22
+ const STATE_ICON_BY_STATUS = new Map([
23
+ ["+", "empty"],
24
+ ["?", "no-results"],
25
+ ["!", "error"],
26
+ ["...", "loading"],
27
+ ]);
13
28
  const HostElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
14
29
  const themeSubscribers = new Set();
15
30
 
@@ -46,6 +61,33 @@ function normalizeToken(value) {
46
61
  return String(value ?? "").trim().toLowerCase();
47
62
  }
48
63
 
64
+ function resolveStateIconName(icon, status, variant) {
65
+ const explicitIcon = normalizeIconName(icon);
66
+ if (explicitIcon) {
67
+ return explicitIcon;
68
+ }
69
+
70
+ const normalizedStatus = normalizeIconName(status);
71
+ if (STATE_ICON_BY_STATUS.has(String(status || "").trim())) {
72
+ return STATE_ICON_BY_STATUS.get(String(status || "").trim());
73
+ }
74
+
75
+ if (ICON_NAME_SET.has(normalizedStatus)) {
76
+ return normalizedStatus;
77
+ }
78
+
79
+ return STATE_ICON_BY_VARIANT.get(normalizeToken(variant)) || "info";
80
+ }
81
+
82
+ function renderDecorativeIcon(container, name, size = 18) {
83
+ replaceIconContents(container, name, {
84
+ className: "inc-icon",
85
+ decorative: true,
86
+ size,
87
+ });
88
+ container.hidden = false;
89
+ }
90
+
49
91
  function getSystemTheme() {
50
92
  if (!window.matchMedia) {
51
93
  return "light";
@@ -206,7 +248,7 @@ function formatRemaining(totalSeconds) {
206
248
  }
207
249
 
208
250
  export class IncStatePanel extends HostElement {
209
- static observedAttributes = ["tone", "variant", "title", "body", "status", "open"];
251
+ static observedAttributes = ["tone", "variant", "title", "body", "status", "icon", "open"];
210
252
 
211
253
  #fallback = null;
212
254
  #appliedVariantClass = "";
@@ -246,6 +288,7 @@ export class IncStatePanel extends HostElement {
246
288
  actions.className = "inc-state-panel__actions";
247
289
 
248
290
  icon.setAttribute("part", "icon");
291
+ icon.setAttribute("aria-hidden", "true");
249
292
  title.setAttribute("part", "title");
250
293
  body.setAttribute("part", "body");
251
294
  actions.setAttribute("part", "actions");
@@ -282,11 +325,19 @@ export class IncStatePanel extends HostElement {
282
325
  const title = this.getAttribute("title") || "";
283
326
  const body = this.getAttribute("body") || "";
284
327
  const status = this.getAttribute("status") || "";
328
+ const iconName = resolveStateIconName(this.getAttribute("icon"), status, nextVariant);
285
329
 
286
330
  this.#fallback.title.textContent = title;
287
331
  this.#fallback.body.textContent = body;
288
- this.#fallback.icon.textContent = status;
289
- this.#fallback.icon.hidden = !status;
332
+ if (iconName === "none") {
333
+ this.#fallback.icon.replaceChildren();
334
+ this.#fallback.icon.hidden = true;
335
+ } else if (ICON_NAME_SET.has(iconName)) {
336
+ renderDecorativeIcon(this.#fallback.icon, iconName, 22);
337
+ } else {
338
+ this.#fallback.icon.textContent = status;
339
+ this.#fallback.icon.hidden = !status;
340
+ }
290
341
  this.#fallback.actions.hidden = true;
291
342
  }
292
343
 
@@ -872,7 +923,7 @@ export class IncAutoRefresh extends HostElement {
872
923
  }
873
924
 
874
925
  if (this.#parts.toggleIcon instanceof HTMLElement) {
875
- this.#parts.toggleIcon.innerHTML = this.#isPaused ? AUTO_REFRESH_PLAY_ICON : AUTO_REFRESH_PAUSE_ICON;
926
+ renderDecorativeIcon(this.#parts.toggleIcon, this.#isPaused ? "play" : "pause", 16);
876
927
  }
877
928
  }
878
929