refactor segmented control
This commit is contained in:
parent
a9f6336427
commit
1b2bee497b
4 changed files with 182 additions and 90 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue