@waggylabs/yumekit 0.4.3-beta.40 → 0.4.3-beta.41

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.
package/CHANGELOG.md CHANGED
@@ -42,6 +42,12 @@ Delete any empty sections before publishing.
42
42
  - New `y-color` component — a form-associated color input with a trigger/popup pattern (like `<y-date>`). Renders a color swatch preview and value string in the trigger; opens a `<y-colorpicker>` popup on click. Supports `value`, `format`, `formats`, `show-alpha`, `placeholder`, `name`, `disabled`, `invalid`, `clearable`, `size`, and `label-position` attributes. Uses `ElementInternals` for `FormData` participation.
43
43
  - New `y-banner` component — a full-width informational banner that renders in a semantic color with matching text. Supports `color` (`"base"` | `"primary"` | `"secondary"` | `"success"` | `"error"` | `"warning"` | `"help"`), `icon` attribute (or `icon` slot), `position` (`"push"` | `"overlap"`), `sticky` (fixed to viewport when overlapping), `dismissable` close button, `size` (`"small"` | `"medium"` | `"large"`), and an `action` slot for CTA elements.
44
44
  - New `y-stack` component — a layout container for arranging child elements in rows, columns, grids, or masonry patterns. Supports `mode` (`"flex"` | `"grid"` | `"masonry"`), `direction`, `columns`, `gap` (maps to `--spacing-*` tokens), `wrap`, `align`, `justify`, and `responsive` attributes. Masonry mode uses JS absolute positioning with `ResizeObserver`. Responsive mode auto-collapses columns at configurable breakpoints.
45
+ - `y-tabs`: `leftIcon` and `rightIcon` properties on option objects — set a `<y-icon>` name directly in the options JSON to render icons without requiring extra child elements or named slots.
46
+ - `y-tabs`: `tab-content-{id}` slot — place any content (icons, badges, custom markup) inside the tab button itself by targeting this slot. Takes full precedence over `leftIcon`/`rightIcon` and the default label rendering.
47
+
48
+ ### Deprecated
49
+
50
+ - `y-tabs`: `left-icon-{id}` and `right-icon-{id}` slots — these slots still function but emit a `console.warn` directing users to the `leftIcon`/`rightIcon` option properties or the `tab-content-{id}` slot. They will be removed before the release of version 1.0.
45
51
 
46
52
  ## [0.4.2] - 2026-04-07
47
53
 
package/CONTRIBUTING.md CHANGED
@@ -36,6 +36,62 @@ Yumekit is authored in plain JavaScript. Please follow the conventions already p
36
36
  - Keep components self-contained: styles live in the Shadow DOM, logic in the class, no shared global state.
37
37
  - Run the linter before submitting: `npm run lint`.
38
38
 
39
+ ## Component Authoring Guidelines
40
+
41
+ ### Class structure
42
+
43
+ Every component class must have exactly four comment-delimited sections in this order — no more, no subdivisions:
44
+
45
+ ```
46
+ // Lifecycle
47
+ // Getters / Setters
48
+ // Public
49
+ // Private
50
+ ```
51
+
52
+ Methods within their subdivision must be listed alphabetically.
53
+
54
+ ### Method style
55
+
56
+ Follow a **define → compute → return/apply** flow within each method where possible: gather inputs and state at the top, do the work in the middle, produce output at the end. Keep methods small and focused — if a method grows long or does multiple distinct things, extract a named helper. Minimize nesting by preferring early returns over deep `if/else` trees. Separate distinct logical units of work with blank lines. This ruleset allows humans and AI to have a predictable and human-readable code structure upon which to base future updates.
57
+
58
+ ### DOM element creation
59
+
60
+ Use the `createElement` helper (imported as `_el`) from `src/modules/helpers.js` for all element creation inside components. Do not use manual `document.createElement` + `setAttribute` chains.
61
+
62
+ ```js
63
+ import { createElement as _el } from "../../modules/helpers.js";
64
+
65
+ const btn = _el("button", { role: "tab", "aria-label": label }, [labelText]);
66
+ ```
67
+
68
+ ### Icons
69
+
70
+ Use `<y-icon name="...">` for all icons. Never inline SVG strings or constants. Import `y-icon.js` at the top of any component that renders icons.
71
+
72
+ ### Slot patterns
73
+
74
+ Always render named slots unconditionally and place default/fallback content as **children of the slot element**. Never use `querySelector` to decide whether to create a slot. This breaks framework rendering (React, Vue, etc.) where children may arrive after the element upgrades.
75
+
76
+ ```js
77
+ // Correct
78
+ const slot = _el("slot", { name: "icon" });
79
+ slot.appendChild(defaultIcon);
80
+ parent.appendChild(slot);
81
+
82
+ // Wrong — slot is conditional on a render-time DOM query
83
+ if (this.querySelector('[slot="icon"]')) { ... }
84
+ ```
85
+
86
+ ### New component checklist
87
+
88
+ Every new component requireschanges to the following: `README.md`, `CHANGELOG.md`, `reference.md`, `SKILL.md`, `react.d.ts`, `variables.css`, `.figma/variables.json`, entry in `llm.txt`, and a `y-*.stories.js` stories file.
89
+
90
+ ### Testing
91
+
92
+ - Tests co-locate with the component source file.
93
+ - Use `sinon.createSandbox()` at the `describe` level with `afterEach(() => sandbox.restore())`.
94
+
39
95
  ## AI Assistance
40
96
 
41
97
  AI tools can be helpful for brainstorming and prototyping, but they are not a substitute for human judgment and expertise.
@@ -395,25 +395,9 @@ class YumeBreadcrumbs extends HTMLElement {
395
395
  part: parts.join(" "),
396
396
  });
397
397
 
398
- const iconSlotName = `${index}-icon`;
399
- const hasIconSlot = !!this.querySelector(`[slot="${iconSlotName}"]`);
400
-
401
- if (hasIconSlot) {
402
- li.appendChild(
403
- createElement("slot", { name: iconSlotName, class: "item-icon" }),
404
- );
405
- } else if (item.icon) {
406
- li.appendChild(
407
- createElement("y-icon", {
408
- name: item.icon,
409
- size: this._getIconSize(),
410
- class: "item-icon",
411
- }),
412
- );
413
- }
414
-
398
+ let linkEl;
415
399
  if (isCurrent || !item.href) {
416
- const span = createElement(
400
+ linkEl = createElement(
417
401
  "span",
418
402
  {
419
403
  class: "link current-text",
@@ -422,9 +406,8 @@ class YumeBreadcrumbs extends HTMLElement {
422
406
  },
423
407
  [item.text],
424
408
  );
425
- li.appendChild(span);
426
409
  } else {
427
- const anchor = createElement(
410
+ linkEl = createElement(
428
411
  "a",
429
412
  {
430
413
  class: "link",
@@ -433,14 +416,20 @@ class YumeBreadcrumbs extends HTMLElement {
433
416
  },
434
417
  [item.text],
435
418
  );
436
-
437
- anchor.addEventListener("click", (e) => {
419
+ linkEl.addEventListener("click", (e) => {
438
420
  if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
439
421
  this._onNavigate(e, item.href);
440
422
  });
423
+ }
441
424
 
442
- li.appendChild(anchor);
425
+ const itemSlot = createElement("slot", { name: `${index}-item` });
426
+ if (item.icon) {
427
+ itemSlot.appendChild(
428
+ createElement("y-icon", { name: item.icon, size: this._getIconSize(), class: "item-icon" }),
429
+ );
443
430
  }
431
+ itemSlot.appendChild(linkEl);
432
+ li.appendChild(itemSlot);
444
433
 
445
434
  return li;
446
435
  }
@@ -1,10 +1,11 @@
1
1
  export class YumeTabs extends HTMLElement {
2
2
  static get observedAttributes(): string[];
3
3
  _activeTab: string;
4
+ _warnedSlots: Set<any>;
4
5
  connectedCallback(): void;
5
6
  attributeChangedCallback(name: any, oldVal: any, newVal: any): void;
6
7
  set options(val: Array<any>);
7
- /** @type {Array<Object>} Tab definitions with id, label, slot, and optional disabled flag. */
8
+ /** @type {Array<Object>} Tab definitions. Each object: `{ id, label, slot, disabled?, leftIcon?, rightIcon? }`. `leftIcon`/`rightIcon` are `y-icon` names rendered inside the tab button. Use the `tab-content-{id}` slot to supply fully custom tab button content instead. */
8
9
  get options(): Array<any>;
9
10
  set position(val: "top" | "bottom" | "left" | "right");
10
11
  /** @type {"top"|"bottom"|"left"|"right"} Which edge the tab strip is placed on. */
@@ -18,8 +19,10 @@ export class YumeTabs extends HTMLElement {
18
19
  */
19
20
  activateTab(id: string): void;
20
21
  render(): void;
21
- _createPanel(slotName: any): HTMLDivElement;
22
- _createTabButton(tab: any): HTMLButtonElement;
22
+ _appendDeprecatedIconSlot(parent: any, side: any, tabId: any): void;
23
+ _createIcon(name: any): HTMLElement;
24
+ _createPanel(slotName: any): HTMLElement;
25
+ _createTabButton(tab: any): HTMLElement;
23
26
  _findSiblingButton(buttons: any, fromIndex: any, direction: any): any;
24
27
  _getStyles(): string;
25
28
  _handleTabKeydown(e: any, buttons: any): void;
@@ -1,10 +1,11 @@
1
1
  export class YumeTabs extends HTMLElement {
2
2
  static get observedAttributes(): string[];
3
3
  _activeTab: string;
4
+ _warnedSlots: Set<any>;
4
5
  connectedCallback(): void;
5
6
  attributeChangedCallback(name: any, oldVal: any, newVal: any): void;
6
7
  set options(val: Array<any>);
7
- /** @type {Array<Object>} Tab definitions with id, label, slot, and optional disabled flag. */
8
+ /** @type {Array<Object>} Tab definitions. Each object: `{ id, label, slot, disabled?, leftIcon?, rightIcon? }`. `leftIcon`/`rightIcon` are `y-icon` names rendered inside the tab button. Use the `tab-content-{id}` slot to supply fully custom tab button content instead. */
8
9
  get options(): Array<any>;
9
10
  set position(val: "top" | "bottom" | "left" | "right");
10
11
  /** @type {"top"|"bottom"|"left"|"right"} Which edge the tab strip is placed on. */
@@ -18,8 +19,10 @@ export class YumeTabs extends HTMLElement {
18
19
  */
19
20
  activateTab(id: string): void;
20
21
  render(): void;
21
- _createPanel(slotName: any): HTMLDivElement;
22
- _createTabButton(tab: any): HTMLButtonElement;
22
+ _appendDeprecatedIconSlot(parent: any, side: any, tabId: any): void;
23
+ _createIcon(name: any): HTMLElement;
24
+ _createPanel(slotName: any): HTMLElement;
25
+ _createTabButton(tab: any): HTMLElement;
23
26
  _findSiblingButton(buttons: any, fromIndex: any, direction: any): any;
24
27
  _getStyles(): string;
25
28
  _handleTabKeydown(e: any, buttons: any): void;
@@ -1,3 +1,222 @@
1
+ import { getIcon } from '../../icons/registry.js';
2
+ import { createElement } from '../../modules/helpers.js';
3
+
4
+ // Allowlist-based SVG sanitizer — only known-safe elements and attributes are kept.
5
+ const ALLOWED_ELEMENTS = new Set([
6
+ "svg", "g", "path", "circle", "ellipse", "rect", "line", "polyline",
7
+ "polygon", "text", "tspan", "defs", "clippath", "mask", "lineargradient",
8
+ "radialgradient", "stop", "symbol", "title", "desc", "metadata",
9
+ ]);
10
+
11
+ const ALLOWED_ATTRS = new Set([
12
+ "viewbox", "xmlns", "fill", "stroke", "stroke-width", "stroke-linecap",
13
+ "stroke-linejoin", "stroke-dasharray", "stroke-dashoffset", "stroke-miterlimit",
14
+ "stroke-opacity", "fill-opacity", "fill-rule", "clip-rule", "opacity",
15
+ "d", "cx", "cy", "r", "rx", "ry", "x", "x1", "x2", "y", "y1", "y2",
16
+ "width", "height", "points", "transform", "id", "class", "clip-path", "mask",
17
+ "offset", "stop-color", "stop-opacity", "gradient-units", "gradienttransform",
18
+ "gradientunits", "spreadmethod", "patternunits", "patterntransform",
19
+ "font-size", "font-family", "font-weight", "text-anchor", "dominant-baseline",
20
+ "alignment-baseline", "dx", "dy", "rotate", "textlength", "lengthadjust",
21
+ "display", "visibility", "color", "vector-effect",
22
+ ]);
23
+
24
+ function sanitizeSvg(raw) {
25
+ if (!raw) return "";
26
+ const doc = new DOMParser().parseFromString(raw, "image/svg+xml");
27
+ const svg = doc.querySelector("svg");
28
+ if (!svg) return "";
29
+
30
+ const walk = (el) => {
31
+ for (const child of [...el.children]) {
32
+ if (!ALLOWED_ELEMENTS.has(child.tagName.toLowerCase())) {
33
+ child.remove();
34
+ continue;
35
+ }
36
+ for (const attr of [...child.attributes]) {
37
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
38
+ child.removeAttribute(attr.name);
39
+ }
40
+ }
41
+ walk(child);
42
+ }
43
+ };
44
+
45
+ for (const attr of [...svg.attributes]) {
46
+ if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
47
+ svg.removeAttribute(attr.name);
48
+ }
49
+ }
50
+ walk(svg);
51
+ return svg.outerHTML;
52
+ }
53
+
54
+ // Cache sanitized SVG markup per icon name to avoid repeated DOMParser + DOM-walk
55
+ // on every render. The cache is naturally bounded by the number of registered icons.
56
+ const sanitizedSvgCache = new Map();
57
+
58
+ function getCachedSvg(name) {
59
+ if (sanitizedSvgCache.has(name)) return sanitizedSvgCache.get(name);
60
+ const result = sanitizeSvg(getIcon(name));
61
+ sanitizedSvgCache.set(name, result);
62
+ return result;
63
+ }
64
+
65
+ class YumeIcon extends HTMLElement {
66
+ static get observedAttributes() {
67
+ return ["name", "size", "color", "label", "weight"];
68
+ }
69
+
70
+ // -------------------------------------------------------------------------
71
+ // Lifecycle
72
+ // -------------------------------------------------------------------------
73
+
74
+ constructor() {
75
+ super();
76
+ this.attachShadow({ mode: "open" });
77
+ }
78
+
79
+ connectedCallback() {
80
+ this.render();
81
+ }
82
+
83
+ attributeChangedCallback(name, oldVal, newVal) {
84
+ if (oldVal === newVal) return;
85
+ this.render();
86
+ }
87
+
88
+ // -------------------------------------------------------------------------
89
+ // Getters / Setters
90
+ // -------------------------------------------------------------------------
91
+
92
+ /** Color theme: "base" | "primary" | "secondary" | "success" | "warning" | "error" | "help". */
93
+ get color() { return this.getAttribute("color") || ""; }
94
+ set color(val) {
95
+ if (val) this.setAttribute("color", val);
96
+ else this.removeAttribute("color");
97
+ }
98
+
99
+ /** Accessible label for the icon. When set, the icon gets role="img". */
100
+ get label() { return this.getAttribute("label") || ""; }
101
+ set label(val) {
102
+ if (val) this.setAttribute("label", val);
103
+ else this.removeAttribute("label");
104
+ }
105
+
106
+ /** The registered icon name to display. */
107
+ get name() { return this.getAttribute("name") || ""; }
108
+ set name(val) { this.setAttribute("name", val); }
109
+
110
+ /** Icon size: "x-small" | "small" | "medium" | "large" | "x-large" (default "medium"). */
111
+ get size() { return this.getAttribute("size") || "medium"; }
112
+ set size(val) { this.setAttribute("size", val); }
113
+
114
+ /** Stroke weight: "thin" | "regular" | "thick". */
115
+ get weight() { return this.getAttribute("weight") || "regular"; }
116
+ set weight(val) {
117
+ if (val) this.setAttribute("weight", val);
118
+ else this.removeAttribute("weight");
119
+ }
120
+
121
+ // -------------------------------------------------------------------------
122
+ // Public
123
+ // -------------------------------------------------------------------------
124
+
125
+ render() {
126
+ const svg = getCachedSvg(this.name);
127
+ const sizeVal = this._getSize(this.size);
128
+ const colorVal = this.color ? this._getColor(this.color) : "inherit";
129
+ const weightVal = this._getWeight(this.weight);
130
+
131
+ this._updateAria();
132
+
133
+ this.shadowRoot.innerHTML = `
134
+ <style>
135
+ :host {
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ width: ${sizeVal};
140
+ height: ${sizeVal};
141
+ color: ${colorVal};
142
+ line-height: 0;
143
+ }
144
+ .icon-wrapper svg {
145
+ width: 100%;
146
+ height: 100%;
147
+ }
148
+ ${this._getWeightCSS(weightVal)}
149
+ </style>
150
+ <span class="icon-wrapper" part="icon">${svg}</span>
151
+ `;
152
+ }
153
+
154
+ // -------------------------------------------------------------------------
155
+ // Private
156
+ // -------------------------------------------------------------------------
157
+
158
+ _getColor(color) {
159
+ const map = {
160
+ base: "var(--base-content--, #f7f7fa)",
161
+ primary: "var(--primary-content--, #0576ff)",
162
+ secondary: "var(--secondary-content--, #04b8b8)",
163
+ success: "var(--success-content--, #2dba73)",
164
+ warning: "var(--warning-content--, #d17f04)",
165
+ error: "var(--error-content--, #b80421)",
166
+ help: "var(--help-content--, #5405ff)",
167
+ };
168
+ if (map[color]) return map[color];
169
+ if (color && (color.startsWith("#") || color.startsWith("rgb") || color.startsWith("hsl"))) {
170
+ return color;
171
+ }
172
+ return map.base;
173
+ }
174
+
175
+ _getSize(size) {
176
+ const map = {
177
+ "x-small": "var(--component-icon-size-x-small, 10px)",
178
+ small: "var(--component-icon-size-small, 14px)",
179
+ medium: "var(--component-icon-size-medium, 18px)",
180
+ large: "var(--component-icon-size-large, 22px)",
181
+ "x-large": "var(--component-icon-size-x-large, 28px)",
182
+ };
183
+ return map[size] || map.medium;
184
+ }
185
+
186
+ _getWeight(weight) {
187
+ const map = {
188
+ "x-thin": "1",
189
+ thin: "1.5",
190
+ regular: "2",
191
+ thick: "2.5",
192
+ "x-thick": "3",
193
+ };
194
+ return map[weight] || "";
195
+ }
196
+
197
+ _getWeightCSS(weightVal) {
198
+ if (!weightVal) return "";
199
+ return `.icon-wrapper svg,
200
+ .icon-wrapper svg * { stroke-width: ${weightVal} !important; }`;
201
+ }
202
+
203
+ _updateAria() {
204
+ if (this.label) {
205
+ this.setAttribute("role", "img");
206
+ this.setAttribute("aria-label", this.label);
207
+ this.removeAttribute("aria-hidden");
208
+ } else {
209
+ this.setAttribute("aria-hidden", "true");
210
+ this.removeAttribute("role");
211
+ this.removeAttribute("aria-label");
212
+ }
213
+ }
214
+ }
215
+
216
+ if (!customElements.get("y-icon")) {
217
+ customElements.define("y-icon", YumeIcon);
218
+ }
219
+
1
220
  class YumeTabs extends HTMLElement {
2
221
  static get observedAttributes() {
3
222
  return ["options", "size", "position"];
@@ -11,6 +230,7 @@ class YumeTabs extends HTMLElement {
11
230
  super();
12
231
  this.attachShadow({ mode: "open" });
13
232
  this._activeTab = "";
233
+ this._warnedSlots = new Set();
14
234
  }
15
235
 
16
236
  connectedCallback() {
@@ -20,16 +240,16 @@ class YumeTabs extends HTMLElement {
20
240
  }
21
241
 
22
242
  attributeChangedCallback(name, oldVal, newVal) {
23
- if ((name === "options" || name === "size" || name === "position") && oldVal !== newVal) {
24
- this.render();
25
- }
243
+ if (oldVal === newVal) return;
244
+ if (name === "options") this._warnedSlots.clear();
245
+ this.render();
26
246
  }
27
247
 
28
248
  // -------------------------------------------------------------------------
29
249
  // Getters / Setters
30
250
  // -------------------------------------------------------------------------
31
251
 
32
- /** @type {Array<Object>} Tab definitions with id, label, slot, and optional disabled flag. */
252
+ /** @type {Array<Object>} Tab definitions. Each object: `{ id, label, slot, disabled?, leftIcon?, rightIcon? }`. `leftIcon`/`rightIcon` are `y-icon` names rendered inside the tab button. Use the `tab-content-{id}` slot to supply fully custom tab button content instead. */
33
253
  get options() {
34
254
  try {
35
255
  return JSON.parse(this.getAttribute("options") || "[]");
@@ -88,10 +308,7 @@ class YumeTabs extends HTMLElement {
88
308
  style.textContent = this._getStyles();
89
309
  this.shadowRoot.appendChild(style);
90
310
 
91
- const tablist = document.createElement("div");
92
- tablist.className = "tablist";
93
- tablist.setAttribute("role", "tablist");
94
- tablist.setAttribute("part", "tablist");
311
+ const tablist = createElement("div", { class: "tablist", role: "tablist", part: "tablist" });
95
312
  tabs.forEach((tab) => tablist.appendChild(this._createTabButton(tab)));
96
313
  this.shadowRoot.appendChild(tablist);
97
314
 
@@ -104,17 +321,38 @@ class YumeTabs extends HTMLElement {
104
321
  // Private
105
322
  // -------------------------------------------------------------------------
106
323
 
324
+ _appendDeprecatedIconSlot(parent, side, tabId) {
325
+ const slotName = `${side}-icon-${tabId}`;
326
+
327
+ if (!this.querySelector(`[slot="${slotName}"]`)) return;
328
+
329
+ if (!this._warnedSlots.has(slotName)) {
330
+ this._warnedSlots.add(slotName);
331
+ // eslint-disable-next-line no-console
332
+ console.warn(
333
+ `[y-tabs] The "${slotName}" slot is deprecated. ` +
334
+ `Use the ${side}Icon property on the tab options object instead, ` +
335
+ `or use the "tab-content-${tabId}" slot for custom content.`
336
+ );
337
+ }
338
+
339
+ parent.appendChild(createElement("slot", { name: slotName, class: "icon-slot" }));
340
+ }
341
+
342
+ _createIcon(name) {
343
+ return createElement("y-icon", { name, size: this.size });
344
+ }
345
+
107
346
  _createPanel(slotName) {
108
- const panel = document.createElement("div");
109
- panel.className = "tabpanel";
110
- panel.id = `panel-${this._activeTab}`;
111
- panel.setAttribute("role", "tabpanel");
112
- panel.setAttribute("part", "content");
113
- panel.setAttribute("aria-labelledby", `tab-${this._activeTab}`);
347
+ const panel = createElement("div", {
348
+ class: "tabpanel",
349
+ id: `panel-${this._activeTab}`,
350
+ role: "tabpanel",
351
+ part: "content",
352
+ "aria-labelledby": `tab-${this._activeTab}`,
353
+ });
114
354
 
115
- const contentSlot = document.createElement("slot");
116
- contentSlot.name = slotName;
117
- panel.appendChild(contentSlot);
355
+ panel.appendChild(createElement("slot", { name: slotName }));
118
356
 
119
357
  return panel;
120
358
  }
@@ -122,35 +360,36 @@ class YumeTabs extends HTMLElement {
122
360
  _createTabButton(tab) {
123
361
  const isActive = tab.id === this._activeTab;
124
362
  const isDisabled = !!tab.disabled;
125
- const btn = document.createElement("button");
126
363
 
127
- btn.id = `tab-${tab.id}`;
128
- btn.setAttribute("role", "tab");
129
- btn.setAttribute("part", "tab");
130
- btn.setAttribute("aria-selected", isActive);
131
- btn.setAttribute("aria-controls", `panel-${tab.id}`);
132
- btn.setAttribute("aria-disabled", isDisabled);
364
+ const btn = createElement("button", {
365
+ id: `tab-${tab.id}`,
366
+ role: "tab",
367
+ part: "tab",
368
+ "aria-label": tab.label,
369
+ "aria-selected": String(isActive),
370
+ "aria-controls": `panel-${tab.id}`,
371
+ "aria-disabled": String(isDisabled),
372
+ });
133
373
 
134
374
  if (isDisabled) btn.disabled = true;
135
375
  btn.tabIndex = isActive && !isDisabled ? 0 : -1;
136
376
  btn.dataset.id = tab.id;
137
377
 
138
- if (this.querySelector(`[slot="left-icon-${tab.id}"]`)) {
139
- const leftSlot = document.createElement("slot");
140
- leftSlot.name = `left-icon-${tab.id}`;
141
- leftSlot.className = "icon-slot";
142
- btn.appendChild(leftSlot);
378
+ const contentSlot = createElement("slot", { name: `tab-content-${tab.id}` });
379
+ btn.appendChild(contentSlot);
380
+
381
+ if (tab.leftIcon) {
382
+ contentSlot.appendChild(this._createIcon(tab.leftIcon));
383
+ } else {
384
+ this._appendDeprecatedIconSlot(contentSlot, "left", tab.id);
143
385
  }
144
386
 
145
- const labelSpan = document.createElement("span");
146
- labelSpan.textContent = tab.label;
147
- btn.appendChild(labelSpan);
387
+ contentSlot.appendChild(createElement("span", {}, [tab.label]));
148
388
 
149
- if (this.querySelector(`[slot="right-icon-${tab.id}"]`)) {
150
- const rightSlot = document.createElement("slot");
151
- rightSlot.name = `right-icon-${tab.id}`;
152
- rightSlot.className = "icon-slot";
153
- btn.appendChild(rightSlot);
389
+ if (tab.rightIcon) {
390
+ contentSlot.appendChild(this._createIcon(tab.rightIcon));
391
+ } else {
392
+ this._appendDeprecatedIconSlot(contentSlot, "right", tab.id);
154
393
  }
155
394
 
156
395
  return btn;
@@ -253,13 +492,20 @@ class YumeTabs extends HTMLElement {
253
492
 
254
493
  _handleTabKeydown(e, buttons) {
255
494
  const idx = buttons.indexOf(e.currentTarget);
495
+
256
496
  if (e.key === "ArrowRight") {
257
497
  e.preventDefault();
258
498
  this._findSiblingButton(buttons, idx, 1)?.focus();
259
- } else if (e.key === "ArrowLeft") {
499
+ return;
500
+ }
501
+
502
+ if (e.key === "ArrowLeft") {
260
503
  e.preventDefault();
261
504
  this._findSiblingButton(buttons, idx, -1)?.focus();
262
- } else if (e.key === "Enter" || e.key === " ") {
505
+ return;
506
+ }
507
+
508
+ if (e.key === "Enter" || e.key === " ") {
263
509
  e.preventDefault();
264
510
  this.activateTab(e.currentTarget.dataset.id);
265
511
  }
@@ -277,9 +523,7 @@ class YumeTabs extends HTMLElement {
277
523
  buttons.forEach((button) => {
278
524
  if (button.disabled) return;
279
525
  button.addEventListener("click", () => this.activateTab(button.dataset.id));
280
- button.addEventListener("keydown", (e) => {
281
- this._handleTabKeydown(e, buttons);
282
- });
526
+ button.addEventListener("keydown", (e) => this._handleTabKeydown(e, buttons));
283
527
  });
284
528
  }
285
529
  }