@monabbous/slidebox 1.2.0 → 1.3.1
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/README.md +358 -158
- package/dist/css/group.css +121 -0
- package/dist/css/slidebox.css +2 -0
- package/experimental/range.css +99 -0
- package/experimental/range.d.mts +14 -0
- package/experimental/range.mjs +213 -0
- package/less/experimental.less +119 -0
- package/less/slidebox.less +139 -0
- package/package.json +7 -3
- package/scss/_experimental.scss +109 -0
- package/scss/_mixins.scss +129 -0
- package/scss/group.scss +2 -0
- package/tailwind/experimental-rules.json +125 -0
- package/tailwind/group-rules.json +152 -0
- package/tailwind/index.cjs +8 -6
- package/tailwind/rules.json +2 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
.slidebox-group {
|
|
2
|
+
--_size: var(--slidebox-size, 36px);
|
|
3
|
+
--_spacing: var(--slidebox-spacing, .12);
|
|
4
|
+
--_deform: var(--slidebox-deform, .06);
|
|
5
|
+
--_duration: var(--slidebox-duration, 200ms);
|
|
6
|
+
--_easing: var(--slidebox-easing, ease-in-out);
|
|
7
|
+
--_track: var(--slidebox-track, Field);
|
|
8
|
+
--_border: var(--slidebox-border, ButtonBorder);
|
|
9
|
+
--_off: var(--slidebox-off, FieldText);
|
|
10
|
+
--_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
|
|
11
|
+
--_text: var(--slidebox-text, var(--slidebox-off, FieldText));
|
|
12
|
+
--_selected-text: var(--slidebox-selected-text, HighlightText);
|
|
13
|
+
--_press-start: 0px;
|
|
14
|
+
--_press-end: 0px;
|
|
15
|
+
display: inline-grid;
|
|
16
|
+
grid-auto-flow: column;
|
|
17
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
padding: calc(var(--_size) * var(--_spacing));
|
|
20
|
+
gap: calc(var(--_size) * var(--_spacing));
|
|
21
|
+
border: 1px solid var(--_border);
|
|
22
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
23
|
+
background: var(--_track);
|
|
24
|
+
color: var(--_text);
|
|
25
|
+
isolation: isolate;
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
.slidebox-group > label {
|
|
29
|
+
position: relative;
|
|
30
|
+
display: grid;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
.slidebox-group > label > input[type="radio"] {
|
|
35
|
+
position: absolute;
|
|
36
|
+
width: 1px;
|
|
37
|
+
height: 1px;
|
|
38
|
+
padding: 0;
|
|
39
|
+
margin: -1px;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
clip-path: inset(50%);
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
}
|
|
44
|
+
.slidebox-group > label > span {
|
|
45
|
+
position: relative;
|
|
46
|
+
isolation: isolate;
|
|
47
|
+
display: grid;
|
|
48
|
+
place-items: center;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
min-height: var(--_size);
|
|
51
|
+
padding: .35em 1em;
|
|
52
|
+
border: 1px solid transparent;
|
|
53
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
54
|
+
color: var(--_text);
|
|
55
|
+
background: transparent;
|
|
56
|
+
transition: color var(--_duration) var(--_easing);
|
|
57
|
+
}
|
|
58
|
+
.slidebox-group > label > input:checked + span {
|
|
59
|
+
color: var(--_selected-text);
|
|
60
|
+
}
|
|
61
|
+
.slidebox-group > label > input:checked + span::before {
|
|
62
|
+
content: "";
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
z-index: -1;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
border-radius: inherit;
|
|
68
|
+
background: var(--_on);
|
|
69
|
+
margin-inline-start: var(--_press-start);
|
|
70
|
+
margin-inline-end: var(--_press-end);
|
|
71
|
+
transition: margin-inline-start var(--_duration) var(--_easing), margin-inline-end var(--_duration) var(--_easing);
|
|
72
|
+
}
|
|
73
|
+
.slidebox-group > label > input:not(:checked) + span { color: var(--_text); }
|
|
74
|
+
.slidebox-group:has(> label > input:checked):has(> label:active > input:not(:checked):not(:disabled)):not(:has(> label:active ~ label > input:checked)) {
|
|
75
|
+
--_press-end: calc(-1 * var(--_size) * var(--_deform));
|
|
76
|
+
}
|
|
77
|
+
.slidebox-group:has(> label:active > input:not(:disabled)):has(> label:active ~ label > input:checked) {
|
|
78
|
+
--_press-start: calc(-1 * var(--_size) * var(--_deform));
|
|
79
|
+
}
|
|
80
|
+
.slidebox-group > label > input:focus-visible + span {
|
|
81
|
+
outline: 2px solid var(--_on);
|
|
82
|
+
outline-offset: 2px;
|
|
83
|
+
}
|
|
84
|
+
.slidebox-group > label:has(> input:disabled) { cursor: not-allowed; }
|
|
85
|
+
.slidebox-group > label > input:disabled + span { opacity: .5; }
|
|
86
|
+
@supports (color: AccentColor) {
|
|
87
|
+
.slidebox-group { --_accent: AccentColor; }
|
|
88
|
+
}
|
|
89
|
+
/* A shared sliding indicator where scoped CSS anchor positioning is available.
|
|
90
|
+
Other browsers retain the per-segment highlight above. Neither needs JS. */
|
|
91
|
+
@supports (anchor-scope: --slidebox-selection) and (left: anchor(left)) {
|
|
92
|
+
.slidebox-group { anchor-scope: --slidebox-selection; }
|
|
93
|
+
.slidebox-group > label:has(> input:checked) { anchor-name: --slidebox-selection; }
|
|
94
|
+
.slidebox-group:has(> label > input:checked)::before {
|
|
95
|
+
content: "";
|
|
96
|
+
position: absolute;
|
|
97
|
+
position-anchor: --slidebox-selection;
|
|
98
|
+
left: anchor(left);
|
|
99
|
+
right: anchor(right);
|
|
100
|
+
top: anchor(top);
|
|
101
|
+
height: anchor-size(height);
|
|
102
|
+
margin-inline-start: var(--_press-start);
|
|
103
|
+
margin-inline-end: var(--_press-end);
|
|
104
|
+
z-index: -1;
|
|
105
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
106
|
+
background: var(--_on);
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
transition: left var(--_duration) var(--_easing), right var(--_duration) var(--_easing), top var(--_duration) var(--_easing), height var(--_duration) var(--_easing), margin-inline-start var(--_duration) var(--_easing), margin-inline-end var(--_duration) var(--_easing);
|
|
109
|
+
}
|
|
110
|
+
.slidebox-group > label > input:checked + span::before { content: none; }
|
|
111
|
+
}
|
|
112
|
+
@media (forced-colors: active) {
|
|
113
|
+
.slidebox-group { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; --_text: FieldText; --_selected-text: HighlightText; }
|
|
114
|
+
.slidebox-group > label > input:checked + span { border-color: Highlight; outline: 2px solid Highlight; outline-offset: -2px; }
|
|
115
|
+
.slidebox-group > label > input:disabled + span { color: GrayText; }
|
|
116
|
+
}
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
.slidebox-group > label > span { transition-duration: .01ms; }
|
|
119
|
+
.slidebox-group > label > input:checked + span::before { transition-duration: .01ms; }
|
|
120
|
+
.slidebox-group::before { transition-duration: .01ms; }
|
|
121
|
+
}
|
package/dist/css/slidebox.css
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
.slidebox::after {
|
|
30
30
|
--_start: 0; --_end: 1;
|
|
31
|
+
box-sizing: border-box;
|
|
31
32
|
content: "";
|
|
32
33
|
display: block;
|
|
33
34
|
flex: 1 1 auto;
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
|
|
39
40
|
margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
|
|
40
41
|
border-radius: inherit;
|
|
42
|
+
border: 1px solid var(--_track);
|
|
41
43
|
background: var(--_off);
|
|
42
44
|
transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
|
|
43
45
|
transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
.slidebox-range {
|
|
2
|
+
--_size: var(--slidebox-size, 24px);
|
|
3
|
+
--_spacing: var(--slidebox-spacing, .33);
|
|
4
|
+
--_range-inset: max(0px, calc(var(--_size) * var(--_spacing) * .5));
|
|
5
|
+
--_thumb-size: calc(var(--_size) * (1 - var(--_spacing)));
|
|
6
|
+
--_deform: var(--slidebox-deform, .1);
|
|
7
|
+
--_duration: var(--slidebox-duration, 200ms);
|
|
8
|
+
--_easing: var(--slidebox-easing, ease-in-out);
|
|
9
|
+
--_track: var(--slidebox-track, Field);
|
|
10
|
+
--_border: var(--slidebox-border, ButtonBorder);
|
|
11
|
+
--_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
|
|
12
|
+
appearance: none;
|
|
13
|
+
-webkit-appearance: none;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
width: 100%;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
height: var(--_size);
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding-block: 0;
|
|
20
|
+
padding-inline: var(--_range-inset);
|
|
21
|
+
border: 1px solid var(--_border);
|
|
22
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
23
|
+
background: var(--_track);
|
|
24
|
+
color: var(--_on);
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
accent-color: var(--_on);
|
|
27
|
+
}
|
|
28
|
+
.slidebox-range[data-slidebox-range-fill] {
|
|
29
|
+
--_fill-size: max(0px, min(var(--_thumb-size), calc(var(--_size) - 2px)));
|
|
30
|
+
--_fill-stop: clamp(0px, calc(var(--_thumb-size) * .5 + (100% - var(--_thumb-size)) * clamp(0, var(--slidebox-range-fill, 0), 1)), 100%);
|
|
31
|
+
background-image:
|
|
32
|
+
radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
|
|
33
|
+
linear-gradient(to right, transparent 0 calc(var(--_fill-size) * .5), var(--_fill-color, var(--_on)) calc(var(--_fill-size) * .5) var(--_fill-stop), transparent var(--_fill-stop)),
|
|
34
|
+
var(--_range-track-image, none);
|
|
35
|
+
background-size: var(--_fill-size) var(--_fill-size), max(0px, calc(100% - 2 * var(--_range-inset))) var(--_fill-size), 100vw 100vh;
|
|
36
|
+
background-position: left var(--_range-inset) center, center, 0 0;
|
|
37
|
+
background-repeat: no-repeat;
|
|
38
|
+
background-attachment: scroll, scroll, fixed;
|
|
39
|
+
background-clip: padding-box;
|
|
40
|
+
}
|
|
41
|
+
.slidebox-range[data-slidebox-range-fill]:dir(rtl) {
|
|
42
|
+
background-image:
|
|
43
|
+
radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
|
|
44
|
+
linear-gradient(to left, transparent 0 calc(var(--_fill-size) * .5), var(--_fill-color, var(--_on)) calc(var(--_fill-size) * .5) var(--_fill-stop), transparent var(--_fill-stop)),
|
|
45
|
+
var(--_range-track-image, none);
|
|
46
|
+
background-position: right var(--_range-inset) center, center, 0 0;
|
|
47
|
+
}
|
|
48
|
+
.slidebox-range::-webkit-slider-runnable-track {
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
height: calc(var(--_size) - 2px);
|
|
51
|
+
border: 0;
|
|
52
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
53
|
+
background: transparent;
|
|
54
|
+
}
|
|
55
|
+
.slidebox-range::-moz-range-track {
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
height: calc(var(--_size) - 2px);
|
|
58
|
+
border: 0;
|
|
59
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
60
|
+
background: transparent;
|
|
61
|
+
}
|
|
62
|
+
.slidebox-range::-webkit-slider-thumb {
|
|
63
|
+
appearance: none;
|
|
64
|
+
-webkit-appearance: none;
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
width: calc(var(--_size) * (1 - var(--_spacing)));
|
|
67
|
+
height: calc(var(--_size) * (1 - var(--_spacing)));
|
|
68
|
+
margin-top: calc(var(--_size) * var(--_spacing) / 2 - 1px);
|
|
69
|
+
border: 1px solid var(--_track);
|
|
70
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
71
|
+
background: var(--_on);
|
|
72
|
+
transition: none;
|
|
73
|
+
}
|
|
74
|
+
.slidebox-range::-moz-range-thumb {
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
width: calc(var(--_size) * (1 - var(--_spacing)));
|
|
77
|
+
height: calc(var(--_size) * (1 - var(--_spacing)));
|
|
78
|
+
border: 1px solid var(--_track);
|
|
79
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
80
|
+
background: var(--_on);
|
|
81
|
+
transition: none;
|
|
82
|
+
}
|
|
83
|
+
.slidebox-range:focus-visible { outline: 2px solid var(--_on); outline-offset: 3px; }
|
|
84
|
+
.slidebox-range[data-slidebox-range-dragging]::-webkit-slider-thumb { background: transparent; border-color: transparent; transition: none; }
|
|
85
|
+
.slidebox-range[data-slidebox-range-dragging]::-moz-range-thumb { background: transparent; border-color: transparent; transition: none; }
|
|
86
|
+
.slidebox-range:disabled { cursor: not-allowed; opacity: .5; }
|
|
87
|
+
@supports (color: AccentColor) {
|
|
88
|
+
.slidebox-range { --_accent: AccentColor; }
|
|
89
|
+
}
|
|
90
|
+
@media (forced-colors: active) {
|
|
91
|
+
.slidebox-range { --_track: Field; --_border: ButtonBorder; --_on: Highlight; }
|
|
92
|
+
.slidebox-range::-webkit-slider-thumb { border-color: Highlight; }
|
|
93
|
+
.slidebox-range::-moz-range-thumb { border-color: Highlight; }
|
|
94
|
+
}
|
|
95
|
+
@media (prefers-reduced-motion: reduce) {
|
|
96
|
+
.slidebox-range { transition-duration: .01ms; }
|
|
97
|
+
.slidebox-range::-webkit-slider-thumb { transition-duration: .01ms; }
|
|
98
|
+
.slidebox-range::-moz-range-thumb { transition-duration: .01ms; }
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental, opt-in directional deformation for a native Slidebox range.
|
|
3
|
+
* Does not replace native values or emit synthetic input/change events.
|
|
4
|
+
* Call after mounting; call the returned cleanup function before unmounting.
|
|
5
|
+
* Importing during server rendering is safe.
|
|
6
|
+
*/
|
|
7
|
+
export declare function enhanceSlideboxRange(input: HTMLInputElement): () => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Experimental progress fill, independent of directional motion.
|
|
11
|
+
* Updates on native input/change and form reset. Call cleanup before unmounting.
|
|
12
|
+
* After programmatic value changes, dispatch an input event to update the fill.
|
|
13
|
+
*/
|
|
14
|
+
export declare function enhanceSlideboxRangeFill(input: HTMLInputElement): () => void;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const enhancements = new WeakMap();
|
|
2
|
+
const clamp = value => Math.max(0, Math.min(1, value));
|
|
3
|
+
|
|
4
|
+
/** Native value = leading edge; a delayed visual tail supplies the deformation. */
|
|
5
|
+
export function enhanceSlideboxRange(input) {
|
|
6
|
+
if (input?.tagName !== "INPUT" || input.type !== "range") {
|
|
7
|
+
throw new TypeError("enhanceSlideboxRange expects a range input");
|
|
8
|
+
}
|
|
9
|
+
if (enhancements.has(input)) return enhancements.get(input);
|
|
10
|
+
const view = input.ownerDocument.defaultView;
|
|
11
|
+
if (!view?.PointerEvent) throw new Error("Slidebox range motion requires a browser with Pointer Events");
|
|
12
|
+
let motion;
|
|
13
|
+
let previous = progress();
|
|
14
|
+
|
|
15
|
+
function progress() {
|
|
16
|
+
const min = Number.parseFloat(input.min) || 0;
|
|
17
|
+
const parsedMax = Number.parseFloat(input.max);
|
|
18
|
+
const max = Number.isFinite(parsedMax) ? parsedMax : 100;
|
|
19
|
+
return max > min ? clamp((input.valueAsNumber - min) / (max - min)) : 0;
|
|
20
|
+
}
|
|
21
|
+
function stop() {
|
|
22
|
+
if (!motion) return;
|
|
23
|
+
const current = motion;
|
|
24
|
+
motion = undefined;
|
|
25
|
+
current.visual.remove();
|
|
26
|
+
if (current.frame !== undefined) view.cancelAnimationFrame(current.frame);
|
|
27
|
+
if (current.attribute === null) input.removeAttribute("data-slidebox-range-dragging");
|
|
28
|
+
else input.setAttribute("data-slidebox-range-dragging", current.attribute);
|
|
29
|
+
if (current.style) input.style.setProperty("--slidebox-range-stretch", current.style, current.priority);
|
|
30
|
+
else input.style.removeProperty("--slidebox-range-stretch");
|
|
31
|
+
if (current.progressStyle) input.style.setProperty("--slidebox-range-progress", current.progressStyle, current.progressPriority);
|
|
32
|
+
else input.style.removeProperty("--slidebox-range-progress");
|
|
33
|
+
}
|
|
34
|
+
function start() {
|
|
35
|
+
if (motion) return motion;
|
|
36
|
+
if (!input.isConnected || !input.parentElement || input.matches(":disabled") || view.matchMedia("(prefers-reduced-motion: reduce)").matches || view.matchMedia("(forced-colors: active)").matches) return;
|
|
37
|
+
const computed = view.getComputedStyle(input);
|
|
38
|
+
const deform = Math.max(0, Math.min(1, Number.parseFloat(computed.getPropertyValue("--_deform")) || 0));
|
|
39
|
+
const time = computed.getPropertyValue("--_duration").trim();
|
|
40
|
+
const duration = (Number.parseFloat(time) || 0) * (time.endsWith("ms") ? 1 : 1000);
|
|
41
|
+
const box = input.getBoundingClientRect();
|
|
42
|
+
if (!deform || !duration || box.height <= 0 || box.width <= box.height) return;
|
|
43
|
+
// Keep the native thumb's layout/hit box constant. Resizing it changes the
|
|
44
|
+
// browser's pointer-to-value mapping mid-drag and makes the head jump.
|
|
45
|
+
const visual = input.ownerDocument.createElement("span");
|
|
46
|
+
visual.setAttribute("data-slidebox-range-visual", "");
|
|
47
|
+
visual.setAttribute("aria-hidden", "true");
|
|
48
|
+
Object.assign(visual.style, {
|
|
49
|
+
position: "absolute", display: "flex", alignItems: "center",
|
|
50
|
+
boxSizing: "border-box", pointerEvents: "none", zIndex: "1",
|
|
51
|
+
border: "1px solid transparent", padding: "0", margin: "0",
|
|
52
|
+
direction: computed.direction, colorScheme: computed.colorScheme,
|
|
53
|
+
});
|
|
54
|
+
const thumb = input.ownerDocument.createElement("span");
|
|
55
|
+
const size = Number.parseFloat(computed.height);
|
|
56
|
+
const spacing = Number.parseFloat(computed.getPropertyValue("--_spacing")) || 0;
|
|
57
|
+
Object.assign(thumb.style, {
|
|
58
|
+
display: "block", flex: "1 1 auto", minWidth: "0", boxSizing: "border-box",
|
|
59
|
+
height: `${size * (1 - spacing)}px`, width: `${size * (1 - spacing)}px`,
|
|
60
|
+
border: `1px solid ${computed.getPropertyValue("--_track").trim()}`,
|
|
61
|
+
borderRadius: computed.getPropertyValue("--slidebox-radius").trim() || "100vw",
|
|
62
|
+
background: computed.getPropertyValue("--_on").trim(), pointerEvents: "none",
|
|
63
|
+
});
|
|
64
|
+
visual.append(thumb);
|
|
65
|
+
input.after(visual);
|
|
66
|
+
motion = {
|
|
67
|
+
deform, duration, tail: previous, target: previous, direction: 0,
|
|
68
|
+
visual, thumb, size, gap: Math.max(0, size * spacing / 2),
|
|
69
|
+
travelSize: size * (1 - spacing) + 2 * Math.max(0, size * spacing / 2),
|
|
70
|
+
holdUntil: view.performance.now(),
|
|
71
|
+
distanceRatio: (box.width - box.height) / box.height,
|
|
72
|
+
attribute: input.getAttribute("data-slidebox-range-dragging"),
|
|
73
|
+
style: input.style.getPropertyValue("--slidebox-range-stretch"),
|
|
74
|
+
priority: input.style.getPropertyPriority("--slidebox-range-stretch"),
|
|
75
|
+
progressStyle: input.style.getPropertyValue("--slidebox-range-progress"),
|
|
76
|
+
progressPriority: input.style.getPropertyPriority("--slidebox-range-progress"),
|
|
77
|
+
};
|
|
78
|
+
input.setAttribute("data-slidebox-range-dragging", "");
|
|
79
|
+
return motion;
|
|
80
|
+
}
|
|
81
|
+
function draw() {
|
|
82
|
+
// Clamp the tail in progress space: it cannot extend beyond either endpoint.
|
|
83
|
+
const lag = Math.max(-motion.deform, Math.min(motion.deform, motion.target - motion.tail));
|
|
84
|
+
input.style.setProperty("--slidebox-range-stretch", String(lag * motion.distanceRatio));
|
|
85
|
+
input.style.setProperty("--slidebox-range-progress", String(motion.target));
|
|
86
|
+
const tail = motion.target - lag;
|
|
87
|
+
const start = Math.min(motion.target, tail);
|
|
88
|
+
const end = 1 - Math.max(motion.target, tail);
|
|
89
|
+
motion.thumb.style.marginInlineStart = `calc((100% - ${motion.travelSize}px) * ${start} + ${motion.gap}px)`;
|
|
90
|
+
motion.thumb.style.marginInlineEnd = `calc((100% - ${motion.travelSize}px) * ${end} + ${motion.gap}px)`;
|
|
91
|
+
positionVisual();
|
|
92
|
+
}
|
|
93
|
+
function positionVisual() {
|
|
94
|
+
if (!motion) return;
|
|
95
|
+
const rect = input.getBoundingClientRect();
|
|
96
|
+
const parent = motion.visual.offsetParent;
|
|
97
|
+
let left = rect.left + view.scrollX;
|
|
98
|
+
let top = rect.top + view.scrollY;
|
|
99
|
+
if (parent && !(parent === input.ownerDocument.body && view.getComputedStyle(parent).position === "static")) {
|
|
100
|
+
const box = parent.getBoundingClientRect();
|
|
101
|
+
left = rect.left - box.left - parent.clientLeft + parent.scrollLeft;
|
|
102
|
+
top = rect.top - box.top - parent.clientTop + parent.scrollTop;
|
|
103
|
+
}
|
|
104
|
+
Object.assign(motion.visual.style, {
|
|
105
|
+
left: `${left}px`, top: `${top}px`, width: `${rect.width}px`, height: `${rect.height}px`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function animate(now) {
|
|
109
|
+
if (!motion) return;
|
|
110
|
+
if (!input.isConnected || input.matches(":disabled") || view.matchMedia("(prefers-reduced-motion: reduce)").matches || view.matchMedia("(forced-colors: active)").matches) { stop(); return; }
|
|
111
|
+
const from = Math.max(motion.lastFrame ?? now, motion.holdUntil);
|
|
112
|
+
const elapsed = Math.max(0, now - from);
|
|
113
|
+
motion.lastFrame = now;
|
|
114
|
+
// The leading edge is always positioned by the browser's native value.
|
|
115
|
+
// Only the tail eases toward that destination after the initial delay.
|
|
116
|
+
motion.tail += (motion.target - motion.tail) * (1 - Math.exp(-elapsed / Math.max(16, motion.duration / 3)));
|
|
117
|
+
if (Math.abs(motion.target - motion.tail) < .0001) { stop(); return; }
|
|
118
|
+
draw();
|
|
119
|
+
motion.frame = view.requestAnimationFrame(animate);
|
|
120
|
+
}
|
|
121
|
+
function changed() {
|
|
122
|
+
const next = progress();
|
|
123
|
+
if (next === previous) return;
|
|
124
|
+
const current = start();
|
|
125
|
+
if (current) {
|
|
126
|
+
const direction = Math.sign(next - current.target);
|
|
127
|
+
if (direction && direction !== current.direction) {
|
|
128
|
+
// On reversal, the old leading edge becomes the new trailing edge.
|
|
129
|
+
current.tail = current.target;
|
|
130
|
+
current.direction = direction;
|
|
131
|
+
current.holdUntil = view.performance.now() + current.duration * current.deform;
|
|
132
|
+
}
|
|
133
|
+
current.target = next;
|
|
134
|
+
draw();
|
|
135
|
+
if (current.frame === undefined) current.frame = view.requestAnimationFrame(animate);
|
|
136
|
+
}
|
|
137
|
+
previous = next;
|
|
138
|
+
}
|
|
139
|
+
function baseline() { if (!motion) previous = progress(); }
|
|
140
|
+
function cancel() { stop(); previous = progress(); }
|
|
141
|
+
input.addEventListener("pointerdown", baseline);
|
|
142
|
+
input.addEventListener("keydown", baseline);
|
|
143
|
+
input.addEventListener("input", changed);
|
|
144
|
+
input.addEventListener("pointercancel", cancel);
|
|
145
|
+
view.addEventListener("blur", cancel);
|
|
146
|
+
view.addEventListener("scroll", positionVisual, true);
|
|
147
|
+
view.addEventListener("resize", positionVisual);
|
|
148
|
+
const form = input.form;
|
|
149
|
+
form?.addEventListener("reset", cancel);
|
|
150
|
+
|
|
151
|
+
function destroy() {
|
|
152
|
+
cancel();
|
|
153
|
+
input.removeEventListener("pointerdown", baseline);
|
|
154
|
+
input.removeEventListener("keydown", baseline);
|
|
155
|
+
input.removeEventListener("input", changed);
|
|
156
|
+
input.removeEventListener("pointercancel", cancel);
|
|
157
|
+
view.removeEventListener("blur", cancel);
|
|
158
|
+
view.removeEventListener("scroll", positionVisual, true);
|
|
159
|
+
view.removeEventListener("resize", positionVisual);
|
|
160
|
+
form?.removeEventListener("reset", cancel);
|
|
161
|
+
enhancements.delete(input);
|
|
162
|
+
}
|
|
163
|
+
enhancements.set(input, destroy);
|
|
164
|
+
return destroy;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const fills = new WeakMap();
|
|
168
|
+
|
|
169
|
+
/** Optional progress paint; independent of directional motion. */
|
|
170
|
+
export function enhanceSlideboxRangeFill(input) {
|
|
171
|
+
if (input?.tagName !== "INPUT" || input.type !== "range") {
|
|
172
|
+
throw new TypeError("enhanceSlideboxRangeFill expects a range input");
|
|
173
|
+
}
|
|
174
|
+
if (fills.has(input)) return fills.get(input);
|
|
175
|
+
const view = input.ownerDocument.defaultView;
|
|
176
|
+
if (!view) throw new Error("Slidebox range fill requires a mounted browser document");
|
|
177
|
+
const attribute = input.getAttribute("data-slidebox-range-fill");
|
|
178
|
+
const value = input.style.getPropertyValue("--slidebox-range-fill");
|
|
179
|
+
const priority = input.style.getPropertyPriority("--slidebox-range-fill");
|
|
180
|
+
let resetFrame;
|
|
181
|
+
function update() {
|
|
182
|
+
const min = Number.parseFloat(input.min) || 0;
|
|
183
|
+
const parsedMax = Number.parseFloat(input.max);
|
|
184
|
+
const max = Number.isFinite(parsedMax) ? parsedMax : 100;
|
|
185
|
+
const progress = max > min ? clamp((input.valueAsNumber - min) / (max - min)) : 0;
|
|
186
|
+
input.style.setProperty("--slidebox-range-fill", String(progress));
|
|
187
|
+
}
|
|
188
|
+
function reset() {
|
|
189
|
+
view.cancelAnimationFrame(resetFrame);
|
|
190
|
+
// Native reset applies defaultValue after dispatching the reset event.
|
|
191
|
+
resetFrame = view.requestAnimationFrame(update);
|
|
192
|
+
}
|
|
193
|
+
input.setAttribute("data-slidebox-range-fill", "");
|
|
194
|
+
input.addEventListener("input", update);
|
|
195
|
+
input.addEventListener("change", update);
|
|
196
|
+
const form = input.form;
|
|
197
|
+
form?.addEventListener("reset", reset);
|
|
198
|
+
update();
|
|
199
|
+
|
|
200
|
+
function destroy() {
|
|
201
|
+
view.cancelAnimationFrame(resetFrame);
|
|
202
|
+
input.removeEventListener("input", update);
|
|
203
|
+
input.removeEventListener("change", update);
|
|
204
|
+
form?.removeEventListener("reset", reset);
|
|
205
|
+
if (attribute === null) input.removeAttribute("data-slidebox-range-fill");
|
|
206
|
+
else input.setAttribute("data-slidebox-range-fill", attribute);
|
|
207
|
+
if (value) input.style.setProperty("--slidebox-range-fill", value, priority);
|
|
208
|
+
else input.style.removeProperty("--slidebox-range-fill");
|
|
209
|
+
fills.delete(input);
|
|
210
|
+
}
|
|
211
|
+
fills.set(input, destroy);
|
|
212
|
+
return destroy;
|
|
213
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Generated by scripts/build.mjs.
|
|
2
|
+
.slidebox-experimental-option(@name; @value) when not (@value = false) {
|
|
3
|
+
--slidebox-@{name}: @value;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.slidebox-range(@width: false; @spacing: false; @deform: false; @duration: false; @easing: false; @track: false; @border: false; @off: false; @on: false; @size: false; @radius: false; @text: false; @selected-text: false) {
|
|
7
|
+
.slidebox-experimental-option(width; @width);
|
|
8
|
+
.slidebox-experimental-option(spacing; @spacing);
|
|
9
|
+
.slidebox-experimental-option(deform; @deform);
|
|
10
|
+
.slidebox-experimental-option(duration; @duration);
|
|
11
|
+
.slidebox-experimental-option(easing; @easing);
|
|
12
|
+
.slidebox-experimental-option(track; @track);
|
|
13
|
+
.slidebox-experimental-option(border; @border);
|
|
14
|
+
.slidebox-experimental-option(off; @off);
|
|
15
|
+
.slidebox-experimental-option(on; @on);
|
|
16
|
+
.slidebox-experimental-option(size; @size);
|
|
17
|
+
.slidebox-experimental-option(radius; @radius);
|
|
18
|
+
.slidebox-experimental-option(text; @text);
|
|
19
|
+
.slidebox-experimental-option(selected-text; @selected-text);
|
|
20
|
+
& {
|
|
21
|
+
--_size: var(--slidebox-size, 24px);
|
|
22
|
+
--_spacing: var(--slidebox-spacing, .33);
|
|
23
|
+
--_range-inset: ~"max(0px, calc(var(--_size) * var(--_spacing) * .5))";
|
|
24
|
+
--_thumb-size: calc(var(--_size) * (1 - var(--_spacing)));
|
|
25
|
+
--_deform: var(--slidebox-deform, .1);
|
|
26
|
+
--_duration: var(--slidebox-duration, 200ms);
|
|
27
|
+
--_easing: var(--slidebox-easing, ease-in-out);
|
|
28
|
+
--_track: var(--slidebox-track, Field);
|
|
29
|
+
--_border: var(--slidebox-border, ButtonBorder);
|
|
30
|
+
--_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
|
|
31
|
+
appearance: none;
|
|
32
|
+
-webkit-appearance: none;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
width: 100%;
|
|
35
|
+
min-width: 0;
|
|
36
|
+
height: var(--_size);
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding-block: 0;
|
|
39
|
+
padding-inline: var(--_range-inset);
|
|
40
|
+
border: 1px solid var(--_border);
|
|
41
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
42
|
+
background: var(--_track);
|
|
43
|
+
color: var(--_on);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
accent-color: var(--_on);
|
|
46
|
+
}
|
|
47
|
+
&[data-slidebox-range-fill] {
|
|
48
|
+
--_fill-size: ~"max(0px, min(var(--_thumb-size), calc(var(--_size) - 2px)))";
|
|
49
|
+
--_fill-stop: ~"clamp(0px, calc(var(--_thumb-size) * .5 + (100% - var(--_thumb-size)) * clamp(0, var(--slidebox-range-fill, 0), 1)), 100%)";
|
|
50
|
+
background-image:
|
|
51
|
+
radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
|
|
52
|
+
linear-gradient(to right, transparent 0 calc(var(--_fill-size) * .5), var(--_fill-color, var(--_on)) calc(var(--_fill-size) * .5) var(--_fill-stop), transparent var(--_fill-stop)),
|
|
53
|
+
var(--_range-track-image, none);
|
|
54
|
+
background-size: ~"var(--_fill-size) var(--_fill-size), max(0px, calc(100% - 2 * var(--_range-inset))) var(--_fill-size), 100vw 100vh";
|
|
55
|
+
background-position: left var(--_range-inset) center, center, 0 0;
|
|
56
|
+
background-repeat: no-repeat;
|
|
57
|
+
background-attachment: scroll, scroll, fixed;
|
|
58
|
+
background-clip: padding-box;
|
|
59
|
+
}
|
|
60
|
+
&[data-slidebox-range-fill]:dir(rtl) {
|
|
61
|
+
background-image:
|
|
62
|
+
radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
|
|
63
|
+
linear-gradient(to left, transparent 0 calc(var(--_fill-size) * .5), var(--_fill-color, var(--_on)) calc(var(--_fill-size) * .5) var(--_fill-stop), transparent var(--_fill-stop)),
|
|
64
|
+
var(--_range-track-image, none);
|
|
65
|
+
background-position: right var(--_range-inset) center, center, 0 0;
|
|
66
|
+
}
|
|
67
|
+
&::-webkit-slider-runnable-track {
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
height: calc(var(--_size) - 2px);
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
72
|
+
background: transparent;
|
|
73
|
+
}
|
|
74
|
+
&::-moz-range-track {
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
height: calc(var(--_size) - 2px);
|
|
77
|
+
border: 0;
|
|
78
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
79
|
+
background: transparent;
|
|
80
|
+
}
|
|
81
|
+
&::-webkit-slider-thumb {
|
|
82
|
+
appearance: none;
|
|
83
|
+
-webkit-appearance: none;
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
width: calc(var(--_size) * (1 - var(--_spacing)));
|
|
86
|
+
height: calc(var(--_size) * (1 - var(--_spacing)));
|
|
87
|
+
margin-top: calc(var(--_size) * var(--_spacing) / 2 - 1px);
|
|
88
|
+
border: 1px solid var(--_track);
|
|
89
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
90
|
+
background: var(--_on);
|
|
91
|
+
transition: none;
|
|
92
|
+
}
|
|
93
|
+
&::-moz-range-thumb {
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
width: calc(var(--_size) * (1 - var(--_spacing)));
|
|
96
|
+
height: calc(var(--_size) * (1 - var(--_spacing)));
|
|
97
|
+
border: 1px solid var(--_track);
|
|
98
|
+
border-radius: var(--slidebox-radius, 100vw);
|
|
99
|
+
background: var(--_on);
|
|
100
|
+
transition: none;
|
|
101
|
+
}
|
|
102
|
+
&:focus-visible { outline: 2px solid var(--_on); outline-offset: 3px; }
|
|
103
|
+
&[data-slidebox-range-dragging]::-webkit-slider-thumb { background: transparent; border-color: transparent; transition: none; }
|
|
104
|
+
&[data-slidebox-range-dragging]::-moz-range-thumb { background: transparent; border-color: transparent; transition: none; }
|
|
105
|
+
&:disabled { cursor: not-allowed; opacity: .5; }
|
|
106
|
+
@supports (color: AccentColor) {
|
|
107
|
+
& { --_accent: AccentColor; }
|
|
108
|
+
}
|
|
109
|
+
@media (forced-colors: active) {
|
|
110
|
+
& { --_track: Field; --_border: ButtonBorder; --_on: Highlight; }
|
|
111
|
+
&::-webkit-slider-thumb { border-color: Highlight; }
|
|
112
|
+
&::-moz-range-thumb { border-color: Highlight; }
|
|
113
|
+
}
|
|
114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
115
|
+
& { transition-duration: .01ms; }
|
|
116
|
+
&::-webkit-slider-thumb { transition-duration: .01ms; }
|
|
117
|
+
&::-moz-range-thumb { transition-duration: .01ms; }
|
|
118
|
+
}
|
|
119
|
+
}
|