@monabbous/slidebox 1.1.0 → 1.3.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,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
+ }
@@ -43,6 +43,7 @@
43
43
  }
44
44
  &::after {
45
45
  --_start: 0; --_end: 1;
46
+ box-sizing: border-box;
46
47
  content: "";
47
48
  display: block;
48
49
  flex: 1 1 auto;
@@ -53,6 +54,7 @@
53
54
  margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
54
55
  margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
55
56
  border-radius: inherit;
57
+ border: 1px solid var(--_track);
56
58
  background: var(--_off);
57
59
  transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
58
60
  transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
@@ -65,6 +67,11 @@
65
67
  &:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
66
68
  &:checked:active::after { --_start: calc(1 - var(--_deform)); }
67
69
  &:disabled { cursor: not-allowed; opacity: .5; }
70
+ &[data-slidebox-dragging]::after {
71
+ margin-inline-start: ~"calc((1em * var(--_container-width-ratio) - 1em) * max(0, var(--slidebox-drag-progress) - max(0, var(--slidebox-drag-stretch, 0))) + var(--_thumb-spacing-ratio) * .5em)";
72
+ margin-inline-end: ~"calc((1em * var(--_container-width-ratio) - 1em) * max(0, 1 - var(--slidebox-drag-progress) - max(0, -1 * var(--slidebox-drag-stretch, 0))) + var(--_thumb-spacing-ratio) * .5em)";
73
+ transition: none;
74
+ }
68
75
  @media (forced-colors: active) {
69
76
  & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
70
77
  }
@@ -72,3 +79,140 @@
72
79
  &::after { transition-duration: .01ms; transition-delay: 0ms; }
73
80
  }
74
81
  }
82
+
83
+ .slidebox-group(@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) {
84
+ .slidebox-option(width; @width);
85
+ .slidebox-option(spacing; @spacing);
86
+ .slidebox-option(deform; @deform);
87
+ .slidebox-option(duration; @duration);
88
+ .slidebox-option(easing; @easing);
89
+ .slidebox-option(track; @track);
90
+ .slidebox-option(border; @border);
91
+ .slidebox-option(off; @off);
92
+ .slidebox-option(on; @on);
93
+ .slidebox-option(size; @size);
94
+ .slidebox-option(radius; @radius);
95
+ .slidebox-option(text; @text);
96
+ .slidebox-option(selected-text; @selected-text);
97
+ & {
98
+ --_size: var(--slidebox-size, 36px);
99
+ --_spacing: var(--slidebox-spacing, .12);
100
+ --_deform: var(--slidebox-deform, .06);
101
+ --_duration: var(--slidebox-duration, 200ms);
102
+ --_easing: var(--slidebox-easing, ease-in-out);
103
+ --_track: var(--slidebox-track, Field);
104
+ --_border: var(--slidebox-border, ButtonBorder);
105
+ --_off: var(--slidebox-off, FieldText);
106
+ --_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
107
+ --_text: var(--slidebox-text, var(--slidebox-off, FieldText));
108
+ --_selected-text: var(--slidebox-selected-text, HighlightText);
109
+ --_press-start: 0px;
110
+ --_press-end: 0px;
111
+ display: inline-grid;
112
+ grid-auto-flow: column;
113
+ grid-auto-columns: minmax(0, 1fr);
114
+ box-sizing: border-box;
115
+ padding: calc(var(--_size) * var(--_spacing));
116
+ gap: calc(var(--_size) * var(--_spacing));
117
+ border: 1px solid var(--_border);
118
+ border-radius: var(--slidebox-radius, 100vw);
119
+ background: var(--_track);
120
+ color: var(--_text);
121
+ isolation: isolate;
122
+ position: relative;
123
+ }
124
+ & > label {
125
+ position: relative;
126
+ display: grid;
127
+ min-width: 0;
128
+ cursor: pointer;
129
+ }
130
+ & > label > input[type="radio"] {
131
+ position: absolute;
132
+ width: 1px;
133
+ height: 1px;
134
+ padding: 0;
135
+ margin: -1px;
136
+ overflow: hidden;
137
+ clip-path: inset(50%);
138
+ white-space: nowrap;
139
+ }
140
+ & > label > span {
141
+ position: relative;
142
+ isolation: isolate;
143
+ display: grid;
144
+ place-items: center;
145
+ box-sizing: border-box;
146
+ min-height: var(--_size);
147
+ padding: .35em 1em;
148
+ border: 1px solid transparent;
149
+ border-radius: var(--slidebox-radius, 100vw);
150
+ color: var(--_text);
151
+ background: transparent;
152
+ transition: color var(--_duration) var(--_easing);
153
+ }
154
+ & > label > input:checked + span {
155
+ color: var(--_selected-text);
156
+ }
157
+ & > label > input:checked + span::before {
158
+ content: "";
159
+ position: absolute;
160
+ inset: 0;
161
+ z-index: -1;
162
+ pointer-events: none;
163
+ border-radius: inherit;
164
+ background: var(--_on);
165
+ margin-inline-start: var(--_press-start);
166
+ margin-inline-end: var(--_press-end);
167
+ transition: margin-inline-start var(--_duration) var(--_easing), margin-inline-end var(--_duration) var(--_easing);
168
+ }
169
+ & > label > input:not(:checked) + span { color: var(--_text); }
170
+ &:has(> label > input:checked):has(> label:active > input:not(:checked):not(:disabled)):not(:has(> label:active ~ label > input:checked)) {
171
+ --_press-end: calc(-1 * var(--_size) * var(--_deform));
172
+ }
173
+ &:has(> label:active > input:not(:disabled)):has(> label:active ~ label > input:checked) {
174
+ --_press-start: calc(-1 * var(--_size) * var(--_deform));
175
+ }
176
+ & > label > input:focus-visible + span {
177
+ outline: 2px solid var(--_on);
178
+ outline-offset: 2px;
179
+ }
180
+ & > label:has(> input:disabled) { cursor: not-allowed; }
181
+ & > label > input:disabled + span { opacity: .5; }
182
+ @supports (color: AccentColor) {
183
+ & { --_accent: AccentColor; }
184
+ }
185
+ /* A shared sliding indicator where scoped CSS anchor positioning is available.
186
+ Other browsers retain the per-segment highlight above. Neither needs JS. */
187
+ @supports (anchor-scope: --slidebox-selection) and (left: anchor(left)) {
188
+ & { anchor-scope: --slidebox-selection; }
189
+ & > label:has(> input:checked) { anchor-name: --slidebox-selection; }
190
+ &:has(> label > input:checked)::before {
191
+ content: "";
192
+ position: absolute;
193
+ position-anchor: --slidebox-selection;
194
+ left: ~"anchor(left)";
195
+ right: ~"anchor(right)";
196
+ top: ~"anchor(top)";
197
+ height: ~"anchor-size(height)";
198
+ margin-inline-start: var(--_press-start);
199
+ margin-inline-end: var(--_press-end);
200
+ z-index: -1;
201
+ border-radius: var(--slidebox-radius, 100vw);
202
+ background: var(--_on);
203
+ pointer-events: none;
204
+ 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);
205
+ }
206
+ & > label > input:checked + span::before { content: none; }
207
+ }
208
+ @media (forced-colors: active) {
209
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; --_text: FieldText; --_selected-text: HighlightText; }
210
+ & > label > input:checked + span { border-color: Highlight; outline: 2px solid Highlight; outline-offset: -2px; }
211
+ & > label > input:disabled + span { color: GrayText; }
212
+ }
213
+ @media (prefers-reduced-motion: reduce) {
214
+ & > label > span { transition-duration: .01ms; }
215
+ & > label > input:checked + span::before { transition-duration: .01ms; }
216
+ &::before { transition-duration: .01ms; }
217
+ }
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monabbous/slidebox",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Elastic, system-color-aware CSS checkbox switch.",
5
5
  "license": "ISC",
6
6
  "style": "./dist/css/slidebox.css",
@@ -8,9 +8,11 @@
8
8
  "less": "./less/slidebox.less",
9
9
  "files": [
10
10
  "dist/css/slidebox.css",
11
+ "dist/css/group.css",
11
12
  "scss/",
12
13
  "less/",
13
- "tailwind/"
14
+ "tailwind/",
15
+ "experimental/"
14
16
  ],
15
17
  "publishConfig": {
16
18
  "access": "public"
@@ -34,6 +36,7 @@
34
36
  "scripts": {
35
37
  "build": "node scripts/build.mjs",
36
38
  "test": "node --test tests/*.test.mjs",
39
+ "test:browser": "playwright test",
37
40
  "prepack": "npm run build"
38
41
  },
39
42
  "keywords": [
@@ -47,6 +50,7 @@
47
50
  "system-colors"
48
51
  ],
49
52
  "devDependencies": {
53
+ "@playwright/test": "^1.64.1",
50
54
  "@tailwindcss/node": "^4.3.3",
51
55
  "less": "^4.9.1",
52
56
  "postcss": "^8.5.32",
@@ -0,0 +1,109 @@
1
+ // Generated by scripts/build.mjs.
2
+ @use "sass:map";
3
+
4
+ $defaults: () !default;
5
+
6
+ @mixin range($options: ()) {
7
+ @each $name, $value in map.merge($defaults, $options) {
8
+ --slidebox-#{$name}: #{$value};
9
+ }
10
+ & {
11
+ --_size: var(--slidebox-size, 24px);
12
+ --_spacing: var(--slidebox-spacing, .33);
13
+ --_range-inset: max(0px, calc(var(--_size) * var(--_spacing) * .5));
14
+ --_thumb-size: calc(var(--_size) * (1 - var(--_spacing)));
15
+ --_deform: var(--slidebox-deform, .1);
16
+ --_duration: var(--slidebox-duration, 200ms);
17
+ --_easing: var(--slidebox-easing, ease-in-out);
18
+ --_track: var(--slidebox-track, Field);
19
+ --_border: var(--slidebox-border, ButtonBorder);
20
+ --_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
21
+ appearance: none;
22
+ -webkit-appearance: none;
23
+ box-sizing: border-box;
24
+ width: 100%;
25
+ min-width: 0;
26
+ height: var(--_size);
27
+ margin: 0;
28
+ padding-block: 0;
29
+ padding-inline: var(--_range-inset);
30
+ border: 1px solid var(--_border);
31
+ border-radius: var(--slidebox-radius, 100vw);
32
+ background: var(--_track);
33
+ color: var(--_on);
34
+ cursor: pointer;
35
+ accent-color: var(--_on);
36
+ }
37
+ &[data-slidebox-range-fill] {
38
+ --_fill-size: max(0px, min(var(--_thumb-size), calc(var(--_size) - 2px)));
39
+ --_fill-stop: clamp(0px, calc(var(--_thumb-size) * .5 + (100% - var(--_thumb-size)) * clamp(0, var(--slidebox-range-fill, 0), 1)), 100%);
40
+ background-image:
41
+ radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
42
+ 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)),
43
+ var(--_range-track-image, none);
44
+ background-size: var(--_fill-size) var(--_fill-size), max(0px, calc(100% - 2 * var(--_range-inset))) var(--_fill-size), 100vw 100vh;
45
+ background-position: left var(--_range-inset) center, center, 0 0;
46
+ background-repeat: no-repeat;
47
+ background-attachment: scroll, scroll, fixed;
48
+ background-clip: padding-box;
49
+ }
50
+ &[data-slidebox-range-fill]:dir(rtl) {
51
+ background-image:
52
+ radial-gradient(circle closest-side, var(--_fill-color, var(--_on)) calc(100% - .5px), transparent 100%),
53
+ 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)),
54
+ var(--_range-track-image, none);
55
+ background-position: right var(--_range-inset) center, center, 0 0;
56
+ }
57
+ &::-webkit-slider-runnable-track {
58
+ box-sizing: border-box;
59
+ height: calc(var(--_size) - 2px);
60
+ border: 0;
61
+ border-radius: var(--slidebox-radius, 100vw);
62
+ background: transparent;
63
+ }
64
+ &::-moz-range-track {
65
+ box-sizing: border-box;
66
+ height: calc(var(--_size) - 2px);
67
+ border: 0;
68
+ border-radius: var(--slidebox-radius, 100vw);
69
+ background: transparent;
70
+ }
71
+ &::-webkit-slider-thumb {
72
+ appearance: none;
73
+ -webkit-appearance: none;
74
+ box-sizing: border-box;
75
+ width: calc(var(--_size) * (1 - var(--_spacing)));
76
+ height: calc(var(--_size) * (1 - var(--_spacing)));
77
+ margin-top: calc(var(--_size) * var(--_spacing) / 2 - 1px);
78
+ border: 1px solid var(--_track);
79
+ border-radius: var(--slidebox-radius, 100vw);
80
+ background: var(--_on);
81
+ transition: none;
82
+ }
83
+ &::-moz-range-thumb {
84
+ box-sizing: border-box;
85
+ width: calc(var(--_size) * (1 - var(--_spacing)));
86
+ height: calc(var(--_size) * (1 - var(--_spacing)));
87
+ border: 1px solid var(--_track);
88
+ border-radius: var(--slidebox-radius, 100vw);
89
+ background: var(--_on);
90
+ transition: none;
91
+ }
92
+ &:focus-visible { outline: 2px solid var(--_on); outline-offset: 3px; }
93
+ &[data-slidebox-range-dragging]::-webkit-slider-thumb { background: transparent; border-color: transparent; transition: none; }
94
+ &[data-slidebox-range-dragging]::-moz-range-thumb { background: transparent; border-color: transparent; transition: none; }
95
+ &:disabled { cursor: not-allowed; opacity: .5; }
96
+ @supports (color: AccentColor) {
97
+ & { --_accent: AccentColor; }
98
+ }
99
+ @media (forced-colors: active) {
100
+ & { --_track: Field; --_border: ButtonBorder; --_on: Highlight; }
101
+ &::-webkit-slider-thumb { border-color: Highlight; }
102
+ &::-moz-range-thumb { border-color: Highlight; }
103
+ }
104
+ @media (prefers-reduced-motion: reduce) {
105
+ & { transition-duration: .01ms; }
106
+ &::-webkit-slider-thumb { transition-duration: .01ms; }
107
+ &::-moz-range-thumb { transition-duration: .01ms; }
108
+ }
109
+ }
package/scss/_mixins.scss CHANGED
@@ -37,6 +37,7 @@ $defaults: () !default;
37
37
  }
38
38
  &::after {
39
39
  --_start: 0; --_end: 1;
40
+ box-sizing: border-box;
40
41
  content: "";
41
42
  display: block;
42
43
  flex: 1 1 auto;
@@ -47,6 +48,7 @@ $defaults: () !default;
47
48
  margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
48
49
  margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
49
50
  border-radius: inherit;
51
+ border: 1px solid var(--_track);
50
52
  background: var(--_off);
51
53
  transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
52
54
  transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
@@ -59,6 +61,11 @@ $defaults: () !default;
59
61
  &:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
60
62
  &:checked:active::after { --_start: calc(1 - var(--_deform)); }
61
63
  &:disabled { cursor: not-allowed; opacity: .5; }
64
+ &[data-slidebox-dragging]::after {
65
+ margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * max(0, var(--slidebox-drag-progress) - max(0, var(--slidebox-drag-stretch, 0))) + var(--_thumb-spacing-ratio) * .5em);
66
+ margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * max(0, 1 - var(--slidebox-drag-progress) - max(0, -1 * var(--slidebox-drag-stretch, 0))) + var(--_thumb-spacing-ratio) * .5em);
67
+ transition: none;
68
+ }
62
69
  @media (forced-colors: active) {
63
70
  & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
64
71
  }
@@ -66,3 +73,130 @@ $defaults: () !default;
66
73
  &::after { transition-duration: .01ms; transition-delay: 0ms; }
67
74
  }
68
75
  }
76
+
77
+ @mixin group($options: ()) {
78
+ @each $name, $value in map.merge($defaults, $options) {
79
+ --slidebox-#{$name}: #{$value};
80
+ }
81
+ & {
82
+ --_size: var(--slidebox-size, 36px);
83
+ --_spacing: var(--slidebox-spacing, .12);
84
+ --_deform: var(--slidebox-deform, .06);
85
+ --_duration: var(--slidebox-duration, 200ms);
86
+ --_easing: var(--slidebox-easing, ease-in-out);
87
+ --_track: var(--slidebox-track, Field);
88
+ --_border: var(--slidebox-border, ButtonBorder);
89
+ --_off: var(--slidebox-off, FieldText);
90
+ --_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
91
+ --_text: var(--slidebox-text, var(--slidebox-off, FieldText));
92
+ --_selected-text: var(--slidebox-selected-text, HighlightText);
93
+ --_press-start: 0px;
94
+ --_press-end: 0px;
95
+ display: inline-grid;
96
+ grid-auto-flow: column;
97
+ grid-auto-columns: minmax(0, 1fr);
98
+ box-sizing: border-box;
99
+ padding: calc(var(--_size) * var(--_spacing));
100
+ gap: calc(var(--_size) * var(--_spacing));
101
+ border: 1px solid var(--_border);
102
+ border-radius: var(--slidebox-radius, 100vw);
103
+ background: var(--_track);
104
+ color: var(--_text);
105
+ isolation: isolate;
106
+ position: relative;
107
+ }
108
+ & > label {
109
+ position: relative;
110
+ display: grid;
111
+ min-width: 0;
112
+ cursor: pointer;
113
+ }
114
+ & > label > input[type="radio"] {
115
+ position: absolute;
116
+ width: 1px;
117
+ height: 1px;
118
+ padding: 0;
119
+ margin: -1px;
120
+ overflow: hidden;
121
+ clip-path: inset(50%);
122
+ white-space: nowrap;
123
+ }
124
+ & > label > span {
125
+ position: relative;
126
+ isolation: isolate;
127
+ display: grid;
128
+ place-items: center;
129
+ box-sizing: border-box;
130
+ min-height: var(--_size);
131
+ padding: .35em 1em;
132
+ border: 1px solid transparent;
133
+ border-radius: var(--slidebox-radius, 100vw);
134
+ color: var(--_text);
135
+ background: transparent;
136
+ transition: color var(--_duration) var(--_easing);
137
+ }
138
+ & > label > input:checked + span {
139
+ color: var(--_selected-text);
140
+ }
141
+ & > label > input:checked + span::before {
142
+ content: "";
143
+ position: absolute;
144
+ inset: 0;
145
+ z-index: -1;
146
+ pointer-events: none;
147
+ border-radius: inherit;
148
+ background: var(--_on);
149
+ margin-inline-start: var(--_press-start);
150
+ margin-inline-end: var(--_press-end);
151
+ transition: margin-inline-start var(--_duration) var(--_easing), margin-inline-end var(--_duration) var(--_easing);
152
+ }
153
+ & > label > input:not(:checked) + span { color: var(--_text); }
154
+ &:has(> label > input:checked):has(> label:active > input:not(:checked):not(:disabled)):not(:has(> label:active ~ label > input:checked)) {
155
+ --_press-end: calc(-1 * var(--_size) * var(--_deform));
156
+ }
157
+ &:has(> label:active > input:not(:disabled)):has(> label:active ~ label > input:checked) {
158
+ --_press-start: calc(-1 * var(--_size) * var(--_deform));
159
+ }
160
+ & > label > input:focus-visible + span {
161
+ outline: 2px solid var(--_on);
162
+ outline-offset: 2px;
163
+ }
164
+ & > label:has(> input:disabled) { cursor: not-allowed; }
165
+ & > label > input:disabled + span { opacity: .5; }
166
+ @supports (color: AccentColor) {
167
+ & { --_accent: AccentColor; }
168
+ }
169
+ /* A shared sliding indicator where scoped CSS anchor positioning is available.
170
+ Other browsers retain the per-segment highlight above. Neither needs JS. */
171
+ @supports (anchor-scope: --slidebox-selection) and (left: anchor(left)) {
172
+ & { anchor-scope: --slidebox-selection; }
173
+ & > label:has(> input:checked) { anchor-name: --slidebox-selection; }
174
+ &:has(> label > input:checked)::before {
175
+ content: "";
176
+ position: absolute;
177
+ position-anchor: --slidebox-selection;
178
+ left: anchor(left);
179
+ right: anchor(right);
180
+ top: anchor(top);
181
+ height: anchor-size(height);
182
+ margin-inline-start: var(--_press-start);
183
+ margin-inline-end: var(--_press-end);
184
+ z-index: -1;
185
+ border-radius: var(--slidebox-radius, 100vw);
186
+ background: var(--_on);
187
+ pointer-events: none;
188
+ 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);
189
+ }
190
+ & > label > input:checked + span::before { content: none; }
191
+ }
192
+ @media (forced-colors: active) {
193
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; --_text: FieldText; --_selected-text: HighlightText; }
194
+ & > label > input:checked + span { border-color: Highlight; outline: 2px solid Highlight; outline-offset: -2px; }
195
+ & > label > input:disabled + span { color: GrayText; }
196
+ }
197
+ @media (prefers-reduced-motion: reduce) {
198
+ & > label > span { transition-duration: .01ms; }
199
+ & > label > input:checked + span::before { transition-duration: .01ms; }
200
+ &::before { transition-duration: .01ms; }
201
+ }
202
+ }
@@ -0,0 +1,2 @@
1
+ @use "mixins";
2
+ .slidebox-group { @include mixins.group(); }