@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 +269 -37
- package/dist/css/slidebox.css +16 -11
- package/experimental/drag.d.mts +15 -0
- package/experimental/drag.mjs +158 -0
- package/less/slidebox.less +79 -0
- package/package.json +25 -3
- package/scss/_mixins.scss +73 -0
- package/scss/slidebox.scss +2 -58
- package/tailwind/index.cjs +28 -0
- package/tailwind/rules.json +82 -0
package/README.md
CHANGED
|
@@ -1,65 +1,297 @@
|
|
|
1
|
-
# Slidebox
|
|
1
|
+
# Slidebox
|
|
2
2
|
|
|
3
|
-
An elastic CSS switch
|
|
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
|
-
[
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
## Install
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```sh
|
|
12
|
+
npm install @monabbous/slidebox
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
+
In v3, add named presets under `theme.extend.slidebox`:
|
|
23
187
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
## Experimental pointer dragging
|
|
29
203
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
const input = document.querySelector("#notifications");
|
|
220
|
+
const disableDragging = enhanceSlidebox(input, { threshold: 5 });
|
|
40
221
|
|
|
41
|
-
|
|
42
|
-
|
|
222
|
+
// Before removing the input, or when disabling this experimental feature:
|
|
223
|
+
// disableDragging();
|
|
43
224
|
```
|
|
44
225
|
|
|
45
|
-
|
|
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
|
-
|
|
228
|
+
### Behavior and configuration
|
|
48
229
|
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
297
|
+
The original implementation is preserved in [`legacy/`](legacy/).
|
package/dist/css/slidebox.css
CHANGED
|
@@ -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 { --
|
|
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.
|
|
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/
|
|
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
|
+
}
|
package/scss/slidebox.scss
CHANGED
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
|
|
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
|
+
}
|