feat: Select, Checkbox, Toggle form primitives
This commit is contained in:
69
src/design-system/primitives/Checkbox/Checkbox.module.css
Normal file
69
src/design-system/primitives/Checkbox/Checkbox.module.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box::after {
|
||||||
|
content: '';
|
||||||
|
display: none;
|
||||||
|
width: 4px;
|
||||||
|
height: 7px;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
transform: rotate(45deg) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked + .box {
|
||||||
|
background: var(--amber);
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked + .box::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus-visible + .box {
|
||||||
|
border-color: var(--amber);
|
||||||
|
box-shadow: 0 0 0 3px var(--amber-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled + .box {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:has(.input:disabled) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
28
src/design-system/primitives/Checkbox/Checkbox.tsx
Normal file
28
src/design-system/primitives/Checkbox/Checkbox.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import styles from './Checkbox.module.css'
|
||||||
|
import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||||
|
label?: ReactNode
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
({ label, id, className, ...rest }, ref) => {
|
||||||
|
const inputId = id ?? `checkbox-${Math.random().toString(36).slice(2)}`
|
||||||
|
return (
|
||||||
|
<label className={`${styles.wrapper} ${className ?? ''}`} htmlFor={inputId}>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
id={inputId}
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.input}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<span className={styles.box} aria-hidden="true" />
|
||||||
|
{label && <span className={styles.label}>{label}</span>}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Checkbox.displayName = 'Checkbox'
|
||||||
40
src/design-system/primitives/Select/Select.module.css
Normal file
40
src/design-system/primitives/Select/Select.module.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 32px 6px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:focus {
|
||||||
|
border-color: var(--amber);
|
||||||
|
box-shadow: 0 0 0 3px var(--amber-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
30
src/design-system/primitives/Select/Select.tsx
Normal file
30
src/design-system/primitives/Select/Select.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import styles from './Select.module.css'
|
||||||
|
import { forwardRef, type SelectHTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
options: SelectOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
|
({ options, className, ...rest }, ref) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.wrap} ${className ?? ''}`}>
|
||||||
|
<select ref={ref} className={styles.select} {...rest}>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<span className={styles.chevron} aria-hidden="true">▾</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Select.displayName = 'Select'
|
||||||
68
src/design-system/primitives/Toggle/Toggle.module.css
Normal file
68
src/design-system/primitives/Toggle/Toggle.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--border);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked + .track {
|
||||||
|
background: var(--amber);
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked + .track .thumb {
|
||||||
|
transform: translateX(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus-visible + .track {
|
||||||
|
box-shadow: 0 0 0 3px var(--amber-bg);
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled + .track {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:has(.input:disabled) {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
30
src/design-system/primitives/Toggle/Toggle.tsx
Normal file
30
src/design-system/primitives/Toggle/Toggle.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import styles from './Toggle.module.css'
|
||||||
|
import { forwardRef, type InputHTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
interface ToggleProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||||
|
label?: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
|
||||||
|
({ label, id, className, ...rest }, ref) => {
|
||||||
|
const inputId = id ?? `toggle-${Math.random().toString(36).slice(2)}`
|
||||||
|
return (
|
||||||
|
<label className={`${styles.wrapper} ${className ?? ''}`} htmlFor={inputId}>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
id={inputId}
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.input}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<span className={styles.track} aria-hidden="true">
|
||||||
|
<span className={styles.thumb} />
|
||||||
|
</span>
|
||||||
|
{label && <span className={styles.label}>{label}</span>}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Toggle.displayName = 'Toggle'
|
||||||
Reference in New Issue
Block a user