@studious-creative/yumekit 0.1.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.
@@ -0,0 +1,299 @@
1
+ class YumeMenu extends HTMLElement {
2
+ static get observedAttributes() {
3
+ return ["items", "anchor", "visible", "direction", "size"];
4
+ }
5
+
6
+ constructor() {
7
+ super();
8
+ this.attachShadow({ mode: "open" });
9
+ this._onAnchorClick = this._onAnchorClick.bind(this);
10
+ this._onDocumentClick = this._onDocumentClick.bind(this);
11
+ this._onScrollOrResize = this._onScrollOrResize.bind(this);
12
+ }
13
+
14
+ connectedCallback() {
15
+ if (!this.hasAttribute("items")) this.items = [];
16
+ this._setupAnchor();
17
+ this.render();
18
+ document.addEventListener("click", this._onDocumentClick);
19
+ window.addEventListener("scroll", this._onScrollOrResize, true);
20
+ window.addEventListener("resize", this._onScrollOrResize);
21
+ this.style.position = "fixed";
22
+ this.style.zIndex = "1000";
23
+ this.style.display = "none";
24
+ }
25
+
26
+ disconnectedCallback() {
27
+ this._teardownAnchor();
28
+ document.removeEventListener("click", this._onDocumentClick);
29
+ window.removeEventListener("scroll", this._onScrollOrResize, true);
30
+ window.removeEventListener("resize", this._onScrollOrResize);
31
+ }
32
+
33
+ attributeChangedCallback(name, oldVal, newVal) {
34
+ if (oldVal === newVal) return;
35
+ if (name === "items" || name === "size") this.render();
36
+ if (name === "anchor") {
37
+ this._teardownAnchor();
38
+ this._setupAnchor();
39
+ }
40
+ if (name === "visible") {
41
+ this._updatePosition();
42
+ }
43
+ if (name === "direction") {
44
+ this._updatePosition();
45
+ }
46
+ }
47
+
48
+ get items() {
49
+ try {
50
+ return JSON.parse(this.getAttribute("items")) || [];
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+ set items(val) {
56
+ this.setAttribute("items", JSON.stringify(val));
57
+ }
58
+
59
+ get anchor() {
60
+ return this.getAttribute("anchor");
61
+ }
62
+ set anchor(val) {
63
+ this.setAttribute("anchor", val);
64
+ }
65
+
66
+ get visible() {
67
+ return this.hasAttribute("visible");
68
+ }
69
+ set visible(val) {
70
+ if (val) this.setAttribute("visible", "");
71
+ else this.removeAttribute("visible");
72
+ }
73
+
74
+ get direction() {
75
+ return this.getAttribute("direction") || "down";
76
+ }
77
+ set direction(val) {
78
+ this.setAttribute("direction", val);
79
+ }
80
+
81
+ get size() {
82
+ const sz = this.getAttribute("size");
83
+ return ["small", "medium", "large"].includes(sz) ? sz : "medium";
84
+ }
85
+ set size(val) {
86
+ if (["small", "medium", "large"].includes(val))
87
+ this.setAttribute("size", val);
88
+ else this.setAttribute("size", "medium");
89
+ }
90
+
91
+ _createMenuList(items) {
92
+ const ul = document.createElement("ul");
93
+
94
+ items.forEach((item) => {
95
+ const li = document.createElement("li");
96
+ li.className = "menuitem";
97
+ li.setAttribute("role", "menuitem");
98
+ li.tabIndex = 0;
99
+
100
+ const contentWrapper = document.createElement("span");
101
+ contentWrapper.className = "item-content";
102
+
103
+ if (item["icon-template"]) {
104
+ const iconTpl = this._findTemplate(item["icon-template"]);
105
+ if (iconTpl)
106
+ contentWrapper.appendChild(iconTpl.content.cloneNode(true));
107
+ }
108
+
109
+ if (item.template) {
110
+ const textTpl = this._findTemplate(item.template);
111
+ if (textTpl) {
112
+ contentWrapper.appendChild(textTpl.content.cloneNode(true));
113
+ } else {
114
+ contentWrapper.textContent = item.text;
115
+ }
116
+ } else {
117
+ contentWrapper.textContent = item.text;
118
+ }
119
+
120
+ li.appendChild(contentWrapper);
121
+
122
+ if (item.url) {
123
+ li.addEventListener("click", () => {
124
+ window.location.href = item.url;
125
+ });
126
+ }
127
+
128
+ if (item.children?.length) {
129
+ const indicator = document.createElement("span");
130
+ indicator.className = "submenu-indicator";
131
+ indicator.textContent = "▶";
132
+ li.appendChild(indicator);
133
+
134
+ const submenu = this._createMenuList(item.children);
135
+ submenu.classList.add("submenu");
136
+ submenu.setAttribute("role", "menu");
137
+ li.appendChild(submenu);
138
+ }
139
+
140
+ ul.appendChild(li);
141
+ });
142
+
143
+ return ul;
144
+ }
145
+
146
+ _findTemplate(name) {
147
+ return this.querySelector(`template[slot="${name}"]`);
148
+ }
149
+
150
+ _onAnchorClick(e) {
151
+ e.stopPropagation();
152
+ this.visible = !this.visible;
153
+ }
154
+
155
+ _onDocumentClick(e) {
156
+ const path = e.composedPath();
157
+ if (this._anchorEl && path.includes(this._anchorEl)) return;
158
+ if (path.includes(this)) return;
159
+ this.visible = false;
160
+ }
161
+
162
+ _onScrollOrResize() {
163
+ if (this.visible) this._updatePosition();
164
+ }
165
+
166
+ _setupAnchor() {
167
+ const id = this.anchor;
168
+ if (id) {
169
+ const root = this.getRootNode();
170
+ const el = root?.getElementById
171
+ ? root.getElementById(id)
172
+ : document.getElementById(id);
173
+ if (el) {
174
+ this._anchorEl = el;
175
+ this._anchorEl.addEventListener("click", this._onAnchorClick);
176
+ }
177
+ }
178
+ }
179
+
180
+ _teardownAnchor() {
181
+ if (this._anchorEl) {
182
+ this._anchorEl.removeEventListener("click", this._onAnchorClick);
183
+ this._anchorEl = null;
184
+ }
185
+ }
186
+
187
+ _updatePosition() {
188
+ if (!this.visible || !this._anchorEl) {
189
+ this.style.display = "none";
190
+ return;
191
+ }
192
+
193
+ const anchorRect = this._anchorEl.getBoundingClientRect();
194
+ const menuRect = this.getBoundingClientRect();
195
+ const vw = window.innerWidth;
196
+ const vh = window.innerHeight;
197
+
198
+ let top, left;
199
+
200
+ if (this.direction === "right") {
201
+ top = anchorRect.top;
202
+ left = anchorRect.right;
203
+
204
+ if (left + menuRect.width > vw) {
205
+ left = anchorRect.left - menuRect.width;
206
+ }
207
+ if (top + menuRect.height > vh) {
208
+ top = vh - menuRect.height - 10;
209
+ }
210
+ } else {
211
+ top = anchorRect.bottom;
212
+ left = anchorRect.left;
213
+
214
+ if (top + menuRect.height > vh) {
215
+ top = anchorRect.top - menuRect.height;
216
+ }
217
+ if (left + menuRect.width > vw) {
218
+ left = vw - menuRect.width - 10;
219
+ }
220
+ }
221
+
222
+ top = Math.max(10, Math.min(top, vh - menuRect.height - 10));
223
+ left = Math.max(10, Math.min(left, vw - menuRect.width - 10));
224
+
225
+ this.style.top = `${top}px`;
226
+ this.style.left = `${left}px`;
227
+ this.style.display = "block";
228
+ }
229
+
230
+ render() {
231
+ this.shadowRoot.innerHTML = "";
232
+
233
+ const paddingVar = `var(--component-button-padding-${this.size}, 0.5rem)`;
234
+
235
+ const style = document.createElement("style");
236
+ style.textContent = `
237
+ ul.menu,
238
+ ul.submenu {
239
+ list-style: none;
240
+ margin: 0;
241
+ padding: 0;
242
+ background: var(--component-menu-background, #fff);
243
+ border: var(--component-menu-border-width, 1px) solid var(--component-menu-border-color, #ccc);
244
+ border-radius: var(--component-menu-border-radius, 4px);
245
+ box-shadow: var(--component-menu-shadow, 0 2px 8px rgba(0, 0, 0, 0.15));
246
+ min-width: 150px;
247
+ max-height: 300px;
248
+ overflow-y: auto;
249
+ }
250
+
251
+ li.menuitem {
252
+ cursor: pointer;
253
+ padding: ${paddingVar};
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: space-between;
257
+ white-space: nowrap;
258
+ color: var(--component-menu-color, #000);
259
+ font-size: var(--font-size-button, 1em);
260
+ }
261
+
262
+ li.menuitem:hover {
263
+ background: var(--component-menu-hover-background, #eee);
264
+ }
265
+
266
+ ul.submenu {
267
+ position: absolute;
268
+ top: 0;
269
+ left: 100%;
270
+ display: none;
271
+ z-index: 1001;
272
+ }
273
+
274
+ li.menuitem:hover > ul.submenu {
275
+ display: block;
276
+ }
277
+
278
+ .submenu-indicator {
279
+ font-size: 0.75em;
280
+ margin-left: 0.5rem;
281
+ opacity: 0.6;
282
+ }
283
+
284
+ .item-content {
285
+ flex: 1;
286
+ }
287
+ `;
288
+ this.shadowRoot.appendChild(style);
289
+
290
+ const rootUl = this._createMenuList(this.items);
291
+ rootUl.classList.add("menu");
292
+ rootUl.setAttribute("role", "menu");
293
+ this.shadowRoot.appendChild(rootUl);
294
+ }
295
+ }
296
+
297
+ if (!customElements.get("y-menu")) {
298
+ customElements.define("y-menu", YumeMenu);
299
+ }
@@ -0,0 +1,350 @@
1
+ /* ================================================================== */
2
+ /* Centralized SVG icon strings for the YumeKit component library. */
3
+ /* */
4
+ /* Each static icon also lives in its own .svg file in this directory */
5
+ /* so it can be used standalone (e.g. <img src="…">, CSS background, */
6
+ /* design tools, etc.). The strings below mirror those files — keep */
7
+ /* them in sync when editing an icon. */
8
+ /* ================================================================== */
9
+
10
+
11
+ const chevronDownLg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true"><path d="M5 7 L10 12 L15 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" /></svg>`;
12
+
13
+ class YumePanel extends HTMLElement {
14
+ static get observedAttributes() {
15
+ return ["selected", "expanded", "href", "history"];
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this.attachShadow({ mode: "open" });
21
+ this._expanded = false;
22
+ this._checkRouteMatchBound = this.checkRouteMatch.bind(this);
23
+ this.render();
24
+ }
25
+
26
+ connectedCallback() {
27
+ this.addHeaderListeners();
28
+ this.checkForChildren();
29
+ this.updateChildState();
30
+ this.updateSelectedState();
31
+ this.updateExpandedState();
32
+
33
+ if (this.hasAttribute("href")) {
34
+ this.checkRouteMatch();
35
+ window.addEventListener("popstate", this._checkRouteMatchBound);
36
+ }
37
+ }
38
+
39
+ disconnectedCallback() {
40
+ if (this.hasAttribute("href")) {
41
+ window.removeEventListener("popstate", this._checkRouteMatchBound);
42
+ }
43
+ }
44
+
45
+ attributeChangedCallback(name, oldValue, newValue) {
46
+ if (oldValue === newValue) return;
47
+
48
+ if (name === "selected") {
49
+ this.updateSelectedState();
50
+ }
51
+
52
+ if (name === "expanded") {
53
+ this.updateExpandedState();
54
+ }
55
+
56
+ if (name === "href") {
57
+ this.checkRouteMatch();
58
+ }
59
+ }
60
+
61
+ get selected() {
62
+ return this.hasAttribute("selected");
63
+ }
64
+
65
+ set selected(val) {
66
+ if (val) this.setAttribute("selected", "");
67
+ else this.removeAttribute("selected");
68
+ }
69
+
70
+ get expanded() {
71
+ return this.hasAttribute("expanded");
72
+ }
73
+
74
+ set expanded(val) {
75
+ if (val) this.setAttribute("expanded", "");
76
+ else this.removeAttribute("expanded");
77
+ }
78
+
79
+ toggle() {
80
+ if (!this.hasChildren()) return;
81
+ if (!this._expanded) {
82
+ const parentBar = this.closest("y-panelbar");
83
+ if (parentBar && parentBar.hasAttribute("exclusive")) {
84
+ const siblingPanels = parentBar.querySelectorAll("y-panel");
85
+ siblingPanels.forEach((panel) => {
86
+ if (panel !== this && panel.expanded) {
87
+ panel.collapse();
88
+ }
89
+ });
90
+ }
91
+ this.expand();
92
+ } else {
93
+ this.collapse();
94
+ }
95
+ this.dispatchEvent(
96
+ new CustomEvent("toggle", {
97
+ detail: { expanded: this._expanded },
98
+ bubbles: true,
99
+ composed: true,
100
+ }),
101
+ );
102
+ }
103
+
104
+ expand() {
105
+ if (!this.hasChildren()) return;
106
+ this.expanded = true;
107
+ this._expanded = true;
108
+ this.updateExpandedState();
109
+ this.dispatchEvent(
110
+ new CustomEvent("expand", {
111
+ detail: { expanded: true },
112
+ bubbles: true,
113
+ composed: true,
114
+ }),
115
+ );
116
+ }
117
+
118
+ collapse() {
119
+ this.expanded = false;
120
+ this._expanded = false;
121
+ this.updateExpandedState();
122
+ this.dispatchEvent(
123
+ new CustomEvent("collapse", {
124
+ detail: { expanded: false },
125
+ bubbles: true,
126
+ composed: true,
127
+ }),
128
+ );
129
+ }
130
+
131
+ updateSelectedState() {
132
+ this.classList.toggle("selected", this.selected);
133
+ }
134
+
135
+ 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");
139
+ }
140
+
141
+ checkRouteMatch() {
142
+ const href = this.getAttribute("href");
143
+ if (href && window.location.pathname === href) {
144
+ this.selected = true;
145
+ } else {
146
+ this.selected = false;
147
+ }
148
+ }
149
+
150
+ addHeaderListeners() {
151
+ const header = this.shadowRoot.querySelector(".header");
152
+ if (!header) return;
153
+
154
+ header.addEventListener("click", () => {
155
+ if (this.hasAttribute("href")) {
156
+ const href = this.getAttribute("href");
157
+ if (this.getAttribute("history") !== "false") {
158
+ history.pushState({}, "", href);
159
+ window.dispatchEvent(
160
+ new PopStateEvent("popstate", { state: {} }),
161
+ );
162
+ } else {
163
+ window.location.href = href;
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (this.hasChildren()) {
169
+ this.toggle();
170
+ } else {
171
+ this.dispatchEvent(
172
+ new CustomEvent("select", {
173
+ detail: { selected: true },
174
+ bubbles: true,
175
+ composed: true,
176
+ }),
177
+ );
178
+ }
179
+ });
180
+
181
+ header.addEventListener("keydown", (e) => {
182
+ if (e.key === " " || e.key === "Enter") {
183
+ e.preventDefault();
184
+ header.click();
185
+ }
186
+ });
187
+
188
+ const childrenSlot = this.shadowRoot.querySelector(
189
+ 'slot[name="children"]',
190
+ );
191
+ if (childrenSlot) {
192
+ childrenSlot.addEventListener("slotchange", () =>
193
+ this.checkForChildren(),
194
+ );
195
+ }
196
+ }
197
+
198
+ hasChildren() {
199
+ const childrenSlot = this.shadowRoot.querySelector(
200
+ 'slot[name="children"]',
201
+ );
202
+ if (!childrenSlot) return false;
203
+ const nodes = childrenSlot.assignedNodes({ flatten: true });
204
+ return nodes.some((n) => {
205
+ if (n.nodeType === Node.TEXT_NODE) {
206
+ return n.textContent.trim() !== "";
207
+ }
208
+ return true;
209
+ });
210
+ }
211
+
212
+ checkForChildren() {
213
+ const hasChildren = this.hasChildren();
214
+ this.setAttribute("data-has-children", hasChildren ? "true" : "false");
215
+ if (!hasChildren && this.expanded) {
216
+ this.expanded = false;
217
+ }
218
+ }
219
+
220
+ updateExpandedState() {
221
+ const hasChildren = this.hasChildren();
222
+ const header = this.shadowRoot.querySelector(".header");
223
+ const isExpanded = this.expanded && hasChildren;
224
+ this._expanded = isExpanded;
225
+
226
+ if (header) {
227
+ header.setAttribute("aria-expanded", String(isExpanded));
228
+ }
229
+ }
230
+
231
+ render() {
232
+ const sheet = new CSSStyleSheet();
233
+ sheet.replaceSync(`
234
+ :host {
235
+ display: block;
236
+ box-sizing: border-box;
237
+ background: var(--component-panel-background);
238
+ color: var(--component-panel-color);
239
+ font-family: var(--font-family-body);
240
+ overflow: hidden;
241
+ }
242
+
243
+ :host([expanded]) {
244
+ background: var(--component-panel-expanded-background);
245
+ }
246
+
247
+ :host([selected]) {
248
+ color: var(--component-panel-accent);
249
+ }
250
+
251
+ :host([data-is-child="true"]) {
252
+ box-shadow: inset var(--component-panelbar-border-width, 2px) 0 0 0 var(--component-panel-active-border);
253
+ }
254
+
255
+ :host([data-is-child="true"][selected]) {
256
+ box-shadow: inset var(--component-panelbar-border-width, 2px) 0 0 0 var(--component-panel-accent);
257
+ }
258
+
259
+ :host([selected]) .header:hover {
260
+ background: var(--component-panel-accent-hover-background);
261
+ }
262
+
263
+ :host([data-is-child="true"]) .header {
264
+ padding-left: calc(var(--component-panelbar-padding, 4px) * 2);
265
+ }
266
+
267
+ .header {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: var(--spacing-medium, 8px);
271
+ padding: var(--component-panelbar-padding, 4px);
272
+ cursor: pointer;
273
+ transition: background 0.2s ease;
274
+ user-select: none;
275
+ }
276
+
277
+ .header:hover {
278
+ background: var(--component-panel-hover-background);
279
+ }
280
+
281
+ :host([data-has-children="false"]) .header {
282
+ cursor: default;
283
+ }
284
+
285
+ .header ::slotted([slot="icon"]) {
286
+ margin-right: 6px;
287
+ }
288
+
289
+ .header ::slotted([slot="label"]) {
290
+ flex-grow: 1;
291
+ cursor: inherit;
292
+ font-size: 1rem;
293
+ line-height: 1.2;
294
+ }
295
+
296
+ .arrow {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ width: 20px;
301
+ height: 20px;
302
+ transition: transform 0.2s ease;
303
+ }
304
+
305
+ :host([expanded]) .arrow {
306
+ transform: rotate(180deg);
307
+ }
308
+
309
+ .children {
310
+ display: none;
311
+ padding: 0;
312
+ width: 100%;
313
+ box-sizing: border-box;
314
+ }
315
+
316
+ .children ::slotted(y-panel) {
317
+ width: 100%;
318
+ box-sizing: border-box;
319
+ }
320
+
321
+ :host([expanded]) .children {
322
+ display: block;
323
+ }
324
+
325
+ :host([data-has-children="false"]) .arrow {
326
+ visibility: hidden;
327
+ }
328
+ `);
329
+
330
+ this.shadowRoot.adoptedStyleSheets = [sheet];
331
+ this.shadowRoot.innerHTML = `
332
+ <div class="header" part="header" role="button" tabindex="0" aria-expanded="false">
333
+ <slot name="icon"></slot>
334
+ <slot name="label"><slot></slot></slot>
335
+ <span class="arrow" id="arrow" part="arrow">
336
+ ${chevronDownLg}
337
+ </span>
338
+ </div>
339
+ <div class="children" id="childrenContainer" part="children">
340
+ <slot name="children"></slot>
341
+ </div>
342
+ `;
343
+ }
344
+ }
345
+
346
+ if (!customElements.get("y-panel")) {
347
+ customElements.define("y-panel", YumePanel);
348
+ }
349
+
350
+ export { YumePanel };
@@ -0,0 +1,27 @@
1
+ class YumePanelBar extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: "open" });
5
+ this.render();
6
+ }
7
+
8
+ render() {
9
+ const sheet = new CSSStyleSheet();
10
+ sheet.replaceSync(`
11
+ :host {
12
+ display: block;
13
+ }
14
+ `);
15
+
16
+ this.shadowRoot.adoptedStyleSheets = [sheet];
17
+ this.shadowRoot.innerHTML = `
18
+ <slot></slot>
19
+ `;
20
+ }
21
+ }
22
+
23
+ if (!customElements.get("y-panelbar")) {
24
+ customElements.define("y-panelbar", YumePanelBar);
25
+ }
26
+
27
+ export { YumePanelBar };