@studious-creative/yumekit 0.1.6 → 0.1.7

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.
@@ -762,9 +762,30 @@ class YumeMenu extends HTMLElement {
762
762
  left = anchorRect.left - menuRect.width;
763
763
  }
764
764
  if (top + menuRect.height > vh) {
765
- top = vh - menuRect.height - 10;
765
+ top = anchorRect.top - menuRect.height;
766
+ }
767
+ } else if (this.direction === "up") {
768
+ top = anchorRect.top - menuRect.height;
769
+ left = anchorRect.left;
770
+
771
+ if (top < 0) {
772
+ top = anchorRect.bottom;
773
+ }
774
+ if (left + menuRect.width > vw) {
775
+ left = vw - menuRect.width - 10;
776
+ }
777
+ } else if (this.direction === "left") {
778
+ top = anchorRect.top;
779
+ left = anchorRect.left - menuRect.width;
780
+
781
+ if (left < 0) {
782
+ left = anchorRect.right;
783
+ }
784
+ if (top + menuRect.height > vh) {
785
+ top = anchorRect.top - menuRect.height;
766
786
  }
767
787
  } else {
788
+ // "down" (default)
768
789
  top = anchorRect.bottom;
769
790
  left = anchorRect.left;
770
791
 
@@ -776,8 +797,8 @@ class YumeMenu extends HTMLElement {
776
797
  }
777
798
  }
778
799
 
779
- top = Math.max(10, Math.min(top, vh - menuRect.height - 10));
780
- left = Math.max(10, Math.min(left, vw - menuRect.width - 10));
800
+ top = Math.max(0, Math.min(top, vh - menuRect.height));
801
+ left = Math.max(0, Math.min(left, vw - menuRect.width));
781
802
 
782
803
  this.style.top = `${top}px`;
783
804
  this.style.left = `${left}px`;
@@ -825,7 +846,7 @@ class YumeMenu extends HTMLElement {
825
846
  top: 0;
826
847
  left: 100%;
827
848
  display: none;
828
- z-index: 1001;
849
+ z-index: var(--component-menu-z-index, 1001);
829
850
  }
830
851
 
831
852
  li.menuitem:hover > ul.submenu {
@@ -1024,7 +1045,7 @@ class YumeAppbar extends HTMLElement {
1024
1045
 
1025
1046
  :host([sticky]) {
1026
1047
  position: sticky;
1027
- z-index: 100;
1048
+ z-index: var(--component-appbar-z-index, 100);
1028
1049
  }
1029
1050
  :host([orientation="vertical"][sticky="start"]),
1030
1051
  :host(:not([orientation])[sticky="start"]) {
@@ -1249,6 +1270,7 @@ class YumeAppbar extends HTMLElement {
1249
1270
 
1250
1271
  const bar = document.createElement("div");
1251
1272
  bar.className = `appbar ${isVertical ? "vertical" : "horizontal"}`;
1273
+
1252
1274
  if (isCollapsed) bar.classList.add("collapsed");
1253
1275
  bar.setAttribute("role", "navigation");
1254
1276
  bar.style.setProperty("--_appbar-padding", cfg.padding);
@@ -1269,6 +1291,7 @@ class YumeAppbar extends HTMLElement {
1269
1291
 
1270
1292
  const logoWrapper = document.createElement("div");
1271
1293
  logoWrapper.className = "logo-wrapper";
1294
+
1272
1295
  const logoSlot = document.createElement("slot");
1273
1296
  logoSlot.name = "logo";
1274
1297
  logoWrapper.appendChild(logoSlot);
@@ -1276,6 +1299,7 @@ class YumeAppbar extends HTMLElement {
1276
1299
 
1277
1300
  const titleWrapper = document.createElement("div");
1278
1301
  titleWrapper.className = "header-title";
1302
+
1279
1303
  const titleSlot = document.createElement("slot");
1280
1304
  titleSlot.name = "title";
1281
1305
  titleWrapper.appendChild(titleSlot);
@@ -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,16 @@
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
+ _getColor(color: any): any;
14
+ _getSize(size: any): any;
15
+ render(): void;
16
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Icon registry — a runtime map of icon names to SVG markup strings.
3
+ *
4
+ * Register only the icons you need for tree-shaking:
5
+ *
6
+ * import { registerIcon } from "@studious-creative/yumekit";
7
+ * registerIcon("home", homeSvgString);
8
+ *
9
+ * Or register all bundled icons at once (separate import):
10
+ *
11
+ * import "@studious-creative/yumekit/icons/all.js";
12
+ */
13
+
14
+ const icons = new Map();
15
+
16
+ function getIcon(name) {
17
+ return icons.get(name) || "";
18
+ }
19
+
20
+ // Allowlist-based SVG sanitizer — only known-safe elements and attributes are kept.
21
+ const ALLOWED_ELEMENTS = new Set([
22
+ "svg",
23
+ "g",
24
+ "path",
25
+ "circle",
26
+ "ellipse",
27
+ "rect",
28
+ "line",
29
+ "polyline",
30
+ "polygon",
31
+ "text",
32
+ "tspan",
33
+ "defs",
34
+ "clippath",
35
+ "mask",
36
+ "lineargradient",
37
+ "radialgradient",
38
+ "stop",
39
+ "symbol",
40
+ "title",
41
+ "desc",
42
+ "metadata",
43
+ ]);
44
+
45
+ const ALLOWED_ATTRS = new Set([
46
+ "viewbox",
47
+ "xmlns",
48
+ "fill",
49
+ "stroke",
50
+ "stroke-width",
51
+ "stroke-linecap",
52
+ "stroke-linejoin",
53
+ "stroke-dasharray",
54
+ "stroke-dashoffset",
55
+ "stroke-miterlimit",
56
+ "stroke-opacity",
57
+ "fill-opacity",
58
+ "fill-rule",
59
+ "clip-rule",
60
+ "opacity",
61
+ "d",
62
+ "cx",
63
+ "cy",
64
+ "r",
65
+ "rx",
66
+ "ry",
67
+ "x",
68
+ "x1",
69
+ "x2",
70
+ "y",
71
+ "y1",
72
+ "y2",
73
+ "width",
74
+ "height",
75
+ "points",
76
+ "transform",
77
+ "id",
78
+ "class",
79
+ "clip-path",
80
+ "mask",
81
+ "offset",
82
+ "stop-color",
83
+ "stop-opacity",
84
+ "gradient-units",
85
+ "gradienttransform",
86
+ "gradientunits",
87
+ "spreadmethod",
88
+ "patternunits",
89
+ "patterntransform",
90
+ "font-size",
91
+ "font-family",
92
+ "font-weight",
93
+ "text-anchor",
94
+ "dominant-baseline",
95
+ "alignment-baseline",
96
+ "dx",
97
+ "dy",
98
+ "rotate",
99
+ "textlength",
100
+ "lengthadjust",
101
+ "display",
102
+ "visibility",
103
+ "color",
104
+ "vector-effect",
105
+ ]);
106
+
107
+ function sanitizeSvg(raw) {
108
+ if (!raw) return "";
109
+ const doc = new DOMParser().parseFromString(raw, "image/svg+xml");
110
+ const svg = doc.querySelector("svg");
111
+ if (!svg) return "";
112
+
113
+ const walk = (el) => {
114
+ for (const child of [...el.children]) {
115
+ if (!ALLOWED_ELEMENTS.has(child.tagName.toLowerCase())) {
116
+ child.remove();
117
+ continue;
118
+ }
119
+ for (const attr of [...child.attributes]) {
120
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
121
+ child.removeAttribute(attr.name);
122
+ }
123
+ }
124
+ walk(child);
125
+ }
126
+ };
127
+
128
+ // Sanitize the <svg> element's own attributes
129
+ for (const attr of [...svg.attributes]) {
130
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
131
+ svg.removeAttribute(attr.name);
132
+ }
133
+ }
134
+ walk(svg);
135
+ return svg.outerHTML;
136
+ }
137
+
138
+ // Cache sanitized SVG markup per icon name to avoid repeated DOMParser + DOM-walk
139
+ // on every render. The cache is naturally bounded by the number of registered icons.
140
+ const sanitizedSvgCache = new Map();
141
+
142
+ function getCachedSvg(name) {
143
+ if (sanitizedSvgCache.has(name)) {
144
+ return sanitizedSvgCache.get(name);
145
+ }
146
+ const result = sanitizeSvg(getIcon(name));
147
+ sanitizedSvgCache.set(name, result);
148
+ return result;
149
+ }
150
+
151
+ class YumeIcon extends HTMLElement {
152
+ static get observedAttributes() {
153
+ return ["name", "size", "color", "label"];
154
+ }
155
+
156
+ constructor() {
157
+ super();
158
+ this.attachShadow({ mode: "open" });
159
+ }
160
+
161
+ connectedCallback() {
162
+ this.render();
163
+ }
164
+
165
+ attributeChangedCallback(name, oldVal, newVal) {
166
+ if (oldVal === newVal) return;
167
+ this.render();
168
+ }
169
+
170
+ get name() {
171
+ return this.getAttribute("name") || "";
172
+ }
173
+ set name(val) {
174
+ this.setAttribute("name", val);
175
+ }
176
+
177
+ get size() {
178
+ return this.getAttribute("size") || "medium";
179
+ }
180
+ set size(val) {
181
+ this.setAttribute("size", val);
182
+ }
183
+
184
+ get color() {
185
+ return this.getAttribute("color") || "base";
186
+ }
187
+ set color(val) {
188
+ this.setAttribute("color", val);
189
+ }
190
+
191
+ get label() {
192
+ return this.getAttribute("label") || "";
193
+ }
194
+ set label(val) {
195
+ if (val) this.setAttribute("label", val);
196
+ else this.removeAttribute("label");
197
+ }
198
+
199
+ _getColor(color) {
200
+ const map = {
201
+ base: "var(--base-content--, #f7f7fa)",
202
+ primary: "var(--primary-content--, #0576ff)",
203
+ secondary: "var(--secondary-content--, #04b8b8)",
204
+ success: "var(--success-content--, #2dba73)",
205
+ warning: "var(--warning-content--, #d17f04)",
206
+ error: "var(--error-content--, #b80421)",
207
+ help: "var(--help-content--, #5405ff)",
208
+ };
209
+ return map[color] || map.base;
210
+ }
211
+
212
+ _getSize(size) {
213
+ const map = {
214
+ small: "var(--component-icon-size-small, 16px)",
215
+ medium: "var(--component-icon-size-medium, 24px)",
216
+ large: "var(--component-icon-size-large, 32px)",
217
+ };
218
+ return map[size] || map.medium;
219
+ }
220
+
221
+ render() {
222
+ const svg = getCachedSvg(this.name);
223
+ const sizeVal = this._getSize(this.size);
224
+ const colorVal = this._getColor(this.color);
225
+ const label = this.label;
226
+
227
+ if (label) {
228
+ this.setAttribute("role", "img");
229
+ this.setAttribute("aria-label", label);
230
+ this.removeAttribute("aria-hidden");
231
+ } else {
232
+ this.setAttribute("aria-hidden", "true");
233
+ this.removeAttribute("role");
234
+ this.removeAttribute("aria-label");
235
+ }
236
+
237
+ this.shadowRoot.innerHTML = `
238
+ <style>
239
+ :host {
240
+ display: inline-flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ width: ${sizeVal};
244
+ height: ${sizeVal};
245
+ color: ${colorVal};
246
+ line-height: 0;
247
+ }
248
+ .icon-wrapper svg {
249
+ width: 100%;
250
+ height: 100%;
251
+ }
252
+ </style>
253
+ <span class="icon-wrapper" part="icon">${svg}</span>
254
+ `;
255
+ }
256
+ }
257
+
258
+ if (!customElements.get("y-icon")) {
259
+ customElements.define("y-icon", YumeIcon);
260
+ }
261
+
262
+ export { YumeIcon };
@@ -206,9 +206,30 @@ class YumeMenu extends HTMLElement {
206
206
  left = anchorRect.left - menuRect.width;
207
207
  }
208
208
  if (top + menuRect.height > vh) {
209
- top = vh - menuRect.height - 10;
209
+ top = anchorRect.top - menuRect.height;
210
+ }
211
+ } else if (this.direction === "up") {
212
+ top = anchorRect.top - menuRect.height;
213
+ left = anchorRect.left;
214
+
215
+ if (top < 0) {
216
+ top = anchorRect.bottom;
217
+ }
218
+ if (left + menuRect.width > vw) {
219
+ left = vw - menuRect.width - 10;
220
+ }
221
+ } else if (this.direction === "left") {
222
+ top = anchorRect.top;
223
+ left = anchorRect.left - menuRect.width;
224
+
225
+ if (left < 0) {
226
+ left = anchorRect.right;
227
+ }
228
+ if (top + menuRect.height > vh) {
229
+ top = anchorRect.top - menuRect.height;
210
230
  }
211
231
  } else {
232
+ // "down" (default)
212
233
  top = anchorRect.bottom;
213
234
  left = anchorRect.left;
214
235
 
@@ -220,8 +241,8 @@ class YumeMenu extends HTMLElement {
220
241
  }
221
242
  }
222
243
 
223
- top = Math.max(10, Math.min(top, vh - menuRect.height - 10));
224
- left = Math.max(10, Math.min(left, vw - menuRect.width - 10));
244
+ top = Math.max(0, Math.min(top, vh - menuRect.height));
245
+ left = Math.max(0, Math.min(left, vw - menuRect.width));
225
246
 
226
247
  this.style.top = `${top}px`;
227
248
  this.style.left = `${left}px`;
@@ -269,7 +290,7 @@ class YumeMenu extends HTMLElement {
269
290
  top: 0;
270
291
  left: 100%;
271
292
  display: none;
272
- z-index: 1001;
293
+ z-index: var(--component-menu-z-index, 1001);
273
294
  }
274
295
 
275
296
  li.menuitem:hover > ul.submenu {
@@ -81,7 +81,12 @@ class YumePanel extends HTMLElement {
81
81
  if (!this._expanded) {
82
82
  const parentBar = this.closest("y-panelbar");
83
83
  if (parentBar && parentBar.hasAttribute("exclusive")) {
84
- const siblingPanels = parentBar.querySelectorAll("y-panel");
84
+ const parent = this.parentElement;
85
+ const siblingPanels = parent
86
+ ? Array.from(parent.children).filter(
87
+ (el) => el.tagName === "Y-PANEL",
88
+ )
89
+ : [];
85
90
  siblingPanels.forEach((panel) => {
86
91
  if (panel !== this && panel.expanded) {
87
92
  panel.collapse();
@@ -133,9 +138,19 @@ class YumePanel extends HTMLElement {
133
138
  }
134
139
 
135
140
  updateChildState() {
136
- const parentPanel = this.parentElement?.closest("y-panel");
137
- const isChild = Boolean(parentPanel && parentPanel !== this);
138
- this.setAttribute("data-is-child", isChild ? "true" : "false");
141
+ let depth = 0;
142
+ let el = this.parentElement;
143
+ while (el) {
144
+ const parent = el.closest("y-panel");
145
+ if (parent && parent !== this) {
146
+ depth++;
147
+ el = parent.parentElement;
148
+ } else {
149
+ break;
150
+ }
151
+ }
152
+ this.setAttribute("data-is-child", depth > 0 ? "true" : "false");
153
+ this.style.setProperty("--panel-depth", depth);
139
154
  }
140
155
 
141
156
  checkRouteMatch() {
@@ -151,7 +166,8 @@ class YumePanel extends HTMLElement {
151
166
  const header = this.shadowRoot.querySelector(".header");
152
167
  if (!header) return;
153
168
 
154
- header.addEventListener("click", () => {
169
+ header.addEventListener("click", (e) => {
170
+ e.stopPropagation();
155
171
  if (this.hasAttribute("href")) {
156
172
  const href = this.getAttribute("href");
157
173
  if (this.getAttribute("history") !== "false") {
@@ -261,7 +277,7 @@ class YumePanel extends HTMLElement {
261
277
  }
262
278
 
263
279
  :host([data-is-child="true"]) .header {
264
- padding-left: calc(var(--component-panelbar-padding, 4px) * 2);
280
+ padding-left: calc(var(--component-panelbar-padding, 4px) + (var(--panel-depth, 1) * var(--component-panelbar-indent, 16px)));
265
281
  }
266
282
 
267
283
  .header {
@@ -3,7 +3,9 @@ export class YumeSelect extends HTMLElement {
3
3
  static get observedAttributes(): string[];
4
4
  _internals: ElementInternals;
5
5
  selectedValues: Set<any>;
6
+ _onDocumentClick(e: any): void;
6
7
  connectedCallback(): void;
8
+ disconnectedCallback(): void;
7
9
  attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
8
10
  set value(val: any);
9
11
  get value(): any;
@@ -24,6 +24,7 @@ class YumeSelect extends HTMLElement {
24
24
  "placeholder",
25
25
  "options",
26
26
  "display-mode",
27
+ "close-on-click-outside",
27
28
  ];
28
29
  }
29
30
 
@@ -32,6 +33,7 @@ class YumeSelect extends HTMLElement {
32
33
  this._internals = this.attachInternals();
33
34
  this.attachShadow({ mode: "open" });
34
35
  this.selectedValues = new Set();
36
+ this._onDocumentClick = this._onDocumentClick.bind(this);
35
37
  this.render();
36
38
  }
37
39
 
@@ -43,6 +45,18 @@ class YumeSelect extends HTMLElement {
43
45
  this._internals.setFormValue(this.value);
44
46
  }
45
47
 
48
+ disconnectedCallback() {
49
+ this.closeDropdown();
50
+ }
51
+
52
+ _onDocumentClick(e) {
53
+ if (this.getAttribute("close-on-click-outside") === "false") return;
54
+ const path = e.composedPath();
55
+ if (!path.includes(this) && this.dropdown?.classList.contains("open")) {
56
+ this.closeDropdown();
57
+ }
58
+ }
59
+
46
60
  attributeChangedCallback(name, oldValue, newValue) {
47
61
  if (oldValue === newValue) return;
48
62
 
@@ -141,15 +155,18 @@ class YumeSelect extends HTMLElement {
141
155
  this._onScrollOrResize = this._positionDropdown.bind(this);
142
156
  window.addEventListener("scroll", this._onScrollOrResize, true);
143
157
  window.addEventListener("resize", this._onScrollOrResize);
158
+ document.addEventListener("click", this._onDocumentClick, true);
144
159
  }
145
160
  }
146
161
 
147
162
  closeDropdown() {
148
- this.dropdown.classList.remove("open");
149
- this.selectContainer.classList.remove("open");
163
+ this.dropdown?.classList.remove("open");
164
+ this.selectContainer?.classList.remove("open");
165
+ document.removeEventListener("click", this._onDocumentClick, true);
150
166
  if (this._onScrollOrResize) {
151
167
  window.removeEventListener("scroll", this._onScrollOrResize, true);
152
168
  window.removeEventListener("resize", this._onScrollOrResize);
169
+ this._onScrollOrResize = null;
153
170
  }
154
171
  }
155
172
 
@@ -309,6 +326,7 @@ class YumeSelect extends HTMLElement {
309
326
  }
310
327
 
311
328
  render() {
329
+ this.closeDropdown();
312
330
  this.applyStyles();
313
331
  this.shadowRoot.innerHTML = this.generateTemplate();
314
332
  this.queryRefs();
@@ -382,7 +400,7 @@ class YumeSelect extends HTMLElement {
382
400
 
383
401
  .dropdown {
384
402
  position: fixed;
385
- z-index: 9999;
403
+ z-index: var(--component-select-z-index, 6000);
386
404
  background: var(--component-select-background);
387
405
  border: var(--component-inputs-border-width) solid var(--component-select-border-color);
388
406
  border-radius: var(--component-inputs-border-radius-outer);