refactor segmented control

This commit is contained in:
Justin Edmund 2025-09-29 23:44:37 -07:00
parent a9f6336427
commit 1b2bee497b
4 changed files with 182 additions and 90 deletions

View file

@ -1,28 +1,42 @@
<!-- Segment Component -->
<svelte:options runes={true} />
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import styles from './segment.module.scss';
import type { HTMLButtonAttributes } from 'svelte/elements';
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
import { getContext } from 'svelte'
import styles from './segment.module.scss'
import type { HTMLButtonAttributes } from 'svelte/elements'
import type { SegmentedControlVariant } from './SegmentedControl.svelte'
interface Props extends Omit<HTMLButtonAttributes, 'value'> {
value: string;
class?: string;
value: string
class?: string
}
let {
value,
class: className,
children: content,
...restProps
}: Props = $props();
let { value, class: className, children: content, ...restProps }: Props = $props()
// Get variant from parent context
const variant = getContext<SegmentedControlVariant>('segmented-control-variant') || 'default'
// Apply variant-specific classes
const variantClasses = {
default: styles.default,
blended: styles.blended,
background: styles.background
}
const segmentClass = $derived(
[
styles.segment,
variantClasses[variant],
className || ''
]
.filter(Boolean)
.join(' ')
)
</script>
<RadioGroupPrimitive.Item
{value}
class={`${styles.segment} ${className || ''}`}
{...restProps}
>
<RadioGroupPrimitive.Item {value} class={segmentClass} {...restProps}>
{#snippet children({ checked })}
{#if checked}
<div class={styles.indicator}></div>

View file

@ -1,19 +1,24 @@
<!-- SegmentedControl Component -->
<svelte:options runes={true} />
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
import styles from './segmented-control.module.scss';
import type { HTMLDivAttributes } from 'svelte/elements';
interface Props extends HTMLDivAttributes {
value?: string;
onValueChange?: (value: string) => void;
variant?: 'default' | 'blended' | 'background';
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | null;
grow?: boolean;
gap?: boolean;
class?: string;
wrapperClass?: string;
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
import { setContext } from 'svelte'
import styles from './segmented-control.module.scss'
import type { HTMLAttributes } from 'svelte/elements'
export type SegmentedControlVariant = 'default' | 'blended' | 'background'
interface Props extends HTMLAttributes<HTMLDivElement> {
value?: string
onValueChange?: (value: string) => void
variant?: SegmentedControlVariant
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | null
grow?: boolean
gap?: boolean
class?: string
wrapperClass?: string
children?: import('svelte').Snippet
}
let {
@ -27,19 +32,22 @@
wrapperClass,
children,
...restProps
}: Props = $props();
}: Props = $props()
// Provide variant to child segments via context
setContext('segmented-control-variant', variant)
$effect(() => {
if (onValueChange && value !== undefined) {
onValueChange(value);
onValueChange(value)
}
});
})
const variantClasses = {
default: '',
blended: styles.blended,
background: styles.background
};
}
const elementClasses = {
wind: styles.wind,
@ -48,15 +56,34 @@
earth: styles.earth,
dark: styles.dark,
light: styles.light
};
}
const classList = $derived(
[
styles.segmentedControl,
variantClasses[variant],
element ? elementClasses[element] : '',
grow ? styles.grow : '',
gap ? styles.gap : '',
className || ''
]
.filter(Boolean)
.join(' ')
)
const wrapperClassList = $derived(
[
styles.wrapper,
grow ? styles.growWrapper : '',
wrapperClass || ''
]
.filter(Boolean)
.join(' ')
)
</script>
<div class={`${styles.wrapper} ${wrapperClass || ''}`}>
<RadioGroupPrimitive.Root
bind:value
class={`${styles.segmentedControl} ${variantClasses[variant]} ${element ? elementClasses[element] : ''} ${grow ? styles.grow : ''} ${gap ? styles.gap : ''} ${className || ''}`}
{...restProps}
>
<div class={wrapperClassList}>
<RadioGroupPrimitive.Root bind:value class={classList} {...restProps}>
{@render children?.()}
</RadioGroupPrimitive.Root>
</div>

View file

@ -5,61 +5,105 @@
@use 'themes/mixins';
.segment {
color: colors.$grey-55;
cursor: pointer;
flex-grow: 1;
font-size: typography.$font-regular;
font-weight: typography.$normal;
min-width: 100px;
position: relative;
background: transparent;
border: none;
padding: 0;
text-align: center;
outline: none;
color: var(--text-secondary);
cursor: pointer;
flex-grow: 1;
font-size: typography.$font-regular;
font-weight: typography.$normal;
min-width: 100px;
position: relative;
background: transparent;
border: none;
padding: 0;
text-align: center;
outline: none;
&:hover .label {
background: var(--page-hover);
color: var(--text-primary);
}
&:focus-visible .label {
outline: 2px solid colors.$blue;
outline-offset: 2px;
}
&[data-state='checked'] .label {
background: var(--background);
color: var(--text-primary);
}
&:focus-visible .label {
outline: 2px solid colors.$blue;
outline-offset: 2px;
}
@include mixins.breakpoint(phone) {
min-width: initial;
width: 100%;
}
@include mixins.breakpoint(phone) {
min-width: initial;
width: 100%;
}
}
.label {
border-radius: spacing.$unit * 3;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
height: 100%;
white-space: nowrap;
overflow: hidden;
padding: 8px 12px;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.15s ease-in-out;
border: 0.5px solid transparent;
border-radius: layout.$full-corner;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
font-weight: typography.$medium;
height: 100%;
white-space: nowrap;
overflow: hidden;
padding: calc(spacing.$unit * 1.5) spacing.$unit-2x;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.indicator {
display: none;
display: none;
}
// Default variant styles
.default {
.label {
background: transparent;
color: var(--text-secondary);
}
&:hover .label {
background: var(--page-hover);
color: var(--text-primary);
}
&[data-state='checked'] .label {
background: var(--background);
color: var(--text-primary);
}
}
// Blended variant styles
:global(.blended) .segment[data-state='checked'] .label {
background: var(--card-bg);
.blended {
.label {
background: var(--segmented-control-blended-segment-bg);
color: var(--segmented-control-blended-segment-text);
}
&:hover .label {
background: var(--segmented-control-blended-segment-bg-hover);
color: var(--segmented-control-blended-segment-text-hover);
}
&[data-state='checked'] .label {
background: var(--segmented-control-blended-segment-bg-checked);
color: var(--segmented-control-blended-segment-text-checked);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.12);
}
}
// Background variant styles
.background {
.label {
background: var(--segmented-control-background-segment-bg);
color: var(--segmented-control-background-segment-text);
}
&:hover .label {
background: var(--segmented-control-background-segment-bg-hover);
color: var(--segmented-control-background-segment-text-hover);
}
&[data-state='checked'] .label {
background: var(--segmented-control-background-segment-bg-checked);
color: var(--segmented-control-background-segment-text-checked);
border: 0.5px solid rgba(0, 0, 0, 0.12);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.08);
}
}

View file

@ -16,6 +16,10 @@
}
}
.growWrapper {
width: 100%;
}
.segmentedControl {
display: inline-flex;
position: relative;
@ -30,17 +34,20 @@
}
&.blended {
background: var(--input-bound-bg);
background: var(--segmented-control-blended-bg);
border-radius: layout.$full-corner;
padding: spacing.$unit-half;
}
&.background {
background: var(--input-bg);
background: var(--segmented-control-background-bg);
border-radius: layout.$full-corner;
padding: spacing.$unit-half;
}
&.grow {
flex-grow: 1;
width: 100%;
}
&.gap {
@ -51,7 +58,7 @@
width: 100%;
}
// Element colors
// Element colors (not used currently, but keeping for future)
&.fire {
[data-state='checked'] {
background: var(--fire-bg);
@ -123,4 +130,4 @@
color: var(--dark-hover-text);
}
}
}
}