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

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
@@ -35,6 +35,7 @@ Delete any empty sections before publishing.
35
35
 
36
36
  ### Added
37
37
 
38
+ - New `y-dock` component — a fixed navigation bar (dock) that displays icon+label items for primary app navigation. Accepts items via a JSON `items` attribute, slotted templates, or direct child elements. Attributes: `items` (JSON array of `{ name, icon, href?, selected?, slot? }`), `position` (`"bottom"` | `"top"`), `breakpoint` (viewport width below which dock is visible — omit for always visible), `size` (`"small"` | `"medium"` | `"large"`), `history` (omit for `pushState` SPA navigation; set to `"false"` for full-page navigation). Events: cancelable `navigate` (`detail: { href }`). Full ARIA support with `role="navigation"`, `aria-current="page"`, and keyboard navigation (Arrow Left/Right, Enter/Space).
38
39
  - New `y-stepper` component — a multi-step wizard that guides users through a sequential flow. Step content is provided via named slots defined in the `items` JSON array (`{ label, slot, description?, icon?, status? }`). Supports `current` (zero-based active step index), `orientation` (`"horizontal"` | `"vertical"`), `position` (`"start"` | `"end"` — controls whether indicators appear before or after the content), `size` (`"small"` | `"medium"` | `"large"`), `linear` (restricts free navigation), and `editable` (allows returning to completed steps). Methods: `next()`, `previous()`, `goTo(index)`, `complete(index?)`, `reset()`. Events: cancelable `change`, `complete`, `finish`. Full ARIA support with `role="list"`, `aria-current="step"`, `aria-controls`/`aria-labelledby` linkage, and keyboard navigation.
39
40
  - New `y-breadcrumbs` component — a navigation breadcrumb trail. Accepts an `items` JSON array (`{ text, href?, icon? }`). Supports `separator` (custom separator character or slotted icon), `max-items` (collapses middle items with an expand button), `size` (`"small"` | `"medium"` | `"large"`), and `history` (set to `"false"` for full-page navigation instead of `pushState`). Fires cancelable `navigate` and `expand` events. Full ARIA with `<nav aria-label="Breadcrumb">`, `<ol>`, `aria-current="page"`, and `aria-hidden` separators.
40
41
  - New `y-gallery` component — a media gallery that accepts `<img>` or `<figure>` children and arranges them in `grid`, `row`, `column`, or `masonry` layouts. Supports `columns`, `gap` (`"small"` | `"medium"` | `"large"` or any CSS length), `aspect-ratio`, `expandable` (lightbox with prev/next navigation), `loop`, and `size` attributes. The expanded view uses `<y-icon>` for nav arrows and supports `data-src` for full-resolution images, `<figcaption>` captions, and image counter. Icon slots (`expand-prev-icon`, `expand-next-icon`, `expand-close-icon`) allow custom icons. Fires `expand`, `close`, and `navigate` events.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## Overview
19
19
 
20
- YumeKit is a collection of 34 production-ready custom elements built with native Web Components. It works with any framework — or none at all — and ships with a comprehensive design token system, built-in theming, an icon registry, and full TypeScript support.
20
+ YumeKit is a collection of 35 production-ready custom elements built with native Web Components. It works with any framework — or none at all — and ships with a comprehensive design token system, built-in theming, an icon registry, and full TypeScript support.
21
21
 
22
22
  - **Zero dependencies** — built entirely on web standards
23
23
  - **Framework-agnostic** — works with React, Vue, Svelte, or plain HTML
@@ -86,6 +86,7 @@ Then use the `<y-theme>` component to apply a theme:
86
86
  | Date | `<y-date>` | Date input |
87
87
  | DatePicker | `<y-datepicker>` | A date and time picker |
88
88
  | Dialog | `<y-dialog>` | Modal dialog |
89
+ | Dock | `<y-dock>` | Fixed navigation dock |
89
90
  | Drawer | `<y-drawer>` | Side drawer / sidebar |
90
91
  | Gallery | `<y-gallery>` | Media gallery with lightbox |
91
92
  | Icon | `<y-icon>` | SVG icon display |
@@ -0,0 +1,32 @@
1
+ export class YumeDock extends HTMLElement {
2
+ static get observedAttributes(): string[];
3
+ _mediaQuery: MediaQueryList;
4
+ _onMediaChange(e: any): void;
5
+ connectedCallback(): void;
6
+ disconnectedCallback(): void;
7
+ attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
8
+ set breakpoint(val: number);
9
+ /** Viewport width below which dock is visible (px). When omitted, always visible. */
10
+ get breakpoint(): number;
11
+ set history(val: string);
12
+ /** Navigation mode. Omit for pushState SPA navigation; set to "false" for full-page navigation. */
13
+ get history(): string;
14
+ set items(val: any);
15
+ /** Navigation items as a JSON array. */
16
+ get items(): any;
17
+ set position(val: "top" | "bottom");
18
+ /** Which edge of the viewport the dock anchors to. */
19
+ get position(): "top" | "bottom";
20
+ set size(val: string);
21
+ /** Controls icon size, label font size, and overall dock height. */
22
+ get size(): string;
23
+ /** Rebuilds the shadow DOM. */
24
+ render(): void;
25
+ _buildBar(): HTMLElement;
26
+ _buildItem(item: any): HTMLElement;
27
+ _buildStyles(): HTMLStyleElement;
28
+ _handleItemClick(item: any): void;
29
+ _setupBreakpoint(): void;
30
+ _setupItemKeyboard(bar: any): void;
31
+ _teardownBreakpoint(): void;
32
+ }
@@ -0,0 +1,32 @@
1
+ export class YumeDock extends HTMLElement {
2
+ static get observedAttributes(): string[];
3
+ _mediaQuery: MediaQueryList;
4
+ _onMediaChange(e: any): void;
5
+ connectedCallback(): void;
6
+ disconnectedCallback(): void;
7
+ attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
8
+ set breakpoint(val: number);
9
+ /** Viewport width below which dock is visible (px). When omitted, always visible. */
10
+ get breakpoint(): number;
11
+ set history(val: string);
12
+ /** Navigation mode. Omit for pushState SPA navigation; set to "false" for full-page navigation. */
13
+ get history(): string;
14
+ set items(val: any);
15
+ /** Navigation items as a JSON array. */
16
+ get items(): any;
17
+ set position(val: "top" | "bottom");
18
+ /** Which edge of the viewport the dock anchors to. */
19
+ get position(): "top" | "bottom";
20
+ set size(val: string);
21
+ /** Controls icon size, label font size, and overall dock height. */
22
+ get size(): string;
23
+ /** Rebuilds the shadow DOM. */
24
+ render(): void;
25
+ _buildBar(): HTMLElement;
26
+ _buildItem(item: any): HTMLElement;
27
+ _buildStyles(): HTMLStyleElement;
28
+ _handleItemClick(item: any): void;
29
+ _setupBreakpoint(): void;
30
+ _setupItemKeyboard(bar: any): void;
31
+ _teardownBreakpoint(): void;
32
+ }
@@ -0,0 +1,546 @@
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
+
220
+ class YumeDock extends HTMLElement {
221
+ static get observedAttributes() {
222
+ return ["items", "position", "breakpoint", "size", "history"];
223
+ }
224
+
225
+ // -------------------------------------------------------------------------
226
+ // Lifecycle
227
+ // -------------------------------------------------------------------------
228
+
229
+ constructor() {
230
+ super();
231
+ this.attachShadow({ mode: "open" });
232
+ this._mediaQuery = null;
233
+ this._onMediaChange = this._onMediaChange.bind(this);
234
+ }
235
+
236
+ connectedCallback() {
237
+ if (!this.hasAttribute("position"))
238
+ this.setAttribute("position", "bottom");
239
+ if (!this.hasAttribute("size")) this.setAttribute("size", "medium");
240
+ if (!this.hasAttribute("role"))
241
+ this.setAttribute("role", "navigation");
242
+ if (!this.hasAttribute("aria-label"))
243
+ this.setAttribute("aria-label", "Dock navigation");
244
+ this.render();
245
+ this._teardownBreakpoint();
246
+ this._setupBreakpoint();
247
+ }
248
+
249
+ disconnectedCallback() {
250
+ this._teardownBreakpoint();
251
+ }
252
+
253
+ attributeChangedCallback(name, oldValue, newValue) {
254
+ if (oldValue === newValue) return;
255
+ if (name === "breakpoint") {
256
+ this._teardownBreakpoint();
257
+ this._setupBreakpoint();
258
+ }
259
+ this.render();
260
+ }
261
+
262
+ // -------------------------------------------------------------------------
263
+ // Getters / Setters
264
+ // -------------------------------------------------------------------------
265
+
266
+ /** Viewport width below which dock is visible (px). When omitted, always visible. */
267
+ get breakpoint() {
268
+ const val = this.getAttribute("breakpoint");
269
+ const num = Number(val);
270
+ return Number.isFinite(num) && num > 0 ? num : null;
271
+ }
272
+ set breakpoint(val) {
273
+ if (val == null) this.removeAttribute("breakpoint");
274
+ else this.setAttribute("breakpoint", String(val));
275
+ }
276
+
277
+ /** Navigation mode. Omit for pushState SPA navigation; set to "false" for full-page navigation. */
278
+ get history() {
279
+ return this.getAttribute("history");
280
+ }
281
+ set history(val) {
282
+ if (val == null) this.removeAttribute("history");
283
+ else this.setAttribute("history", val);
284
+ }
285
+
286
+ /** Navigation items as a JSON array. */
287
+ get items() {
288
+ try {
289
+ return JSON.parse(this.getAttribute("items") || "[]");
290
+ } catch {
291
+ return [];
292
+ }
293
+ }
294
+ set items(val) {
295
+ this.setAttribute("items", JSON.stringify(val));
296
+ }
297
+
298
+ /** Which edge of the viewport the dock anchors to. */
299
+ get position() {
300
+ const pos = this.getAttribute("position");
301
+ return pos === "top" ? "top" : "bottom";
302
+ }
303
+ set position(val) {
304
+ this.setAttribute("position", val === "top" ? "top" : "bottom");
305
+ }
306
+
307
+ /** Controls icon size, label font size, and overall dock height. */
308
+ get size() {
309
+ const sz = this.getAttribute("size");
310
+ return ["small", "medium", "large"].includes(sz) ? sz : "medium";
311
+ }
312
+ set size(val) {
313
+ this.setAttribute(
314
+ "size",
315
+ ["small", "medium", "large"].includes(val) ? val : "medium",
316
+ );
317
+ }
318
+
319
+ // -------------------------------------------------------------------------
320
+ // Public
321
+ // -------------------------------------------------------------------------
322
+
323
+ /** Rebuilds the shadow DOM. */
324
+ render() {
325
+ this.shadowRoot.innerHTML = "";
326
+ this.shadowRoot.appendChild(this._buildStyles());
327
+ this.shadowRoot.appendChild(this._buildBar());
328
+ }
329
+
330
+ // -------------------------------------------------------------------------
331
+ // Private
332
+ // -------------------------------------------------------------------------
333
+
334
+ _buildBar() {
335
+ const bar = createElement("div", { class: "bar", part: "bar" });
336
+
337
+ this.items.forEach((item) => bar.appendChild(this._buildItem(item)));
338
+ bar.appendChild(createElement("slot", {}));
339
+
340
+ this._setupItemKeyboard(bar);
341
+
342
+ return bar;
343
+ }
344
+
345
+ _buildItem(item) {
346
+ if (item.slot && this.querySelector(`[slot="${item.slot}"]`)) {
347
+ const wrapper = createElement("div", {
348
+ class: "item",
349
+ part: "item",
350
+ tabindex: "0",
351
+ role: "group",
352
+ });
353
+ wrapper.appendChild(createElement("slot", { name: item.slot }));
354
+ return wrapper;
355
+ }
356
+
357
+ const btn = createElement("button", {
358
+ class: "item",
359
+ part: "item",
360
+ "aria-label": item.name,
361
+ });
362
+
363
+ if (item.selected) btn.setAttribute("aria-current", "page");
364
+ if (item.href) btn.dataset.href = item.href;
365
+
366
+ const iconSize = { small: "medium", medium: "large", large: "x-large" }[
367
+ this.size
368
+ ];
369
+ const icon = createElement("y-icon", {
370
+ name: item.icon,
371
+ size: iconSize,
372
+ });
373
+ btn.appendChild(icon);
374
+
375
+ const label = createElement("span", { class: "item-label" }, [item.name]);
376
+ btn.appendChild(label);
377
+
378
+ btn.addEventListener("click", () => this._handleItemClick(item));
379
+
380
+ return btn;
381
+ }
382
+
383
+ _buildStyles() {
384
+ const style = document.createElement("style");
385
+ const pos = this.position;
386
+ const sz = this.size;
387
+ const heightVar = `var(--component-dock-height, var(--component-dock-height-${sz}))`;
388
+ const fontSize =
389
+ sz === "small" ? "10px" : sz === "large" ? "13px" : "11px";
390
+ style.textContent = `
391
+ :host {
392
+ display: block;
393
+ position: fixed;
394
+ ${pos === "top" ? "top: 0;" : "bottom: 0;"}
395
+ left: 0;
396
+ right: 0;
397
+ z-index: var(--component-dock-z-index, 8000);
398
+ ${
399
+ pos === "bottom"
400
+ ? "padding-bottom: env(safe-area-inset-bottom, 0px);"
401
+ : "padding-top: env(safe-area-inset-top, 0px);"
402
+ }
403
+ background: var(--component-dock-background);
404
+ ${
405
+ pos === "bottom"
406
+ ? `border-top: var(--component-dock-border-width, 1px) solid var(--component-dock-border-color);`
407
+ : `border-bottom: var(--component-dock-border-width, 1px) solid var(--component-dock-border-color);`
408
+ }
409
+ }
410
+ :host([hidden]) {
411
+ display: none !important;
412
+ }
413
+ .bar {
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: space-around;
417
+ height: ${heightVar};
418
+ position: relative;
419
+ }
420
+ .item {
421
+ display: flex;
422
+ flex-direction: column;
423
+ align-items: center;
424
+ justify-content: center;
425
+ gap: 2px;
426
+ background: none;
427
+ border: none;
428
+ padding: 4px 8px;
429
+ cursor: pointer;
430
+ color: var(--component-dock-color);
431
+ font-family: inherit;
432
+ font-size: ${fontSize};
433
+ min-width: 48px;
434
+ outline: none;
435
+ transition: color 0.15s ease;
436
+ -webkit-tap-highlight-color: transparent;
437
+ }
438
+ .item:focus-visible {
439
+ outline: 2px solid var(--component-dock-color-active);
440
+ outline-offset: -2px;
441
+ border-radius: 4px;
442
+ }
443
+ .item[aria-current="page"],
444
+ .item:hover {
445
+ color: var(--component-dock-color-active);
446
+ }
447
+ .item-label {
448
+ white-space: nowrap;
449
+ overflow: hidden;
450
+ text-overflow: ellipsis;
451
+ max-width: 64px;
452
+ }
453
+ ::slotted(*) {
454
+ display: flex;
455
+ flex-direction: column;
456
+ align-items: center;
457
+ justify-content: center;
458
+ gap: 2px;
459
+ color: var(--component-dock-color);
460
+ min-width: 48px;
461
+ }
462
+ `;
463
+ return style;
464
+ }
465
+
466
+ _handleItemClick(item) {
467
+ if (!item.href) return;
468
+
469
+ const event = new CustomEvent("navigate", {
470
+ detail: { href: item.href },
471
+ bubbles: true,
472
+ composed: true,
473
+ cancelable: true,
474
+ });
475
+
476
+ if (!this.dispatchEvent(event)) return;
477
+
478
+ if (this.history === "false") {
479
+ window.location.href = item.href;
480
+ } else {
481
+ window.history.pushState({}, "", item.href);
482
+ window.dispatchEvent(new PopStateEvent("popstate", { state: {} }));
483
+ }
484
+ }
485
+
486
+ _onMediaChange(e) {
487
+ this.hidden = !e.matches;
488
+ }
489
+
490
+ _setupBreakpoint() {
491
+ const bp = this.breakpoint;
492
+ if (bp == null) {
493
+ this.hidden = false;
494
+ return;
495
+ }
496
+
497
+ this._mediaQuery = window.matchMedia(`(max-width: ${bp}px)`);
498
+ this._mediaQuery.addEventListener("change", this._onMediaChange);
499
+ this.hidden = !this._mediaQuery.matches;
500
+ }
501
+
502
+ _setupItemKeyboard(bar) {
503
+ const slot = bar.querySelector("slot:not([name])");
504
+
505
+ const getItems = () => {
506
+ const shadowItems = Array.from(bar.querySelectorAll(".item"));
507
+ const slottedItems = slot
508
+ ? Array.from(slot.assignedElements({ flatten: true }))
509
+ : [];
510
+ return [...shadowItems, ...slottedItems];
511
+ };
512
+
513
+ bar.addEventListener("keydown", (e) => {
514
+ const items = getItems();
515
+ const path = e.composedPath();
516
+ const idx = items.findIndex(
517
+ (item) => item === e.target || path.includes(item),
518
+ );
519
+ if (idx === -1) return;
520
+
521
+ if (e.key === "ArrowRight") {
522
+ e.preventDefault();
523
+ items[(idx + 1) % items.length].focus();
524
+ } else if (e.key === "ArrowLeft") {
525
+ e.preventDefault();
526
+ items[(idx - 1 + items.length) % items.length].focus();
527
+ } else if (e.key === "Enter" || e.key === " ") {
528
+ e.preventDefault();
529
+ items[idx].click();
530
+ }
531
+ });
532
+ }
533
+
534
+ _teardownBreakpoint() {
535
+ if (this._mediaQuery) {
536
+ this._mediaQuery.removeEventListener("change", this._onMediaChange);
537
+ this._mediaQuery = null;
538
+ }
539
+ }
540
+ }
541
+
542
+ if (!customElements.get("y-dock")) {
543
+ customElements.define("y-dock", YumeDock);
544
+ }
545
+
546
+ export { YumeDock };