refactor(form): use nested data selectors

- keep only semantic data attributes on the form primitives
- move variant styling into nested CSS module selectors
- preserve the existing visual treatment while simplifying the component layer
This commit is contained in:
Antti Kettunen 2026-05-18 22:01:01 +03:00
parent e65ec37c9e
commit b4ca5efe39
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
2 changed files with 226 additions and 282 deletions

View file

@ -96,6 +96,8 @@
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
min-height: 36px;
padding: 8px 32px 8px 12px;
transition: transition:
border-color 0.18s ease, border-color 0.18s ease,
background 0.18s ease, background 0.18s ease,
@ -105,46 +107,36 @@
-webkit-appearance: none; -webkit-appearance: none;
color-scheme: dark; color-scheme: dark;
cursor: pointer; cursor: pointer;
} &:hover {
border-color: rgba(255, 255, 255, 0.16);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.045)),
rgba(255, 255, 255, 0.06);
}
.selectSizeSm { &:focus {
min-height: 32px; outline: none;
padding: 6px 30px 6px 10px; border-color: rgba(var(--accent-light-rgb), 0.55);
background-position: background:
0 0, linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05)),
0 0, rgba(255, 255, 255, 0.07);
right 10px center; box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
} }
.selectSizeMd { &[data-size='sm'] {
min-height: 36px; min-height: 32px;
padding: 8px 32px 8px 12px; padding: 6px 30px 6px 10px;
background-position: background-position:
0 0, 0 0,
0 0, 0 0,
right 12px center; right 10px center;
} }
.select:hover { & option,
border-color: rgba(255, 255, 255, 0.16); & optgroup {
background: background: #1a1a2e;
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.045)), color: #fff;
rgba(255, 255, 255, 0.06); }
}
.select:focus {
outline: none;
border-color: rgba(var(--accent-light-rgb), 0.55);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05)),
rgba(255, 255, 255, 0.07);
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
}
.select option,
.select optgroup {
background: #1a1a2e;
color: #fff;
} }
.checkbox { .checkbox {
@ -167,16 +159,19 @@
box-shadow 0.18s ease, box-shadow 0.18s ease,
transform 0.18s ease; transform 0.18s ease;
color-scheme: dark; color-scheme: dark;
} &[data-checked] {
border-color: rgb(var(--accent-light-rgb));
background: rgb(var(--accent-light-rgb));
}
.checkbox[data-checked] { &[data-focused] {
border-color: rgb(var(--accent-light-rgb)); outline: none;
background: rgb(var(--accent-light-rgb)); box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.14);
} }
.checkbox[data-focused] { &[data-checked] .checkboxIndicator {
outline: none; opacity: 1;
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.14); }
} }
.checkboxIndicator { .checkboxIndicator {
@ -189,10 +184,6 @@
transition: opacity 0.18s ease; transition: opacity 0.18s ease;
} }
.checkbox[data-checked] .checkboxIndicator {
opacity: 1;
}
.checkboxIcon { .checkboxIcon {
color: #000; color: #000;
font-size: 12px; font-size: 12px;
@ -223,23 +214,26 @@
box-shadow 0.18s ease, box-shadow 0.18s ease,
transform 0.18s ease; transform 0.18s ease;
color-scheme: dark; color-scheme: dark;
} &[data-checked] {
border-color: rgba(var(--accent-light-rgb), 0.55);
background:
linear-gradient(180deg, rgba(var(--accent-light-rgb), 0.55), rgba(var(--accent-rgb), 0.8)),
rgba(var(--accent-rgb), 0.45);
}
.switch[data-checked] { &[data-focused] {
border-color: rgba(var(--accent-light-rgb), 0.55); outline: none;
background: box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.14);
linear-gradient(180deg, rgba(var(--accent-light-rgb), 0.55), rgba(var(--accent-rgb), 0.8)), }
rgba(var(--accent-rgb), 0.45);
}
.switch[data-focused] { &[data-disabled] {
outline: none; opacity: 0.55;
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.14); cursor: not-allowed;
} }
.switch[data-disabled] { &[data-checked] .switchThumb {
opacity: 0.55; transform: translateX(20px);
cursor: not-allowed; }
} }
.switchThumb { .switchThumb {
@ -253,10 +247,6 @@
transition: transform 0.18s ease; transition: transform 0.18s ease;
} }
.switch[data-checked] .switchThumb {
transform: translateX(20px);
}
.rangeRoot { .rangeRoot {
display: inline-flex; display: inline-flex;
flex: 0 0 160px; flex: 0 0 160px;
@ -342,25 +332,24 @@
border-color 0.18s ease, border-color 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
background 0.18s ease; background 0.18s ease;
} &:hover {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.14);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.04)),
rgba(255, 255, 255, 0.04);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
}
.optionCard:hover { &[data-selected='true'] {
transform: translateY(-1px); border-color: rgba(var(--accent-light-rgb), 0.45);
border-color: rgba(255, 255, 255, 0.14); background:
background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.18), rgba(255, 255, 255, 0.04)),
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.04)), rgba(255, 255, 255, 0.05);
rgba(255, 255, 255, 0.04); box-shadow:
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22); 0 0 0 1px rgba(var(--accent-light-rgb), 0.1),
} 0 14px 32px rgba(0, 0, 0, 0.26);
}
.optionCardSelected {
border-color: rgba(var(--accent-light-rgb), 0.45);
background:
linear-gradient(180deg, rgba(var(--accent-rgb), 0.18), rgba(255, 255, 255, 0.04)),
rgba(255, 255, 255, 0.05);
box-shadow:
0 0 0 1px rgba(var(--accent-light-rgb), 0.1),
0 14px 32px rgba(0, 0, 0, 0.26);
} }
.optionCardIcon { .optionCardIcon {
@ -394,17 +383,16 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
} &[data-size='sm'] {
gap: 6px;
}
.optionButtonGroupSizeSm { &[data-size='sm'] .optionButton {
gap: 6px; min-width: 0;
} padding: 6px 12px;
font-size: 12px;
.optionButtonGroupSizeSm .optionButton { gap: 6px;
min-width: 0; }
padding: 6px 12px;
font-size: 12px;
gap: 6px;
} }
.optionButton { .optionButton {
@ -426,26 +414,25 @@
border-color 0.18s ease, border-color 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
background 0.18s ease; background 0.18s ease;
} &:hover {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.08);
}
.optionButton:hover { &[data-selected='true'] {
transform: translateY(-1px); border-color: rgba(var(--accent-light-rgb), 0.5);
border-color: rgba(255, 255, 255, 0.18); background: rgba(var(--accent-rgb), 0.18);
background: rgba(255, 255, 255, 0.08); box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08);
} }
.optionButtonSelected { &[data-selected='true']:hover {
border-color: rgba(var(--accent-light-rgb), 0.5); border-color: rgba(var(--accent-light-rgb), 0.62);
background: rgba(var(--accent-rgb), 0.18); background: rgba(var(--accent-rgb), 0.26);
box-shadow: 0 0 0 1px rgba(var(--accent-light-rgb), 0.08); box-shadow:
} 0 0 0 1px rgba(var(--accent-light-rgb), 0.12),
0 10px 24px rgba(0, 0, 0, 0.14);
.optionButtonSelected:hover { }
border-color: rgba(var(--accent-light-rgb), 0.62);
background: rgba(var(--accent-rgb), 0.26);
box-shadow:
0 0 0 1px rgba(var(--accent-light-rgb), 0.12),
0 10px 24px rgba(0, 0, 0, 0.14);
} }
.badge { .badge {
@ -461,36 +448,33 @@
line-height: 1.2; line-height: 1.2;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
}
.badgeNeutral {
color: rgba(255, 255, 255, 0.45); color: rgba(255, 255, 255, 0.45);
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.08);
}
.badgeInfo { &[data-tone='info'] {
color: rgb(var(--accent-light-rgb)); color: rgb(var(--accent-light-rgb));
background: rgba(var(--accent-rgb), 0.12); background: rgba(var(--accent-rgb), 0.12);
border-color: rgba(var(--accent-light-rgb), 0.12); border-color: rgba(var(--accent-light-rgb), 0.12);
} }
.badgeSuccess { &[data-tone='success'] {
color: #4ade80; color: #4ade80;
background: rgba(74, 222, 128, 0.12); background: rgba(74, 222, 128, 0.12);
border-color: rgba(74, 222, 128, 0.12); border-color: rgba(74, 222, 128, 0.12);
} }
.badgeWarning { &[data-tone='warning'] {
color: #fbbf24; color: #fbbf24;
background: rgba(251, 191, 36, 0.12); background: rgba(251, 191, 36, 0.12);
border-color: rgba(251, 191, 36, 0.12); border-color: rgba(251, 191, 36, 0.12);
} }
.badgeDanger { &[data-tone='danger'] {
color: #f87171; color: #f87171;
background: rgba(248, 113, 113, 0.12); background: rgba(248, 113, 113, 0.12);
border-color: rgba(248, 113, 113, 0.12); border-color: rgba(248, 113, 113, 0.12);
}
} }
.button { .button {
@ -504,134 +488,130 @@
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.2;
min-height: 36px;
padding: 8px 12px;
transition: transition:
transform 0.18s ease, transform 0.18s ease,
border-color 0.18s ease, border-color 0.18s ease,
box-shadow 0.18s ease, box-shadow 0.18s ease,
background 0.18s ease, background 0.18s ease,
color 0.18s ease; color 0.18s ease;
} &:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.08);
}
.buttonSizeSm { &:focus-visible {
min-height: 32px; outline: none;
padding: 6px 10px; border-color: rgba(var(--accent-light-rgb), 0.55);
font-size: 13px; box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
} }
.buttonSizeMd { &:disabled {
min-height: 36px; opacity: 0.55;
padding: 8px 12px; cursor: not-allowed;
font-size: 13px; transform: none;
} }
.buttonSizeLg { &[data-size='sm'] {
min-height: 40px; min-height: 32px;
padding: 10px 20px; padding: 6px 10px;
font-size: 14px; font-size: 13px;
} }
.buttonIcon { &[data-size='lg'] {
width: var(--button-icon-size, 36px); min-height: 40px;
height: var(--button-icon-size, 36px); padding: 10px 20px;
min-height: var(--button-icon-size, 36px); font-size: 14px;
min-width: var(--button-icon-size, 36px); }
padding: 0;
font-size: var(--button-icon-font-size, 15px);
line-height: 1;
}
.button:hover:not(:disabled) { &[data-size='icon'] {
transform: translateY(-1px); width: var(--button-icon-size, 36px);
border-color: rgba(255, 255, 255, 0.18); height: var(--button-icon-size, 36px);
background: rgba(255, 255, 255, 0.08); min-height: var(--button-icon-size, 36px);
} min-width: var(--button-icon-size, 36px);
padding: 0;
font-size: var(--button-icon-font-size, 15px);
line-height: 1;
}
.button:focus-visible { &[data-variant='primary'] {
outline: none; border-color: rgba(var(--accent-light-rgb), 0.45);
border-color: rgba(var(--accent-light-rgb), 0.55); background: rgb(var(--accent-light-rgb));
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12); color: #000;
} }
.button:disabled { &[data-variant='primary']:hover:not(:disabled) {
opacity: 0.55; border-color: rgba(var(--accent-light-rgb), 0.55);
cursor: not-allowed; background: rgba(var(--accent-light-rgb), 0.95);
transform: none; color: #000;
} }
.buttonPrimary { &[data-variant='primary']:focus-visible {
border-color: rgba(var(--accent-light-rgb), 0.45); border-color: rgba(var(--accent-light-rgb), 0.8);
background: rgb(var(--accent-light-rgb)); box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.16);
color: #000; }
}
.buttonPrimary:hover:not(:disabled) { &[data-variant='primary']:disabled {
border-color: rgba(var(--accent-light-rgb), 0.55); opacity: 0.62;
background: rgba(var(--accent-light-rgb), 0.95); background: rgba(var(--accent-light-rgb), 0.45);
color: #000; color: rgba(0, 0, 0, 0.55);
} }
.buttonPrimary:focus-visible { &[data-variant='primary'] .badge {
border-color: rgba(var(--accent-light-rgb), 0.8); color: #fff;
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.16); background: rgba(0, 0, 0, 0.18);
} border-color: rgba(0, 0, 0, 0.22);
}
.buttonPrimary:disabled { &[data-variant='secondary'] {
opacity: 0.62; border-color: rgba(255, 255, 255, 0.12);
background: rgba(var(--accent-light-rgb), 0.45); background: rgba(255, 255, 255, 0.08);
color: rgba(0, 0, 0, 0.55); color: #ccc;
} }
.buttonPrimary .badge { &[data-variant='secondary']:hover:not(:disabled) {
color: #fff; border-color: rgba(255, 255, 255, 0.18);
background: rgba(0, 0, 0, 0.18); background: rgba(255, 255, 255, 0.14);
border-color: rgba(0, 0, 0, 0.22); color: #fff;
} }
.buttonSecondary { &[data-variant='secondary']:focus-visible {
border-color: rgba(255, 255, 255, 0.12); border-color: rgba(var(--accent-light-rgb), 0.45);
background: rgba(255, 255, 255, 0.08); box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
color: #ccc; }
}
.buttonSecondary:hover:not(:disabled) { &[data-variant='secondary']:disabled {
border-color: rgba(255, 255, 255, 0.18); opacity: 0.55;
background: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.05);
color: #fff; color: rgba(255, 255, 255, 0.45);
} }
.buttonSecondary:focus-visible { &[data-variant='ghost'] {
border-color: rgba(var(--accent-light-rgb), 0.45); border-color: transparent;
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12); background: transparent;
} color: rgba(255, 255, 255, 0.55);
}
.buttonSecondary:disabled { &[data-variant='ghost']:hover:not(:disabled) {
opacity: 0.55; transform: none;
background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.14);
color: rgba(255, 255, 255, 0.45); background: rgba(255, 255, 255, 0.07);
} color: #fff;
}
.buttonGhost { &[data-variant='ghost']:focus-visible {
border-color: transparent; border-color: rgba(var(--accent-light-rgb), 0.42);
background: transparent; box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
color: rgba(255, 255, 255, 0.55); }
}
.buttonGhost:hover:not(:disabled) { &[data-variant='ghost']:disabled {
transform: none; opacity: 0.5;
border-color: rgba(255, 255, 255, 0.14); }
background: rgba(255, 255, 255, 0.07);
color: #fff;
}
.buttonGhost:focus-visible {
border-color: rgba(var(--accent-light-rgb), 0.42);
box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12);
}
.buttonGhost:disabled {
opacity: 0.5;
} }
.formError { .formError {

View file

@ -91,14 +91,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
return ( return (
<select <select
ref={ref} ref={ref}
className={clsx( className={clsx(styles.select, className)}
styles.select,
{
[styles.selectSizeSm]: size === 'sm',
[styles.selectSizeMd]: size === 'md',
},
className,
)}
data-size={size} data-size={size}
{...props} {...props}
/> />
@ -230,7 +223,8 @@ export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(functio
<BaseToggle <BaseToggle
ref={ref} ref={ref}
pressed={selected} pressed={selected}
className={clsx(styles.optionCard, selected && styles.optionCardSelected, className)} className={clsx(styles.optionCard, className)}
data-selected={selected ? 'true' : undefined}
type={type} type={type}
{...props} {...props}
> >
@ -261,16 +255,7 @@ export function OptionButtonGroup({
size = 'md', size = 'md',
}: OptionButtonGroupProps) { }: OptionButtonGroupProps) {
return ( return (
<div <div className={clsx(styles.optionButtonGroup, className)} data-size={size}>
className={clsx(
styles.optionButtonGroup,
{
[styles.optionButtonGroupSizeSm]: size === 'sm',
},
className,
)}
data-size={size}
>
{children} {children}
</div> </div>
); );
@ -290,7 +275,8 @@ export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(fun
<BaseToggle <BaseToggle
ref={ref} ref={ref}
pressed={selected} pressed={selected}
className={clsx(styles.optionButton, selected && styles.optionButtonSelected, className)} className={clsx(styles.optionButton, className)}
data-selected={selected ? 'true' : undefined}
type={type} type={type}
{...props} {...props}
> >
@ -315,17 +301,7 @@ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
return ( return (
<span <span
ref={ref} ref={ref}
className={clsx( className={clsx(styles.badge, className)}
styles.badge,
{
[styles.badgeNeutral]: tone === 'neutral',
[styles.badgeInfo]: tone === 'info',
[styles.badgeSuccess]: tone === 'success',
[styles.badgeWarning]: tone === 'warning',
[styles.badgeDanger]: tone === 'danger',
},
className,
)}
data-tone={tone} data-tone={tone}
{...props} {...props}
/> />
@ -347,19 +323,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
return ( return (
<BaseButton <BaseButton
ref={ref} ref={ref}
className={clsx( className={clsx(styles.button, className)}
styles.button,
{
[styles.buttonSizeSm]: size === 'sm',
[styles.buttonSizeMd]: size === 'md',
[styles.buttonSizeLg]: size === 'lg',
[styles.buttonIcon]: size === 'icon',
[styles.buttonPrimary]: variant === 'primary',
[styles.buttonSecondary]: variant === 'secondary',
[styles.buttonGhost]: variant === 'ghost',
},
className,
)}
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
type={type} type={type}