@m3e/switch 1.0.0-rc.1

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.
Files changed (45) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +174 -0
  3. package/cem.config.mjs +16 -0
  4. package/demo/index.html +65 -0
  5. package/dist/css-custom-data.json +352 -0
  6. package/dist/custom-elements.json +633 -0
  7. package/dist/html-custom-data.json +38 -0
  8. package/dist/index.js +774 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/index.min.js +399 -0
  11. package/dist/index.min.js.map +1 -0
  12. package/dist/src/SwitchElement.d.ts +147 -0
  13. package/dist/src/SwitchElement.d.ts.map +1 -0
  14. package/dist/src/SwitchIcons.d.ts +3 -0
  15. package/dist/src/SwitchIcons.d.ts.map +1 -0
  16. package/dist/src/index.d.ts +3 -0
  17. package/dist/src/index.d.ts.map +1 -0
  18. package/dist/src/styles/SwitchHandleStyle.d.ts +6 -0
  19. package/dist/src/styles/SwitchHandleStyle.d.ts.map +1 -0
  20. package/dist/src/styles/SwitchIconStyle.d.ts +6 -0
  21. package/dist/src/styles/SwitchIconStyle.d.ts.map +1 -0
  22. package/dist/src/styles/SwitchStateLayerStyle.d.ts +6 -0
  23. package/dist/src/styles/SwitchStateLayerStyle.d.ts.map +1 -0
  24. package/dist/src/styles/SwitchStyle.d.ts +6 -0
  25. package/dist/src/styles/SwitchStyle.d.ts.map +1 -0
  26. package/dist/src/styles/SwitchToken.d.ts +76 -0
  27. package/dist/src/styles/SwitchToken.d.ts.map +1 -0
  28. package/dist/src/styles/SwitchTrackStyle.d.ts +6 -0
  29. package/dist/src/styles/SwitchTrackStyle.d.ts.map +1 -0
  30. package/dist/src/styles/index.d.ts +6 -0
  31. package/dist/src/styles/index.d.ts.map +1 -0
  32. package/eslint.config.mjs +13 -0
  33. package/package.json +48 -0
  34. package/rollup.config.js +32 -0
  35. package/src/SwitchElement.ts +268 -0
  36. package/src/SwitchIcons.ts +2 -0
  37. package/src/index.ts +2 -0
  38. package/src/styles/SwitchHandleStyle.ts +140 -0
  39. package/src/styles/SwitchIconStyle.ts +89 -0
  40. package/src/styles/SwitchStateLayerStyle.ts +43 -0
  41. package/src/styles/SwitchStyle.ts +30 -0
  42. package/src/styles/SwitchToken.ts +145 -0
  43. package/src/styles/SwitchTrackStyle.ts +104 -0
  44. package/src/styles/index.ts +5 -0
  45. package/tsconfig.json +9 -0
@@ -0,0 +1,32 @@
1
+ import resolve from "@rollup/plugin-node-resolve";
2
+ import terser from "@rollup/plugin-terser";
3
+ import typescript from "@rollup/plugin-typescript";
4
+
5
+ const banner = `/**
6
+ * @license MIT
7
+ * Copyright (c) 2025 matraic
8
+ * See LICENSE file in the project root for full license text.
9
+ */`;
10
+
11
+ export default [
12
+ {
13
+ input: "src/index.ts",
14
+ output: [
15
+ {
16
+ file: "dist/index.js",
17
+ format: "esm",
18
+ sourcemap: true,
19
+ banner: banner,
20
+ },
21
+ {
22
+ file: "dist/index.min.js",
23
+ format: "esm",
24
+ sourcemap: true,
25
+ banner: banner,
26
+ plugins: [terser({ mangle: true })],
27
+ },
28
+ ],
29
+ external: ["@m3e/core", "lit"],
30
+ plugins: [resolve(), typescript()],
31
+ },
32
+ ];
@@ -0,0 +1,268 @@
1
+ import { CSSResultGroup, html, LitElement, PropertyValues } from "lit";
2
+ import { customElement, property, query } from "lit/decorators.js";
3
+
4
+ import {
5
+ Labelled,
6
+ Checked,
7
+ ConstraintValidation,
8
+ Dirty,
9
+ Disabled,
10
+ FormAssociated,
11
+ formValue,
12
+ Touched,
13
+ AttachInternals,
14
+ Role,
15
+ M3eFocusRingElement,
16
+ M3eStateLayerElement,
17
+ Focusable,
18
+ KeyboardClick,
19
+ PressedController,
20
+ HoverController,
21
+ } from "@m3e/core";
22
+
23
+ import { SwitchHandleStyle, SwitchIconStyle, SwitchStateLayerStyle, SwitchStyle, SwitchTrackStyle } from "./styles";
24
+
25
+ import { SwitchIcons } from "./SwitchIcons";
26
+
27
+ /**
28
+ * @summary
29
+ * An on/off control that can be toggled by clicking.
30
+ *
31
+ * @description
32
+ * The `m3e-switch` component is a semantic, accessible toggle control that reflects a binary state.
33
+ * Designed according to Material Design 3 guidelines, it supports shape transitions, and adaptive color
34
+ * theming across selected, unselected, and disabled states. The component responds to user interaction
35
+ * with smooth motion and expressive feedback. It supports optional icons (`none`, `selected`, or `both`)
36
+ * and integrates with form-associated behavior, emitting `input` and `change` events when toggled.
37
+ *
38
+ * @example
39
+ * The following example illustrates a switch wrapped by a `label`.
40
+ *
41
+ * ```html
42
+ * <label>Switch label&nbsp;<m3e-switch></m3e-switch></label>
43
+ * ```
44
+ *
45
+ * @example
46
+ * By default, icons are not presented. Use the `icons` attribute to control which icons to show. The next
47
+ * example illustrates showing both the unselected and selected icons.
48
+ *
49
+ * ```html
50
+ * <label>Switch label&nbsp;<m3e-switch icons="both"></m3e-switch></label>
51
+ * ```
52
+ *
53
+ * @tag m3e-switch
54
+ *
55
+ * @attr checked - Whether the element is checked.
56
+ * @attr disabled - Whether the element is disabled.
57
+ * @attr icons - The icons to present.
58
+ * @attr name - The name that identifies the element when submitting the associated form.
59
+ * @attr value - A string representing the value of the switch.
60
+ *
61
+ * @fires input - Emitted when the checked state changes.
62
+ * @fires change - Emitted when the checked state changes.
63
+ *
64
+ * @cssprop --m3e-switch-selected-icon-color - Color of the icon when the switch is selected.
65
+ * @cssprop --m3e-switch-selected-icon-size - Size of the icon in the selected state.
66
+ * @cssprop --m3e-switch-unselected-icon-color - Color of the icon when the switch is unselected.
67
+ * @cssprop --m3e-switch-unselected-icon-size - Size of the icon in the unselected state.
68
+ * @cssprop --m3e-switch-track-height - Height of the switch track.
69
+ * @cssprop --m3e-switch-track-width - Width of the switch track.
70
+ * @cssprop --m3e-switch-track-outline-color - Color of the track’s outline.
71
+ * @cssprop --m3e-switch-track-outline-width - Thickness of the track’s outline.
72
+ * @cssprop --m3e-switch-track-shape - Corner shape of the track.
73
+ * @cssprop --m3e-switch-selected-track-color - Track color when selected.
74
+ * @cssprop --m3e-switch-unselected-track-color - Track color when unselected.
75
+ * @cssprop --m3e-switch-unselected-handle-height - Height of the handle when unselected.
76
+ * @cssprop --m3e-switch-unselected-handle-width - Width of the handle when unselected.
77
+ * @cssprop --m3e-switch-with-icon-handle-height - Height of the handle when icons are present.
78
+ * @cssprop --m3e-switch-with-icon-handle-width - Width of the handle when icons are present.
79
+ * @cssprop --m3e-switch-selected-handle-height - Height of the handle when selected.
80
+ * @cssprop --m3e-switch-selected-handle-width - Width of the handle when selected.
81
+ * @cssprop --m3e-switch-pressed-handle-height - Height of the handle during press.
82
+ * @cssprop --m3e-switch-pressed-handle-width - Width of the handle during press.
83
+ * @cssprop --m3e-switch-handle-shape - Corner shape of the handle.
84
+ * @cssprop --m3e-switch-selected-handle-color - Handle color when selected.
85
+ * @cssprop --m3e-switch-unselected-handle-color - Handle color when unselected.
86
+ * @cssprop --m3e-switch-state-layer-size - Diameter of the state layer overlay.
87
+ * @cssprop --m3e-switch-state-layer-shape - Corner shape of the state layer.
88
+ * @cssprop --m3e-switch-disabled-selected-icon-color - Icon color when selected and disabled.
89
+ * @cssprop --m3e-switch-disabled-selected-icon-opacity - Icon opacity when selected and disabled.
90
+ * @cssprop --m3e-switch-disabled-unselected-icon-color - Icon color when unselected and disabled.
91
+ * @cssprop --m3e-switch-disabled-unselected-icon-opacity - Icon opacity when unselected and disabled.
92
+ * @cssprop --m3e-switch-disabled-track-opacity - Track opacity when disabled.
93
+ * @cssprop --m3e-switch-disabled-selected-track-color - Track color when selected and disabled.
94
+ * @cssprop --m3e-switch-disabled-unselected-track-color - Track color when unselected and disabled.
95
+ * @cssprop --m3e-switch-disabled-unselected-track-outline-color - Outline color when unselected and disabled.
96
+ * @cssprop --m3e-switch-disabled-unselected-handle-opacity - Handle opacity when unselected and disabled.
97
+ * @cssprop --m3e-switch-disabled-selected-handle-opacity - Handle opacity when selected and disabled.
98
+ * @cssprop --m3e-switch-disabled-selected-handle-color - Handle color when selected and disabled.
99
+ * @cssprop --m3e-switch-disabled-unselected-handle-color - Handle color when unselected and disabled.
100
+ * @cssprop --m3e-switch-selected-hover-icon-color - Icon color when selected and hovered.
101
+ * @cssprop --m3e-switch-unselected-hover-icon-color - Icon color when unselected and hovered.
102
+ * @cssprop --m3e-switch-selected-hover-track-color - Track color when selected and hovered.
103
+ * @cssprop --m3e-switch-selected-hover-state-layer-color - State layer color when selected and hovered.
104
+ * @cssprop --m3e-switch-selected-hover-state-layer-opacity - State layer opacity when selected and hovered.
105
+ * @cssprop --m3e-switch-unselected-hover-track-color - Track color when unselected and hovered.
106
+ * @cssprop --m3e-switch-unselected-hover-track-outline-color - Outline color when unselected and hovered.
107
+ * @cssprop --m3e-switch-unselected-hover-state-layer-color - State layer color when unselected and hovered.
108
+ * @cssprop --m3e-switch-unselected-hover-state-layer-opacity - State layer opacity when unselected and hovered.
109
+ * @cssprop --m3e-switch-selected-hover-handle-color - Handle color when selected and hovered.
110
+ * @cssprop --m3e-switch-unselected-hover-handle-color - Handle color when unselected and hovered.
111
+ * @cssprop --m3e-switch-selected-focus-icon-color - Icon color when selected and focused.
112
+ * @cssprop --m3e-switch-unselected-focus-icon-color - Icon color when unselected and focused.
113
+ * @cssprop --m3e-switch-selected-focus-track-color - Track color when selected and focused.
114
+ * @cssprop --m3e-switch-selected-focus-state-layer-color - State layer color when selected and focused.
115
+ * @cssprop --m3e-switch-selected-focus-state-layer-opacity - State layer opacity when selected and focused.
116
+ * @cssprop --m3e-switch-unselected-focus-track-color - Track color when unselected and focused.
117
+ * @cssprop --m3e-switch-unselected-focus-track-outline-color - Outline color when unselected and focused.
118
+ * @cssprop --m3e-switch-unselected-focus-state-layer-color - State layer color when unselected and focused.
119
+ * @cssprop --m3e-switch-unselected-focus-state-layer-opacity - State layer opacity when unselected and focused.
120
+ * @cssprop --m3e-switch-selected-focus-handle-color - Handle color when selected and focused.
121
+ * @cssprop --m3e-switch-unselected-focus-handle-color - Handle color when unselected and focused.
122
+ * @cssprop --m3e-switch-selected-pressed-icon-color - Icon color when selected and pressed.
123
+ * @cssprop --m3e-switch-unselected-pressed-icon-color - Icon color when unselected and pressed.
124
+ * @cssprop --m3e-switch-selected-pressed-track-color - Track color when selected and pressed.
125
+ * @cssprop --m3e-switch-selected-pressed-state-layer-color - State layer color when selected and pressed.
126
+ * @cssprop --m3e-switch-selected-pressed-state-layer-opacity - State layer opacity when selected and pressed.
127
+ * @cssprop --m3e-switch-unselected-pressed-track-color - Track color when unselected and pressed.
128
+ * @cssprop --m3e-switch-unselected-pressed-track-outline-color - Outline color when unselected and pressed.
129
+ * @cssprop --m3e-switch-unselected-pressed-state-layer-color - State layer color when unselected and pressed.
130
+ * @cssprop --m3e-switch-unselected-pressed-state-layer-opacity - State layer opacity when unselected and pressed.
131
+ * @cssprop --m3e-switch-selected-pressed-handle-color - Handle color when selected and pressed.
132
+ * @cssprop --m3e-switch-unselected-pressed-handle-color - Handle color when unselected and pressed.
133
+ */
134
+ @customElement("m3e-switch")
135
+ export class M3eSwitchElement extends Labelled(
136
+ Dirty(
137
+ Touched(
138
+ ConstraintValidation(
139
+ Checked(FormAssociated(KeyboardClick(Focusable(Disabled(AttachInternals(Role(LitElement, "switch")))))))
140
+ )
141
+ )
142
+ )
143
+ ) {
144
+ /** The styles of the element. */
145
+ static override styles: CSSResultGroup = [
146
+ SwitchStyle,
147
+ SwitchStateLayerStyle,
148
+ SwitchTrackStyle,
149
+ SwitchHandleStyle,
150
+ SwitchIconStyle,
151
+ ];
152
+
153
+ /** @private */ @query(".track") private readonly _track?: HTMLElement;
154
+ /** @private */ @query(".focus-ring") private readonly _focusRing?: M3eFocusRingElement;
155
+ /** @private */ @query(".state-layer") private readonly _stateLayer?: M3eStateLayerElement;
156
+ /** @private */ readonly #clickHandler = (e: Event) => this.#handleClick(e);
157
+
158
+ /** @private */ readonly #hoverController = new HoverController(this, {
159
+ target: null,
160
+ callback: (hovering) => {
161
+ if (this.disabled) return;
162
+ if (hovering) {
163
+ this._stateLayer?.show("hover");
164
+ } else {
165
+ this._stateLayer?.hide("hover");
166
+ }
167
+ },
168
+ });
169
+
170
+ /** @private */ readonly #pressedController = new PressedController(this, {
171
+ target: null,
172
+ callback: (pressed) => this._track?.classList.toggle("pressed", pressed && !this.disabled),
173
+ });
174
+
175
+ constructor() {
176
+ super();
177
+
178
+ new PressedController(this, {
179
+ isPressedKey: (key) => key === " " || key === "Enter",
180
+ callback: (pressed) => this._track?.classList.toggle("pressed", pressed && !this.disabled),
181
+ });
182
+ }
183
+
184
+ /**
185
+ * The icons to present.
186
+ * @default "none"
187
+ */
188
+ @property({ reflect: true }) icons: SwitchIcons = "none";
189
+
190
+ /**
191
+ * A string representing the value of the switch.
192
+ * @default "on"
193
+ */
194
+ @property() value = "on";
195
+
196
+ /** @inheritdoc @private */
197
+ override get [formValue](): string | File | FormData | null {
198
+ return !this.checked ? null : this.value;
199
+ }
200
+
201
+ /** @inheritdoc */
202
+ override connectedCallback(): void {
203
+ super.connectedCallback();
204
+
205
+ this.addEventListener("click", this.#clickHandler);
206
+ for (const label of this.labels) {
207
+ this.#hoverController.observe(label);
208
+ this.#pressedController.observe(label);
209
+ }
210
+ }
211
+
212
+ /** @inheritdoc */
213
+ override disconnectedCallback(): void {
214
+ super.disconnectedCallback();
215
+
216
+ this.removeEventListener("click", this.#clickHandler);
217
+ for (const label of this.labels) {
218
+ this.#hoverController.unobserve(label);
219
+ this.#pressedController.unobserve(label);
220
+ }
221
+ }
222
+
223
+ /** @inheritdoc */
224
+ protected override firstUpdated(_changedProperties: PropertyValues): void {
225
+ super.firstUpdated(_changedProperties);
226
+ [this._focusRing, this._stateLayer].forEach((x) => x?.attach(this));
227
+ }
228
+
229
+ /** @inheritdoc */
230
+ protected override render(): unknown {
231
+ return html`<m3e-focus-ring class="focus-ring"></m3e-focus-ring>
232
+ <div class="track" aria-hidden="true">
233
+ <div class="touch" aria-hidden="true"></div>
234
+ <div class="handle">
235
+ <m3e-state-layer class="state-layer" ?disabled="${this.disabled}"></m3e-state-layer>
236
+ <div class="base">${this.#renderIcon()}</div>
237
+ </div>
238
+ </div>`;
239
+ }
240
+
241
+ /** @private */
242
+ #renderIcon(): unknown {
243
+ return this.checked
244
+ ? html`<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
245
+ <path fill="currentColor" d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z"></path>
246
+ </svg>`
247
+ : html`<svg class="icon" viewBox="0 -960 960 960" fill="currentColor">
248
+ <path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
249
+ </svg>`;
250
+ }
251
+
252
+ /** @private */
253
+ #handleClick(e: Event): void {
254
+ if (e.defaultPrevented) return;
255
+ this.checked = !this.checked;
256
+ if (this.dispatchEvent(new Event("input", { bubbles: true, composed: true, cancelable: true }))) {
257
+ this.dispatchEvent(new Event("change", { bubbles: true }));
258
+ } else {
259
+ this.checked = !this.checked;
260
+ }
261
+ }
262
+ }
263
+
264
+ declare global {
265
+ interface HTMLElementTagNameMap {
266
+ "m3e-switch": M3eSwitchElement;
267
+ }
268
+ }
@@ -0,0 +1,2 @@
1
+ /** Specifies the possible icons to display in a switch. */
2
+ export type SwitchIcons = "none" | "selected" | "both";
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./SwitchElement";
2
+ export * from "./SwitchIcons";
@@ -0,0 +1,140 @@
1
+ import { css, unsafeCSS } from "lit";
2
+ import { DesignToken } from "@m3e/core";
3
+
4
+ import { SwitchToken } from "./SwitchToken";
5
+
6
+ /**
7
+ * Handle styles for `M3eSwitchElement`.
8
+ * @internal
9
+ */
10
+ export const SwitchHandleStyle = css`
11
+ .handle {
12
+ position: relative;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ pointer-events: none;
17
+ transform-origin: center center;
18
+ border-radius: ${SwitchToken.handleShape};
19
+ transition: ${unsafeCSS(
20
+ `background-color ${DesignToken.motion.duration.short4} ${DesignToken.motion.easing.standard},
21
+ transform var(--_switch-handle-effect),
22
+ width ${DesignToken.motion.spring.fastEffects},
23
+ height ${DesignToken.motion.spring.fastEffects}`
24
+ )};
25
+ }
26
+ .track:not(.pressed) .handle {
27
+ --_switch-handle-effect: ${DesignToken.motion.spring.fastSpatial};
28
+ }
29
+ .track.pressed .handle {
30
+ --_switch-handle-effect: ${DesignToken.motion.spring.fastEffects};
31
+ }
32
+ :host(:not([aria-disabled="true"]):not([checked])[icons="both"]) .track:not(.pressed) .handle {
33
+ width: ${SwitchToken.withIconHandleWidth};
34
+ height: ${SwitchToken.withIconHandleHeight};
35
+ }
36
+ :host(:not([checked]):not([icons="both"])) .track:not(.pressed) .handle,
37
+ :host([aria-disabled="true"]:not([checked])) .handle {
38
+ width: ${SwitchToken.unselectedHandleWidth};
39
+ height: ${SwitchToken.unselectedHandleHeight};
40
+ }
41
+ :host([checked]) .track:not(.pressed) .handle {
42
+ width: ${SwitchToken.selectedHandleWidth};
43
+ height: ${SwitchToken.selectedHandleHeight};
44
+ }
45
+ .track.pressed .handle {
46
+ width: ${SwitchToken.pressedHandleWidth};
47
+ height: ${SwitchToken.pressedHandleHeight};
48
+ }
49
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):not(:hover)) .track:not(.pressed) .handle {
50
+ background-color: ${SwitchToken.unselectedHandleColor};
51
+ }
52
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):hover) .track:not(.pressed) .handle {
53
+ background-color: ${SwitchToken.unselectedHoverHandleColor};
54
+ }
55
+ :host(:not([aria-disabled="true"]):not([checked]):focus) .track:not(.pressed) .handle {
56
+ background-color: ${SwitchToken.unselectedFocusHandleColor};
57
+ }
58
+ :host(:not([aria-disabled="true"]):not([checked])) .track.pressed .handle {
59
+ background-color: ${SwitchToken.unselectedPressedHandleColor};
60
+ }
61
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):not(:hover)) .track:not(.pressed) .handle {
62
+ background-color: ${SwitchToken.selectedHandleColor};
63
+ }
64
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):hover) .track:not(.pressed) .handle {
65
+ background-color: ${SwitchToken.selectedHoverHandleColor};
66
+ }
67
+ :host(:not([aria-disabled="true"])[checked]:focus) .track:not(.pressed) .handle {
68
+ background-color: ${SwitchToken.selectedFocusHandleColor};
69
+ }
70
+ :host(:not([aria-disabled="true"])[checked]) .track.pressed .handle {
71
+ background-color: ${SwitchToken.selectedPressedHandleColor};
72
+ }
73
+ :host([aria-disabled="true"]:not([checked])) .handle {
74
+ background-color: color-mix(
75
+ in srgb,
76
+ ${SwitchToken.disabledUnselectedHandleColor} ${SwitchToken.disabledUnselectedHandleOpacity},
77
+ transparent
78
+ );
79
+ }
80
+ :host([aria-disabled="true"][checked]) .handle {
81
+ background-color: color-mix(
82
+ in srgb,
83
+ ${SwitchToken.disabledSelectedHandleColor} ${SwitchToken.disabledSelectedHandleOpacity},
84
+ transparent
85
+ );
86
+ }
87
+ :host([checked]) .track:not(.pressed) .handle {
88
+ transform: translateX(
89
+ calc(${SwitchToken.trackWidth} - ${SwitchToken.selectedHandleWidth} - ${SwitchToken.trackOutlineWidth})
90
+ );
91
+ }
92
+ :host([checked]) .track.pressed .handle {
93
+ transform: translateX(
94
+ calc(${SwitchToken.trackWidth} - ${SwitchToken.pressedHandleWidth} - ${SwitchToken.trackOutlineWidth})
95
+ );
96
+ }
97
+ :host(:not([checked]):not([icons="both"])) .track:not(.pressed) .handle,
98
+ :host([aria-disabled="true"]:not([checked])) .handle {
99
+ transform: translateX(
100
+ calc(
101
+ ${SwitchToken.trackOutlineWidth} + calc(${SwitchToken.pressedHandleWidth} - ${SwitchToken.withIconHandleWidth})
102
+ )
103
+ );
104
+ }
105
+ :host(:not([aria-disabled="true"]):not([checked])[icons="both"]) .track:not(.pressed) .handle {
106
+ transform: translateX(${SwitchToken.trackOutlineWidth});
107
+ }
108
+ @media (forced-colors: active) {
109
+ .handle {
110
+ transition: ${unsafeCSS(
111
+ `transform var(--_switch-handle-effect),
112
+ width ${DesignToken.motion.spring.fastEffects},
113
+ height ${DesignToken.motion.spring.fastEffects}`
114
+ )};
115
+ }
116
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):not(:hover)) .track:not(.pressed) .handle,
117
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):hover) .track:not(.pressed) .handle,
118
+ :host(:not([aria-disabled="true"]):not([checked]):focus) .track:not(.pressed) .handle,
119
+ :host(:not([aria-disabled="true"]):not([checked])) .track.pressed .handle {
120
+ background-color: ButtonText;
121
+ }
122
+ :host([aria-disabled="true"]:not([checked])) .handle {
123
+ background-color: GrayText;
124
+ }
125
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):not(:hover)) .track:not(.pressed) .handle,
126
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):hover) .track:not(.pressed) .handle,
127
+ :host(:not([aria-disabled="true"])[checked]:focus) .track:not(.pressed) .handle,
128
+ :host(:not([aria-disabled="true"])[checked]) .track.pressed .handle {
129
+ background-color: Canvas;
130
+ }
131
+ :host([aria-disabled="true"][checked]) .handle {
132
+ background-color: Canvas;
133
+ }
134
+ }
135
+ @media (prefers-reduced-motion) {
136
+ .handle {
137
+ transition: none;
138
+ }
139
+ }
140
+ `;
@@ -0,0 +1,89 @@
1
+ import { css, unsafeCSS } from "lit";
2
+ import { DesignToken } from "@m3e/core";
3
+
4
+ import { SwitchToken } from "./SwitchToken";
5
+
6
+ /**
7
+ * Icon styles for `M3eSwitchElement`.
8
+ * @internal
9
+ */
10
+ export const SwitchIconStyle = css`
11
+ :host([icons="none"]) .icon,
12
+ :host([icons="selected"]:not([checked])) .icon,
13
+ :host([aria-disabled="true"]:not([checked])) .icon {
14
+ display: none;
15
+ }
16
+ .icon {
17
+ width: 1em;
18
+ transition: ${unsafeCSS(`color ${DesignToken.motion.duration.short4} ${DesignToken.motion.easing.standard}`)};
19
+ }
20
+ :host(:not([checked])) .icon {
21
+ font-size: ${SwitchToken.unselectedIconSize};
22
+ }
23
+ :host([checked]) .icon {
24
+ font-size: ${SwitchToken.selectedIconSize};
25
+ }
26
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):not(:hover)) .track:not(.pressed) .icon {
27
+ color: ${SwitchToken.unselectedIconColor};
28
+ }
29
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):hover) .track:not(.pressed) .icon {
30
+ color: ${SwitchToken.unselectedHoverIconColor};
31
+ }
32
+ :host(:not([aria-disabled="true"]):not([checked]):focus) .track:not(.pressed) .icon {
33
+ color: ${SwitchToken.unselectedFocusIconColor};
34
+ }
35
+ :host(:not([aria-disabled="true"]):not([checked])) .track.pressed .icon {
36
+ color: ${SwitchToken.unselectedPressedIconColor};
37
+ }
38
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):not(:hover)) .track:not(.pressed) .icon {
39
+ color: ${SwitchToken.selectedIconColor};
40
+ }
41
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):hover) .track:not(.pressed) .icon {
42
+ color: ${SwitchToken.selectedHoverIconColor};
43
+ }
44
+ :host(:not([aria-disabled="true"])[checked]:focus) .track:not(.pressed) .icon {
45
+ color: ${SwitchToken.selectedFocusIconColor};
46
+ }
47
+ :host(:not([aria-disabled="true"])[checked]) .track.pressed .icon {
48
+ color: ${SwitchToken.selectedPressedIconColor};
49
+ }
50
+ :host([aria-disabled="true"]:not([checked])) .icon {
51
+ color: color-mix(
52
+ in srgb,
53
+ ${SwitchToken.disabledUnselectedIconColor} ${SwitchToken.disabledUnselectedIconOpacity},
54
+ transparent
55
+ );
56
+ }
57
+ :host([aria-disabled="true"][checked]) .icon {
58
+ color: color-mix(
59
+ in srgb,
60
+ ${SwitchToken.disabledSelectedIconColor} ${SwitchToken.disabledSelectedIconOpacity},
61
+ transparent
62
+ );
63
+ }
64
+ @media (forced-colors: active) {
65
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):not(:hover)) .track:not(.pressed) .icon,
66
+ :host(:not([aria-disabled="true"]):not([checked]):not(:focus):hover) .track:not(.pressed) .icon,
67
+ :host(:not([aria-disabled="true"]):not([checked]):focus) .track:not(.pressed) .icon,
68
+ :host(:not([aria-disabled="true"]):not([checked])) .track.pressed .icon {
69
+ color: Canvas;
70
+ }
71
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):not(:hover)) .track:not(.pressed) .icon,
72
+ :host(:not([aria-disabled="true"])[checked]:not(:focus):hover) .track:not(.pressed) .icon,
73
+ :host(:not([aria-disabled="true"])[checked]:focus) .track:not(.pressed) .icon,
74
+ :host(:not([aria-disabled="true"])[checked]) .track.pressed .icon {
75
+ color: CanvasText;
76
+ }
77
+ :host([aria-disabled="true"]:not([checked])) .icon {
78
+ color: Canvas;
79
+ }
80
+ :host([aria-disabled="true"][checked]) .icon {
81
+ color: GrayText;
82
+ }
83
+ }
84
+ @media (prefers-reduced-motion) {
85
+ .icon {
86
+ transition: none;
87
+ }
88
+ }
89
+ `;
@@ -0,0 +1,43 @@
1
+ import { css } from "lit";
2
+
3
+ import { SwitchToken } from "./SwitchToken";
4
+
5
+ /**
6
+ * State layer styles for `M3eSwitchElement`.
7
+ * @internal
8
+ */
9
+ export const SwitchStateLayerStyle = css`
10
+ .state-layer {
11
+ width: ${SwitchToken.stateLayerSize};
12
+ height: ${SwitchToken.stateLayerSize};
13
+ border-radius: ${SwitchToken.stateLayerShape};
14
+ }
15
+ :host(:not([checked])[icons="both"]) .track:not(.pressed) .state-layer {
16
+ left: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.withIconHandleWidth}) / 2));
17
+ top: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.withIconHandleHeight}) / 2));
18
+ }
19
+ :host(:not([checked]):not([icons="both"])) .track:not(.pressed) .state-layer {
20
+ left: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.unselectedHandleWidth}) / 2));
21
+ top: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.unselectedHandleHeight}) / 2));
22
+ }
23
+ :host([checked]) .track:not(.pressed) .state-layer {
24
+ left: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.selectedHandleWidth}) / 2));
25
+ top: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.selectedHandleHeight}) / 2));
26
+ }
27
+ .track.pressed .state-layer {
28
+ left: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.pressedHandleWidth}) / 2));
29
+ top: calc(0px - calc(calc(${SwitchToken.stateLayerSize} - ${SwitchToken.pressedHandleWidth}) / 2));
30
+ }
31
+ :host(:not([checked])) .state-layer {
32
+ --m3e-state-layer-hover-color: ${SwitchToken.unselectedHoverStateLayerColor};
33
+ --m3e-state-layer-hover-opacity: ${SwitchToken.unselectedHoverStateLayerOpacity};
34
+ --m3e-state-layer-focus-color: ${SwitchToken.unselectedFocusStateLayerColor};
35
+ --m3e-state-layer-focus-opacity: ${SwitchToken.unselectedFocusStateLayerOpacity};
36
+ }
37
+ :host([checked]) .state-layer {
38
+ --m3e-state-layer-hover-color: ${SwitchToken.selectedHoverStateLayerColor};
39
+ --m3e-state-layer-hover-opacity: ${SwitchToken.selectedHoverStateLayerOpacity};
40
+ --m3e-state-layer-focus-color: ${SwitchToken.selectedFocusStateLayerColor};
41
+ --m3e-state-layer-focus-opacity: ${SwitchToken.selectedFocusStateLayerOpacity};
42
+ }
43
+ `;
@@ -0,0 +1,30 @@
1
+ import { css } from "lit";
2
+ import { SwitchToken } from "./SwitchToken";
3
+
4
+ /**
5
+ * Baseline styles for `M3eSwitchElement`.
6
+ * @internal
7
+ */
8
+ export const SwitchStyle = css`
9
+ :host {
10
+ display: inline-block;
11
+ position: relative;
12
+ outline: none;
13
+ height: fit-content;
14
+ width: fit-content;
15
+ }
16
+ .focus-ring {
17
+ border-radius: ${SwitchToken.trackShape};
18
+ }
19
+ .touch {
20
+ position: absolute;
21
+ height: 3rem;
22
+ left: 0;
23
+ right: 0;
24
+ }
25
+ .base {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ }
30
+ `;