add segmented control / tabs component
This commit is contained in:
parent
1b0294a683
commit
dc9898f334
5 changed files with 291 additions and 0 deletions
28
src/lib/components/ui/segmented-control/Segment.svelte
Normal file
28
src/lib/components/ui/segmented-control/Segment.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!-- Segment Component -->
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import styles from './segment.module.scss';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<HTMLButtonAttributes, 'value'> {
|
||||
value: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
value,
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
{value}
|
||||
class={cn(styles.segment, className)}
|
||||
{...restProps}
|
||||
>
|
||||
<RadioGroupPrimitive.ItemIndicator class={styles.indicator} />
|
||||
<span class={styles.label}>{children}</span>
|
||||
</RadioGroupPrimitive.Item>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<!-- SegmentedControl Component -->
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
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;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(),
|
||||
onValueChange,
|
||||
variant = 'default',
|
||||
element = null,
|
||||
grow = false,
|
||||
gap = false,
|
||||
class: className,
|
||||
wrapperClass,
|
||||
children,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
$: if (onValueChange && value !== undefined) {
|
||||
onValueChange(value);
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: '',
|
||||
blended: styles.blended,
|
||||
background: styles.background
|
||||
};
|
||||
|
||||
const elementClasses = {
|
||||
wind: styles.wind,
|
||||
fire: styles.fire,
|
||||
water: styles.water,
|
||||
earth: styles.earth,
|
||||
dark: styles.dark,
|
||||
light: styles.light
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={cn(styles.wrapper, wrapperClass)}>
|
||||
<RadioGroupPrimitive.Root
|
||||
bind:value
|
||||
class={cn(
|
||||
styles.segmentedControl,
|
||||
variantClasses[variant],
|
||||
element && elementClasses[element],
|
||||
{
|
||||
[styles.grow]: grow,
|
||||
[styles.gap]: gap
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</RadioGroupPrimitive.Root>
|
||||
</div>
|
||||
2
src/lib/components/ui/segmented-control/index.ts
Normal file
2
src/lib/components/ui/segmented-control/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as SegmentedControl } from './SegmentedControl.svelte';
|
||||
export { default as Segment } from './Segment.svelte';
|
||||
65
src/lib/components/ui/segmented-control/segment.module.scss
Normal file
65
src/lib/components/ui/segmented-control/segment.module.scss
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
@use 'themes/spacing';
|
||||
@use 'themes/colors';
|
||||
@use 'themes/typography';
|
||||
@use 'themes/layout';
|
||||
@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;
|
||||
|
||||
&:hover .label {
|
||||
background: var(--page-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&[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%;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Blended variant styles
|
||||
:global(.blended) .segment[data-state='checked'] .label {
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
@use 'themes/spacing';
|
||||
@use 'themes/colors';
|
||||
@use 'themes/layout';
|
||||
@use 'themes/mixins';
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&.raid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include mixins.breakpoint(small-tablet) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.segmentedControl {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
position: relative;
|
||||
gap: spacing.$unit-half;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
@include mixins.breakpoint(small-tablet) {
|
||||
background: var(--card-bg);
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
&.blended {
|
||||
background: var(--input-bound-bg);
|
||||
border-radius: layout.$full-corner;
|
||||
}
|
||||
|
||||
&.background {
|
||||
background: var(--input-bg);
|
||||
border-radius: layout.$full-corner;
|
||||
}
|
||||
|
||||
&.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.gap {
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
|
||||
&.raid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Element colors
|
||||
&.fire {
|
||||
[data-state='checked'] {
|
||||
background: var(--fire-bg);
|
||||
color: var(--fire-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--fire-hover-bg);
|
||||
color: var(--fire-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.water {
|
||||
[data-state='checked'] {
|
||||
background: var(--water-bg);
|
||||
color: var(--water-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--water-hover-bg);
|
||||
color: var(--water-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.earth {
|
||||
[data-state='checked'] {
|
||||
background: var(--earth-bg);
|
||||
color: var(--earth-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--earth-hover-bg);
|
||||
color: var(--earth-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.wind {
|
||||
[data-state='checked'] {
|
||||
background: var(--wind-bg);
|
||||
color: var(--wind-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--wind-hover-bg);
|
||||
color: var(--wind-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
[data-state='checked'] {
|
||||
background: var(--light-bg);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--light-hover-bg);
|
||||
color: var(--light-hover-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
[data-state='checked'] {
|
||||
background: var(--dark-bg);
|
||||
color: var(--dark-text);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--dark-hover-bg);
|
||||
color: var(--dark-hover-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue