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 -->
|
<!-- Segment Component -->
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
|
||||||
import styles from './segment.module.scss';
|
import { getContext } from 'svelte'
|
||||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
import styles from './segment.module.scss'
|
||||||
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||||
|
import type { SegmentedControlVariant } from './SegmentedControl.svelte'
|
||||||
|
|
||||||
interface Props extends Omit<HTMLButtonAttributes, 'value'> {
|
interface Props extends Omit<HTMLButtonAttributes, 'value'> {
|
||||||
value: string;
|
value: string
|
||||||
class?: string;
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { value, class: className, children: content, ...restProps }: Props = $props()
|
||||||
value,
|
|
||||||
class: className,
|
// Get variant from parent context
|
||||||
children: content,
|
const variant = getContext<SegmentedControlVariant>('segmented-control-variant') || 'default'
|
||||||
...restProps
|
|
||||||
}: Props = $props();
|
// 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>
|
</script>
|
||||||
|
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item {value} class={segmentClass} {...restProps}>
|
||||||
{value}
|
|
||||||
class={`${styles.segment} ${className || ''}`}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{#snippet children({ checked })}
|
{#snippet children({ checked })}
|
||||||
{#if checked}
|
{#if checked}
|
||||||
<div class={styles.indicator}></div>
|
<div class={styles.indicator}></div>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
<!-- SegmentedControl Component -->
|
<!-- SegmentedControl Component -->
|
||||||
<svelte:options runes={true} />
|
<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 {
|
<script lang="ts">
|
||||||
value?: string;
|
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
|
||||||
onValueChange?: (value: string) => void;
|
import { setContext } from 'svelte'
|
||||||
variant?: 'default' | 'blended' | 'background';
|
import styles from './segmented-control.module.scss'
|
||||||
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | null;
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
grow?: boolean;
|
|
||||||
gap?: boolean;
|
export type SegmentedControlVariant = 'default' | 'blended' | 'background'
|
||||||
class?: string;
|
|
||||||
wrapperClass?: string;
|
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 {
|
let {
|
||||||
|
|
@ -27,19 +32,22 @@
|
||||||
wrapperClass,
|
wrapperClass,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: Props = $props();
|
}: Props = $props()
|
||||||
|
|
||||||
|
// Provide variant to child segments via context
|
||||||
|
setContext('segmented-control-variant', variant)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (onValueChange && value !== undefined) {
|
if (onValueChange && value !== undefined) {
|
||||||
onValueChange(value);
|
onValueChange(value)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: '',
|
default: '',
|
||||||
blended: styles.blended,
|
blended: styles.blended,
|
||||||
background: styles.background
|
background: styles.background
|
||||||
};
|
}
|
||||||
|
|
||||||
const elementClasses = {
|
const elementClasses = {
|
||||||
wind: styles.wind,
|
wind: styles.wind,
|
||||||
|
|
@ -48,15 +56,34 @@
|
||||||
earth: styles.earth,
|
earth: styles.earth,
|
||||||
dark: styles.dark,
|
dark: styles.dark,
|
||||||
light: styles.light
|
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>
|
</script>
|
||||||
|
|
||||||
<div class={`${styles.wrapper} ${wrapperClass || ''}`}>
|
<div class={wrapperClassList}>
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root bind:value class={classList} {...restProps}>
|
||||||
bind:value
|
|
||||||
class={`${styles.segmentedControl} ${variantClasses[variant]} ${element ? elementClasses[element] : ''} ${grow ? styles.grow : ''} ${gap ? styles.gap : ''} ${className || ''}`}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</RadioGroupPrimitive.Root>
|
</RadioGroupPrimitive.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5,61 +5,105 @@
|
||||||
@use 'themes/mixins';
|
@use 'themes/mixins';
|
||||||
|
|
||||||
.segment {
|
.segment {
|
||||||
color: colors.$grey-55;
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: typography.$font-regular;
|
font-size: typography.$font-regular;
|
||||||
font-weight: typography.$normal;
|
font-weight: typography.$normal;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:hover .label {
|
&:focus-visible .label {
|
||||||
background: var(--page-hover);
|
outline: 2px solid colors.$blue;
|
||||||
color: var(--text-primary);
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-state='checked'] .label {
|
@include mixins.breakpoint(phone) {
|
||||||
background: var(--background);
|
min-width: initial;
|
||||||
color: var(--text-primary);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible .label {
|
|
||||||
outline: 2px solid colors.$blue;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mixins.breakpoint(phone) {
|
|
||||||
min-width: initial;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
border-radius: spacing.$unit * 3;
|
border: 0.5px solid transparent;
|
||||||
display: flex;
|
border-radius: layout.$full-corner;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
text-align: center;
|
||||||
height: 100%;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
font-weight: typography.$medium;
|
||||||
overflow: hidden;
|
height: 100%;
|
||||||
padding: 8px 12px;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
padding: calc(spacing.$unit * 1.5) spacing.$unit-2x;
|
||||||
transition: all 0.15s ease-in-out;
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator {
|
.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
|
// Blended variant styles
|
||||||
:global(.blended) .segment[data-state='checked'] .label {
|
.blended {
|
||||||
background: var(--card-bg);
|
.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 {
|
.segmentedControl {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -30,17 +34,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blended {
|
&.blended {
|
||||||
background: var(--input-bound-bg);
|
background: var(--segmented-control-blended-bg);
|
||||||
border-radius: layout.$full-corner;
|
border-radius: layout.$full-corner;
|
||||||
|
padding: spacing.$unit-half;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.background {
|
&.background {
|
||||||
background: var(--input-bg);
|
background: var(--segmented-control-background-bg);
|
||||||
border-radius: layout.$full-corner;
|
border-radius: layout.$full-corner;
|
||||||
|
padding: spacing.$unit-half;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.grow {
|
&.grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.gap {
|
&.gap {
|
||||||
|
|
@ -51,7 +58,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Element colors
|
// Element colors (not used currently, but keeping for future)
|
||||||
&.fire {
|
&.fire {
|
||||||
[data-state='checked'] {
|
[data-state='checked'] {
|
||||||
background: var(--fire-bg);
|
background: var(--fire-bg);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue