@monabbous/slidebox 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,65 +1,215 @@
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.
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";
18
111
  ```
19
112
 
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`.
113
+ ## Less mixin
114
+
115
+ Import the mixin, then use named arguments. Create a wrapper mixin for project defaults:
21
116
 
22
- ## Manual npm releases
117
+ ```less
118
+ @import "@monabbous/slidebox/less/slidebox.less";
23
119
 
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.
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
+ ```
25
128
 
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.
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`.
27
130
 
28
- Before the first release:
131
+ ## Tailwind CSS
29
132
 
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`.
133
+ The plugin is included in the same npm package and supports Tailwind v3.4 and v4. Install Tailwind separately using its normal setup.
34
134
 
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/`.
135
+ ### Tailwind v4
36
136
 
37
- ## Run locally
137
+ Register the plugin in your Tailwind stylesheet. The options block sets project defaults; omit it to keep Slidebox's defaults:
38
138
 
39
- Clone this repository, then serve its root directory:
139
+ ```css
140
+ @import "tailwindcss";
141
+ @plugin "@monabbous/slidebox/tailwind/index.cjs" {
142
+ size: 24px;
143
+ on: #7c3aed;
144
+ duration: 250ms;
145
+ }
146
+ ```
40
147
 
41
- ```bash
42
- python3 -m http.server 8080
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
+ };
43
163
  ```
44
164
 
45
- Open `http://localhost:8080`. The lab uses plain HTML, CSS, and JavaScript, without a build step.
165
+ ### Utilities and overrides
46
166
 
47
- ## Playground
167
+ Add `slidebox` for the component, then control it with `slidebox-NAME-[VALUE]` utilities:
48
168
 
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.
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>
177
+ ```
54
178
 
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.
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.
185
+
186
+ In v3, add named presets under `theme.extend.slidebox`:
187
+
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
+ ```
56
199
 
57
- Controls can be adjusted without triggering transition animations. The selected duration is restored after two animation frames.
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.
58
201
 
59
- The easing functions follow [easings.net](https://easings.net/). Elastic and Bounce use sampled CSS `linear()` timing functions.
202
+ ## Development
203
+
204
+ ```sh
205
+ npm ci
206
+ npm run build
207
+ npm test
208
+ npm pack --dry-run
209
+ ```
60
210
 
61
- ## Original Slidebox package
211
+ 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. The lab itself is plain HTML/CSS/JavaScript; serve the repository with `python3 -m http.server 8080` and open `http://localhost:8080`.
62
212
 
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.
213
+ 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
214
 
65
- [Source](https://github.com/monabbous/slidebox) · [nabous.dev](https://nabous.dev)
215
+ 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;
@@ -0,0 +1,74 @@
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
+ @media (forced-colors: active) {
69
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
70
+ }
71
+ @media (prefers-reduced-motion: reduce) {
72
+ &::after { transition-duration: .01ms; transition-delay: 0ms; }
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,17 +1,28 @@
1
1
  {
2
2
  "name": "@monabbous/slidebox",
3
- "version": "1.0.0",
3
+ "version": "1.1.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/"
11
14
  ],
12
15
  "publishConfig": {
13
16
  "access": "public"
14
17
  },
18
+ "peerDependencies": {
19
+ "tailwindcss": ">=3.4.0 <5"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "tailwindcss": {
23
+ "optional": true
24
+ }
25
+ },
15
26
  "repository": {
16
27
  "type": "git",
17
28
  "url": "git+https://github.com/monabbous/slidebox.git"
@@ -34,5 +45,13 @@
34
45
  "scss",
35
46
  "elastic",
36
47
  "system-colors"
37
- ]
48
+ ],
49
+ "devDependencies": {
50
+ "@tailwindcss/node": "^4.3.3",
51
+ "less": "^4.9.1",
52
+ "postcss": "^8.5.32",
53
+ "sass": "^1.107.1",
54
+ "tailwindcss": "^3.4.19",
55
+ "tailwindcss-v4": "npm:tailwindcss@^4.3.3"
56
+ }
38
57
  }
@@ -0,0 +1,68 @@
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
+ @media (forced-colors: active) {
63
+ & { --_track: Field; --_border: ButtonBorder; --_off: FieldText; --_on: Highlight; }
64
+ }
65
+ @media (prefers-reduced-motion: reduce) {
66
+ &::after { transition-duration: .01ms; transition-delay: 0ms; }
67
+ }
68
+ }
@@ -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,77 @@
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
+ "@media (forced-colors: active)": {
64
+ "&": {
65
+ "--_track": "Field",
66
+ "--_border": "ButtonBorder",
67
+ "--_off": "FieldText",
68
+ "--_on": "Highlight"
69
+ }
70
+ },
71
+ "@media (prefers-reduced-motion: reduce)": {
72
+ "&::after": {
73
+ "transition-duration": ".01ms",
74
+ "transition-delay": "0ms"
75
+ }
76
+ }
77
+ }