@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 +1 -0
- package/README.md +2 -1
- package/dist/components/y-dock/y-dock.d.ts +32 -0
- package/dist/components/y-dock.d.ts +32 -0
- package/dist/components/y-dock.js +546 -0
- package/dist/components/y-theme.js +1 -1
- package/dist/icons/all.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +370 -2
- package/dist/styles/variables.css +11 -0
- package/dist/yumekit.min.js +1 -1
- package/llm.txt +31 -6
- package/package.json +8 -8
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
|
|
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 };
|