From bc67d22c4b8dd92170a0367f01e1ca592bead0ad Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 30 Nov 2025 06:02:50 -0800 Subject: [PATCH] checkbox and switch theming with hover states --- .../components/ui/checkbox/Checkbox.svelte | 227 +++++++++++------- src/lib/components/ui/switch/Switch.svelte | 193 ++++++++++++--- src/themes/_colors.scss | 10 + src/themes/themes.scss | 31 +++ 4 files changed, 336 insertions(+), 125 deletions(-) diff --git a/src/lib/components/ui/checkbox/Checkbox.svelte b/src/lib/components/ui/checkbox/Checkbox.svelte index 31e1706a..60d9d632 100644 --- a/src/lib/components/ui/checkbox/Checkbox.svelte +++ b/src/lib/components/ui/checkbox/Checkbox.svelte @@ -1,19 +1,27 @@ + {#snippet children({ checked: isChecked, indeterminate: isIndeterminate })} {#if isIndeterminate} - + {:else if isChecked} - + {@html CheckIcon} {/if} {/snippet} @@ -66,96 +79,140 @@ @use '$src/themes/typography' as *; @use '$src/themes/effects' as *; - .checkbox { + // Base checkbox styles + :global(.checkbox) { + // Default (no element) colors - light grey bg with dark grey check + --cb-checked-bg: var(--null-bg); + --cb-checked-bg-hover: var(--null-bg-hover); + --cb-checked-fg: #{$grey-45}; + background-color: var(--input-bg); - border: 2px solid var(--separator-bg); - border-radius: $item-corner-small; + border: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; @include smooth-transition($duration-zoom, all); + } - &:hover:not(:disabled) { - background-color: var(--input-bg-hover); - border-color: var(--separator-bg-hover); - } + :global(.checkbox:hover:not(:disabled)) { + background-color: var(--input-bg-hover); + } - &:focus, - &:focus-visible { - @include focus-ring($blue); - } + :global(.checkbox:focus), + :global(.checkbox:focus-visible) { + @include focus-ring($blue); + } - &[data-state='checked'], - &[data-state='indeterminate'] { - background-color: var(--accent-blue); - border-color: var(--accent-blue); + :global(.checkbox[data-state='checked']), + :global(.checkbox[data-state='indeterminate']) { + background-color: var(--cb-checked-bg); + } - &:hover:not(:disabled) { - background-color: var(--accent-blue-hover); - border-color: var(--accent-blue-hover); - } - } + :global(.checkbox[data-state='checked']:hover:not(:disabled)), + :global(.checkbox[data-state='indeterminate']:hover:not(:disabled)) { + background-color: var(--cb-checked-bg-hover); + } - &:disabled { - cursor: not-allowed; - opacity: 0.5; - } + :global(.checkbox:disabled) { + cursor: not-allowed; + opacity: 0.5; + } - &.bound { - background-color: var(--input-bound-bg); + :global(.checkbox.bound) { + background-color: var(--input-bound-bg); + } - &:hover:not(:disabled) { - background-color: var(--input-bound-bg-hover); - } + :global(.checkbox.bound:hover:not(:disabled)) { + background-color: var(--input-bound-bg-hover); + } - &[data-state='checked'], - &[data-state='indeterminate'] { - background-color: var(--accent-blue); - border-color: var(--accent-blue); - } - } + // Element-specific color overrides + :global(.checkbox.wind) { + --cb-checked-bg: var(--wind-button-bg); + --cb-checked-bg-hover: var(--wind-button-bg-hover); + --cb-checked-fg: white; + } + + :global(.checkbox.fire) { + --cb-checked-bg: var(--fire-button-bg); + --cb-checked-bg-hover: var(--fire-button-bg-hover); + --cb-checked-fg: white; + } + + :global(.checkbox.water) { + --cb-checked-bg: var(--water-button-bg); + --cb-checked-bg-hover: var(--water-button-bg-hover); + --cb-checked-fg: white; + } + + :global(.checkbox.earth) { + --cb-checked-bg: var(--earth-button-bg); + --cb-checked-bg-hover: var(--earth-button-bg-hover); + --cb-checked-fg: white; + } + + :global(.checkbox.dark) { + --cb-checked-bg: var(--dark-button-bg); + --cb-checked-bg-hover: var(--dark-button-bg-hover); + --cb-checked-fg: white; + } + + :global(.checkbox.light) { + --cb-checked-bg: var(--light-button-bg); + --cb-checked-bg-hover: var(--light-button-bg-hover); + --cb-checked-fg: white; } // Size variations - .small { - width: $unit-2x; - height: $unit-2x; - - .icon { - width: calc($unit * 1.5); - height: calc($unit * 1.5); - } - } - - .medium { - width: calc($unit * 2.5); - height: calc($unit * 2.5); - - .icon { - width: calc($unit * 1.75); - height: calc($unit * 1.75); - } - } - - .large { + :global(.checkbox.small) { + --cb-icon-size: #{calc($unit * 1.5)}; + --cb-dash-height: 3px; + border-radius: $item-corner; width: $unit-3x; height: $unit-3x; - - .icon { - width: calc($unit * 2.25); - height: calc($unit * 2.25); - } } - .indicator { + :global(.checkbox.medium) { + --cb-icon-size: #{$unit-2x}; + --cb-dash-height: 4px; + border-radius: $card-corner; + width: $unit-4x; + height: $unit-4x; + } + + :global(.checkbox.large) { + --cb-icon-size: #{calc($unit * 2.5)}; + --cb-dash-height: 4px; + border-radius: $card-corner; + width: $unit-5x; + height: $unit-5x; + } + + // Indicator and icon styles + :global(.checkbox .indicator) { display: flex; align-items: center; justify-content: center; - color: white; + color: var(--cb-checked-fg); } - .icon { - stroke-width: 3; + :global(.checkbox .icon) { + display: flex; + align-items: center; + justify-content: center; + + :global(svg) { + width: var(--cb-icon-size); + height: var(--cb-icon-size); + fill: currentColor; + } + + &.indeterminate { + width: var(--cb-icon-size); + height: var(--cb-dash-height); + background-color: var(--cb-checked-fg); + border-radius: $unit-fourth; + } } diff --git a/src/lib/components/ui/switch/Switch.svelte b/src/lib/components/ui/switch/Switch.svelte index 03b22e2b..0eaf1fe7 100644 --- a/src/lib/components/ui/switch/Switch.svelte +++ b/src/lib/components/ui/switch/Switch.svelte @@ -9,6 +9,12 @@ 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; @@ -20,6 +26,9 @@ required = false, name, value, + size = 'medium', + fullWidth = false, + element, onCheckedChange, class: className, thumbClass @@ -30,6 +39,10 @@ onCheckedChange(checked); } }); + + const switchClass = $derived( + ['switch', size, fullWidth && 'full', element, className].filter(Boolean).join(' ') + ); @@ -49,53 +62,153 @@ @use '$src/themes/layout' as *; @use '$src/themes/effects' as *; - .switch { - $height: calc($unit-4x + $unit-fourth); // 34px + // 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-radius: calc($height / 2); - border: none; - padding-left: $unit-half; - padding-right: $unit-half; + border: 2px solid transparent; + box-sizing: border-box; position: relative; - width: $unit-7x + $unit-fourth; // 58px - height: $height; cursor: pointer; @include smooth-transition($duration-instant, background-color); - - &:focus, - &:focus-visible { - @include focus-ring($blue); - } - - &[data-state='checked'] { - background: var(--accent-blue); - } - - &:disabled { - box-shadow: none; - cursor: not-allowed; - opacity: 0.5; - - .thumb { - background: $grey-80; - cursor: not-allowed; - } - } } - .thumb { - background: $grey-100; - border-radius: calc($unit-3x + $unit-fourth / 2); // 13px - display: block; + :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 - @include smooth-transition($duration-instant, transform); - transform: translateX(0px); - cursor: pointer; + border-radius: calc(($unit-3x + $unit-fourth) / 2); + } - &[data-state='checked'] { - background: $grey-100; - transform: translateX($unit-3x); // 24px - } + :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; } diff --git a/src/themes/_colors.scss b/src/themes/_colors.scss index e10c9e20..076e155f 100644 --- a/src/themes/_colors.scss +++ b/src/themes/_colors.scss @@ -261,6 +261,16 @@ $input--bound--bg--light--hover: $grey-85; $input--bound--bg--dark: $grey-15; $input--bound--bg--dark--hover: $grey-10; +// Color Definitions: Checkbox +$checkbox--bg--light: $grey-100; +$checkbox--bg--dark: $grey-10; +$checkbox--bg--light--hover: $grey-95; +$checkbox--bg--dark--hover: $grey-30; +$checkbox--text--light: $grey-50; +$checkbox--text--dark: $grey-80; +$checkbox--text--light--hover: $grey-30; +$checkbox--text--dark--hover: $grey-90; + // Color Definitions: Select $select--trigger--bg--light: $grey-100; $select--trigger--bg--dark: $grey-10; diff --git a/src/themes/themes.scss b/src/themes/themes.scss index b78bcc58..f3e792c8 100644 --- a/src/themes/themes.scss +++ b/src/themes/themes.scss @@ -1,3 +1,4 @@ +@use 'sass:color'; @use 'colors'; @use 'typography'; @use 'spacing'; @@ -150,6 +151,12 @@ --input-bound-bg: #{colors.$input--bound--bg--light}; --input-bound-bg-hover: #{colors.$input--bound--bg--light--hover}; + // Light - Checkboxes + --checkbox-bg: #{colors.$checkbox--bg--light}; + --checkbox-bg-hover: #{colors.$checkbox--bg--light--hover}; + --checkbox-text: #{colors.$checkbox--text--light}; + --checkbox-text-hover: #{colors.$checkbox--text--light--hover}; + // Light - Selects --select-bg: #{colors.$select--trigger--bg--light}; --select-contained-bg: #{colors.$select--trigger--contained--bg--light}; @@ -268,6 +275,15 @@ --light-button-bg: #{colors.$light-text-30}; --dark-button-bg: #{colors.$dark-text-30}; + // Light - Element button hover colors (slightly lighter than button-bg) + --null-button-bg-hover: #{colors.$grey-60}; + --wind-button-bg-hover: #{color.adjust(colors.$wind-text-30, $lightness: 8%)}; + --fire-button-bg-hover: #{color.adjust(colors.$fire-text-30, $lightness: 8%)}; + --water-button-bg-hover: #{color.adjust(colors.$water-text-30, $lightness: 8%)}; + --earth-button-bg-hover: #{color.adjust(colors.$earth-text-20, $lightness: 8%)}; + --light-button-bg-hover: #{color.adjust(colors.$light-text-30, $lightness: 8%)}; + --dark-button-bg-hover: #{color.adjust(colors.$dark-text-30, $lightness: 8%)}; + // Light - Element navigation selected background --null-nav-selected-bg: #{colors.$grey-85}; --wind-nav-selected-bg: #{colors.$wind-bg-20}; @@ -486,6 +502,12 @@ --input-bound-bg: #{colors.$input--bound--bg--dark}; --input-bound-bg-hover: #{colors.$input--bound--bg--dark--hover}; + // Dark - Checkboxes + --checkbox-bg: #{colors.$checkbox--bg--dark}; + --checkbox-bg-hover: #{colors.$checkbox--bg--dark--hover}; + --checkbox-text: #{colors.$checkbox--text--dark}; + --checkbox-text-hover: #{colors.$checkbox--text--dark--hover}; + // Dark - Selects --select-bg: #{colors.$select--trigger--bg--dark}; --select-contained-bg: #{colors.$select--trigger--contained--bg--dark}; @@ -604,6 +626,15 @@ --light-button-bg: #{colors.$light-text-30}; --dark-button-bg: #{colors.$dark-text-30}; + // Dark - Element button hover colors (same as light theme) + --null-button-bg-hover: #{colors.$grey-60}; + --wind-button-bg-hover: #{color.adjust(colors.$wind-text-30, $lightness: 8%)}; + --fire-button-bg-hover: #{color.adjust(colors.$fire-text-30, $lightness: 8%)}; + --water-button-bg-hover: #{color.adjust(colors.$water-text-30, $lightness: 8%)}; + --earth-button-bg-hover: #{color.adjust(colors.$earth-text-20, $lightness: 8%)}; + --light-button-bg-hover: #{color.adjust(colors.$light-text-30, $lightness: 8%)}; + --dark-button-bg-hover: #{color.adjust(colors.$dark-text-30, $lightness: 8%)}; + // Dark - Element navigation selected background (same as light theme) --null-nav-selected-bg: #{colors.$grey-85}; --wind-nav-selected-bg: #{colors.$wind-bg-20};