From e37a4d323c396390583d3bb65b357bdd252a0251 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:29:00 +0100 Subject: [PATCH] feat: Select, Checkbox, Toggle form primitives --- .../primitives/Checkbox/Checkbox.module.css | 69 +++++++++++++++++++ .../primitives/Checkbox/Checkbox.tsx | 28 ++++++++ .../primitives/Select/Select.module.css | 40 +++++++++++ .../primitives/Select/Select.tsx | 30 ++++++++ .../primitives/Toggle/Toggle.module.css | 68 ++++++++++++++++++ .../primitives/Toggle/Toggle.tsx | 30 ++++++++ 6 files changed, 265 insertions(+) create mode 100644 src/design-system/primitives/Checkbox/Checkbox.module.css create mode 100644 src/design-system/primitives/Checkbox/Checkbox.tsx create mode 100644 src/design-system/primitives/Select/Select.module.css create mode 100644 src/design-system/primitives/Select/Select.tsx create mode 100644 src/design-system/primitives/Toggle/Toggle.module.css create mode 100644 src/design-system/primitives/Toggle/Toggle.tsx diff --git a/src/design-system/primitives/Checkbox/Checkbox.module.css b/src/design-system/primitives/Checkbox/Checkbox.module.css new file mode 100644 index 0000000..babb3bb --- /dev/null +++ b/src/design-system/primitives/Checkbox/Checkbox.module.css @@ -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); +} diff --git a/src/design-system/primitives/Checkbox/Checkbox.tsx b/src/design-system/primitives/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..2662f0e --- /dev/null +++ b/src/design-system/primitives/Checkbox/Checkbox.tsx @@ -0,0 +1,28 @@ +import styles from './Checkbox.module.css' +import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react' + +interface CheckboxProps extends Omit, 'type'> { + label?: ReactNode + id?: string +} + +export const Checkbox = forwardRef( + ({ label, id, className, ...rest }, ref) => { + const inputId = id ?? `checkbox-${Math.random().toString(36).slice(2)}` + return ( + + ) + }, +) + +Checkbox.displayName = 'Checkbox' diff --git a/src/design-system/primitives/Select/Select.module.css b/src/design-system/primitives/Select/Select.module.css new file mode 100644 index 0000000..95c5c46 --- /dev/null +++ b/src/design-system/primitives/Select/Select.module.css @@ -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; +} diff --git a/src/design-system/primitives/Select/Select.tsx b/src/design-system/primitives/Select/Select.tsx new file mode 100644 index 0000000..1a821bb --- /dev/null +++ b/src/design-system/primitives/Select/Select.tsx @@ -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 { + options: SelectOption[] +} + +export const Select = forwardRef( + ({ options, className, ...rest }, ref) => { + return ( +
+ + +
+ ) + }, +) + +Select.displayName = 'Select' diff --git a/src/design-system/primitives/Toggle/Toggle.module.css b/src/design-system/primitives/Toggle/Toggle.module.css new file mode 100644 index 0000000..e1125e6 --- /dev/null +++ b/src/design-system/primitives/Toggle/Toggle.module.css @@ -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); +} diff --git a/src/design-system/primitives/Toggle/Toggle.tsx b/src/design-system/primitives/Toggle/Toggle.tsx new file mode 100644 index 0000000..ff005b0 --- /dev/null +++ b/src/design-system/primitives/Toggle/Toggle.tsx @@ -0,0 +1,30 @@ +import styles from './Toggle.module.css' +import { forwardRef, type InputHTMLAttributes } from 'react' + +interface ToggleProps extends Omit, 'type'> { + label?: string + id?: string +} + +export const Toggle = forwardRef( + ({ label, id, className, ...rest }, ref) => { + const inputId = id ?? `toggle-${Math.random().toString(36).slice(2)}` + return ( + + ) + }, +) + +Toggle.displayName = 'Toggle'