@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,283 @@
1
+ class YumeTabs extends HTMLElement {
2
+ static get observedAttributes() {
3
+ return ["options", "size", "position"];
4
+ }
5
+
6
+ constructor() {
7
+ super();
8
+ this.attachShadow({ mode: "open" });
9
+ this._activeTab = "";
10
+ }
11
+
12
+ connectedCallback() {
13
+ if (!this.hasAttribute("size")) this.setAttribute("size", "medium");
14
+ if (!this.hasAttribute("position"))
15
+ this.setAttribute("position", "top");
16
+ this.render();
17
+ }
18
+
19
+ attributeChangedCallback(name, oldVal, newVal) {
20
+ if (
21
+ (name === "options" || name === "size" || name === "position") &&
22
+ oldVal !== newVal
23
+ ) {
24
+ this.render();
25
+ }
26
+ }
27
+
28
+ get options() {
29
+ try {
30
+ return JSON.parse(this.getAttribute("options") || "[]");
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ set options(val) {
37
+ this.setAttribute("options", JSON.stringify(val));
38
+ this.render();
39
+ }
40
+
41
+ get size() {
42
+ const sz = this.getAttribute("size");
43
+ return ["small", "medium", "large"].includes(sz) ? sz : "medium";
44
+ }
45
+
46
+ set size(val) {
47
+ if (["small", "medium", "large"].includes(val))
48
+ this.setAttribute("size", val);
49
+ else this.setAttribute("size", "medium");
50
+ }
51
+
52
+ get position() {
53
+ const pos = this.getAttribute("position");
54
+ return ["top", "bottom", "left", "right"].includes(pos) ? pos : "top";
55
+ }
56
+
57
+ set position(val) {
58
+ if (["top", "bottom", "left", "right"].includes(val))
59
+ this.setAttribute("position", val);
60
+ else this.setAttribute("position", "top");
61
+ }
62
+
63
+ activateTab(id) {
64
+ const tab = this.options.find((t) => t.id === id);
65
+ if (!tab || tab.disabled) return;
66
+ if (this._activeTab === id) return;
67
+ this._activeTab = id;
68
+ this.render();
69
+ }
70
+
71
+ _setupEvents() {
72
+ const buttons = Array.from(this.shadowRoot.querySelectorAll("button"));
73
+ buttons.forEach((button) => {
74
+ if (button.disabled) return;
75
+ button.addEventListener("click", () =>
76
+ this.activateTab(button.dataset.id),
77
+ );
78
+ button.addEventListener("keydown", (e) => {
79
+ this._handleTabKeydown(e, buttons);
80
+ });
81
+ });
82
+ }
83
+
84
+ _handleTabKeydown(e, buttons) {
85
+ const idx = buttons.indexOf(e.currentTarget);
86
+ if (e.key === "ArrowRight") {
87
+ e.preventDefault();
88
+ this._findSiblingButton(buttons, idx, 1)?.focus();
89
+ } else if (e.key === "ArrowLeft") {
90
+ e.preventDefault();
91
+ this._findSiblingButton(buttons, idx, -1)?.focus();
92
+ } else if (e.key === "Enter" || e.key === " ") {
93
+ e.preventDefault();
94
+ this.activateTab(e.currentTarget.dataset.id);
95
+ }
96
+ }
97
+
98
+ _findSiblingButton(buttons, fromIndex, direction) {
99
+ for (let i = 1; i <= buttons.length; i++) {
100
+ const b =
101
+ buttons[
102
+ (fromIndex + i * direction + buttons.length) %
103
+ buttons.length
104
+ ];
105
+ if (!b.disabled) return b;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ _resolveActiveTab(tabs) {
111
+ const currentInvalid =
112
+ !this._activeTab ||
113
+ tabs.find((t) => t.id === this._activeTab)?.disabled;
114
+ if (tabs.length && currentInvalid) {
115
+ this._activeTab = tabs.find((t) => !t.disabled)?.id || "";
116
+ }
117
+ }
118
+
119
+ _getStyles() {
120
+ const paddingVar = `var(--component-tab-padding-${this.size})`;
121
+ const gapVar = `var(--component-tab-gap-${this.size})`;
122
+ return `
123
+ :host {
124
+ display: flex;
125
+ font-family: var(--component-tabs-font-family, var(--font-family-body));
126
+ }
127
+ :host([position="top"]) { flex-direction: column; }
128
+ :host([position="bottom"]) { flex-direction: column-reverse; }
129
+ :host([position="left"]) { flex-direction: row; }
130
+ :host([position="right"]) { flex-direction: row-reverse; }
131
+
132
+ .tablist {
133
+ display: flex;
134
+ gap: 0;
135
+ position: relative;
136
+ z-index: 1;
137
+ }
138
+ :host([position="top"]) .tablist { margin-bottom: -1px; margin-top: 0; }
139
+ :host([position="bottom"]) .tablist { margin-top: -1px; margin-bottom: 0; }
140
+ :host([position="left"]) .tablist { flex-direction: column; margin-right: -1px; margin-left: 0; }
141
+ :host([position="right"]) .tablist { flex-direction: column; margin-left: -1px; margin-right: 0; }
142
+
143
+ :host([position="top"]) .tablist button { border-bottom: none; }
144
+ :host([position="bottom"]) .tablist button { border-top: none; }
145
+ :host([position="left"]) .tablist button { border-right: none; }
146
+ :host([position="right"]) .tablist button { border-left: none; }
147
+
148
+ button {
149
+ background: var(--component-tabs-border-color);
150
+ color: var(--component-tabs-color);
151
+ border: var(--component-tab-border-width) solid var(--component-tabs-border-color);
152
+ margin: 0;
153
+ padding: ${paddingVar};
154
+ cursor: pointer;
155
+ font-size: var(--font-size-label);
156
+ display: inline-flex;
157
+ align-items: center;
158
+ gap: ${gapVar};
159
+ transition: background 0.2s ease;
160
+ outline: none;
161
+ font-family: inherit;
162
+ }
163
+ :host([position="top"]) .tablist button:first-child { border-top-left-radius: var(--component-tab-border-radius-outer); }
164
+ :host([position="top"]) .tablist button:last-child { border-top-right-radius: var(--component-tab-border-radius-outer); }
165
+ :host([position="bottom"]) .tablist button:first-child { border-bottom-left-radius: var(--component-tab-border-radius-outer); }
166
+ :host([position="bottom"]) .tablist button:last-child { border-bottom-right-radius: var(--component-tab-border-radius-outer); }
167
+ :host([position="left"]) .tablist button:first-child { border-top-left-radius: var(--component-tab-border-radius-outer); }
168
+ :host([position="left"]) .tablist button:last-child { border-bottom-left-radius: var(--component-tab-border-radius-outer); }
169
+ :host([position="right"]) .tablist button:first-child { border-top-right-radius: var(--component-tab-border-radius-outer); }
170
+ :host([position="right"]) .tablist button:last-child { border-bottom-right-radius: var(--component-tab-border-radius-outer); }
171
+
172
+ button[aria-selected="true"] {
173
+ background: var(--component-tabs-background);
174
+ }
175
+ button:focus-visible {
176
+ outline: 2px solid var(--component-tabs-accent);
177
+ outline-offset: -1px;
178
+ }
179
+ button[disabled] {
180
+ opacity: 0.5;
181
+ cursor: not-allowed;
182
+ }
183
+ .icon-slot {
184
+ display: inline-flex;
185
+ align-items: center;
186
+ margin: 0 4px;
187
+ }
188
+ .tabpanel {
189
+ position: relative;
190
+ z-index: 0;
191
+ border: var(--component-tab-border-width) solid var(--component-tabs-border-color);
192
+ border-radius: var(--component-tab-border-radius-outer);
193
+ padding: var(--spacing-large);
194
+ background: var(--component-tabs-background);
195
+ }
196
+ :host([position="top"]) .tabpanel { border-top-left-radius: 0; }
197
+ :host([position="bottom"]) .tabpanel { border-bottom-left-radius: 0; }
198
+ :host([position="left"]) .tabpanel { border-top-left-radius: 0; }
199
+ :host([position="right"]) .tabpanel { border-top-right-radius: 0; }
200
+ :host([position="top"]) .tabpanel { margin-top: -1px; }
201
+ :host([position="bottom"]) .tabpanel { margin-bottom: -1px; }
202
+ :host([position="left"]) .tabpanel { margin-left: -1px; }
203
+ :host([position="right"]) .tabpanel { margin-right: -1px; }
204
+ `;
205
+ }
206
+
207
+ _createTabButton(tab) {
208
+ const isActive = tab.id === this._activeTab;
209
+ const isDisabled = !!tab.disabled;
210
+ const btn = document.createElement("button");
211
+ btn.id = `tab-${tab.id}`;
212
+ btn.setAttribute("role", "tab");
213
+ btn.setAttribute("aria-selected", isActive);
214
+ btn.setAttribute("aria-controls", `panel-${tab.id}`);
215
+ btn.setAttribute("aria-disabled", isDisabled);
216
+
217
+ if (isDisabled) btn.disabled = true;
218
+ btn.tabIndex = isActive && !isDisabled ? 0 : -1;
219
+ btn.dataset.id = tab.id;
220
+
221
+ if (this.querySelector(`[slot="left-icon-${tab.id}"]`)) {
222
+ const leftSlot = document.createElement("slot");
223
+ leftSlot.name = `left-icon-${tab.id}`;
224
+ leftSlot.className = "icon-slot";
225
+ btn.appendChild(leftSlot);
226
+ }
227
+
228
+ const labelSpan = document.createElement("span");
229
+ labelSpan.textContent = tab.label;
230
+ btn.appendChild(labelSpan);
231
+
232
+ if (this.querySelector(`[slot="right-icon-${tab.id}"]`)) {
233
+ const rightSlot = document.createElement("slot");
234
+ rightSlot.name = `right-icon-${tab.id}`;
235
+ rightSlot.className = "icon-slot";
236
+ btn.appendChild(rightSlot);
237
+ }
238
+
239
+ return btn;
240
+ }
241
+
242
+ _createPanel(slotName) {
243
+ const panel = document.createElement("div");
244
+ panel.className = "tabpanel";
245
+ panel.id = `panel-${this._activeTab}`;
246
+ panel.setAttribute("role", "tabpanel");
247
+ panel.setAttribute("aria-labelledby", `tab-${this._activeTab}`);
248
+
249
+ const contentSlot = document.createElement("slot");
250
+ contentSlot.name = slotName;
251
+ panel.appendChild(contentSlot);
252
+ return panel;
253
+ }
254
+
255
+ render() {
256
+ const tabs = this.options;
257
+ this._resolveActiveTab(tabs);
258
+
259
+ const activeDef = tabs.find((t) => t.id === this._activeTab);
260
+
261
+ this.shadowRoot.innerHTML = "";
262
+
263
+ const style = document.createElement("style");
264
+ style.textContent = this._getStyles();
265
+ this.shadowRoot.appendChild(style);
266
+
267
+ const tablist = document.createElement("div");
268
+ tablist.className = "tablist";
269
+ tablist.setAttribute("role", "tablist");
270
+ tabs.forEach((tab) => tablist.appendChild(this._createTabButton(tab)));
271
+ this.shadowRoot.appendChild(tablist);
272
+
273
+ this.shadowRoot.appendChild(this._createPanel(activeDef?.slot || ""));
274
+
275
+ this._setupEvents();
276
+ }
277
+ }
278
+
279
+ if (!customElements.get("y-tabs")) {
280
+ customElements.define("y-tabs", YumeTabs);
281
+ }
282
+
283
+ export { YumeTabs };
@@ -0,0 +1,186 @@
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
+ /* ── Close / remove ───────────────────────────────────────────────── */
12
+
13
+ const close = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"><line x1="6" y1="6" x2="14" y2="14" /><line x1="14" y1="6" x2="6" y2="14" /></svg>`;
14
+
15
+ class YumeTag extends HTMLElement {
16
+ static get observedAttributes() {
17
+ return ["removable", "color", "style-type", "shape"];
18
+ }
19
+
20
+ constructor() {
21
+ super();
22
+ this.attachShadow({ mode: "open" });
23
+ this.render();
24
+ }
25
+
26
+ connectedCallback() {
27
+ this.render();
28
+ }
29
+
30
+ attributeChangedCallback(name, oldValue, newValue) {
31
+ if (oldValue !== newValue) this.render();
32
+ }
33
+
34
+ render() {
35
+ const removable = this.hasAttribute("removable");
36
+ const color = this.getAttribute("color") || "base";
37
+ const styleType = this.getAttribute("style-type") || "filled";
38
+ const shape = this.getAttribute("shape") || "square";
39
+
40
+ const style = document.createElement("style");
41
+ style.textContent = this.getStyle(color, styleType, shape);
42
+
43
+ this.shadowRoot.innerHTML = "";
44
+ this.shadowRoot.appendChild(style);
45
+ this.shadowRoot.innerHTML += `
46
+ <span class="tag">
47
+ <slot></slot>
48
+ ${
49
+ removable
50
+ ? `
51
+ <button class="remove" aria-label="Remove tag">
52
+ ${close}
53
+ </button>
54
+ `
55
+ : ""
56
+ }
57
+ </span>
58
+ `;
59
+
60
+ if (removable) {
61
+ this.shadowRoot
62
+ .querySelector(".remove")
63
+ .addEventListener("click", (e) => {
64
+ e.stopPropagation();
65
+ this.dispatchEvent(
66
+ new CustomEvent("remove", {
67
+ bubbles: true,
68
+ composed: true,
69
+ }),
70
+ );
71
+ });
72
+ }
73
+ }
74
+
75
+ getStyle(color, styleType, shape) {
76
+ const vars = {
77
+ primary: [
78
+ "--primary-content--",
79
+ "--primary-content-hover",
80
+ "--primary-background-component",
81
+ ],
82
+ secondary: [
83
+ "--secondary-content--",
84
+ "--secondary-content-hover",
85
+ "--secondary-background-component",
86
+ ],
87
+ base: [
88
+ "--base-content--",
89
+ "--base-content-lighter",
90
+ "--base-background-component",
91
+ ],
92
+ success: [
93
+ "--success-content--",
94
+ "--success-content-hover",
95
+ "--success-background-component",
96
+ ],
97
+ error: [
98
+ "--error-content--",
99
+ "--error-content-hover",
100
+ "--error-background-component",
101
+ ],
102
+ warning: [
103
+ "--warning-content--",
104
+ "--warning-content-hover",
105
+ "--warning-background-component",
106
+ ],
107
+ help: [
108
+ "--help-content--",
109
+ "--help-content-hover",
110
+ "--help-background-component",
111
+ ],
112
+ };
113
+
114
+ const [content, hover, background] = vars[color] || vars.base;
115
+
116
+ const borderRadius =
117
+ shape === "round"
118
+ ? "var(--component-tag-border-radius-circle)"
119
+ : "var(--component-tag-border-radius-square)";
120
+
121
+ const baseStyle = `
122
+ :host {
123
+ display: inline-block;
124
+ font-family: var(--font-family-body, sans-serif);
125
+ font-size: var(--font-size-label, 0.83em);
126
+ }
127
+ .tag {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ gap: var(--spacing-2x-small);
131
+ padding: 0 var(--component-tag-padding-medium, var(--spacing-x-small));
132
+ border: 1px solid transparent;
133
+ transition: background-color 0.2s, color 0.2s;
134
+ border-radius: ${borderRadius};
135
+ }
136
+ .remove {
137
+ all: unset;
138
+ cursor: pointer;
139
+ display: flex;
140
+ align-items: center;
141
+ }
142
+ .remove svg {
143
+ pointer-events: none;
144
+ }
145
+ `;
146
+
147
+ const styleVariants = {
148
+ filled: `
149
+ .tag {
150
+ background: var(${content});
151
+ color: var(${background});
152
+ }
153
+ .remove {
154
+ color: var(${background});
155
+ }
156
+ `,
157
+ outlined: `
158
+ .tag {
159
+ border: 1px solid var(${content});
160
+ background: transparent;
161
+ color: var(${content});
162
+ }
163
+ .remove {
164
+ color: var(${content});
165
+ }
166
+ `,
167
+ flat: `
168
+ .tag {
169
+ background: transparent;
170
+ color: var(${content});
171
+ }
172
+ .remove {
173
+ color: var(${content});
174
+ }
175
+ `,
176
+ };
177
+
178
+ return baseStyle + (styleVariants[styleType] || styleVariants.filled);
179
+ }
180
+ }
181
+
182
+ if (!customElements.get("y-tag")) {
183
+ customElements.define("y-tag", YumeTag);
184
+ }
185
+
186
+ export { YumeTag };
@@ -0,0 +1,84 @@
1
+ class YumeTheme extends HTMLElement {
2
+ static defaultVariablesLoaded = false;
3
+ static defaultVariablesCSS = "";
4
+
5
+ static get observedAttributes() {
6
+ return ["theme-path"];
7
+ }
8
+
9
+ constructor() {
10
+ super();
11
+ this.attachShadow({ mode: "open" });
12
+ }
13
+
14
+ connectedCallback() {
15
+ this.loadDefaultVariables().then(() => {
16
+ const themePath = this.getAttribute("theme-path");
17
+ this.loadTheme(themePath);
18
+ });
19
+ }
20
+
21
+ attributeChangedCallback(name, oldValue, newValue) {
22
+ if (name === "theme-path" && oldValue !== newValue) {
23
+ this.loadTheme(newValue);
24
+ }
25
+ }
26
+
27
+ async loadDefaultVariables() {
28
+ if (!YumeTheme.defaultVariablesLoaded) {
29
+ try {
30
+ const variablesUrl = new URL(
31
+ "styles/variables.css",
32
+ document.baseURI,
33
+ );
34
+ const response = await fetch(variablesUrl.href);
35
+ YumeTheme.defaultVariablesCSS = await response.text();
36
+ YumeTheme.defaultVariablesLoaded = true;
37
+ } catch (e) {
38
+ console.error(
39
+ "Failed to load default variables from styles/variables.css:",
40
+ e,
41
+ );
42
+ }
43
+ }
44
+ return Promise.resolve();
45
+ }
46
+
47
+ async loadTheme(themePath) {
48
+ let themeCSS = "";
49
+ if (themePath) {
50
+ try {
51
+ const themeUrl = new URL(themePath, document.baseURI);
52
+ const response = await fetch(themeUrl.href);
53
+ themeCSS = await response.text();
54
+ } catch (e) {
55
+ console.error(`Failed to load theme from ${themePath}:`, e);
56
+ }
57
+ }
58
+
59
+ const combinedCSS = `
60
+ <style>
61
+ ${YumeTheme.defaultVariablesCSS}
62
+ </style>
63
+ ${themeCSS ? `<style>${themeCSS}</style>` : ""}
64
+ `;
65
+
66
+ this.shadowRoot.innerHTML = `${combinedCSS}<slot></slot>`;
67
+ this.applyVariablesToHost(YumeTheme.defaultVariablesCSS + themeCSS);
68
+ }
69
+
70
+ applyVariablesToHost(cssText) {
71
+ const regex = /--([\w-]+):\s*([^;]+);/g;
72
+ let match;
73
+
74
+ while ((match = regex.exec(cssText)) !== null) {
75
+ this.style.setProperty(`--${match[1]}`, match[2].trim());
76
+ }
77
+ }
78
+ }
79
+
80
+ if (!customElements.get("y-theme")) {
81
+ customElements.define("y-theme", YumeTheme);
82
+ }
83
+
84
+ export { YumeTheme };