svelte-toggle-switch 1.0.0 → 2.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.
@@ -1,259 +1,809 @@
1
- <script>
2
-
3
- export let label;
4
- export let design = 'inner label'
5
- export let options = [];
6
- export let fontSize = 16;
7
- export let value = 'on';
8
-
9
- let checked = true;
10
- const uniqueID = Math.floor(Math.random() * 100)
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+
4
+ const dispatch = createEventDispatcher();
5
+
6
+ // Core Props
7
+ export let value: boolean | string = false;
8
+ export let label: string = '';
9
+ export let design: 'inner' | 'slider' | 'modern' | 'ios' | 'material' | 'multi' = 'slider';
10
+
11
+ // Multi-option props (for multi design)
12
+ export let options: string[] = [];
13
+
14
+ // Styling Props
15
+ export let size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number = 'md';
16
+ export let color: string = '#007AFF';
17
+ export let offColor: string = '#E5E7EB';
18
+ export let colorScheme: 'blue' | 'green' | 'red' | 'purple' | 'orange' | 'pink' | 'yellow' | 'indigo' | 'teal' | 'custom' = 'blue';
19
+
20
+ // State Props
21
+ export let disabled: boolean = false;
22
+ export let loading: boolean = false;
23
+ export let readonly: boolean = false;
24
+
25
+ // Icon Props
26
+ export let showIcons: boolean = false;
27
+ export let onIcon: string = '✓';
28
+ export let offIcon: string = '✕';
29
+
30
+ // Animation Props
31
+ export let animationDuration: number = 300;
32
+ export let animationEasing: string = 'ease-in-out';
33
+
34
+ // Accessibility Props
35
+ export let ariaLabel: string = '';
36
+ export let ariaDescribedBy: string = '';
37
+ export let id: string = '';
38
+ export let name: string = '';
39
+ export let tabIndex: number = 0;
40
+
41
+ // Advanced Props
42
+ export let labelPosition: 'left' | 'right' = 'right';
43
+ export let rounded: boolean = true;
44
+ export let shadow: boolean = false;
45
+ export let outline: boolean = false;
46
+
47
+ // v2.1.0 New Features
48
+ // Custom Text Labels
49
+ export let onText: string = 'ON';
50
+ export let offText: string = 'OFF';
51
+
52
+ // Form Validation
53
+ export let helperText: string = '';
54
+ export let errorText: string = '';
55
+ export let required: boolean = false;
56
+ export let error: boolean = false;
57
+
58
+ // Internal state
59
+ let checked: boolean = typeof value === 'boolean' ? value : value === 'on';
60
+ const uniqueID = id || `switch-${Math.floor(Math.random() * 1000000)}`;
61
+
62
+ // Color schemes (v2.1.0 - added yellow, indigo, teal)
63
+ const colorSchemes = {
64
+ blue: '#007AFF',
65
+ green: '#10B981',
66
+ red: '#EF4444',
67
+ purple: '#8B5CF6',
68
+ orange: '#F97316',
69
+ pink: '#EC4899',
70
+ yellow: '#F59E0B',
71
+ indigo: '#6366F1',
72
+ teal: '#14B8A6',
73
+ custom: color
74
+ };
75
+
76
+ const activeColor = colorScheme === 'custom' ? color : colorSchemes[colorScheme];
77
+
78
+ // Size variants (in rem)
79
+ const sizeMap = {
80
+ xs: 0.75,
81
+ sm: 0.875,
82
+ md: 1,
83
+ lg: 1.25,
84
+ xl: 1.5
85
+ };
86
+
87
+ const fontSize = typeof size === 'number' ? size : sizeMap[size];
88
+
89
+ // Sync checked state with value prop (one-way: value -> checked)
90
+ $: if (design !== 'multi') {
91
+ const newChecked = typeof value === 'boolean' ? value : value === 'on';
92
+ if (newChecked !== checked) {
93
+ checked = newChecked;
94
+ }
95
+ }
11
96
 
12
- function handleClick(event){
13
- const target = event.target
97
+ function handleClick(event: MouseEvent) {
98
+ if (disabled || loading || readonly) {
99
+ event.preventDefault();
100
+ return;
101
+ }
102
+
103
+ if (design !== 'multi') {
104
+ const newChecked = !checked;
105
+ checked = newChecked;
106
+ const newValue = newChecked ? (typeof value === 'boolean' ? true : 'on') : (typeof value === 'boolean' ? false : 'off');
107
+ value = newValue;
108
+
109
+ // v2.1.0 - Dispatch change event
110
+ dispatch('change', { value: newValue, checked: newChecked });
111
+ }
112
+ }
14
113
 
15
- const state = target.getAttribute('aria-checked')
114
+ function handleKeyDown(event: KeyboardEvent) {
115
+ if (disabled || loading || readonly) return;
116
+
117
+ if (event.key === ' ' || event.key === 'Enter') {
118
+ event.preventDefault();
119
+ if (design !== 'multi') {
120
+ const newChecked = !checked;
121
+ checked = newChecked;
122
+ const newValue = newChecked ? (typeof value === 'boolean' ? true : 'on') : (typeof value === 'boolean' ? false : 'off');
123
+ value = newValue;
124
+
125
+ // v2.1.0 - Dispatch change event
126
+ dispatch('change', { value: newValue, checked: newChecked });
127
+ }
128
+ }
129
+ }
16
130
 
17
- checked = state === 'true' ? false : true
131
+ // v2.1.0 - Focus/Blur event handlers
132
+ function handleFocus(event: FocusEvent) {
133
+ dispatch('focus', { event });
134
+ }
18
135
 
19
- value = checked === true ? 'on' : 'off'
20
- }
21
-
22
- const slugify = (str = "") =>
23
- str.toLowerCase().replace(/ /g, "-").replace(/\./g, "");
136
+ function handleBlur(event: FocusEvent) {
137
+ dispatch('blur', { event });
138
+ }
24
139
 
140
+ // v2.1.0 - Multi-select change handler
141
+ function handleMultiChange() {
142
+ dispatch('change', { value });
143
+ }
25
144
  </script>
26
145
 
27
- {#if design == 'inner'}
28
- <div class="s s--inner">
29
- <span id={`switch-${uniqueID}`}>{label}</span>
30
- <button
31
- role="switch"
32
- aria-checked={checked}
33
- aria-labelledby={`switch-${uniqueID}`}
34
- on:click={handleClick}>
35
- <span>on</span>
36
- <span>off</span>
37
- </button>
38
- </div>
39
- {:else if design == 'slider'}
40
- <div class="s s--slider" style="font-size:{fontSize}px">
41
- <span id={`switch-${uniqueID}`}>{label}</span>
42
- <button
43
- role="switch"
44
- aria-checked={checked}
45
- aria-labelledby={`switch-${uniqueID}`}
46
- on:click={handleClick}>
47
- </button>
48
- </div>
49
- {:else}
50
- <div class="s s--multi">
51
- <div role='radiogroup'
52
- class="group-container"
53
- aria-labelledby={`label-${uniqueID}`}
54
- style="font-size:{fontSize}px"
55
- id={`group-${uniqueID}`}>
56
- <div class='legend' id={`label-${uniqueID}`}>{label}</div>
57
- {#each options as option}
58
- <input type="radio" id={`${option}-${uniqueID}`} value={option} bind:group={value}>
59
- <label for={`${option}-${uniqueID}`}>
60
- {option}
61
- </label>
62
- {/each}
63
- </div>
64
- </div>
65
-
146
+ {#if design === 'inner'}
147
+ <div
148
+ class="switch-container"
149
+ class:disabled
150
+ class:readonly
151
+ style="font-size: {fontSize}rem;"
152
+ >
153
+ {#if label && labelPosition === 'left'}
154
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
155
+ {/if}
156
+ <button
157
+ type="button"
158
+ role="switch"
159
+ aria-checked={checked}
160
+ aria-label={ariaLabel || label}
161
+ aria-labelledby={label ? `${uniqueID}-label` : undefined}
162
+ aria-describedby={ariaDescribedBy || (helperText || errorText ? `${uniqueID}-helper` : undefined)}
163
+ aria-required={required}
164
+ aria-invalid={error}
165
+ {disabled}
166
+ tabindex={tabIndex}
167
+ class="switch switch--inner"
168
+ class:checked
169
+ class:loading
170
+ class:rounded
171
+ class:shadow
172
+ class:outline
173
+ class:error
174
+ on:click={handleClick}
175
+ on:keydown={handleKeyDown}
176
+ on:focus={handleFocus}
177
+ on:blur={handleBlur}
178
+ style="
179
+ --active-color: {activeColor};
180
+ --off-color: {offColor};
181
+ --animation-duration: {animationDuration}ms;
182
+ --animation-easing: {animationEasing};
183
+ "
184
+ >
185
+ {#if loading}
186
+ <span class="spinner"></span>
187
+ {:else}
188
+ <span class="switch-text">{checked ? onText : offText}</span>
189
+ {/if}
190
+ </button>
191
+ {#if label && labelPosition === 'right'}
192
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
193
+ {/if}
194
+ {#if helperText || errorText}
195
+ <span class="switch-helper-text" id="{uniqueID}-helper" class:error-text={error}>
196
+ {error && errorText ? errorText : helperText}
197
+ </span>
198
+ {/if}
199
+ </div>
200
+
201
+ {:else if design === 'slider' || design === 'ios'}
202
+ <div
203
+ class="switch-container"
204
+ class:disabled
205
+ class:readonly
206
+ style="font-size: {fontSize}rem;"
207
+ >
208
+ {#if label && labelPosition === 'left'}
209
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
210
+ {/if}
211
+ <button
212
+ type="button"
213
+ role="switch"
214
+ aria-checked={checked}
215
+ aria-label={ariaLabel || label}
216
+ aria-labelledby={label ? `${uniqueID}-label` : undefined}
217
+ aria-describedby={ariaDescribedBy || (helperText || errorText ? `${uniqueID}-helper` : undefined)}
218
+ aria-required={required}
219
+ aria-invalid={error}
220
+ {disabled}
221
+ tabindex={tabIndex}
222
+ class="switch switch--slider"
223
+ class:checked
224
+ class:loading
225
+ class:shadow
226
+ class:outline
227
+ class:error
228
+ on:click={handleClick}
229
+ on:keydown={handleKeyDown}
230
+ on:focus={handleFocus}
231
+ on:blur={handleBlur}
232
+ style="
233
+ --active-color: {activeColor};
234
+ --off-color: {offColor};
235
+ --animation-duration: {animationDuration}ms;
236
+ --animation-easing: {animationEasing};
237
+ "
238
+ >
239
+ <span class="switch-track">
240
+ <span class="switch-thumb">
241
+ {#if loading}
242
+ <span class="spinner-small"></span>
243
+ {:else if showIcons}
244
+ <span class="switch-icon">{checked ? onIcon : offIcon}</span>
245
+ {/if}
246
+ </span>
247
+ </span>
248
+ </button>
249
+ {#if label && labelPosition === 'right'}
250
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
251
+ {/if}
252
+ {#if helperText || errorText}
253
+ <span class="switch-helper-text" id="{uniqueID}-helper" class:error-text={error}>
254
+ {error && errorText ? errorText : helperText}
255
+ </span>
256
+ {/if}
257
+ </div>
258
+
259
+ {:else if design === 'modern'}
260
+ <div
261
+ class="switch-container"
262
+ class:disabled
263
+ class:readonly
264
+ style="font-size: {fontSize}rem;"
265
+ >
266
+ {#if label && labelPosition === 'left'}
267
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
268
+ {/if}
269
+ <button
270
+ type="button"
271
+ role="switch"
272
+ aria-checked={checked}
273
+ aria-label={ariaLabel || label}
274
+ aria-labelledby={label ? `${uniqueID}-label` : undefined}
275
+ aria-describedby={ariaDescribedBy || (helperText || errorText ? `${uniqueID}-helper` : undefined)}
276
+ aria-required={required}
277
+ aria-invalid={error}
278
+ {disabled}
279
+ tabindex={tabIndex}
280
+ class="switch switch--modern"
281
+ class:checked
282
+ class:loading
283
+ class:shadow
284
+ class:outline
285
+ class:error
286
+ on:click={handleClick}
287
+ on:keydown={handleKeyDown}
288
+ on:focus={handleFocus}
289
+ on:blur={handleBlur}
290
+ style="
291
+ --active-color: {activeColor};
292
+ --off-color: {offColor};
293
+ --animation-duration: {animationDuration}ms;
294
+ --animation-easing: {animationEasing};
295
+ "
296
+ >
297
+ <span class="switch-track">
298
+ <span class="switch-thumb-modern">
299
+ {#if loading}
300
+ <span class="spinner-small"></span>
301
+ {:else if showIcons}
302
+ <span class="switch-icon-modern">{checked ? onIcon : offIcon}</span>
303
+ {/if}
304
+ </span>
305
+ {#if !loading && showIcons}
306
+ <span class="track-icons">
307
+ <span class="track-icon track-icon--on">{onIcon}</span>
308
+ <span class="track-icon track-icon--off">{offIcon}</span>
309
+ </span>
310
+ {/if}
311
+ </span>
312
+ </button>
313
+ {#if label && labelPosition === 'right'}
314
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
315
+ {/if}
316
+ {#if helperText || errorText}
317
+ <span class="switch-helper-text" id="{uniqueID}-helper" class:error-text={error}>
318
+ {error && errorText ? errorText : helperText}
319
+ </span>
320
+ {/if}
321
+ </div>
322
+
323
+ {:else if design === 'material'}
324
+ <div
325
+ class="switch-container"
326
+ class:disabled
327
+ class:readonly
328
+ style="font-size: {fontSize}rem;"
329
+ >
330
+ {#if label && labelPosition === 'left'}
331
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
332
+ {/if}
333
+ <button
334
+ type="button"
335
+ role="switch"
336
+ aria-checked={checked}
337
+ aria-label={ariaLabel || label}
338
+ aria-labelledby={label ? `${uniqueID}-label` : undefined}
339
+ aria-describedby={ariaDescribedBy || (helperText || errorText ? `${uniqueID}-helper` : undefined)}
340
+ aria-required={required}
341
+ aria-invalid={error}
342
+ {disabled}
343
+ tabindex={tabIndex}
344
+ class="switch switch--material"
345
+ class:checked
346
+ class:loading
347
+ class:shadow
348
+ class:outline
349
+ class:error
350
+ on:click={handleClick}
351
+ on:keydown={handleKeyDown}
352
+ on:focus={handleFocus}
353
+ on:blur={handleBlur}
354
+ style="
355
+ --active-color: {activeColor};
356
+ --off-color: {offColor};
357
+ --animation-duration: {animationDuration}ms;
358
+ --animation-easing: {animationEasing};
359
+ "
360
+ >
361
+ <span class="switch-track-material">
362
+ <span class="switch-thumb-material">
363
+ {#if loading}
364
+ <span class="spinner-small"></span>
365
+ {/if}
366
+ </span>
367
+ </span>
368
+ </button>
369
+ {#if label && labelPosition === 'right'}
370
+ <span class="switch-label" id="{uniqueID}-label">{label}</span>
371
+ {/if}
372
+ {#if helperText || errorText}
373
+ <span class="switch-helper-text" id="{uniqueID}-helper" class:error-text={error}>
374
+ {error && errorText ? errorText : helperText}
375
+ </span>
376
+ {/if}
377
+ </div>
378
+
379
+ {:else if design === 'multi'}
380
+ <div
381
+ class="switch-container switch-container--multi"
382
+ class:disabled
383
+ class:readonly
384
+ style="font-size: {fontSize}rem;"
385
+ >
386
+ <div
387
+ role="radiogroup"
388
+ class="switch-multi"
389
+ aria-labelledby="{uniqueID}-legend"
390
+ aria-describedby={ariaDescribedBy || undefined}
391
+ style="
392
+ --active-color: {activeColor};
393
+ --off-color: {offColor};
394
+ --animation-duration: {animationDuration}ms;
395
+ --animation-easing: {animationEasing};
396
+ "
397
+ >
398
+ {#if label}
399
+ <div class="switch-multi-legend" id="{uniqueID}-legend">{label}</div>
400
+ {/if}
401
+ <div class="switch-multi-options" class:shadow class:outline>
402
+ {#each options as option, index}
403
+ <input
404
+ type="radio"
405
+ id="{uniqueID}-{option}"
406
+ {name}
407
+ value={option}
408
+ bind:group={value}
409
+ on:change={handleMultiChange}
410
+ {disabled}
411
+ {required}
412
+ tabindex={tabIndex}
413
+ class="switch-multi-input"
414
+ />
415
+ <label
416
+ for="{uniqueID}-{option}"
417
+ class="switch-multi-label"
418
+ class:first={index === 0}
419
+ class:last={index === options.length - 1}
420
+ >
421
+ {option}
422
+ </label>
423
+ {/each}
424
+ </div>
425
+ </div>
426
+ {#if helperText || errorText}
427
+ <span class="switch-helper-text" id="{uniqueID}-helper" class:error-text={error}>
428
+ {error && errorText ? errorText : helperText}
429
+ </span>
430
+ {/if}
431
+ </div>
66
432
  {/if}
67
433
 
68
434
  <style>
69
- :root {
70
- --accent-color: CornflowerBlue;
71
- --gray: #ccc;
72
- }
73
- /* Inner Design Option */
74
- .s--inner button {
75
- padding: 0.5em;
76
- background-color: #fff;
77
- border: 1px solid var(--gray);
78
- }
79
- [role='switch'][aria-checked='true'] :first-child,
80
- [role='switch'][aria-checked='false'] :last-child {
81
- display: none;
82
- color: #fff;
83
- }
84
-
85
- .s--inner button span {
86
- user-select: none;
87
- pointer-events:none;
88
- padding: 0.25em;
89
- }
90
-
91
- .s--inner button:focus {
92
- outline: var(--accent-color) solid 1px;
93
- }
94
-
95
- /* Slider Design Option */
96
-
97
- .s--slider {
98
- display: flex;
99
- align-items: center;
100
- }
101
-
102
- .s--slider button {
103
- width: 3em;
104
- height: 1.6em;
105
- position: relative;
106
- margin: 0 0 0 0.5em;
107
- background: var(--gray);
108
- border: none;
109
- }
110
-
111
- .s--slider button::before {
112
- content: '';
113
- position: absolute;
114
- width: 1.3em;
115
- height: 1.3em;
116
- background: #fff;
117
- top: 0.13em;
118
- right: 1.5em;
119
- transition: transform 0.3s;
120
- }
121
-
122
- .s--slider button[aria-checked='true']{
123
- background-color: var(--accent-color)
124
- }
125
-
126
- .s--slider button[aria-checked='true']::before{
127
- transform: translateX(1.3em);
128
- transition: transform 0.3s;
129
- }
130
-
131
- .s--slider button:focus {
132
- box-shadow: 0 0px 0px 1px var(--accent-color);
133
- }
134
-
135
- /* Multi Design Option */
136
-
137
- /* Based on suggestions from Sara Soueidan https://www.sarasoueidan.com/blog/toggle-switch-design/
138
- and this example from Scott O'hara https://codepen.io/scottohara/pen/zLZwNv */
139
-
140
- .s--multi .group-container {
141
- border: none;
142
- padding: 0;
143
- white-space: nowrap;
144
- }
145
-
146
- /* .s--multi legend {
147
- font-size: 2px;
148
- opacity: 0;
149
- position: absolute;
150
- } */
151
-
152
- .s--multi label {
153
- display: inline-block;
154
- line-height: 1.6;
155
- position: relative;
156
- z-index: 2;
157
- }
158
-
159
- .s--multi input {
160
- opacity: 0;
161
- position: absolute;
162
- }
163
-
164
- .s--multi label:first-of-type {
165
- padding-right: 5em;
166
- }
167
-
168
- .s--multi label:last-child {
169
- margin-left: -5em;
170
- padding-left: 5em;
171
- }
172
-
173
- .s--multi:focus-within label:first-of-type:after {
174
- box-shadow: 0 0px 8px var(--accent-color);
175
- border-radius: 1.5em;
176
- }
177
-
178
-
179
-
180
- /* making the switch UI. */
181
- .s--multi label:first-of-type:before,
182
- .s--multi label:first-of-type:after {
183
- content: "";
184
- height: 1.25em;
185
- overflow: hidden;
186
- pointer-events: none;
187
- position: absolute;
188
- vertical-align: middle;
189
- }
190
-
191
- .s--multi label:first-of-type:before {
192
- border-radius: 100%;
193
- z-index: 2;
194
- position: absolute;
195
- width: 1.2em;
196
- height: 1.2em;
197
- background: #fff;
198
- top: 0.2em;
199
- right: 1.2em;
200
- transition: transform 0.3s;
201
- }
202
-
203
- .s--multi label:first-of-type:after {
204
- background: var(--accent-color);
205
- border-radius: 1em;
206
- margin: 0 1em;
207
- transition: background .2s ease-in-out;
208
- width: 3em;
209
- height: 1.6em;
210
- }
211
-
212
- .s--multi input:first-of-type:checked ~ label:first-of-type:after {
213
- background: var(--gray);
214
- }
215
-
216
- .s--multi input:first-of-type:checked ~ label:first-of-type:before {
217
- transform: translateX(-1.4em);
218
- }
219
-
220
- .s--multi input:last-of-type:checked ~ label:last-of-type {
221
- z-index: 1;
222
- }
223
-
224
- .s--multi input:focus {
225
- box-shadow: 0 0px 8px var(--accent-color);
226
- border-radius: 1.5em;
227
- }
228
-
229
- /* gravy */
230
-
231
- /* Inner Design Option */
232
- [role='switch'][aria-checked='true'] :first-child,
233
- [role='switch'][aria-checked='false'] :last-child {
234
- border-radius: 0.25em;
235
- background: var(--accent-color);
236
- display: inline-block;
237
- }
238
-
239
- .s--inner button:focus {
240
- box-shadow: 0 0px 8px var(--accent-color);
241
- border-radius: 0.1em;
242
- }
243
-
244
- /* Slider Design Option */
245
- .s--slider button {
246
- border-radius: 1.5em;
247
- }
248
-
249
- .s--slider button::before {
250
- border-radius: 100%;
251
- }
252
-
253
- .s--slider button:focus {
254
- box-shadow: 0 0px 8px var(--accent-color);
255
- border-radius: 1.5em;
256
- }
257
-
258
-
259
- </style>
435
+ :root {
436
+ --active-color: #007AFF;
437
+ --off-color: #E5E7EB;
438
+ --animation-duration: 300ms;
439
+ --animation-easing: ease-in-out;
440
+ }
441
+
442
+ /* Container */
443
+ .switch-container {
444
+ display: inline-flex;
445
+ align-items: center;
446
+ gap: 0.75em;
447
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
448
+ }
449
+
450
+ .switch-container.disabled {
451
+ opacity: 0.5;
452
+ cursor: not-allowed;
453
+ }
454
+
455
+ .switch-container.readonly {
456
+ cursor: default;
457
+ }
458
+
459
+ .switch-label {
460
+ user-select: none;
461
+ font-size: 1em;
462
+ color: #374151;
463
+ }
464
+
465
+ /* v2.1.0 - Helper Text */
466
+ .switch-helper-text {
467
+ display: block;
468
+ font-size: 0.875em;
469
+ color: #6B7280;
470
+ margin-top: 0.375em;
471
+ line-height: 1.4;
472
+ }
473
+
474
+ .switch-helper-text.error-text {
475
+ color: #EF4444;
476
+ }
477
+
478
+ /* Base Switch */
479
+ .switch {
480
+ position: relative;
481
+ border: none;
482
+ cursor: pointer;
483
+ transition: all var(--animation-duration) var(--animation-easing);
484
+ font-family: inherit;
485
+ }
486
+
487
+ .switch:disabled {
488
+ cursor: not-allowed;
489
+ }
490
+
491
+ .switch:focus-visible {
492
+ outline: 2px solid var(--active-color);
493
+ outline-offset: 2px;
494
+ }
495
+
496
+ .switch.shadow {
497
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
498
+ }
499
+
500
+ .switch.outline {
501
+ border: 1px solid #D1D5DB;
502
+ }
503
+
504
+ /* v2.1.0 - Error State */
505
+ .switch.error {
506
+ border-color: #EF4444;
507
+ }
508
+
509
+ .switch.error:focus-visible {
510
+ outline-color: #EF4444;
511
+ }
512
+
513
+ /* Inner Design */
514
+ .switch--inner {
515
+ padding: 0.4em 0.8em;
516
+ background-color: var(--off-color);
517
+ color: #6B7280;
518
+ font-weight: 500;
519
+ font-size: 0.875em;
520
+ min-width: 4em;
521
+ border-radius: 0.25em;
522
+ }
523
+
524
+ .switch--inner.rounded {
525
+ border-radius: 0.5em;
526
+ }
527
+
528
+ .switch--inner.checked {
529
+ background-color: var(--active-color);
530
+ color: white;
531
+ }
532
+
533
+ .switch--inner .switch-text {
534
+ display: block;
535
+ user-select: none;
536
+ pointer-events: none;
537
+ }
538
+
539
+ /* Slider/iOS Design */
540
+ .switch--slider {
541
+ padding: 0;
542
+ background: transparent;
543
+ width: 3.5em;
544
+ height: 2em;
545
+ }
546
+
547
+ .switch-track {
548
+ position: relative;
549
+ display: block;
550
+ width: 100%;
551
+ height: 100%;
552
+ background-color: var(--off-color);
553
+ border-radius: 1em;
554
+ transition: background-color var(--animation-duration) var(--animation-easing);
555
+ }
556
+
557
+ .switch--slider.checked .switch-track {
558
+ background-color: var(--active-color);
559
+ }
560
+
561
+ .switch-thumb {
562
+ position: absolute;
563
+ top: 0.15em;
564
+ left: 0.15em;
565
+ width: 1.7em;
566
+ height: 1.7em;
567
+ background-color: white;
568
+ border-radius: 50%;
569
+ transition: transform var(--animation-duration) var(--animation-easing);
570
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
571
+ display: flex;
572
+ align-items: center;
573
+ justify-content: center;
574
+ }
575
+
576
+ .switch--slider.checked .switch-thumb {
577
+ transform: translateX(1.5em);
578
+ }
579
+
580
+ .switch-icon {
581
+ font-size: 0.75em;
582
+ user-select: none;
583
+ }
584
+
585
+ /* Modern Design */
586
+ .switch--modern {
587
+ padding: 0;
588
+ background: transparent;
589
+ width: 4em;
590
+ height: 2.2em;
591
+ }
592
+
593
+ .switch--modern .switch-track {
594
+ border-radius: 1.1em;
595
+ }
596
+
597
+ .switch-thumb-modern {
598
+ position: absolute;
599
+ top: 0.2em;
600
+ left: 0.2em;
601
+ width: 1.8em;
602
+ height: 1.8em;
603
+ background-color: white;
604
+ border-radius: 50%;
605
+ transition: transform var(--animation-duration) var(--animation-easing), background-color var(--animation-duration) var(--animation-easing);
606
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
607
+ display: flex;
608
+ align-items: center;
609
+ justify-content: center;
610
+ }
611
+
612
+ .switch--modern.checked .switch-thumb-modern {
613
+ transform: translateX(1.8em);
614
+ }
615
+
616
+ .track-icons {
617
+ position: absolute;
618
+ top: 0;
619
+ left: 0;
620
+ width: 100%;
621
+ height: 100%;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: space-between;
625
+ padding: 0 0.5em;
626
+ pointer-events: none;
627
+ }
628
+
629
+ .track-icon {
630
+ font-size: 0.7em;
631
+ color: white;
632
+ opacity: 0;
633
+ transition: opacity var(--animation-duration) var(--animation-easing);
634
+ }
635
+
636
+ .switch--modern.checked .track-icon--on {
637
+ opacity: 1;
638
+ }
639
+
640
+ .switch--modern:not(.checked) .track-icon--off {
641
+ opacity: 0.7;
642
+ }
643
+
644
+ .switch-icon-modern {
645
+ font-size: 0.8em;
646
+ user-select: none;
647
+ color: var(--active-color);
648
+ }
649
+
650
+ /* Material Design */
651
+ .switch--material {
652
+ padding: 0;
653
+ background: transparent;
654
+ width: 3.5em;
655
+ height: 1.5em;
656
+ }
657
+
658
+ .switch-track-material {
659
+ position: relative;
660
+ display: block;
661
+ width: 100%;
662
+ height: 100%;
663
+ background-color: var(--off-color);
664
+ border-radius: 0.75em;
665
+ transition: background-color var(--animation-duration) var(--animation-easing);
666
+ }
667
+
668
+ .switch--material.checked .switch-track-material {
669
+ background-color: var(--active-color);
670
+ opacity: 0.5;
671
+ }
672
+
673
+ .switch-thumb-material {
674
+ position: absolute;
675
+ top: 50%;
676
+ left: 0;
677
+ transform: translateY(-50%);
678
+ width: 1.5em;
679
+ height: 1.5em;
680
+ background-color: #FAFAFA;
681
+ border-radius: 50%;
682
+ transition: transform var(--animation-duration) var(--animation-easing), background-color var(--animation-duration) var(--animation-easing);
683
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
684
+ display: flex;
685
+ align-items: center;
686
+ justify-content: center;
687
+ }
688
+
689
+ .switch--material.checked .switch-thumb-material {
690
+ transform: translateY(-50%) translateX(2em);
691
+ background-color: var(--active-color);
692
+ }
693
+
694
+ /* Multi Design */
695
+ .switch-container--multi {
696
+ display: block;
697
+ }
698
+
699
+ .switch-multi {
700
+ display: inline-block;
701
+ }
702
+
703
+ .switch-multi-legend {
704
+ font-size: 0.9em;
705
+ color: #374151;
706
+ margin-bottom: 0.5em;
707
+ font-weight: 500;
708
+ }
709
+
710
+ .switch-multi-options {
711
+ display: inline-flex;
712
+ background-color: var(--off-color);
713
+ border-radius: 0.5em;
714
+ padding: 0.25em;
715
+ position: relative;
716
+ }
717
+
718
+ .switch-multi-options.shadow {
719
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
720
+ }
721
+
722
+ .switch-multi-options.outline {
723
+ border: 1px solid #D1D5DB;
724
+ }
725
+
726
+ .switch-multi-input {
727
+ position: absolute;
728
+ opacity: 0;
729
+ pointer-events: none;
730
+ }
731
+
732
+ .switch-multi-label {
733
+ position: relative;
734
+ padding: 0.5em 1.2em;
735
+ border-radius: 0.375em;
736
+ cursor: pointer;
737
+ user-select: none;
738
+ transition: all var(--animation-duration) var(--animation-easing);
739
+ font-size: 0.875em;
740
+ font-weight: 500;
741
+ color: #6B7280;
742
+ z-index: 1;
743
+ }
744
+
745
+ .switch-multi-label.first {
746
+ margin-left: 0;
747
+ }
748
+
749
+ .switch-multi-label.last {
750
+ margin-right: 0;
751
+ }
752
+
753
+ .switch-multi-input:checked + .switch-multi-label {
754
+ background-color: var(--active-color);
755
+ color: white;
756
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
757
+ }
758
+
759
+ .switch-multi-input:focus-visible + .switch-multi-label {
760
+ outline: 2px solid var(--active-color);
761
+ outline-offset: 2px;
762
+ }
763
+
764
+ .switch-multi-input:disabled + .switch-multi-label {
765
+ cursor: not-allowed;
766
+ opacity: 0.5;
767
+ }
768
+
769
+ /* Loading Spinner */
770
+ .spinner,
771
+ .spinner-small {
772
+ display: inline-block;
773
+ border: 2px solid rgba(255, 255, 255, 0.3);
774
+ border-top-color: white;
775
+ border-radius: 50%;
776
+ animation: spin 0.6s linear infinite;
777
+ }
778
+
779
+ .spinner {
780
+ width: 1em;
781
+ height: 1em;
782
+ }
783
+
784
+ .spinner-small {
785
+ width: 0.8em;
786
+ height: 0.8em;
787
+ border-width: 1.5px;
788
+ }
789
+
790
+ @keyframes spin {
791
+ to {
792
+ transform: rotate(360deg);
793
+ }
794
+ }
795
+
796
+ /* Hover states */
797
+ .switch:not(:disabled):hover {
798
+ opacity: 0.9;
799
+ }
800
+
801
+ .switch-multi-label:hover {
802
+ background-color: rgba(0, 0, 0, 0.05);
803
+ }
804
+
805
+ .switch-multi-input:checked + .switch-multi-label:hover {
806
+ background-color: var(--active-color);
807
+ opacity: 0.9;
808
+ }
809
+ </style>