@studious-creative/yumekit 0.1.7 → 0.1.8

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.
@@ -554,6 +554,292 @@ if (!customElements.get("y-button")) {
554
554
  customElements.define("y-button", YumeButton);
555
555
  }
556
556
 
557
+ /**
558
+ * Icon registry — a runtime map of icon names to SVG markup strings.
559
+ *
560
+ * Register only the icons you need for tree-shaking:
561
+ *
562
+ * import { registerIcon } from "@studious-creative/yumekit";
563
+ * registerIcon("home", homeSvgString);
564
+ *
565
+ * Or register all bundled icons at once (separate import):
566
+ *
567
+ * import "@studious-creative/yumekit/icons/all.js";
568
+ */
569
+
570
+ const icons = new Map();
571
+
572
+ function getIcon(name) {
573
+ return icons.get(name) || "";
574
+ }
575
+
576
+ // Allowlist-based SVG sanitizer — only known-safe elements and attributes are kept.
577
+ const ALLOWED_ELEMENTS = new Set([
578
+ "svg",
579
+ "g",
580
+ "path",
581
+ "circle",
582
+ "ellipse",
583
+ "rect",
584
+ "line",
585
+ "polyline",
586
+ "polygon",
587
+ "text",
588
+ "tspan",
589
+ "defs",
590
+ "clippath",
591
+ "mask",
592
+ "lineargradient",
593
+ "radialgradient",
594
+ "stop",
595
+ "symbol",
596
+ "title",
597
+ "desc",
598
+ "metadata",
599
+ ]);
600
+
601
+ const ALLOWED_ATTRS = new Set([
602
+ "viewbox",
603
+ "xmlns",
604
+ "fill",
605
+ "stroke",
606
+ "stroke-width",
607
+ "stroke-linecap",
608
+ "stroke-linejoin",
609
+ "stroke-dasharray",
610
+ "stroke-dashoffset",
611
+ "stroke-miterlimit",
612
+ "stroke-opacity",
613
+ "fill-opacity",
614
+ "fill-rule",
615
+ "clip-rule",
616
+ "opacity",
617
+ "d",
618
+ "cx",
619
+ "cy",
620
+ "r",
621
+ "rx",
622
+ "ry",
623
+ "x",
624
+ "x1",
625
+ "x2",
626
+ "y",
627
+ "y1",
628
+ "y2",
629
+ "width",
630
+ "height",
631
+ "points",
632
+ "transform",
633
+ "id",
634
+ "class",
635
+ "clip-path",
636
+ "mask",
637
+ "offset",
638
+ "stop-color",
639
+ "stop-opacity",
640
+ "gradient-units",
641
+ "gradienttransform",
642
+ "gradientunits",
643
+ "spreadmethod",
644
+ "patternunits",
645
+ "patterntransform",
646
+ "font-size",
647
+ "font-family",
648
+ "font-weight",
649
+ "text-anchor",
650
+ "dominant-baseline",
651
+ "alignment-baseline",
652
+ "dx",
653
+ "dy",
654
+ "rotate",
655
+ "textlength",
656
+ "lengthadjust",
657
+ "display",
658
+ "visibility",
659
+ "color",
660
+ "vector-effect",
661
+ ]);
662
+
663
+ function sanitizeSvg(raw) {
664
+ if (!raw) return "";
665
+ const doc = new DOMParser().parseFromString(raw, "image/svg+xml");
666
+ const svg = doc.querySelector("svg");
667
+ if (!svg) return "";
668
+
669
+ const walk = (el) => {
670
+ for (const child of [...el.children]) {
671
+ if (!ALLOWED_ELEMENTS.has(child.tagName.toLowerCase())) {
672
+ child.remove();
673
+ continue;
674
+ }
675
+ for (const attr of [...child.attributes]) {
676
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
677
+ child.removeAttribute(attr.name);
678
+ }
679
+ }
680
+ walk(child);
681
+ }
682
+ };
683
+
684
+ // Sanitize the <svg> element's own attributes
685
+ for (const attr of [...svg.attributes]) {
686
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
687
+ svg.removeAttribute(attr.name);
688
+ }
689
+ }
690
+ walk(svg);
691
+ return svg.outerHTML;
692
+ }
693
+
694
+ // Cache sanitized SVG markup per icon name to avoid repeated DOMParser + DOM-walk
695
+ // on every render. The cache is naturally bounded by the number of registered icons.
696
+ const sanitizedSvgCache = new Map();
697
+
698
+ function getCachedSvg(name) {
699
+ if (sanitizedSvgCache.has(name)) {
700
+ return sanitizedSvgCache.get(name);
701
+ }
702
+ const result = sanitizeSvg(getIcon(name));
703
+ sanitizedSvgCache.set(name, result);
704
+ return result;
705
+ }
706
+
707
+ class YumeIcon extends HTMLElement {
708
+ static get observedAttributes() {
709
+ return ["name", "size", "color", "label", "weight"];
710
+ }
711
+
712
+ constructor() {
713
+ super();
714
+ this.attachShadow({ mode: "open" });
715
+ }
716
+
717
+ connectedCallback() {
718
+ this.render();
719
+ }
720
+
721
+ attributeChangedCallback(name, oldVal, newVal) {
722
+ if (oldVal === newVal) return;
723
+ this.render();
724
+ }
725
+
726
+ get name() {
727
+ return this.getAttribute("name") || "";
728
+ }
729
+ set name(val) {
730
+ this.setAttribute("name", val);
731
+ }
732
+
733
+ get size() {
734
+ return this.getAttribute("size") || "medium";
735
+ }
736
+ set size(val) {
737
+ this.setAttribute("size", val);
738
+ }
739
+
740
+ get color() {
741
+ return this.getAttribute("color") || "";
742
+ }
743
+ set color(val) {
744
+ if (val) this.setAttribute("color", val);
745
+ else this.removeAttribute("color");
746
+ }
747
+
748
+ get label() {
749
+ return this.getAttribute("label") || "";
750
+ }
751
+ set label(val) {
752
+ if (val) this.setAttribute("label", val);
753
+ else this.removeAttribute("label");
754
+ }
755
+
756
+ get weight() {
757
+ return this.getAttribute("weight") || "";
758
+ }
759
+ set weight(val) {
760
+ if (val) this.setAttribute("weight", val);
761
+ else this.removeAttribute("weight");
762
+ }
763
+
764
+ _getColor(color) {
765
+ const map = {
766
+ base: "var(--base-content--, #f7f7fa)",
767
+ primary: "var(--primary-content--, #0576ff)",
768
+ secondary: "var(--secondary-content--, #04b8b8)",
769
+ success: "var(--success-content--, #2dba73)",
770
+ warning: "var(--warning-content--, #d17f04)",
771
+ error: "var(--error-content--, #b80421)",
772
+ help: "var(--help-content--, #5405ff)",
773
+ };
774
+ return map[color] || map.base;
775
+ }
776
+
777
+ _getSize(size) {
778
+ const map = {
779
+ small: "var(--component-icon-size-small, 16px)",
780
+ medium: "var(--component-icon-size-medium, 24px)",
781
+ large: "var(--component-icon-size-large, 32px)",
782
+ };
783
+ return map[size] || map.medium;
784
+ }
785
+
786
+ _getWeight(weight) {
787
+ const map = {
788
+ thin: "1",
789
+ regular: "1.5",
790
+ thick: "2",
791
+ };
792
+ return map[weight] || "";
793
+ }
794
+
795
+ render() {
796
+ const svg = getCachedSvg(this.name);
797
+ const sizeVal = this._getSize(this.size);
798
+ const colorVal = this.color ? this._getColor(this.color) : "inherit";
799
+ const weightVal = this._getWeight(this.weight);
800
+ const label = this.label;
801
+
802
+ if (label) {
803
+ this.setAttribute("role", "img");
804
+ this.setAttribute("aria-label", label);
805
+ this.removeAttribute("aria-hidden");
806
+ } else {
807
+ this.setAttribute("aria-hidden", "true");
808
+ this.removeAttribute("role");
809
+ this.removeAttribute("aria-label");
810
+ }
811
+
812
+ const weightCSS = weightVal
813
+ ? `.icon-wrapper svg,
814
+ .icon-wrapper svg * { stroke-width: ${weightVal} !important; }`
815
+ : "";
816
+
817
+ this.shadowRoot.innerHTML = `
818
+ <style>
819
+ :host {
820
+ display: inline-flex;
821
+ align-items: center;
822
+ justify-content: center;
823
+ width: ${sizeVal};
824
+ height: ${sizeVal};
825
+ color: ${colorVal};
826
+ line-height: 0;
827
+ }
828
+ .icon-wrapper svg {
829
+ width: 100%;
830
+ height: 100%;
831
+ }
832
+ ${weightCSS}
833
+ </style>
834
+ <span class="icon-wrapper" part="icon">${svg}</span>
835
+ `;
836
+ }
837
+ }
838
+
839
+ if (!customElements.get("y-icon")) {
840
+ customElements.define("y-icon", YumeIcon);
841
+ }
842
+
557
843
  class YumeMenu extends HTMLElement {
558
844
  static get observedAttributes() {
559
845
  return ["items", "anchor", "visible", "direction", "size"];
@@ -1328,10 +1614,18 @@ class YumeAppbar extends HTMLElement {
1328
1614
  btn.setAttribute("size", cfg.buttonSize);
1329
1615
 
1330
1616
  if (item.icon) {
1331
- const iconEl = document.createElement("span");
1332
- iconEl.slot = "left-icon";
1333
- iconEl.innerHTML = item.icon;
1334
- btn.appendChild(iconEl);
1617
+ if (item.icon.trim().startsWith("<")) {
1618
+ const iconEl = document.createElement("span");
1619
+ iconEl.slot = "left-icon";
1620
+ iconEl.innerHTML = item.icon;
1621
+ btn.appendChild(iconEl);
1622
+ } else {
1623
+ const iconEl = document.createElement("y-icon");
1624
+ iconEl.slot = "left-icon";
1625
+ iconEl.setAttribute("name", item.icon);
1626
+ iconEl.setAttribute("size", "small");
1627
+ btn.appendChild(iconEl);
1628
+ }
1335
1629
  }
1336
1630
 
1337
1631
  if (item.text && !isCollapsed) {
@@ -10,7 +10,10 @@ export class YumeIcon extends HTMLElement {
10
10
  get color(): string;
11
11
  set label(val: string);
12
12
  get label(): string;
13
+ set weight(val: string);
14
+ get weight(): string;
13
15
  _getColor(color: any): any;
14
16
  _getSize(size: any): any;
17
+ _getWeight(weight: any): any;
15
18
  render(): void;
16
19
  }
@@ -150,7 +150,7 @@ function getCachedSvg(name) {
150
150
 
151
151
  class YumeIcon extends HTMLElement {
152
152
  static get observedAttributes() {
153
- return ["name", "size", "color", "label"];
153
+ return ["name", "size", "color", "label", "weight"];
154
154
  }
155
155
 
156
156
  constructor() {
@@ -182,10 +182,11 @@ class YumeIcon extends HTMLElement {
182
182
  }
183
183
 
184
184
  get color() {
185
- return this.getAttribute("color") || "base";
185
+ return this.getAttribute("color") || "";
186
186
  }
187
187
  set color(val) {
188
- this.setAttribute("color", val);
188
+ if (val) this.setAttribute("color", val);
189
+ else this.removeAttribute("color");
189
190
  }
190
191
 
191
192
  get label() {
@@ -196,6 +197,14 @@ class YumeIcon extends HTMLElement {
196
197
  else this.removeAttribute("label");
197
198
  }
198
199
 
200
+ get weight() {
201
+ return this.getAttribute("weight") || "";
202
+ }
203
+ set weight(val) {
204
+ if (val) this.setAttribute("weight", val);
205
+ else this.removeAttribute("weight");
206
+ }
207
+
199
208
  _getColor(color) {
200
209
  const map = {
201
210
  base: "var(--base-content--, #f7f7fa)",
@@ -218,10 +227,20 @@ class YumeIcon extends HTMLElement {
218
227
  return map[size] || map.medium;
219
228
  }
220
229
 
230
+ _getWeight(weight) {
231
+ const map = {
232
+ thin: "1",
233
+ regular: "1.5",
234
+ thick: "2",
235
+ };
236
+ return map[weight] || "";
237
+ }
238
+
221
239
  render() {
222
240
  const svg = getCachedSvg(this.name);
223
241
  const sizeVal = this._getSize(this.size);
224
- const colorVal = this._getColor(this.color);
242
+ const colorVal = this.color ? this._getColor(this.color) : "inherit";
243
+ const weightVal = this._getWeight(this.weight);
225
244
  const label = this.label;
226
245
 
227
246
  if (label) {
@@ -234,6 +253,11 @@ class YumeIcon extends HTMLElement {
234
253
  this.removeAttribute("aria-label");
235
254
  }
236
255
 
256
+ const weightCSS = weightVal
257
+ ? `.icon-wrapper svg,
258
+ .icon-wrapper svg * { stroke-width: ${weightVal} !important; }`
259
+ : "";
260
+
237
261
  this.shadowRoot.innerHTML = `
238
262
  <style>
239
263
  :host {
@@ -249,6 +273,7 @@ class YumeIcon extends HTMLElement {
249
273
  width: 100%;
250
274
  height: 100%;
251
275
  }
276
+ ${weightCSS}
252
277
  </style>
253
278
  <span class="icon-wrapper" part="icon">${svg}</span>
254
279
  `;
@@ -16,7 +16,7 @@ export class YumeToast extends HTMLElement {
16
16
  * @param {string} [opts.color] — base|primary|secondary|success|warning|error|help (default base).
17
17
  * @param {number} [opts.duration] — Override container-level duration for this toast.
18
18
  * @param {boolean} [opts.dismissible] — Show a close button (default true).
19
- * @param {string} [opts.icon] — Optional Font Awesome class e.g. "fas fa-check".
19
+ * @param {string} [opts.icon] — Optional y-icon name e.g. "checkmark".
20
20
  * @returns {HTMLElement} The toast element (for manual removal).
21
21
  */
22
22
  show(opts?: {
@@ -124,7 +124,7 @@ class YumeToast extends HTMLElement {
124
124
  * @param {string} [opts.color] — base|primary|secondary|success|warning|error|help (default base).
125
125
  * @param {number} [opts.duration] — Override container-level duration for this toast.
126
126
  * @param {boolean} [opts.dismissible] — Show a close button (default true).
127
- * @param {string} [opts.icon] — Optional Font Awesome class e.g. "fas fa-check".
127
+ * @param {string} [opts.icon] — Optional y-icon name e.g. "checkmark".
128
128
  * @returns {HTMLElement} The toast element (for manual removal).
129
129
  */
130
130
  show(opts = {}) {
@@ -157,8 +157,10 @@ class YumeToast extends HTMLElement {
157
157
  toast.style.color = textColor;
158
158
 
159
159
  if (icon) {
160
- const iconEl = document.createElement("i");
161
- iconEl.className = `toast-icon ${icon}`;
160
+ const iconEl = document.createElement("y-icon");
161
+ iconEl.setAttribute("name", icon);
162
+ iconEl.setAttribute("size", "small");
163
+ iconEl.className = "toast-icon";
162
164
  iconEl.setAttribute("part", "icon");
163
165
  toast.appendChild(iconEl);
164
166
  }