@studious-creative/yumekit 0.1.6 → 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"];
@@ -762,9 +1048,30 @@ class YumeMenu extends HTMLElement {
762
1048
  left = anchorRect.left - menuRect.width;
763
1049
  }
764
1050
  if (top + menuRect.height > vh) {
765
- top = vh - menuRect.height - 10;
1051
+ top = anchorRect.top - menuRect.height;
1052
+ }
1053
+ } else if (this.direction === "up") {
1054
+ top = anchorRect.top - menuRect.height;
1055
+ left = anchorRect.left;
1056
+
1057
+ if (top < 0) {
1058
+ top = anchorRect.bottom;
1059
+ }
1060
+ if (left + menuRect.width > vw) {
1061
+ left = vw - menuRect.width - 10;
1062
+ }
1063
+ } else if (this.direction === "left") {
1064
+ top = anchorRect.top;
1065
+ left = anchorRect.left - menuRect.width;
1066
+
1067
+ if (left < 0) {
1068
+ left = anchorRect.right;
1069
+ }
1070
+ if (top + menuRect.height > vh) {
1071
+ top = anchorRect.top - menuRect.height;
766
1072
  }
767
1073
  } else {
1074
+ // "down" (default)
768
1075
  top = anchorRect.bottom;
769
1076
  left = anchorRect.left;
770
1077
 
@@ -776,8 +1083,8 @@ class YumeMenu extends HTMLElement {
776
1083
  }
777
1084
  }
778
1085
 
779
- top = Math.max(10, Math.min(top, vh - menuRect.height - 10));
780
- left = Math.max(10, Math.min(left, vw - menuRect.width - 10));
1086
+ top = Math.max(0, Math.min(top, vh - menuRect.height));
1087
+ left = Math.max(0, Math.min(left, vw - menuRect.width));
781
1088
 
782
1089
  this.style.top = `${top}px`;
783
1090
  this.style.left = `${left}px`;
@@ -825,7 +1132,7 @@ class YumeMenu extends HTMLElement {
825
1132
  top: 0;
826
1133
  left: 100%;
827
1134
  display: none;
828
- z-index: 1001;
1135
+ z-index: var(--component-menu-z-index, 1001);
829
1136
  }
830
1137
 
831
1138
  li.menuitem:hover > ul.submenu {
@@ -1024,7 +1331,7 @@ class YumeAppbar extends HTMLElement {
1024
1331
 
1025
1332
  :host([sticky]) {
1026
1333
  position: sticky;
1027
- z-index: 100;
1334
+ z-index: var(--component-appbar-z-index, 100);
1028
1335
  }
1029
1336
  :host([orientation="vertical"][sticky="start"]),
1030
1337
  :host(:not([orientation])[sticky="start"]) {
@@ -1249,6 +1556,7 @@ class YumeAppbar extends HTMLElement {
1249
1556
 
1250
1557
  const bar = document.createElement("div");
1251
1558
  bar.className = `appbar ${isVertical ? "vertical" : "horizontal"}`;
1559
+
1252
1560
  if (isCollapsed) bar.classList.add("collapsed");
1253
1561
  bar.setAttribute("role", "navigation");
1254
1562
  bar.style.setProperty("--_appbar-padding", cfg.padding);
@@ -1269,6 +1577,7 @@ class YumeAppbar extends HTMLElement {
1269
1577
 
1270
1578
  const logoWrapper = document.createElement("div");
1271
1579
  logoWrapper.className = "logo-wrapper";
1580
+
1272
1581
  const logoSlot = document.createElement("slot");
1273
1582
  logoSlot.name = "logo";
1274
1583
  logoWrapper.appendChild(logoSlot);
@@ -1276,6 +1585,7 @@ class YumeAppbar extends HTMLElement {
1276
1585
 
1277
1586
  const titleWrapper = document.createElement("div");
1278
1587
  titleWrapper.className = "header-title";
1588
+
1279
1589
  const titleSlot = document.createElement("slot");
1280
1590
  titleSlot.name = "title";
1281
1591
  titleWrapper.appendChild(titleSlot);
@@ -1304,10 +1614,18 @@ class YumeAppbar extends HTMLElement {
1304
1614
  btn.setAttribute("size", cfg.buttonSize);
1305
1615
 
1306
1616
  if (item.icon) {
1307
- const iconEl = document.createElement("span");
1308
- iconEl.slot = "left-icon";
1309
- iconEl.innerHTML = item.icon;
1310
- 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
+ }
1311
1629
  }
1312
1630
 
1313
1631
  if (item.text && !isCollapsed) {
@@ -180,6 +180,16 @@ class YumeCheckbox extends HTMLElement {
180
180
  line-height: 0;
181
181
  }
182
182
 
183
+ :host([disabled]) .checkbox {
184
+ border-color: var(--component-checkbox-border-color);
185
+ background: var(--component-checkbox-disabled-background, var(--component-checkbox-background));
186
+ cursor: not-allowed;
187
+ }
188
+
189
+ :host([disabled]) .checkbox:hover {
190
+ border-color: var(--component-checkbox-border-color);
191
+ }
192
+
183
193
  .checkbox:hover {
184
194
  border-color: var(--component-checkbox-accent);
185
195
  }
@@ -191,7 +201,7 @@ class YumeCheckbox extends HTMLElement {
191
201
  display: block;
192
202
  }
193
203
 
194
- .label {
204
+ [part="label"] {
195
205
  display: inline-flex;
196
206
  align-items: center;
197
207
  font-size: 0.9em;
@@ -104,7 +104,7 @@ class YumeDialog extends HTMLElement {
104
104
  align-items: center;
105
105
  justify-content: center;
106
106
  background: rgba(0,0,0,0.5);
107
- z-index: 1000;
107
+ z-index: var(--component-dialog-z-index, 1000);
108
108
  }
109
109
  :host([visible]) { display: flex; }
110
110
  .dialog {
@@ -261,7 +261,7 @@ class YumeDrawer extends HTMLElement {
261
261
  position: fixed;
262
262
  top: 0; left: 0; right: 0; bottom: 0;
263
263
  display: none;
264
- z-index: 1000;
264
+ z-index: var(--component-drawer-z-index, 5000);
265
265
  }
266
266
  :host([visible]) {
267
267
  display: block;
@@ -0,0 +1,19 @@
1
+ export class YumeIcon extends HTMLElement {
2
+ static get observedAttributes(): string[];
3
+ connectedCallback(): void;
4
+ attributeChangedCallback(name: any, oldVal: any, newVal: any): void;
5
+ set name(val: string);
6
+ get name(): string;
7
+ set size(val: string);
8
+ get size(): string;
9
+ set color(val: string);
10
+ get color(): string;
11
+ set label(val: string);
12
+ get label(): string;
13
+ set weight(val: string);
14
+ get weight(): string;
15
+ _getColor(color: any): any;
16
+ _getSize(size: any): any;
17
+ _getWeight(weight: any): any;
18
+ render(): void;
19
+ }