add select component with bits-ui
This commit is contained in:
parent
7f7661f542
commit
205e1045a6
3 changed files with 373 additions and 0 deletions
3
src/assets/icons/close.svg
Normal file
3
src/assets/icons/close.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.9425 11.8186C11.1844 12.0605 11.5766 12.0605 11.8186 11.8186C12.0605 11.5766 12.0605 11.1844 11.8186 10.9425L7.8761 7L11.8186 3.05755C12.0605 2.81562 12.0605 2.42338 11.8186 2.18145C11.5766 1.93952 11.1844 1.93952 10.9425 2.18145L7 6.1239L3.05755 2.18145C2.81562 1.93952 2.42338 1.93952 2.18145 2.18145C1.93952 2.42338 1.93952 2.81562 2.18145 3.05755L6.1239 7L2.18145 10.9425C1.93952 11.1844 1.93952 11.5766 2.18145 11.8186C2.42338 12.0605 2.81562 12.0605 3.05755 11.8186L7 7.8761L10.9425 11.8186Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 B |
117
src/lib/components/ui/select/Select.svelte
Normal file
117
src/lib/components/ui/select/Select.svelte
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<script lang="ts" generics="T extends string | number">
|
||||
import { Select } from 'bits-ui';
|
||||
import { ChevronDown, Check } from 'lucide-svelte';
|
||||
import styles from './select.module.scss';
|
||||
|
||||
interface Option {
|
||||
value: T;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
options: Option[];
|
||||
value?: T;
|
||||
onValueChange?: (value: T | undefined) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
variant?: 'default' | 'bound' | 'small' | 'table';
|
||||
position?: 'left' | 'right';
|
||||
fullWidth?: boolean;
|
||||
hidden?: boolean;
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
options = [],
|
||||
value = $bindable(),
|
||||
onValueChange,
|
||||
placeholder = 'Select an option',
|
||||
disabled = false,
|
||||
variant = 'default',
|
||||
position = 'left',
|
||||
fullWidth = false,
|
||||
hidden = false,
|
||||
name,
|
||||
required = false,
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
const selected = $derived(options.find(opt => opt.value === value));
|
||||
|
||||
const triggerClasses = [
|
||||
styles.trigger,
|
||||
variant === 'bound' && styles.bound,
|
||||
variant === 'small' && styles.small,
|
||||
variant === 'table' && styles.table,
|
||||
position === 'right' && styles.right,
|
||||
position === 'left' && styles.left,
|
||||
fullWidth && styles.full,
|
||||
hidden && styles.hidden,
|
||||
disabled && styles.disabled,
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const contentClasses = [
|
||||
styles.select,
|
||||
variant === 'bound' && styles.bound
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
function handleValueChange(newValue: string | undefined) {
|
||||
if (newValue !== undefined && onValueChange) {
|
||||
onValueChange(newValue as T);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select.Root
|
||||
bind:value
|
||||
onValueChange={handleValueChange}
|
||||
{disabled}
|
||||
{name}
|
||||
{required}
|
||||
>
|
||||
<Select.Trigger
|
||||
class={triggerClasses}
|
||||
data-placeholder={!selected}
|
||||
>
|
||||
{#if selected?.image}
|
||||
<img src={selected.image} alt={selected.label} />
|
||||
{/if}
|
||||
<span>{selected?.label || placeholder}</span>
|
||||
<span class={styles.icon}>
|
||||
<ChevronDown size={16} />
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class={contentClasses}>
|
||||
<div class={`${styles.scroll} ${styles.up}`}>
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
|
||||
<Select.Viewport>
|
||||
{#each options as option}
|
||||
<Select.Item
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
class={styles.item}
|
||||
>
|
||||
{#if option.image}
|
||||
<img src={option.image} alt={option.label} />
|
||||
{/if}
|
||||
<span>{option.label}</span>
|
||||
<Select.ItemIndicator class={styles.indicator}>
|
||||
<Check size={16} />
|
||||
</Select.ItemIndicator>
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
|
||||
<div class={`${styles.scroll} ${styles.down}`}>
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
253
src/lib/components/ui/select/select.module.scss
Normal file
253
src/lib/components/ui/select/select.module.scss
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
@use 'themes/spacing';
|
||||
@use 'themes/colors';
|
||||
@use 'themes/typography';
|
||||
@use 'themes/layout';
|
||||
@use 'themes/effects';
|
||||
@use 'themes/mixins';
|
||||
|
||||
.trigger {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: layout.$input-corner;
|
||||
border: 2px solid transparent;
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
padding: (spacing.$unit * 1.5) spacing.$unit-2x;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.18s ease-out;
|
||||
|
||||
&.small {
|
||||
& > span:not(.icon) {
|
||||
font-size: typography.$font-small;
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@include mixins.breakpoint(tablet) {
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: calc(spacing.$unit-2x - 1px);
|
||||
}
|
||||
|
||||
& > span:not(.icon) {
|
||||
width: 100%;
|
||||
max-width: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.left {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.right {
|
||||
flex-grow: 0;
|
||||
text-align: right;
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
&.bound {
|
||||
background-color: var(--select-contained-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--select-contained-bg-hover);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--select-contained-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.table {
|
||||
min-width: spacing.$unit * 30;
|
||||
|
||||
@include mixins.breakpoint(phone) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--input-bg-hover);
|
||||
|
||||
span:not(.icon),
|
||||
&[data-placeholder] > span:not(.icon) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
fill: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
background-color: var(--input-bg);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-placeholder='true'] > span:not(.icon) {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
& > span:not(.icon) {
|
||||
color: var(--text-primary);
|
||||
flex-grow: 1;
|
||||
font-size: typography.$font-regular;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img {
|
||||
width: spacing.$unit-4x;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
animation: scaleIn effects.$duration-zoom ease-out;
|
||||
background: var(--dialog-bg);
|
||||
border-radius: layout.$card-corner;
|
||||
border: 1px solid rgba(0, 0, 0, 0.24);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
|
||||
padding: 0 spacing.$unit;
|
||||
min-width: var(--radix-select-trigger-width);
|
||||
transform-origin: var(--radix-select-content-transform-origin);
|
||||
max-height: 40vh;
|
||||
z-index: 40;
|
||||
|
||||
&.bound {
|
||||
background-color: var(--select-content-contained-bg);
|
||||
}
|
||||
|
||||
.scroll.up,
|
||||
.scroll.down {
|
||||
padding: spacing.$unit 0;
|
||||
text-align: center;
|
||||
|
||||
&:hover svg {
|
||||
fill: var(--icon-secondary-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--icon-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll.up {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
20% {
|
||||
opacity: 0.2;
|
||||
transform: scale(0.4);
|
||||
}
|
||||
40% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
60% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
65% {
|
||||
opacity: 0.65;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.75;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
80% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
90% {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
border-radius: layout.$item-corner-small;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
padding: spacing.$unit spacing.$unit-2x;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
transition: background-color 0.12s ease-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--option-bg-hover);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
background-color: var(--option-bg-hover);
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
|
||||
img {
|
||||
width: spacing.$unit-3x;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
|
||||
svg {
|
||||
fill: var(--accent-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue