@studious-creative/yumekit 0.1.7 → 0.1.9

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"];
@@ -1016,18 +1302,21 @@ class YumeAppbar extends HTMLElement {
1016
1302
  collapsedWidth: "40px",
1017
1303
  bodyGap: "2px",
1018
1304
  buttonSize: "small",
1305
+ iconSize: "small",
1019
1306
  },
1020
1307
  medium: {
1021
1308
  padding: "var(--spacing-small, 6px)",
1022
1309
  collapsedWidth: "52px",
1023
1310
  bodyGap: "3px",
1024
1311
  buttonSize: "medium",
1312
+ iconSize: "medium",
1025
1313
  },
1026
1314
  large: {
1027
1315
  padding: "var(--spacing-medium, 8px)",
1028
1316
  collapsedWidth: "64px",
1029
1317
  bodyGap: "4px",
1030
1318
  buttonSize: "large",
1319
+ iconSize: "large",
1031
1320
  },
1032
1321
  };
1033
1322
  const cfg = sizeConfig[size] || sizeConfig.medium;
@@ -1142,7 +1431,7 @@ class YumeAppbar extends HTMLElement {
1142
1431
 
1143
1432
  .appbar.vertical .appbar-header {
1144
1433
  border-bottom: var(--component-appbar-inner-border-width, var(--component-sidebar-border-width, 2px)) solid var(--component-appbar-border-color, #37383a);
1145
- padding-bottom: var(--_appbar-padding);
1434
+ padding: var(--_appbar-padding);
1146
1435
  margin-bottom: var(--_appbar-padding);
1147
1436
  }
1148
1437
  .appbar.vertical .appbar-footer {
@@ -1153,7 +1442,7 @@ class YumeAppbar extends HTMLElement {
1153
1442
 
1154
1443
  .appbar.horizontal .appbar-header {
1155
1444
  border-right: var(--component-appbar-inner-border-width, var(--component-sidebar-border-width, 2px)) solid var(--component-appbar-border-color, #37383a);
1156
- padding-right: var(--_appbar-padding);
1445
+ padding: var(--_appbar-padding);
1157
1446
  margin-right: var(--_appbar-padding);
1158
1447
  }
1159
1448
  .appbar.horizontal .appbar-footer {
@@ -1305,7 +1594,10 @@ class YumeAppbar extends HTMLElement {
1305
1594
  titleWrapper.appendChild(titleSlot);
1306
1595
  headerContent.appendChild(titleWrapper);
1307
1596
 
1597
+ const headerSlot = document.createElement("slot");
1598
+ headerSlot.name = "header";
1308
1599
  header.appendChild(headerContent);
1600
+ header.appendChild(headerSlot);
1309
1601
  bar.appendChild(header);
1310
1602
 
1311
1603
  /* --- Body: y-button nav items --- */
@@ -1328,10 +1620,20 @@ class YumeAppbar extends HTMLElement {
1328
1620
  btn.setAttribute("size", cfg.buttonSize);
1329
1621
 
1330
1622
  if (item.icon) {
1331
- const iconEl = document.createElement("span");
1332
- iconEl.slot = "left-icon";
1333
- iconEl.innerHTML = item.icon;
1334
- btn.appendChild(iconEl);
1623
+ if (item.icon.trim().startsWith("<")) {
1624
+ const iconEl = document.createElement("span");
1625
+ iconEl.slot = "left-icon";
1626
+ iconEl.setAttribute("part", "icon");
1627
+ iconEl.innerHTML = item.icon;
1628
+ btn.appendChild(iconEl);
1629
+ } else {
1630
+ const iconEl = document.createElement("y-icon");
1631
+ iconEl.slot = "left-icon";
1632
+ iconEl.setAttribute("part", "icon");
1633
+ iconEl.setAttribute("name", item.icon);
1634
+ iconEl.setAttribute("size", cfg.iconSize);
1635
+ btn.appendChild(iconEl);
1636
+ }
1335
1637
  }
1336
1638
 
1337
1639
  if (item.text && !isCollapsed) {
@@ -1352,7 +1654,14 @@ class YumeAppbar extends HTMLElement {
1352
1654
  });
1353
1655
  }
1354
1656
 
1355
- wrapper.appendChild(btn);
1657
+ if (item.slot) {
1658
+ const slot = document.createElement("slot");
1659
+ slot.name = item.slot;
1660
+ slot.appendChild(btn);
1661
+ wrapper.appendChild(slot);
1662
+ } else {
1663
+ wrapper.appendChild(btn);
1664
+ }
1356
1665
 
1357
1666
  if (hasChildren) {
1358
1667
  const menu = document.createElement("y-menu");
@@ -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
  `;
@@ -3,5 +3,5 @@ export class YumeTag extends HTMLElement {
3
3
  connectedCallback(): void;
4
4
  attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
5
5
  render(): void;
6
- getStyle(color: any, styleType: any, shape: any): string;
6
+ getStyle(color: any, styleType: any, shape: any, size: any): string;
7
7
  }
@@ -14,7 +14,7 @@ const close = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width
14
14
 
15
15
  class YumeTag extends HTMLElement {
16
16
  static get observedAttributes() {
17
- return ["removable", "color", "style-type", "shape"];
17
+ return ["removable", "color", "style-type", "shape", "size"];
18
18
  }
19
19
 
20
20
  constructor() {
@@ -36,9 +36,10 @@ class YumeTag extends HTMLElement {
36
36
  const color = this.getAttribute("color") || "base";
37
37
  const styleType = this.getAttribute("style-type") || "filled";
38
38
  const shape = this.getAttribute("shape") || "square";
39
+ const size = this.getAttribute("size") || "medium";
39
40
 
40
41
  const style = document.createElement("style");
41
- style.textContent = this.getStyle(color, styleType, shape);
42
+ style.textContent = this.getStyle(color, styleType, shape, size);
42
43
 
43
44
  this.shadowRoot.innerHTML = "";
44
45
  this.shadowRoot.appendChild(style);
@@ -72,7 +73,7 @@ class YumeTag extends HTMLElement {
72
73
  }
73
74
  }
74
75
 
75
- getStyle(color, styleType, shape) {
76
+ getStyle(color, styleType, shape, size) {
76
77
  const vars = {
77
78
  primary: [
78
79
  "--primary-content--",
@@ -125,20 +126,44 @@ class YumeTag extends HTMLElement {
125
126
  ? "var(--component-tag-border-radius-circle)"
126
127
  : "var(--component-tag-border-radius-square)";
127
128
 
129
+ const sizeConfig = {
130
+ small: {
131
+ height: "var(--component-tag-height-small, 22px)",
132
+ padding:
133
+ "var(--component-tag-padding-small, var(--spacing-2x-small))",
134
+ fontSize: "var(--font-size-small, 0.8em)",
135
+ },
136
+ medium: {
137
+ height: "var(--component-tag-height-medium, 28px)",
138
+ padding:
139
+ "var(--component-tag-padding-medium, var(--spacing-x-small))",
140
+ fontSize: "var(--font-size-label, 0.83em)",
141
+ },
142
+ large: {
143
+ height: "var(--component-tag-height-large, 38px)",
144
+ padding:
145
+ "var(--component-tag-padding-large, var(--spacing-small))",
146
+ fontSize: "var(--font-size-paragraph, 1em)",
147
+ },
148
+ };
149
+ const cfg = sizeConfig[size] || sizeConfig.medium;
150
+
128
151
  const baseStyle = `
129
152
  :host {
130
153
  display: inline-block;
131
154
  font-family: var(--font-family-body, sans-serif);
132
- font-size: var(--font-size-label, 0.83em);
155
+ font-size: ${cfg.fontSize};
133
156
  }
134
157
  .tag {
135
158
  display: inline-flex;
136
159
  align-items: center;
137
160
  gap: var(--spacing-2x-small);
138
- padding: 0 var(--component-tag-padding-medium, var(--spacing-x-small));
161
+ height: ${cfg.height};
162
+ padding: 0 ${cfg.padding};
139
163
  border: 1px solid transparent;
140
164
  transition: background-color 0.2s, color 0.2s;
141
165
  border-radius: ${borderRadius};
166
+ box-sizing: border-box;
142
167
  }
143
168
  .remove {
144
169
  all: unset;