kmaterialize 2.3.3-1.0.36 → 2.3.3-1.0.38

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.
@@ -72,6 +72,8 @@ export class Editor {
72
72
  private prism?: typeof Prism;
73
73
  private dropCaret = make('span', 'editor-drop-caret');
74
74
  private dragPoint?: { x: number; y: number };
75
+ private _dragScroll?: { top: number; left: number; anchorY?: number; direction: number; speed: number };
76
+ private _dragScrollFrame?: number;
75
77
  private helpers: EditorHelpers;
76
78
  private undoStack: EditorEdit[] = [];
77
79
  private redoStack: EditorEdit[] = [];
@@ -310,7 +312,16 @@ export class Editor {
310
312
  this.lastTyping = undefined;
311
313
  }
312
314
  }, { signal });
313
- this.source.addEventListener('scroll', () => { this.lines.scrollTop = this.source.scrollTop; this.syncHighlight(); if (this.dragPoint) this.showDropCaret(); }, { signal });
315
+ this.source.addEventListener('scroll', () => {
316
+ // Native textarea drag scrolling must not bypass the controlled edge scroll.
317
+ if (this._dragScroll) {
318
+ this.source.scrollTop = this._dragScroll.top;
319
+ this.source.scrollLeft = this._dragScroll.left;
320
+ }
321
+ this.lines.scrollTop = this.source.scrollTop;
322
+ this.syncHighlight();
323
+ if (this.dragPoint) this.showDropCaret();
324
+ }, { signal });
314
325
  this.selector.addEventListener('change', () => { void this.setSource(this.selector.value || this.activeSource.id).catch(() => {}); }, { signal });
315
326
  this.templateSelector.addEventListener('change', () => { void this.selectTemplate(this.templateSelector.value || this.activeTemplate.id).catch(() => {}); }, { signal });
316
327
  this.search.addEventListener('input', () => this.renderTokens(), { signal });
@@ -327,6 +338,7 @@ export class Editor {
327
338
  event.dataTransfer.setData(helperMime, helper.dataset.editorHelper!);
328
339
  event.dataTransfer.setData('text/plain', `{{${helper.dataset.editorHelper} }}`);
329
340
  event.dataTransfer.effectAllowed = 'copy';
341
+ this._startDragScroll();
330
342
  return;
331
343
  }
332
344
  if (!token || token.disabled || !event.dataTransfer) return;
@@ -334,19 +346,22 @@ export class Editor {
334
346
  event.dataTransfer.setData(tokenMime, JSON.stringify(path));
335
347
  event.dataTransfer.setData('text/plain', this.expression(path));
336
348
  event.dataTransfer.effectAllowed = 'copy';
349
+ this._startDragScroll();
350
+ }, { signal });
351
+ this.source.addEventListener('dragenter', event => {
352
+ if (!this.source.readOnly && event.dataTransfer?.types.some(type => type === tokenMime || type === helperMime)) this._startDragScroll();
337
353
  }, { signal });
338
354
  this.source.addEventListener('dragover', event => {
339
355
  if (!this.source.readOnly && event.dataTransfer?.types.some(type => type === tokenMime || type === helperMime)) {
340
356
  event.preventDefault(); event.dataTransfer.dropEffect = 'copy';
341
357
  code.classList.add('is-drop-target');
342
358
  this.dragPoint = { x: event.clientX, y: event.clientY };
343
- const bounds = this.source.getBoundingClientRect();
344
- if (event.clientY < bounds.top + 20) this.source.scrollTop -= 16;
345
- else if (event.clientY > bounds.bottom - 20) this.source.scrollTop += 16;
359
+ this._updateDragScroll(event.clientX, event.clientY);
346
360
  this.showDropCaret();
347
361
  }
348
362
  }, { signal });
349
363
  const clearDrop = () => {
364
+ this._stopDragScroll();
350
365
  this.spreadsheetView?.clearDropCarets();
351
366
  code.classList.remove('is-drop-target');
352
367
  this.dropCaret.hidden = true;
@@ -382,6 +397,57 @@ export class Editor {
382
397
  this.renderTokens();
383
398
  }
384
399
 
400
+ /** Freeze native scrolling until movement inside the source requests edge scrolling. */
401
+ private _startDragScroll(): void {
402
+ if (this._dragScroll || this.spreadsheetView || this.source.readOnly) return;
403
+ const state = this._dragScroll = {
404
+ top: this.source.scrollTop, left: this.source.scrollLeft, anchorY: undefined as number | undefined,
405
+ direction: 0, speed: 0
406
+ };
407
+ let previous = performance.now();
408
+ const step = (time: number) => {
409
+ if (this._dragScroll !== state) return;
410
+ const elapsed = Math.min(32, Math.max(0, time - previous));
411
+ previous = time;
412
+ // Retain fractional pixels so slow movement also works on high-refresh displays.
413
+ state.top = Math.max(0, Math.min(this.source.scrollHeight - this.source.clientHeight, state.top + state.speed * elapsed / 1000));
414
+ this.source.scrollTop = state.top;
415
+ this.source.scrollLeft = state.left;
416
+ if (this.dragPoint) this.showDropCaret();
417
+ this._dragScrollFrame = requestAnimationFrame(step);
418
+ };
419
+ this._dragScrollFrame = requestAnimationFrame(step);
420
+ }
421
+
422
+ private _updateDragScroll(x: number, y: number): void {
423
+ this._startDragScroll();
424
+ const state = this._dragScroll;
425
+ if (!state) return;
426
+ const bounds = this.source.getBoundingClientRect();
427
+ if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) {
428
+ state.speed = state.direction = 0;
429
+ state.anchorY = undefined;
430
+ return;
431
+ }
432
+ // The first point establishes entry; small pointer jitter does not enable scrolling.
433
+ if (state.anchorY === undefined) state.anchorY = y;
434
+ const movement = y - state.anchorY;
435
+ if (Math.abs(movement) >= 6) {
436
+ state.direction = Math.sign(movement);
437
+ state.anchorY = y;
438
+ }
439
+ const edge = Math.min(32, bounds.height / 4);
440
+ const depth = state.direction < 0 ? bounds.top + edge - y : y - (bounds.bottom - edge);
441
+ state.speed = edge > 0 && state.direction && depth > 0
442
+ ? state.direction * 120 * Math.min(1, depth / edge) : 0;
443
+ }
444
+
445
+ private _stopDragScroll(): void {
446
+ if (this._dragScrollFrame !== undefined) cancelAnimationFrame(this._dragScrollFrame);
447
+ this._dragScrollFrame = undefined;
448
+ this._dragScroll = undefined;
449
+ }
450
+
385
451
  private expression(path: string[]): string {
386
452
  // Segment literals preserve dotted keys and numeric array indices.
387
453
  if (path.some(key => key.includes(']') || key.includes('[') || key === '')) {
@@ -932,6 +998,7 @@ export class Editor {
932
998
  destroy(): void {
933
999
  if (this.destroyed) return;
934
1000
  this.destroyed = true; this.version++;
1001
+ this._stopDragScroll();
935
1002
  this.templateRequest?.abort(); this.dataRequest?.abort();
936
1003
  this.listRequests.forEach(request => request.abort()); this.listRequests.clear();
937
1004
  this.spreadsheetView?.destroy();
@@ -430,4 +430,93 @@ describe('Editor Handlebars workspace', function () {
430
430
  parent.replaceWith(host);
431
431
  }
432
432
  });
433
+ describe('token drag scrolling', function () {
434
+ let input, bounds, transfer, frames, clock, nextFrame;
435
+ async function begin(helper = false) {
436
+ await start({ template: ('long line '.repeat(30) + '\n').repeat(100), data: { name: 'Ada' }, helpers: { greet: value => value }, templateSelect: false, sourceSelect: false });
437
+ input = host.querySelector('.editor-source');
438
+ input.scrollTop = 240;
439
+ input.scrollLeft = 50;
440
+ bounds = input.getBoundingClientRect();
441
+ frames = new Map(); clock = performance.now(); nextFrame = 0;
442
+ spyOn(window, 'requestAnimationFrame').and.callFake(callback => { frames.set(++nextFrame, callback); return nextFrame; });
443
+ spyOn(window, 'cancelAnimationFrame').and.callFake(id => frames.delete(id));
444
+ transfer = new DataTransfer();
445
+ host.querySelector(helper ? '[data-editor-helper="greet"]' : '[aria-label="Insert name"]').dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: transfer }));
446
+ }
447
+ function over(y) {
448
+ input.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: transfer, clientX: bounds.left + 60, clientY: y }));
449
+ }
450
+ function tick(count = 1) {
451
+ for (let i = 0; i < count; i++) {
452
+ clock += 16;
453
+ const pending = [...frames.values()]; frames.clear();
454
+ pending.forEach(callback => callback(clock));
455
+ }
456
+ }
457
+ it('freezes entry at the bottom and rejects native scroll jumps and pointer jitter', async function () {
458
+ await begin();
459
+ over(bounds.bottom - 4); tick(20);
460
+ expect(input.scrollTop).toBe(240);
461
+ over(bounds.bottom - 3); tick(20);
462
+ expect(input.scrollTop).toBe(240);
463
+ input.scrollTop = 900; input.scrollLeft = 200;
464
+ input.dispatchEvent(new Event('scroll'));
465
+ expect(input.scrollTop).toBe(240);
466
+ expect(input.scrollLeft).toBe(50);
467
+ });
468
+ it('scrolls gradually only after downward movement inside and stops away from the edge', async function () {
469
+ await begin();
470
+ over(bounds.top + bounds.height / 2);
471
+ over(bounds.bottom - 2);
472
+ expect(input.scrollTop).toBe(240);
473
+ tick();
474
+ expect(input.scrollTop).toBeGreaterThan(240);
475
+ expect(input.scrollTop).toBeLessThanOrEqual(244);
476
+ tick(15);
477
+ expect(input.scrollTop).toBeLessThan(290);
478
+ over(bounds.top + bounds.height / 2);
479
+ const stopped = input.scrollTop; tick(20);
480
+ expect(input.scrollTop).toBe(stopped);
481
+ });
482
+ it('accumulates slow scrolling at the beginning of the edge zone', async function () {
483
+ await begin();
484
+ over(bounds.top + bounds.height / 2); over(bounds.bottom - 31); tick(20);
485
+ expect(input.scrollTop).toBeGreaterThan(240);
486
+ expect(input.scrollTop).toBeLessThan(244);
487
+ });
488
+ it('supports gentle upward scrolling for helper drags too', async function () {
489
+ await begin(true);
490
+ over(bounds.top + 2); tick(10);
491
+ expect(input.scrollTop).toBe(240);
492
+ over(bounds.top + bounds.height / 2);
493
+ over(bounds.top + 2); tick(10);
494
+ expect(input.scrollTop).toBeLessThan(240);
495
+ expect(input.scrollTop).toBeGreaterThan(210);
496
+ });
497
+ it('resets the entry gate on leaving and reentering', async function () {
498
+ await begin();
499
+ over(bounds.top + bounds.height / 2); over(bounds.bottom - 2); tick(10);
500
+ input.dispatchEvent(new DragEvent('dragleave'));
501
+ const stopped = input.scrollTop; tick(10);
502
+ expect(input.scrollTop).toBe(stopped);
503
+ over(bounds.bottom - 2); tick(10);
504
+ expect(input.scrollTop).toBe(stopped);
505
+ });
506
+ for (const end of ['drop', 'dragend', 'Escape', 'destroy']) {
507
+ it(`releases scrolling and cancels animation on ${end}`, async function () {
508
+ await begin();
509
+ over(bounds.top + bounds.height / 2); over(bounds.bottom - 2);
510
+ if (end === 'destroy') editor.destroy();
511
+ else if (end === 'Escape') input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
512
+ else document.dispatchEvent(new DragEvent(end));
513
+ expect(frames.size).toBe(0);
514
+ if (end !== 'destroy') {
515
+ input.scrollTop = 400; input.dispatchEvent(new Event('scroll'));
516
+ expect(input.scrollTop).toBe(400);
517
+ }
518
+ });
519
+ }
520
+ });
521
+
433
522
  });
@@ -33,6 +33,10 @@
33
33
  font-size: 1rem;
34
34
  }
35
35
 
36
+ &:has(> .supporting-text) .filepond--root {
37
+ margin-bottom: 0;
38
+ }
39
+
36
40
  .filepond--panel-root {
37
41
  border-radius: 12px;
38
42
  background-color: var(--md-sys-color-surface-variant);
@@ -6,6 +6,11 @@
6
6
  width: max-content;
7
7
  max-width: 100%;
8
8
  margin-block: 8px;
9
+ margin-left: auto;
10
+ margin-right: auto;
11
+
12
+ &[data-otp-align='left'] { margin-left: 0; margin-right: auto; }
13
+ &[data-otp-align='right'] { margin-left: auto; margin-right: 0; }
9
14
  border-radius: var(--otp-slot-radius);
10
15
 
11
16
  // One native control preserves selection, password semantics, autofill and form validation.
@@ -61,12 +66,21 @@
61
66
  // Standard text-field markup: input, label, supporting text. Keep the label
62
67
  // above the entire code so it never overlaps an individual slot or separator.
63
68
  .input-field:has(> .otp-input) {
64
- display: flex;
65
- flex-direction: column;
66
- align-items: flex-start;
67
- margin-block: 24px;
68
- > .otp-input { margin: 0; }
69
+ display: grid;
70
+ grid-template-columns: minmax(0, 1fr) minmax(0, max-content) minmax(0, 1fr);
71
+ justify-items: start;
72
+ &:has(> .otp-input[data-otp-align='left']) { grid-template-columns: 0 minmax(0, max-content) minmax(0, 1fr); }
73
+ &:has(> .otp-input[data-otp-align='right']) { grid-template-columns: minmax(0, 1fr) minmax(0, max-content) 0; }
74
+ > .otp-input {
75
+ grid-column: 2;
76
+ grid-row: 2;
77
+ margin: 0;
78
+ &[data-otp-align] { margin: 0; }
79
+ }
69
80
  > label {
81
+ grid-column: 2;
82
+ grid-row: 1;
83
+ max-width: 100%;
70
84
  position: static;
71
85
  order: -1;
72
86
  transform: none;
@@ -76,7 +90,7 @@
76
90
  line-height: 1.4;
77
91
  background: transparent;
78
92
  }
79
- > .supporting-text { display: block; padding-inline: 0; }
93
+ > .supporting-text { grid-column: 2; grid-row: 3; max-width: 100%; padding-inline: 0; }
80
94
  &:focus-within > label { color: var(--input-color); }
81
95
  &:has(input:disabled) > label { opacity: .38; }
82
96
  &.error, &:has(input[aria-invalid='true']), &:has(.otp-input[data-invalid]), &:has(input:user-invalid) {
@@ -6,6 +6,8 @@ export interface OtpInputOptions extends BaseOptions {
6
6
  /** Number of editable slots (1–32); inferred when pattern is supplied. */
7
7
  length?: number;
8
8
  characters?: 'digits' | 'alphanumeric';
9
+ /** Horizontal alignment of the code within its container. Defaults to center. */
10
+ align?: 'left' | 'center' | 'right';
9
11
  /** # = digit, A = ASCII letter, * = alphanumeric; backslash escapes literals. */
10
12
  pattern?: string;
11
13
  /** Maskito processing and overwrite behavior. The slot pattern owns the mask. */
@@ -40,6 +42,7 @@ export class OtpInput extends Component<OtpInputOptions> {
40
42
  constructor(el: HTMLInputElement, options: Partial<OtpInputOptions> = {}) {
41
43
  if (!['text', 'tel', 'password'].includes(el.type)) throw new TypeError('OtpInput requires a text, tel, or password input.');
42
44
  const settings = { ...OtpInput.defaults, ...OtpInput._markup(el), ...options };
45
+ if (!['left', 'center', 'right'].includes(settings.align!)) throw new TypeError('Unknown OTP alignment.');
43
46
  if (!['digits', 'alphanumeric'].includes(settings.characters!)) throw new TypeError('Unknown OTP character set.');
44
47
  if (!Number.isInteger(settings.groupSize) || settings.groupSize! < 0 || settings.groupSize! > 32) throw new TypeError('OTP groupSize must be an integer between 0 and 32.');
45
48
  if (settings.length !== undefined && (!Number.isInteger(settings.length) || settings.length < 1 || settings.length > 32)) throw new TypeError('OTP length must be an integer between 1 and 32.');
@@ -65,7 +68,7 @@ export class OtpInput extends Component<OtpInputOptions> {
65
68
  this._hadClass = el.classList.contains('otp-input-native');
66
69
  this.ready = this._setup().catch(error => { this.destroy(); throw error; });
67
70
  }
68
- static get defaults(): OtpInputOptions { return { characters: 'digits', groupSize: 0 }; }
71
+ static get defaults(): OtpInputOptions { return { characters: 'digits', groupSize: 0, align: 'center' }; }
69
72
  static init(el: HTMLInputElement, options?: Partial<OtpInputOptions>): OtpInput;
70
73
  static init(els: InitElements<HTMLInputElement | MElement>, options?: Partial<OtpInputOptions>): OtpInput[];
71
74
  static init(els: HTMLInputElement | InitElements<HTMLInputElement | MElement>, options: Partial<OtpInputOptions> = {}): OtpInput | OtpInput[] {
@@ -74,6 +77,7 @@ export class OtpInput extends Component<OtpInputOptions> {
74
77
  static getInstance(el: HTMLInputElement): OtpInput { return el['M_OtpInput']; }
75
78
  private static _markup(el: HTMLInputElement): Partial<OtpInputOptions> {
76
79
  return {
80
+ ...(el.dataset.otpAlign !== undefined ? { align: el.dataset.otpAlign as OtpInputOptions['align'] } : {}),
77
81
  ...(el.dataset.otpLength !== undefined ? { length: Number(el.dataset.otpLength) } : {}),
78
82
  ...(el.dataset.otpPattern !== undefined ? { pattern: el.dataset.otpPattern } : {}),
79
83
  ...(el.dataset.otpGroupSize !== undefined ? { groupSize: Number(el.dataset.otpGroupSize) } : {}),
@@ -102,6 +106,7 @@ export class OtpInput extends Component<OtpInputOptions> {
102
106
  const wrapper = this._wrapper = document.createElement('div');
103
107
  wrapper.className = 'otp-input';
104
108
  wrapper.dir = 'ltr';
109
+ wrapper.dataset.otpAlign = this.options.align;
105
110
  const cells = document.createElement('div');
106
111
  cells.className = 'otp-input-slots';
107
112
  cells.setAttribute('aria-hidden', 'true');
@@ -128,3 +128,50 @@
128
128
  border-radius: 4px;
129
129
  }
130
130
  }
131
+
132
+ // A semantic group needs its own layout; floating text-field labels do not apply.
133
+ fieldset.radio-group {
134
+ min-width: 0;
135
+ margin: 0;
136
+ padding: 8px 16px 12px;
137
+ border: 1px solid var(--md-sys-color-outline);
138
+ border-radius: 4px;
139
+
140
+ > legend {
141
+ padding: 0 4px;
142
+ color: var(--md-sys-color-on-surface-variant);
143
+ font-size: 12px;
144
+ line-height: 16px;
145
+ }
146
+
147
+ > p:not(.supporting-text) { margin: 0; }
148
+
149
+ label {
150
+ position: relative;
151
+ display: flex;
152
+ align-items: center;
153
+ min-height: 44px;
154
+ color: var(--md-sys-color-on-surface);
155
+ cursor: pointer;
156
+ }
157
+
158
+ input[type="radio"] + span {
159
+ height: auto;
160
+ min-height: 24px;
161
+ padding-left: 28px;
162
+ line-height: 24px;
163
+ &::before, &::after { margin: 4px 0; }
164
+ }
165
+
166
+ input:focus-visible + span {
167
+ outline: 2px solid var(--md-sys-color-primary);
168
+ outline-offset: 4px;
169
+ border-radius: 4px;
170
+ }
171
+
172
+ &.error {
173
+ background: transparent;
174
+ border-color: var(--md-sys-color-error);
175
+ > legend { color: var(--md-sys-color-error); }
176
+ }
177
+ }
@@ -1,4 +1,32 @@
1
- .range-field { position: relative; min-width: 0; }
1
+ .range-field {
2
+ position: relative;
3
+ min-width: 0;
4
+
5
+ // Keep supporting messages close without reducing the slider's interaction area.
6
+ &:has(> .supporting-text),
7
+ &:has(+ .supporting-text) {
8
+ > input[type='range'] { margin-bottom: 0; }
9
+ }
10
+
11
+ > .supporting-text,
12
+ + .supporting-text { margin-top: 0; }
13
+ }
14
+
15
+ .range-field-header {
16
+ display: flex;
17
+ align-items: baseline;
18
+ justify-content: space-between;
19
+ gap: 16px;
20
+
21
+ > label { min-width: 0; }
22
+ > output {
23
+ flex-shrink: 0;
24
+ color: var(--md-sys-color-on-surface);
25
+ font-size: 14px;
26
+ font-variant-numeric: tabular-nums;
27
+ }
28
+ }
29
+
2
30
 
3
31
  input[type='range'] {
4
32
  --range-active-color: var(--md-sys-color-primary);
@@ -39,7 +39,7 @@
39
39
  &:checked + .lever {
40
40
  &:before, &:after {
41
41
  top: var(--gap-on);
42
- left: calc(var(--track-width) - var(--size-on) - var(--gap-on) - 2 * var(--border-width));
42
+ inset-inline-start: calc(var(--track-width) - var(--size-on) - var(--gap-on) - 2 * var(--border-width));
43
43
  width: var(--size-on);
44
44
  height: var(--size-on);
45
45
  }
@@ -66,6 +66,7 @@
66
66
  margin-right: 10px;
67
67
  transition: background 0.3s ease;
68
68
  vertical-align: middle;
69
+ touch-action: pan-y;
69
70
  margin: 0 16px;
70
71
 
71
72
  // DOT
@@ -77,10 +78,10 @@
77
78
  height: var(--size-off);
78
79
  border-radius: 50%;
79
80
 
80
- left: var(--gap-off);
81
+ inset-inline-start: var(--gap-off);
81
82
  top: var(--gap-off);
82
83
 
83
- transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;
84
+ transition: inset-inline-start 0.3s ease, top .3s ease, width .3s ease, height .3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;
84
85
  }
85
86
 
86
87
  // NOT CHECKED [DOT]
@@ -150,7 +151,54 @@ input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before {
150
151
  color: var(--md-sys-color-primary);
151
152
  }
152
153
 
153
- .switch.switch-with-icon label input[type="checkbox"]:focus-visible + .lever {
154
+ .switch label input[type="checkbox"]:focus-visible + .lever {
154
155
  outline: 2px solid var(--md-sys-color-primary);
155
156
  outline-offset: 4px;
156
157
  }
158
+
159
+ /// Preview the pointer position while the native checked state waits for release.
160
+ .switch label input[type="checkbox"] + .lever.is-dragging {
161
+ --switch-drag-size: calc(var(--size-off) + (var(--size-on) - var(--size-off)) * var(--switch-drag-progress));
162
+ background-color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-surface-variant));
163
+ border-color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-outline));
164
+ transition: none;
165
+
166
+ &::before,
167
+ &::after {
168
+ width: var(--switch-drag-size);
169
+ height: var(--switch-drag-size);
170
+ top: calc((var(--track-height) - var(--switch-drag-size)) / 2 - var(--border-width));
171
+ inset-inline-start: calc(var(--track-height) / 2 - var(--border-width) + (var(--track-width) - var(--track-height)) * var(--switch-drag-progress) - var(--switch-drag-size) / 2);
172
+ transition: none;
173
+ }
174
+
175
+ &::after {
176
+ background-color: color-mix(in srgb, var(--md-sys-color-surface) var(--switch-drag-percent), var(--md-sys-color-outline));
177
+ }
178
+ }
179
+
180
+ // Keep the drag halo equally visible for plain and icon thumbs, on either side.
181
+ .switch label input[type="checkbox"]:not(:disabled) + .lever.is-dragging::before {
182
+ width: var(--size-on);
183
+ height: var(--size-on);
184
+ top: var(--gap-on);
185
+ inset-inline-start: calc(var(--track-height) / 2 - var(--border-width) + (var(--track-width) - var(--track-height)) * var(--switch-drag-progress) - var(--size-on) / 2);
186
+ transform: scale(2.4);
187
+ background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 18%);
188
+ pointer-events: none;
189
+ }
190
+
191
+ .switch.switch-with-icon label input[type="checkbox"] + .lever.is-dragging::after {
192
+ content: var(--switch-icon-off, "\e5cd");
193
+ color: color-mix(in srgb, var(--md-sys-color-primary) var(--switch-drag-percent), var(--md-sys-color-surface));
194
+ }
195
+
196
+ .switch.switch-with-icon label input[type="checkbox"] + .lever.is-dragging.is-drag-on::after {
197
+ content: var(--switch-icon-on, "\e5ca");
198
+ }
199
+
200
+ @media (prefers-reduced-motion: reduce) {
201
+ .switch label .lever,
202
+ .switch label .lever::before,
203
+ .switch label .lever::after { transition: none; }
204
+ }