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
pull/686/head
Antti Kettunen 1 week ago
parent e65ec37c9e
commit b4ca5efe39
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

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

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

Loading…
Cancel
Save