hensei-web/src/lib/components/ui/switch/Switch.svelte

214 lines
5.4 KiB
Svelte

<!-- Switch Component -->
<svelte:options runes={true} />
<script lang="ts">
import { Switch as SwitchPrimitive } from 'bits-ui';
interface Props {
checked?: boolean;
disabled?: boolean;
required?: boolean;
name?: string;
value?: string;
/** Switch size */
size?: 'small' | 'medium' | 'large';
/** Full width switch */
fullWidth?: boolean;
/** Element color theme for checked state */
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | undefined;
onCheckedChange?: (checked: boolean) => void;
class?: string;
thumbClass?: string;
}
let {
checked = $bindable(false),
disabled = false,
required = false,
name,
value,
size = 'medium',
fullWidth = false,
element,
onCheckedChange,
class: className,
thumbClass
}: Props = $props();
$effect(() => {
if (onCheckedChange && checked !== undefined) {
onCheckedChange(checked);
}
});
const switchClass = $derived(
['switch', size, fullWidth && 'full', element, className].filter(Boolean).join(' ')
);
</script>
<SwitchPrimitive.Root
bind:checked
{disabled}
{required}
{...(name !== undefined ? { name } : {})}
{...(value !== undefined ? { value } : {})}
class={switchClass}
>
<SwitchPrimitive.Thumb class="thumb {thumbClass || ''}" />
</SwitchPrimitive.Root>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/effects' as *;
// Base switch styles - wrapped in :global() for Bits UI
:global([data-switch-root].switch) {
// Default (no element) colors
--sw-checked-bg: var(--null-button-bg);
--sw-checked-bg-hover: var(--null-button-bg-hover);
background: $grey-70;
border: 2px solid transparent;
box-sizing: border-box;
position: relative;
cursor: pointer;
@include smooth-transition($duration-instant, background-color);
}
:global([data-switch-root].switch:focus),
:global([data-switch-root].switch:focus-visible) {
@include focus-ring($blue);
}
:global([data-switch-root].switch:hover:not(:disabled)) {
background: $grey-75;
}
:global([data-switch-root].switch[data-state='checked']) {
background: var(--sw-checked-bg);
}
:global([data-switch-root].switch[data-state='checked']:hover:not(:disabled)) {
background: var(--sw-checked-bg-hover);
}
:global([data-switch-root].switch:disabled) {
box-shadow: none;
cursor: not-allowed;
opacity: 0.5;
}
:global([data-switch-root].switch.full) {
width: 100%;
}
// Element-specific color overrides
:global([data-switch-root].switch.wind) {
--sw-checked-bg: var(--wind-button-bg);
--sw-checked-bg-hover: var(--wind-button-bg-hover);
}
:global([data-switch-root].switch.fire) {
--sw-checked-bg: var(--fire-button-bg);
--sw-checked-bg-hover: var(--fire-button-bg-hover);
}
:global([data-switch-root].switch.water) {
--sw-checked-bg: var(--water-button-bg);
--sw-checked-bg-hover: var(--water-button-bg-hover);
}
:global([data-switch-root].switch.earth) {
--sw-checked-bg: var(--earth-button-bg);
--sw-checked-bg-hover: var(--earth-button-bg-hover);
}
:global([data-switch-root].switch.dark) {
--sw-checked-bg: var(--dark-button-bg);
--sw-checked-bg-hover: var(--dark-button-bg-hover);
}
:global([data-switch-root].switch.light) {
--sw-checked-bg: var(--light-button-bg);
--sw-checked-bg-hover: var(--light-button-bg-hover);
}
:global([data-switch-root].switch:disabled .thumb) {
background: $grey-80;
cursor: not-allowed;
}
// Size: Small
:global([data-switch-root].switch.small) {
$height: $unit-3x; // 24px
border-radius: calc($height / 2);
padding-left: $unit-fourth;
padding-right: $unit-fourth;
width: calc($unit-5x + $unit-half); // 44px
height: $height;
}
:global([data-switch-root].switch.small .thumb) {
height: calc($unit-2x + $unit-half); // 20px
width: calc($unit-2x + $unit-half); // 20px
border-radius: calc(($unit-2x + $unit-half) / 2);
}
:global([data-switch-root].switch.small .thumb[data-state='checked']) {
transform: translateX(calc($unit-2x + $unit-half)); // 20px
}
// Size: Medium (default)
:global([data-switch-root].switch.medium) {
$height: calc($unit-4x + $unit-fourth); // 34px
border-radius: calc($height / 2);
padding-left: $unit-half;
padding-right: $unit-half;
width: $unit-7x + $unit-fourth; // 58px
height: $height;
}
:global([data-switch-root].switch.medium .thumb) {
height: $unit-3x + $unit-fourth; // 26px
width: $unit-3x + $unit-fourth; // 26px
border-radius: calc(($unit-3x + $unit-fourth) / 2);
}
:global([data-switch-root].switch.medium .thumb[data-state='checked']) {
transform: translateX(21px);
}
// Size: Large
:global([data-switch-root].switch.large) {
$height: $unit-5x; // 40px
border-radius: calc($height / 2);
padding-left: $unit-half;
padding-right: $unit-half;
width: calc($unit-8x + $unit); // 72px
height: $height;
}
:global([data-switch-root].switch.large .thumb) {
height: calc($unit-4x); // 32px
width: calc($unit-4x); // 32px
border-radius: $unit-2x;
}
:global([data-switch-root].switch.large .thumb[data-state='checked']) {
transform: translateX(calc($unit-4x)); // 32px
}
// Thumb base styles
:global([data-switch-root] .thumb) {
background: $grey-100;
display: block;
@include smooth-transition($duration-instant, transform);
transform: translateX(-1px);
cursor: pointer;
}
:global([data-switch-root] .thumb[data-state='checked']) {
background: $grey-100;
}
</style>