@monabbous/slidebox 1.0.0 → 1.2.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.
package/README.md CHANGED
@@ -1,65 +1,297 @@
1
- # Slidebox Lab
1
+ # Slidebox
2
2
 
3
- An elastic CSS switch with a playground for geometry, colors, and motion.
3
+ An elastic CSS switch built on a native checkbox. No JavaScript, extra elements, or runtime dependencies required. Add optional [experimental pointer dragging](#experimental-pointer-dragging) when you want the thumb to follow your pointer.
4
4
 
5
- [Preview the interactive playground online](https://htmlpreview.github.io/?https://github.com/monabbous/slidebox/blob/main/index.html)
5
+ ![Slidebox switch animation](assets/slidebox-demo.webp)
6
6
 
7
- ![Slidebox Lab demo: default geometry, 200ms ease-in-out](assets/slidebox-demo.webp)
7
+ [Try the playground](https://htmlpreview.github.io/?https://github.com/monabbous/slidebox/blob/main/index.html) · [npm](https://www.npmjs.com/package/@monabbous/slidebox)
8
8
 
9
- The animation uses the default width (2), thumb spacing (0.33), deformation (0.1), and a 200ms `ease-in-out` transition.
9
+ ## Install
10
10
 
11
- ## npm package
11
+ ```sh
12
+ npm install @monabbous/slidebox
13
+ ```
12
14
 
13
- The published CSS package keeps the name [`@monabbous/slidebox`](https://www.npmjs.com/package/@monabbous/slidebox).
14
- The package includes the switch stylesheet at `dist/css/slidebox.css` and a Sass-compatible entrypoint at `scss/slidebox.scss`. The playground is not included in npm.
15
+ Choose **one** integration below: CSS, Sass, Less, or Tailwind. You don't need to import the plain CSS when using a mixin or the Tailwind plugin.
15
16
 
16
- ```bash
17
- npm install @monabbous/slidebox
17
+ ## Plain CSS
18
+
19
+ Import the stylesheet through your bundler:
20
+
21
+ ```js
22
+ import "@monabbous/slidebox/dist/css/slidebox.css";
23
+ ```
24
+
25
+ Add the class to a checkbox and give it an accessible label:
26
+
27
+ ```html
28
+ <label>
29
+ <input class="slidebox" type="checkbox" role="switch" name="notifications">
30
+ Enable notifications
31
+ </label>
32
+ ```
33
+
34
+ Use normal checkbox attributes: `checked`, `disabled`, `required`, `name`, and `value`. Read or change its state with your framework's normal checkbox binding. `role="switch"` is optional; omit it for checkbox semantics.
35
+
36
+ ### Set defaults and override individual switches
37
+
38
+ Public custom properties inherit, so you can set site-wide defaults on `:root`, section defaults on a parent, or overrides on a single input:
39
+
40
+ ```css
41
+ :root {
42
+ color-scheme: light dark;
43
+ --slidebox-size: 24px;
44
+ --slidebox-on: #7c3aed;
45
+ --slidebox-duration: 250ms;
46
+ }
47
+
48
+ .settings {
49
+ --slidebox-width: 2.4;
50
+ }
51
+
52
+ .slidebox.compact {
53
+ --slidebox-size: 18px;
54
+ --slidebox-spacing: 0.25;
55
+ }
56
+ ```
57
+
58
+ `color-scheme: light dark` lets the system colors follow the user's theme. Use `light` or `dark` to force a scheme.
59
+
60
+ ## Controls
61
+
62
+ The same names work as CSS properties (`--slidebox-NAME`), Sass map keys, Less arguments (`@NAME`), and Tailwind plugin options/utilities.
63
+
64
+ | Control | Default | Meaning |
65
+ | --- | --- | --- |
66
+ | `size` | `54px` | Switch height; width and thumb geometry scale with it. Use a CSS length. |
67
+ | `width` | `2` | Track width divided by height. Use a unitless number greater than 1. |
68
+ | `spacing` | `.33` | Space around the thumb as a fraction of the height. Use a number from 0 to less than 1. |
69
+ | `deform` | `.1` | Elastic press deformation and motion delay ratio. Use a number from 0 to less than 1; 0 removes the deformation. |
70
+ | `duration` | `200ms` | Animation duration, including a time unit. |
71
+ | `easing` | `ease-in-out` | Any CSS timing function, including `cubic-bezier()` and `linear()`. |
72
+ | `track` | `Field` | Track background color. |
73
+ | `border` | `ButtonBorder` | Track border color. |
74
+ | `off` | `FieldText` | Unchecked thumb color. |
75
+ | `on` | System accent, when supported | Checked thumb color; falls back to `light-dark(#0b75ff, SelectedItem)`. |
76
+
77
+ Custom colors do not override forced-colors accessibility mode. Reduced-motion users get effectively instant transitions. Logical margins support both LTR and RTL layouts. Keep the browser's focus outline, or provide a visible `:focus-visible` style yourself.
78
+
79
+ ## Sass mixin
80
+
81
+ Use Dart Sass's module system. Configure shared defaults with `$defaults`, then override any control per mixin call:
82
+
83
+ ```scss
84
+ @use "@monabbous/slidebox/scss/mixins" as slidebox with (
85
+ $defaults: (
86
+ size: 24px,
87
+ on: #7c3aed,
88
+ duration: 250ms
89
+ )
90
+ );
91
+
92
+ .toggle {
93
+ @include slidebox.slidebox();
94
+ }
95
+
96
+ .toggle-large {
97
+ @include slidebox.slidebox((size: 36px, width: 2.5));
98
+ }
99
+ ```
100
+
101
+ ```html
102
+ <label><input class="toggle" type="checkbox"> Enable notifications</label>
103
+ ```
104
+
105
+ Importing the mixin does not emit CSS until you include it. Configure the module once, before any other module loads it. Your Sass bundler must resolve npm imports (with the Sass CLI, add `--load-path=node_modules`).
106
+
107
+ For the ready-made `.slidebox` class instead of a mixin:
108
+
109
+ ```scss
110
+ @use "@monabbous/slidebox/scss/slidebox";
111
+ ```
112
+
113
+ ## Less mixin
114
+
115
+ Import the mixin, then use named arguments. Create a wrapper mixin for project defaults:
116
+
117
+ ```less
118
+ @import "@monabbous/slidebox/less/slidebox.less";
119
+
120
+ .app-switch(@size: 24px; @on: #7c3aed; @duration: 250ms) {
121
+ .slidebox(@size: @size; @on: @on; @duration: @duration);
122
+ }
123
+
124
+ .toggle { .app-switch(); }
125
+ .toggle-large { .app-switch(@size: 36px); }
126
+ .toggle-wide { .slidebox(@size: 24px; @width: 2.5; @spacing: 0.25); }
127
+ ```
128
+
129
+ The import emits no CSS on its own. Use semicolons between arguments, especially when passing timing functions containing commas. Your Less bundler must resolve npm imports; with the Less CLI, add `--include-path=node_modules`.
130
+
131
+ ## Tailwind CSS
132
+
133
+ The plugin is included in the same npm package and supports Tailwind v3.4 and v4. Install Tailwind separately using its normal setup.
134
+
135
+ ### Tailwind v4
136
+
137
+ Register the plugin in your Tailwind stylesheet. The options block sets project defaults; omit it to keep Slidebox's defaults:
138
+
139
+ ```css
140
+ @import "tailwindcss";
141
+ @plugin "@monabbous/slidebox/tailwind/index.cjs" {
142
+ size: 24px;
143
+ on: #7c3aed;
144
+ duration: 250ms;
145
+ }
146
+ ```
147
+
148
+ ### Tailwind v3
149
+
150
+ Register it in `tailwind.config.cjs`:
151
+
152
+ ```js
153
+ module.exports = {
154
+ content: ["./src/**/*.{html,js,ts,jsx,tsx,vue,svelte}"],
155
+ plugins: [
156
+ require("@monabbous/slidebox/tailwind/index.cjs")({
157
+ size: "24px",
158
+ on: "#7c3aed",
159
+ duration: "250ms",
160
+ }),
161
+ ],
162
+ };
163
+ ```
164
+
165
+ ### Utilities and overrides
166
+
167
+ Add `slidebox` for the component, then control it with `slidebox-NAME-[VALUE]` utilities:
168
+
169
+ ```html
170
+ <label>
171
+ <input
172
+ type="checkbox"
173
+ class="slidebox slidebox-size-[28px] slidebox-width-[2.5] slidebox-spacing-[0.25] slidebox-deform-[0.15] slidebox-duration-[300ms] slidebox-on-violet-600 dark:slidebox-on-violet-400"
174
+ >
175
+ Enable notifications
176
+ </label>
18
177
  ```
19
178
 
20
- Import `@monabbous/slidebox/dist/css/slidebox.css` in a stylesheet or through your bundler, then use `<input class="slidebox" type="checkbox">`. The package is built from the component rules in `style.css` using `npm run build`.
179
+ - All controls accept arbitrary values: `slidebox-track-[#222]`, `slidebox-easing-[cubic-bezier(0.4,0,0.2,1)]`, etc.
180
+ - `track`, `border`, `off`, and `on` expose your Tailwind color palette, such as `slidebox-on-emerald-500`.
181
+ - `duration` and `easing` also expose Tailwind's transition theme, such as `slidebox-duration-300` and `slidebox-easing-linear`.
182
+ - Variants work normally: `md:slidebox-size-[32px]`, `hover:slidebox-on-emerald-600`, etc.
183
+ - Use `slidebox-size-*` rather than `h-*` to resize: thumb geometry uses the switch's font size.
184
+ - Keep complete class names in source; dynamically assembled class strings may not be detected by Tailwind.
21
185
 
22
- ## Manual npm releases
186
+ In v3, add named presets under `theme.extend.slidebox`:
23
187
 
24
- The release workflow is [Publish to npm](.github/workflows/publish-npm.yml). It is triggered **only** through GitHub Actions > Run workflow on `main`, with a `patch`, `minor`, or `major` choice.
188
+ ```js
189
+ theme: {
190
+ extend: {
191
+ slidebox: {
192
+ size: { compact: "18px", comfortable: "28px" },
193
+ width: { wide: "2.5" },
194
+ },
195
+ },
196
+ },
197
+ // Now use slidebox-size-compact or slidebox-width-wide.
198
+ ```
25
199
 
26
- The manifest starts at `0.3.0`, so the first `patch` dispatch will release `0.3.1` (once npm credentials are configured). The workflow builds and tests the package, creates a `chore(release): vX.Y.Z` commit and tag, pushes both to `main`, then publishes to npm.
200
+ CSS custom properties work with every integration. Avoid setting a control both through plugin/mixin defaults and an inherited variable: a value assigned directly to the switch wins over inheritance. Use a utility or a selector targeting that switch to override it.
27
201
 
28
- Before the first release:
202
+ ## Experimental pointer dragging
29
203
 
30
- 1. In GitHub Settings > General > Default branch, change `master` to `main`.
31
- 2. Add a granular npm publish token as the repository Actions secret `NPM_TOKEN`, scoped to `@monabbous/slidebox`, with permission to publish. Never commit the token.
32
- 3. Ensure GitHub Actions has permission to write repository contents.
33
- 4. Manually dispatch the release workflow on `main`.
204
+ **Opt-in and experimental.** The switch works without JavaScript. This separate, dependency-free ES module adds mouse, touch, and pen dragging to a checkbox styled with Slidebox. It never initializes controls automatically. The API and gesture feel may change in future releases.
34
205
 
35
- The workflow does not run on pushes or tags. A failed npm publish after the Git tag has been pushed requires fixing the publish failure for that version before choosing a new bump. The old library is preserved in `legacy/`.
206
+ Keep using your chosen CSS, Sass, Less, or Tailwind integration, then attach the enhancement **after the input is mounted**:
207
+
208
+ ```html
209
+ <label>
210
+ <input id="notifications" class="slidebox" type="checkbox" role="switch" name="notifications">
211
+ Enable notifications
212
+ </label>
213
+ ```
36
214
 
37
- ## Run locally
215
+ ```js
216
+ import "@monabbous/slidebox/dist/css/slidebox.css"; // Omit for Sass, Less, or Tailwind.
217
+ import { enhanceSlidebox } from "@monabbous/slidebox/experimental/drag.mjs";
38
218
 
39
- Clone this repository, then serve its root directory:
219
+ const input = document.querySelector("#notifications");
220
+ const disableDragging = enhanceSlidebox(input, { threshold: 5 });
40
221
 
41
- ```bash
42
- python3 -m http.server 8080
222
+ // Before removing the input, or when disabling this experimental feature:
223
+ // disableDragging();
43
224
  ```
44
225
 
45
- Open `http://localhost:8080`. The lab uses plain HTML, CSS, and JavaScript, without a build step.
226
+ The JavaScript and drag styles must come from the **same package version**. Sass and Less mixins also include the necessary styles, so your checkbox can use a custom class instead of `.slidebox`.
46
227
 
47
- ## Playground
228
+ ### Behavior and configuration
48
229
 
49
- - Adjust track width, thumb spacing, press deformation, and transition duration.
50
- - Change easing family and direction (In, Out, or In-Out), including Elastic and Bounce.
51
- - Compare Slidebox against native controls in light and dark schemes.
52
- - Choose browser-default, CSS system, nabous.dev, or custom colors.
53
- - Copy complete CSS, Tailwind v4 `@utility` CSS, or configuration variables.
230
+ - `threshold` is the horizontal movement, in CSS pixels, required to begin dragging. Default: `5`. Must be a finite, non-negative number.
231
+ - The thumb follows the pointer; its trailing edge stretches and relaxes using `--slidebox-deform` and `--slidebox-duration`. Set `--slidebox-deform: 0` to disable stretching. Reduced-motion preferences also disable drag stretching.
232
+ - Releasing at or beyond the midpoint checks the input; releasing before it unchecks it. A changed state emits one bubbling `input` event followed by one bubbling `change` event. Drag movement alone does not emit either event.
233
+ - Normal clicks, label clicks, Space-key activation, form values, and form resets remain native. A completed drag suppresses its following click to prevent a second toggle. No synthetic `click` is emitted for a drag.
234
+ - RTL reverses the gesture direction. Disabled inputs, including those inside a disabled fieldset, cannot be dragged.
235
+ - Vertical touch gestures remain available for scrolling, and pinch zoom remains enabled. Cancellation or lost pointer capture abandons the gesture without committing it.
236
+ - Call the returned cleanup function to stop dragging and restore temporary styles. Cleanup can be called repeatedly. Disabling it during a drag cancels that drag.
237
+ - Repeated calls for the same input return its existing cleanup function. To change `threshold`, clean up and initialize again.
54
238
 
55
- Chromium currently lacks support for the `AccentColor` CSS color keyword. In browser-default mode, the custom thumb uses a blue approximation in light mode and `SelectedItem` in dark mode, while native controls retain `accent-color: auto`. Browsers that support `AccentColor` use it automatically.
239
+ Use normal checkbox bindings in your framework. In React, for example, initialize from a ref and return cleanup from an effect:
56
240
 
57
- Controls can be adjusted without triggering transition animations. The selected duration is restored after two animation frames.
241
+ ```jsx
242
+ import { useEffect, useRef, useState } from "react";
243
+ import { enhanceSlidebox } from "@monabbous/slidebox/experimental/drag.mjs";
244
+
245
+ function Notifications() {
246
+ const input = useRef(null);
247
+ const [enabled, setEnabled] = useState(false);
248
+
249
+ useEffect(() => {
250
+ const element = input.current;
251
+ // Native listeners also receive the enhancement's programmatic change event.
252
+ const sync = () => setEnabled(element.checked);
253
+ element.addEventListener("change", sync);
254
+ const destroy = enhanceSlidebox(element);
255
+ return () => {
256
+ destroy();
257
+ element.removeEventListener("change", sync);
258
+ };
259
+ }, []);
260
+
261
+ return (
262
+ <label>
263
+ <input ref={input} className="slidebox" type="checkbox" role="switch"
264
+ checked={enabled} onChange={event => setEnabled(event.target.checked)} />
265
+ Enable notifications
266
+ </label>
267
+ );
268
+ }
269
+ ```
58
270
 
59
- The easing functions follow [easings.net](https://easings.net/). Elastic and Bounce use sampled CSS `linear()` timing functions.
271
+ TypeScript declarations ship alongside `drag.mjs`. Importing the module is safe during server rendering, but calling `enhanceSlidebox()` requires a mounted checkbox in a browser supporting Pointer Events and pointer capture. Use a sufficiently wide horizontal Slidebox (width ratio greater than 1). Test touch, scrolling, and your framework's controlled-input behavior before adopting it in production; vertical switches are not supported.
272
+
273
+ Try it in the playground under **03 / Experimental → Enable pointer dragging**. Reset turns the option off. The playground's glass lighting is separate and is not shipped as part of the drag module.
274
+
275
+ ## Development
276
+
277
+ ```sh
278
+ npm ci
279
+ npm run build
280
+ npm test
281
+ npm pack --dry-run
282
+ ```
283
+
284
+ The build extracts component rules from `style.css` and generates CSS, Sass, Less, and Tailwind rules. Tests compile both mixins and the plugin with Tailwind v3 and v4, and check the drag module's lifecycle.
285
+
286
+ For real-browser drag and playground checks:
287
+
288
+ ```sh
289
+ npx playwright install chromium
290
+ npm run test:browser
291
+ ```
60
292
 
61
- ## Original Slidebox package
293
+ These run in Chromium; other browsers and devices still need testing. The lab itself is plain HTML/CSS/JavaScript; serve the repository with `python3 -m http.server 8080` and open `http://localhost:8080`.
62
294
 
63
- The original Sass package, compiled CSS, package manifests, and example are preserved in [`legacy/`](legacy/). See the [legacy README](legacy/README.md) for the previous installation instructions.
295
+ Releases use the manual [Publish to npm](.github/workflows/publish-npm.yml) workflow on `main`, with npm trusted publishing through the `production` environment. Choose a version bump for a new release, or enable `publish_existing_version` to retry the current version without a bump or new tag.
64
296
 
65
- [Source](https://github.com/monabbous/slidebox) · [nabous.dev](https://nabous.dev)
297
+ The original implementation is preserved in [`legacy/`](legacy/).
@@ -1,13 +1,13 @@
1
1
  .slidebox {
2
- --_container-width-ratio: 2;
3
- --_thumb-spacing-ratio: .33;
4
- --_deform: .1;
5
- --_duration: 200ms;
6
- --_easing-function: ease-in-out;
7
- --_track: Field;
8
- --_border: ButtonBorder;
9
- --_off: FieldText;
10
- --_on: light-dark(#0b75ff, SelectedItem);
2
+ --_container-width-ratio: var(--slidebox-width, 2);
3
+ --_thumb-spacing-ratio: var(--slidebox-spacing, .33);
4
+ --_deform: var(--slidebox-deform, .1);
5
+ --_duration: var(--slidebox-duration, 200ms);
6
+ --_easing-function: 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
11
  appearance: none;
12
12
  -webkit-appearance: none;
13
13
  box-sizing: border-box;
@@ -16,7 +16,7 @@
16
16
  justify-content: center;
17
17
  width: calc(1em * var(--_container-width-ratio));
18
18
  height: 1em;
19
- font-size: 54px;
19
+ font-size: var(--slidebox-size, 54px);
20
20
  padding: 0;
21
21
  border: 1px solid var(--_border);
22
22
  border-radius: 100vw;
@@ -24,7 +24,7 @@
24
24
  cursor: pointer;
25
25
  }
26
26
  @supports (color: AccentColor) {
27
- .slidebox { --_on: AccentColor; }
27
+ .slidebox { --_accent: AccentColor; }
28
28
  }
29
29
  .slidebox::after {
30
30
  --_start: 0; --_end: 1;
@@ -50,6 +50,11 @@
50
50
  .slidebox:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
51
51
  .slidebox:checked:active::after { --_start: calc(1 - var(--_deform)); }
52
52
  .slidebox:disabled { cursor: not-allowed; opacity: .5; }
53
+ .slidebox[data-slidebox-dragging]::after {
54
+ 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);
55
+ 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);
56
+ transition: none;
57
+ }
53
58
  @media (forced-colors: active) {
54
59
  .slidebox { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
55
60
  }
@@ -0,0 +1,15 @@
1
+ export interface SlideboxDragOptions {
2
+ /** Horizontal movement in CSS pixels before a drag starts. Default: 5. */
3
+ threshold?: number;
4
+ }
5
+
6
+ /**
7
+ * Experimental opt-in pointer dragging for a native checkbox styled by Slidebox.
8
+ * Call after mounting the input. Returns an idempotent cleanup function.
9
+ * Calling again for the same input returns its existing cleanup function;
10
+ * destroy it first to change options. Safe to import during server rendering.
11
+ */
12
+ export declare function enhanceSlidebox(
13
+ input: HTMLInputElement,
14
+ options?: SlideboxDragOptions,
15
+ ): () => void;
@@ -0,0 +1,158 @@
1
+ const enhancements = new WeakMap();
2
+
3
+ /** Opt in a native checkbox to experimental horizontal pointer dragging. */
4
+ export function enhanceSlidebox(input, { threshold = 5 } = {}) {
5
+ if (input?.tagName !== "INPUT" || input.type !== "checkbox") {
6
+ throw new TypeError("enhanceSlidebox expects a checkbox input");
7
+ }
8
+ if (!Number.isFinite(threshold) || threshold < 0) {
9
+ throw new RangeError("threshold must be a non-negative number of pixels");
10
+ }
11
+ if (enhancements.has(input)) return enhancements.get(input);
12
+
13
+ const view = input.ownerDocument.defaultView;
14
+ if (!view?.PointerEvent || typeof input.setPointerCapture !== "function") {
15
+ throw new Error("Slidebox dragging requires a browser with Pointer Events and pointer capture");
16
+ }
17
+ const originalTouchAction = input.style.getPropertyValue("touch-action");
18
+ const originalTouchPriority = input.style.getPropertyPriority("touch-action");
19
+ let gesture;
20
+ let suppressed;
21
+ let suppressionTimer;
22
+ input.style.setProperty("touch-action", "pan-y pinch-zoom");
23
+
24
+ function disabled() { return input.matches(":disabled"); }
25
+ function clearSuppression() {
26
+ suppressed = undefined;
27
+ view.clearTimeout(suppressionTimer);
28
+ }
29
+ function suppressClick(id) {
30
+ clearSuppression();
31
+ suppressed = id;
32
+ suppressionTimer = view.setTimeout(clearSuppression, 500);
33
+ }
34
+ function finish(commit = false) {
35
+ if (!gesture) return;
36
+ const current = gesture;
37
+ gesture = undefined;
38
+ if (current.frame !== undefined) view.cancelAnimationFrame(current.frame);
39
+ if (current.dragging) {
40
+ if (current.attribute === null) input.removeAttribute("data-slidebox-dragging");
41
+ else input.setAttribute("data-slidebox-dragging", current.attribute);
42
+ if (current.progressStyle) input.style.setProperty("--slidebox-drag-progress", current.progressStyle, current.progressPriority);
43
+ else input.style.removeProperty("--slidebox-drag-progress");
44
+ if (current.stretchStyle) input.style.setProperty("--slidebox-drag-stretch", current.stretchStyle, current.stretchPriority);
45
+ else input.style.removeProperty("--slidebox-drag-stretch");
46
+ suppressClick(current.id);
47
+ }
48
+ if (input.hasPointerCapture(current.id)) input.releasePointerCapture(current.id);
49
+ if (commit && current.dragging && !disabled()) {
50
+ const checked = current.progress >= .5;
51
+ if (checked !== input.checked) {
52
+ input.checked = checked;
53
+ input.dispatchEvent(new view.Event("input", { bubbles: true }));
54
+ input.dispatchEvent(new view.Event("change", { bubbles: true }));
55
+ }
56
+ }
57
+ }
58
+ function down(event) {
59
+ if (gesture || disabled() || !event.isPrimary || event.button !== 0) return;
60
+ clearSuppression();
61
+ const box = input.getBoundingClientRect();
62
+ const travel = box.width - box.height;
63
+ if (travel <= 0) return;
64
+ const computed = view.getComputedStyle(input);
65
+ const deformation = Number.parseFloat(computed.getPropertyValue("--_deform")) || 0;
66
+ const durationValue = computed.getPropertyValue("--_duration").trim();
67
+ const duration = (Number.parseFloat(durationValue) || 0) * (durationValue.endsWith("ms") ? 1 : 1000);
68
+ gesture = {
69
+ id: event.pointerId, x: event.clientX, y: event.clientY, travel,
70
+ start: input.checked ? 1 : 0, progress: input.checked ? 1 : 0,
71
+ direction: computed.direction === "rtl" ? -1 : 1,
72
+ deform: view.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : Math.max(0, Math.min(1, deformation)),
73
+ duration, stretch: 0,
74
+ attribute: input.getAttribute("data-slidebox-dragging"),
75
+ progressStyle: input.style.getPropertyValue("--slidebox-drag-progress"),
76
+ progressPriority: input.style.getPropertyPriority("--slidebox-drag-progress"),
77
+ stretchStyle: input.style.getPropertyValue("--slidebox-drag-stretch"),
78
+ stretchPriority: input.style.getPropertyPriority("--slidebox-drag-stretch"),
79
+ dragging: false,
80
+ };
81
+ input.setPointerCapture(event.pointerId);
82
+ }
83
+ function relax(now) {
84
+ if (!gesture) return;
85
+ const elapsed = gesture.lastFrame === undefined ? 16 : Math.max(0, now - gesture.lastFrame);
86
+ gesture.lastFrame = now;
87
+ gesture.stretch *= Math.exp(-elapsed / Math.max(16, gesture.duration * .35));
88
+ if (Math.abs(gesture.stretch) < .0001) gesture.stretch = 0;
89
+ input.style.setProperty("--slidebox-drag-stretch", String(gesture.stretch));
90
+ gesture.frame = gesture.stretch ? view.requestAnimationFrame(relax) : undefined;
91
+ if (!gesture.stretch) gesture.lastFrame = undefined;
92
+ }
93
+ function move(event) {
94
+ if (!gesture || event.pointerId !== gesture.id) return;
95
+ if (disabled()) { finish(); return; }
96
+ const dx = event.clientX - gesture.x;
97
+ const dy = event.clientY - gesture.y;
98
+ if (!gesture.dragging) {
99
+ // Leave vertical gestures to page scrolling and taps to native clicks.
100
+ if (Math.abs(dy) > threshold && Math.abs(dy) > Math.abs(dx)) { finish(); return; }
101
+ if (Math.abs(dx) <= threshold) return;
102
+ gesture.dragging = true;
103
+ input.setAttribute("data-slidebox-dragging", "");
104
+ input.focus({ preventScroll: true });
105
+ }
106
+ const progress = Math.max(0, Math.min(1, gesture.start + dx * gesture.direction / gesture.travel));
107
+ gesture.stretch = Math.max(-gesture.deform, Math.min(gesture.deform, gesture.stretch + progress - gesture.progress));
108
+ gesture.progress = progress;
109
+ input.style.setProperty("--slidebox-drag-progress", String(gesture.progress));
110
+ input.style.setProperty("--slidebox-drag-stretch", String(gesture.stretch));
111
+ if (gesture.stretch && gesture.frame === undefined) gesture.frame = view.requestAnimationFrame(relax);
112
+ event.preventDefault();
113
+ }
114
+ function up(event) {
115
+ if (!gesture || event.pointerId !== gesture.id) return;
116
+ move(event);
117
+ finish(true);
118
+ }
119
+ function cancel(event) {
120
+ if (gesture && event.pointerId === gesture.id) finish();
121
+ }
122
+ function click(event) {
123
+ // Keyboard activation has detail=0 and must retain native behavior.
124
+ if (suppressed === undefined || event.detail === 0) return;
125
+ if ("pointerId" in event && event.pointerId !== suppressed) return;
126
+ event.preventDefault();
127
+ event.stopImmediatePropagation();
128
+ clearSuppression();
129
+ }
130
+ function reset() { finish(); }
131
+ const form = input.form;
132
+ input.addEventListener("pointerdown", down);
133
+ input.addEventListener("pointermove", move);
134
+ input.addEventListener("pointerup", up);
135
+ input.addEventListener("pointercancel", cancel);
136
+ input.addEventListener("lostpointercapture", cancel);
137
+ input.addEventListener("click", click, true);
138
+ form?.addEventListener("reset", reset);
139
+
140
+ function destroy() {
141
+ finish();
142
+ clearSuppression();
143
+ input.removeEventListener("pointerdown", down);
144
+ input.removeEventListener("pointermove", move);
145
+ input.removeEventListener("pointerup", up);
146
+ input.removeEventListener("pointercancel", cancel);
147
+ input.removeEventListener("lostpointercapture", cancel);
148
+ input.removeEventListener("click", click, true);
149
+ form?.removeEventListener("reset", reset);
150
+ if (input.style.getPropertyValue("touch-action") === "pan-y pinch-zoom") {
151
+ if (originalTouchAction) input.style.setProperty("touch-action", originalTouchAction, originalTouchPriority);
152
+ else input.style.removeProperty("touch-action");
153
+ }
154
+ enhancements.delete(input);
155
+ }
156
+ enhancements.set(input, destroy);
157
+ return destroy;
158
+ }
@@ -0,0 +1,79 @@
1
+ // Generated by scripts/build.mjs.
2
+ .slidebox-option(@name; @value) when not (@value = false) {
3
+ --slidebox-@{name}: @value;
4
+ }
5
+ .slidebox(@width: false; @spacing: false; @deform: false; @duration: false; @easing: false; @track: false; @border: false; @off: false; @on: false; @size: false) {
6
+ .slidebox-option(width; @width);
7
+ .slidebox-option(spacing; @spacing);
8
+ .slidebox-option(deform; @deform);
9
+ .slidebox-option(duration; @duration);
10
+ .slidebox-option(easing; @easing);
11
+ .slidebox-option(track; @track);
12
+ .slidebox-option(border; @border);
13
+ .slidebox-option(off; @off);
14
+ .slidebox-option(on; @on);
15
+ .slidebox-option(size; @size);
16
+ & {
17
+ --_container-width-ratio: var(--slidebox-width, 2);
18
+ --_thumb-spacing-ratio: var(--slidebox-spacing, .33);
19
+ --_deform: var(--slidebox-deform, .1);
20
+ --_duration: var(--slidebox-duration, 200ms);
21
+ --_easing-function: var(--slidebox-easing, ease-in-out);
22
+ --_track: var(--slidebox-track, Field);
23
+ --_border: var(--slidebox-border, ButtonBorder);
24
+ --_off: var(--slidebox-off, FieldText);
25
+ --_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
26
+ appearance: none;
27
+ -webkit-appearance: none;
28
+ box-sizing: border-box;
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ width: calc(1em * var(--_container-width-ratio));
33
+ height: 1em;
34
+ font-size: var(--slidebox-size, 54px);
35
+ padding: 0;
36
+ border: 1px solid var(--_border);
37
+ border-radius: 100vw;
38
+ background: var(--_track);
39
+ cursor: pointer;
40
+ }
41
+ @supports (color: AccentColor) {
42
+ & { --_accent: AccentColor; }
43
+ }
44
+ &::after {
45
+ --_start: 0; --_end: 1;
46
+ content: "";
47
+ display: block;
48
+ flex: 1 1 auto;
49
+ min-width: 0;
50
+ width: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
51
+ height: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
52
+ margin-block: calc(var(--_thumb-spacing-ratio) * .5em);
53
+ margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
54
+ margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
55
+ border-radius: inherit;
56
+ background: var(--_off);
57
+ transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
58
+ transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
59
+ }
60
+ &:checked::after {
61
+ --_start: 1; --_end: 0;
62
+ background: var(--_on);
63
+ transition-delay: 0s, calc(var(--_duration) * var(--_deform)), 0s;
64
+ }
65
+ &:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
66
+ &:checked:active::after { --_start: calc(1 - var(--_deform)); }
67
+ &:disabled { cursor: not-allowed; opacity: .5; }
68
+ &[data-slidebox-dragging]::after {
69
+ 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)";
70
+ 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)";
71
+ transition: none;
72
+ }
73
+ @media (forced-colors: active) {
74
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
75
+ }
76
+ @media (prefers-reduced-motion: reduce) {
77
+ &::after { transition-duration: .01ms; transition-delay: 0ms; }
78
+ }
79
+ }
package/package.json CHANGED
@@ -1,17 +1,29 @@
1
1
  {
2
2
  "name": "@monabbous/slidebox",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Elastic, system-color-aware CSS checkbox switch.",
5
5
  "license": "ISC",
6
6
  "style": "./dist/css/slidebox.css",
7
7
  "sass": "./scss/slidebox.scss",
8
+ "less": "./less/slidebox.less",
8
9
  "files": [
9
10
  "dist/css/slidebox.css",
10
- "scss/slidebox.scss"
11
+ "scss/",
12
+ "less/",
13
+ "tailwind/",
14
+ "experimental/"
11
15
  ],
12
16
  "publishConfig": {
13
17
  "access": "public"
14
18
  },
19
+ "peerDependencies": {
20
+ "tailwindcss": ">=3.4.0 <5"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "tailwindcss": {
24
+ "optional": true
25
+ }
26
+ },
15
27
  "repository": {
16
28
  "type": "git",
17
29
  "url": "git+https://github.com/monabbous/slidebox.git"
@@ -23,6 +35,7 @@
23
35
  "scripts": {
24
36
  "build": "node scripts/build.mjs",
25
37
  "test": "node --test tests/*.test.mjs",
38
+ "test:browser": "playwright test",
26
39
  "prepack": "npm run build"
27
40
  },
28
41
  "keywords": [
@@ -34,5 +47,14 @@
34
47
  "scss",
35
48
  "elastic",
36
49
  "system-colors"
37
- ]
50
+ ],
51
+ "devDependencies": {
52
+ "@playwright/test": "^1.64.1",
53
+ "@tailwindcss/node": "^4.3.3",
54
+ "less": "^4.9.1",
55
+ "postcss": "^8.5.32",
56
+ "sass": "^1.107.1",
57
+ "tailwindcss": "^3.4.19",
58
+ "tailwindcss-v4": "npm:tailwindcss@^4.3.3"
59
+ }
38
60
  }
@@ -0,0 +1,73 @@
1
+ // Generated by scripts/build.mjs.
2
+ @use "sass:map";
3
+
4
+ $defaults: () !default;
5
+
6
+ @mixin slidebox($options: ()) {
7
+ @each $name, $value in map.merge($defaults, $options) {
8
+ --slidebox-#{$name}: #{$value};
9
+ }
10
+ & {
11
+ --_container-width-ratio: var(--slidebox-width, 2);
12
+ --_thumb-spacing-ratio: var(--slidebox-spacing, .33);
13
+ --_deform: var(--slidebox-deform, .1);
14
+ --_duration: var(--slidebox-duration, 200ms);
15
+ --_easing-function: var(--slidebox-easing, ease-in-out);
16
+ --_track: var(--slidebox-track, Field);
17
+ --_border: var(--slidebox-border, ButtonBorder);
18
+ --_off: var(--slidebox-off, FieldText);
19
+ --_on: var(--slidebox-on, var(--_accent, light-dark(#0b75ff, SelectedItem)));
20
+ appearance: none;
21
+ -webkit-appearance: none;
22
+ box-sizing: border-box;
23
+ display: inline-flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ width: calc(1em * var(--_container-width-ratio));
27
+ height: 1em;
28
+ font-size: var(--slidebox-size, 54px);
29
+ padding: 0;
30
+ border: 1px solid var(--_border);
31
+ border-radius: 100vw;
32
+ background: var(--_track);
33
+ cursor: pointer;
34
+ }
35
+ @supports (color: AccentColor) {
36
+ & { --_accent: AccentColor; }
37
+ }
38
+ &::after {
39
+ --_start: 0; --_end: 1;
40
+ content: "";
41
+ display: block;
42
+ flex: 1 1 auto;
43
+ min-width: 0;
44
+ width: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
45
+ height: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
46
+ margin-block: calc(var(--_thumb-spacing-ratio) * .5em);
47
+ margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
48
+ margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
49
+ border-radius: inherit;
50
+ background: var(--_off);
51
+ transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
52
+ transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
53
+ }
54
+ &:checked::after {
55
+ --_start: 1; --_end: 0;
56
+ background: var(--_on);
57
+ transition-delay: 0s, calc(var(--_duration) * var(--_deform)), 0s;
58
+ }
59
+ &:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
60
+ &:checked:active::after { --_start: calc(1 - var(--_deform)); }
61
+ &:disabled { cursor: not-allowed; opacity: .5; }
62
+ &[data-slidebox-dragging]::after {
63
+ 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);
64
+ 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);
65
+ transition: none;
66
+ }
67
+ @media (forced-colors: active) {
68
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
69
+ }
70
+ @media (prefers-reduced-motion: reduce) {
71
+ &::after { transition-duration: .01ms; transition-delay: 0ms; }
72
+ }
73
+ }
@@ -1,58 +1,2 @@
1
- .slidebox {
2
- --_container-width-ratio: 2;
3
- --_thumb-spacing-ratio: .33;
4
- --_deform: .1;
5
- --_duration: 200ms;
6
- --_easing-function: ease-in-out;
7
- --_track: Field;
8
- --_border: ButtonBorder;
9
- --_off: FieldText;
10
- --_on: light-dark(#0b75ff, SelectedItem);
11
- appearance: none;
12
- -webkit-appearance: none;
13
- box-sizing: border-box;
14
- display: inline-flex;
15
- align-items: center;
16
- justify-content: center;
17
- width: calc(1em * var(--_container-width-ratio));
18
- height: 1em;
19
- font-size: 54px;
20
- padding: 0;
21
- border: 1px solid var(--_border);
22
- border-radius: 100vw;
23
- background: var(--_track);
24
- cursor: pointer;
25
- }
26
- @supports (color: AccentColor) {
27
- .slidebox { --_on: AccentColor; }
28
- }
29
- .slidebox::after {
30
- --_start: 0; --_end: 1;
31
- content: "";
32
- display: block;
33
- flex: 1 1 auto;
34
- min-width: 0;
35
- width: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
36
- height: calc((1 - var(--_thumb-spacing-ratio)) * 1em);
37
- margin-block: calc(var(--_thumb-spacing-ratio) * .5em);
38
- margin-inline-start: calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em);
39
- margin-inline-end: calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em);
40
- border-radius: inherit;
41
- background: var(--_off);
42
- transition: background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function);
43
- transition-delay: 0s, 0s, calc(var(--_duration) * var(--_deform));
44
- }
45
- .slidebox:checked::after {
46
- --_start: 1; --_end: 0;
47
- background: var(--_on);
48
- transition-delay: 0s, calc(var(--_duration) * var(--_deform)), 0s;
49
- }
50
- .slidebox:not(:checked):active::after { --_end: calc(1 - var(--_deform)); }
51
- .slidebox:checked:active::after { --_start: calc(1 - var(--_deform)); }
52
- .slidebox:disabled { cursor: not-allowed; opacity: .5; }
53
- @media (forced-colors: active) {
54
- .slidebox { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
55
- }
56
- @media (prefers-reduced-motion: reduce) {
57
- .slidebox::after { transition-duration: .01ms; transition-delay: 0ms; }
58
- }
1
+ @use "mixins";
2
+ .slidebox { @include mixins.slidebox(); }
@@ -0,0 +1,28 @@
1
+ const plugin = require("tailwindcss/plugin");
2
+ const rules = require("./rules.json");
3
+
4
+ function flatten(colors, prefix = "") {
5
+ return Object.fromEntries(Object.entries(colors).flatMap(([key, value]) => {
6
+ const name = key === "DEFAULT" ? prefix : [prefix, key].filter(Boolean).join("-");
7
+ return typeof value === "object" && value !== null
8
+ ? Object.entries(flatten(value, name)) : [[name, value]];
9
+ }));
10
+ }
11
+
12
+ module.exports = plugin.withOptions((defaults = {}) => ({ addUtilities, matchUtilities, theme }) => {
13
+ const controls = ["size", "width", "spacing", "deform", "duration", "easing", "track", "border", "off", "on"];
14
+ for (const key of Object.keys(defaults)) {
15
+ if (!controls.includes(key)) throw new Error(`Unknown Slidebox control: ${key}`);
16
+ }
17
+ addUtilities({ ".slidebox": {
18
+ ...Object.fromEntries(Object.entries(defaults).map(([key, value]) => [`--slidebox-${key}`, String(value)])),
19
+ ...rules,
20
+ } });
21
+ for (const key of controls) {
22
+ let values = theme(`slidebox.${key}`, {});
23
+ if (["track", "border", "off", "on"].includes(key)) values = { ...flatten(theme("colors", {})), ...values };
24
+ if (key === "duration") values = { ...theme("transitionDuration", {}), ...values };
25
+ if (key === "easing") values = { ...theme("transitionTimingFunction", {}), ...values };
26
+ matchUtilities({ [`slidebox-${key}`]: value => ({ [`--slidebox-${key}`]: value }) }, { values });
27
+ }
28
+ });
@@ -0,0 +1,82 @@
1
+ {
2
+ "--_container-width-ratio": "var(--slidebox-width, 2)",
3
+ "--_thumb-spacing-ratio": "var(--slidebox-spacing, .33)",
4
+ "--_deform": "var(--slidebox-deform, .1)",
5
+ "--_duration": "var(--slidebox-duration, 200ms)",
6
+ "--_easing-function": "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
+ "appearance": "none",
12
+ "-webkit-appearance": "none",
13
+ "box-sizing": "border-box",
14
+ "display": "inline-flex",
15
+ "align-items": "center",
16
+ "justify-content": "center",
17
+ "width": "calc(1em * var(--_container-width-ratio))",
18
+ "height": "1em",
19
+ "font-size": "var(--slidebox-size, 54px)",
20
+ "padding": "0",
21
+ "border": "1px solid var(--_border)",
22
+ "border-radius": "100vw",
23
+ "background": "var(--_track)",
24
+ "cursor": "pointer",
25
+ "@supports (color: AccentColor)": {
26
+ "&": {
27
+ "--_accent": "AccentColor"
28
+ }
29
+ },
30
+ "&::after": {
31
+ "--_start": "0",
32
+ "--_end": "1",
33
+ "content": "\"\"",
34
+ "display": "block",
35
+ "flex": "1 1 auto",
36
+ "min-width": "0",
37
+ "width": "calc((1 - var(--_thumb-spacing-ratio)) * 1em)",
38
+ "height": "calc((1 - var(--_thumb-spacing-ratio)) * 1em)",
39
+ "margin-block": "calc(var(--_thumb-spacing-ratio) * .5em)",
40
+ "margin-inline-start": "calc((1em * var(--_container-width-ratio) - 1em) * var(--_start) + var(--_thumb-spacing-ratio) * .5em)",
41
+ "margin-inline-end": "calc((1em * var(--_container-width-ratio) - 1em) * var(--_end) + var(--_thumb-spacing-ratio) * .5em)",
42
+ "border-radius": "inherit",
43
+ "background": "var(--_off)",
44
+ "transition": "background var(--_duration) var(--_easing-function), margin-inline-start var(--_duration) var(--_easing-function), margin-inline-end var(--_duration) var(--_easing-function)",
45
+ "transition-delay": "0s, 0s, calc(var(--_duration) * var(--_deform))"
46
+ },
47
+ "&:checked::after": {
48
+ "--_start": "1",
49
+ "--_end": "0",
50
+ "background": "var(--_on)",
51
+ "transition-delay": "0s, calc(var(--_duration) * var(--_deform)), 0s"
52
+ },
53
+ "&:not(:checked):active::after": {
54
+ "--_end": "calc(1 - var(--_deform))"
55
+ },
56
+ "&:checked:active::after": {
57
+ "--_start": "calc(1 - var(--_deform))"
58
+ },
59
+ "&:disabled": {
60
+ "cursor": "not-allowed",
61
+ "opacity": ".5"
62
+ },
63
+ "&[data-slidebox-dragging]::after": {
64
+ "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)",
65
+ "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)",
66
+ "transition": "none"
67
+ },
68
+ "@media (forced-colors: active)": {
69
+ "&": {
70
+ "--_track": "Field",
71
+ "--_border": "ButtonBorder",
72
+ "--_off": "FieldText",
73
+ "--_on": "Highlight"
74
+ }
75
+ },
76
+ "@media (prefers-reduced-motion: reduce)": {
77
+ "&::after": {
78
+ "transition-duration": ".01ms",
79
+ "transition-delay": "0ms"
80
+ }
81
+ }
82
+ }