@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,387 @@
1
+ class YumeSlider extends HTMLElement {
2
+ static formAssociated = true;
3
+
4
+ static get observedAttributes() {
5
+ return [
6
+ "value",
7
+ "min",
8
+ "max",
9
+ "step",
10
+ "size",
11
+ "color",
12
+ "disabled",
13
+ "name",
14
+ "orientation",
15
+ ];
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this._internals = this.attachInternals();
21
+ this.attachShadow({ mode: "open" });
22
+ this._dragging = false;
23
+ }
24
+
25
+ connectedCallback() {
26
+ if (!this.hasAttribute("size")) this.setAttribute("size", "medium");
27
+ if (!this.hasAttribute("min")) this.setAttribute("min", "0");
28
+ if (!this.hasAttribute("max")) this.setAttribute("max", "100");
29
+ if (!this.hasAttribute("value")) this.setAttribute("value", "50");
30
+
31
+ this._internals.setFormValue(this.value);
32
+ this.render();
33
+ this._bindEvents();
34
+ }
35
+
36
+ attributeChangedCallback(name, oldValue, newValue) {
37
+ if (oldValue !== newValue) {
38
+ if (name === "value" || name === "name") {
39
+ this._internals.setFormValue(
40
+ this.value,
41
+ this.getAttribute("name"),
42
+ );
43
+ }
44
+
45
+ if (
46
+ name === "value" &&
47
+ this.shadowRoot.querySelector(".slider-track")
48
+ ) {
49
+ this._updateVisuals();
50
+ } else {
51
+ this.render();
52
+ this._bindEvents();
53
+ }
54
+ }
55
+ }
56
+
57
+ get value() {
58
+ const val = parseFloat(this.getAttribute("value"));
59
+ return Number.isNaN(val) ? 0 : val;
60
+ }
61
+
62
+ set value(val) {
63
+ const clamped = Math.max(this.min, Math.min(this.max, Number(val)));
64
+ const stepped = this._snapToStep(clamped);
65
+ this.setAttribute("value", String(stepped));
66
+ }
67
+
68
+ get min() {
69
+ return parseFloat(this.getAttribute("min")) || 0;
70
+ }
71
+
72
+ set min(val) {
73
+ this.setAttribute("min", String(val));
74
+ }
75
+
76
+ get max() {
77
+ return parseFloat(this.getAttribute("max")) || 100;
78
+ }
79
+
80
+ set max(val) {
81
+ this.setAttribute("max", String(val));
82
+ }
83
+
84
+ get step() {
85
+ const s = parseFloat(this.getAttribute("step"));
86
+ return Number.isNaN(s) || s <= 0 ? null : s;
87
+ }
88
+
89
+ set step(val) {
90
+ if (val === null || val === undefined) {
91
+ this.removeAttribute("step");
92
+ } else {
93
+ this.setAttribute("step", String(val));
94
+ }
95
+ }
96
+
97
+ get size() {
98
+ return this.getAttribute("size") || "medium";
99
+ }
100
+
101
+ set size(val) {
102
+ this.setAttribute("size", val);
103
+ }
104
+
105
+ get color() {
106
+ return this.getAttribute("color") || "primary";
107
+ }
108
+
109
+ set color(val) {
110
+ this.setAttribute("color", val);
111
+ }
112
+
113
+ get disabled() {
114
+ return this.hasAttribute("disabled");
115
+ }
116
+
117
+ set disabled(val) {
118
+ if (val) this.setAttribute("disabled", "");
119
+ else this.removeAttribute("disabled");
120
+ }
121
+
122
+ get orientation() {
123
+ return this.getAttribute("orientation") || "horizontal";
124
+ }
125
+
126
+ set orientation(val) {
127
+ this.setAttribute("orientation", val);
128
+ }
129
+
130
+ get percentage() {
131
+ const range = this.max - this.min;
132
+ if (range <= 0) return 0;
133
+ const pct = ((this.value - this.min) / range) * 100;
134
+ return Math.max(0, Math.min(100, pct));
135
+ }
136
+
137
+ _snapToStep(val) {
138
+ if (!this.step) return val;
139
+ const steps = Math.round((val - this.min) / this.step);
140
+ return Math.max(
141
+ this.min,
142
+ Math.min(this.max, this.min + steps * this.step),
143
+ );
144
+ }
145
+
146
+ getTrackColor(color) {
147
+ const colorMap = {
148
+ primary: "var(--primary-background-hover)",
149
+ secondary: "var(--secondary-background-hover)",
150
+ base: "var(--base-background-active)",
151
+ success: "var(--success-background-hover)",
152
+ warning: "var(--warning-background-hover)",
153
+ error: "var(--error-background-hover)",
154
+ help: "var(--help-background-hover)",
155
+ };
156
+ return colorMap[color] || color;
157
+ }
158
+
159
+ getThumbColor(color) {
160
+ const colorMap = {
161
+ primary: "var(--primary-content--)",
162
+ secondary: "var(--secondary-content--)",
163
+ base: "var(--base-content--)",
164
+ success: "var(--success-content--)",
165
+ warning: "var(--warning-content--)",
166
+ error: "var(--error-content--)",
167
+ help: "var(--help-content--)",
168
+ };
169
+ return colorMap[color] || color;
170
+ }
171
+
172
+ getSizeVars(size) {
173
+ const map = {
174
+ small: {
175
+ trackHeight: "var(--sizing-small, 27px)",
176
+ },
177
+ medium: {
178
+ trackHeight: "var(--sizing-medium, 35px)",
179
+ },
180
+ large: {
181
+ trackHeight: "var(--sizing-large, 51px)",
182
+ },
183
+ };
184
+ return map[size] || map.medium;
185
+ }
186
+
187
+ _bindEvents() {
188
+ const track = this.shadowRoot.querySelector(".slider-track");
189
+ const thumb = this.shadowRoot.querySelector(".thumb");
190
+ if (!track || !thumb) return;
191
+
192
+ const onPointerDown = (e) => {
193
+ if (this.disabled) return;
194
+ e.preventDefault();
195
+ this._dragging = true;
196
+ this._updateFromPointer(e);
197
+ document.addEventListener("pointermove", onPointerMove);
198
+ document.addEventListener("pointerup", onPointerUp);
199
+ };
200
+
201
+ const onPointerMove = (e) => {
202
+ if (!this._dragging) return;
203
+ this._updateFromPointer(e);
204
+ };
205
+
206
+ const onPointerUp = () => {
207
+ this._dragging = false;
208
+ document.removeEventListener("pointermove", onPointerMove);
209
+ document.removeEventListener("pointerup", onPointerUp);
210
+ this.dispatchEvent(
211
+ new Event("change", { bubbles: true, composed: true }),
212
+ );
213
+ };
214
+
215
+ track.addEventListener("pointerdown", onPointerDown);
216
+ thumb.addEventListener("pointerdown", onPointerDown);
217
+
218
+ track.addEventListener("keydown", (e) => {
219
+ if (this.disabled) return;
220
+ const s = this.step || 1;
221
+ let handled = true;
222
+ switch (e.key) {
223
+ case "ArrowRight":
224
+ case "ArrowUp":
225
+ this.value = this.value + s;
226
+ break;
227
+ case "ArrowLeft":
228
+ case "ArrowDown":
229
+ this.value = this.value - s;
230
+ break;
231
+ case "Home":
232
+ this.value = this.min;
233
+ break;
234
+ case "End":
235
+ this.value = this.max;
236
+ break;
237
+ default:
238
+ handled = false;
239
+ }
240
+ if (handled) {
241
+ e.preventDefault();
242
+ this.dispatchEvent(
243
+ new Event("input", { bubbles: true, composed: true }),
244
+ );
245
+ this.dispatchEvent(
246
+ new Event("change", { bubbles: true, composed: true }),
247
+ );
248
+ }
249
+ });
250
+ }
251
+
252
+ _updateFromPointer(e) {
253
+ const track = this.shadowRoot.querySelector(".slider-track");
254
+ const inner = this.shadowRoot.querySelector(".slider-inner");
255
+ const target = inner || track;
256
+ const rect = target.getBoundingClientRect();
257
+ const ratio = Math.max(
258
+ 0,
259
+ Math.min(1, (e.clientX - rect.left) / rect.width),
260
+ );
261
+ const rawValue = this.min + ratio * (this.max - this.min);
262
+ this.value = rawValue;
263
+ this.dispatchEvent(
264
+ new Event("input", { bubbles: true, composed: true }),
265
+ );
266
+ }
267
+
268
+ /** Fast path: update only the dynamic parts (fill, thumb, label, aria) */
269
+ _updateVisuals() {
270
+ const pct = this.percentage;
271
+ const fill = this.shadowRoot.querySelector(".fill");
272
+ const thumb = this.shadowRoot.querySelector(".thumb");
273
+ const track = this.shadowRoot.querySelector(".slider-track");
274
+
275
+ if (fill) fill.style.width = `${pct}%`;
276
+ if (thumb) thumb.style.left = `${pct}%`;
277
+ if (track) {
278
+ track.setAttribute("aria-valuenow", String(this.value));
279
+ }
280
+ }
281
+
282
+ render() {
283
+ const pct = this.percentage;
284
+ const trackFillColor = this.getTrackColor(this.color);
285
+ const thumbColor = this.getThumbColor(this.color);
286
+ const { trackHeight } = this.getSizeVars(this.size);
287
+ const isDisabled = this.disabled;
288
+
289
+ this.shadowRoot.innerHTML = `
290
+ <style>
291
+ :host {
292
+ display: inline-block;
293
+ width: var(--component-slider-width, 129px);
294
+ font-family: var(--font-family-body);
295
+ color: var(--base-content--);
296
+ opacity: ${isDisabled ? "0.5" : "1"};
297
+ pointer-events: ${isDisabled ? "none" : "auto"};
298
+ }
299
+
300
+ .slider-wrapper {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: var(--spacing-2x-small, 4px);
304
+ }
305
+
306
+ .slider-track {
307
+ position: relative;
308
+ width: 100%;
309
+ height: ${trackHeight};
310
+ background: var(--base-background-component);
311
+ border: var(--component-slider-border-width) solid var(--base-background-border);
312
+ border-radius: var(--component-slider-border-radius-outer);
313
+ box-sizing: border-box;
314
+ padding: var(--component-slider-padding);
315
+ cursor: pointer;
316
+ outline: none;
317
+ }
318
+
319
+ .slider-track:focus-visible {
320
+ border-color: ${thumbColor};
321
+ }
322
+
323
+ .slider-inner {
324
+ position: relative;
325
+ width: 100%;
326
+ height: 100%;
327
+ overflow: hidden;
328
+ }
329
+
330
+ .fill {
331
+ position: absolute;
332
+ top: 0;
333
+ left: 0;
334
+ height: 100%;
335
+ background: ${trackFillColor};
336
+ border-radius: var(--component-slider-border-radius-inner) 0 0 var(--component-slider-border-radius-inner);
337
+ width: ${pct}%;
338
+ pointer-events: none;
339
+ }
340
+
341
+ .thumb {
342
+ position: absolute;
343
+ top: 0;
344
+ left: ${pct}%;
345
+ transform: translateX(-50%);
346
+ width: 8px;
347
+ height: 100%;
348
+ background: ${thumbColor};
349
+ border-radius: var(--component-slider-thumb-border-radius);
350
+ cursor: grab;
351
+ z-index: 1;
352
+ touch-action: none;
353
+ }
354
+
355
+ .thumb:active {
356
+ cursor: grabbing;
357
+ }
358
+ </style>
359
+
360
+ <div class="slider-wrapper">
361
+ <div
362
+ class="slider-track"
363
+ part="track"
364
+ role="slider"
365
+ tabindex="${isDisabled ? "-1" : "0"}"
366
+ aria-valuenow="${this.value}"
367
+ aria-valuemin="${this.min}"
368
+ aria-valuemax="${this.max}"
369
+ ${this.step ? `aria-valuestep="${this.step}"` : ""}
370
+ aria-orientation="${this.orientation}"
371
+ ${isDisabled ? 'aria-disabled="true"' : ""}
372
+ >
373
+ <div class="slider-inner">
374
+ <div class="fill" part="fill"></div>
375
+ <div class="thumb" part="thumb"></div>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ `;
380
+ }
381
+ }
382
+
383
+ if (!customElements.get("y-slider")) {
384
+ customElements.define("y-slider", YumeSlider);
385
+ }
386
+
387
+ export { YumeSlider };