Files
2026-07-13 12:12:04 +08:00

184 lines
5.7 KiB
CSS

/* ==========================================================================
Base radio styles
Structure (explicit composition):
.radio → RadioField wrapper (column)
.radio__content → RadioButton: the clickable row (control + label)
.radio__control
[data-slot="label"]
[data-slot="description"] → field-level sibling, below the row
[data-slot="field-error"]
========================================================================== */
.radio {
@apply flex flex-col items-start gap-1 outline-none no-highlight;
cursor: var(--cursor-interactive);
[data-slot="label"] {
@apply select-none;
}
.radio__content [data-slot="label"] {
cursor: var(--cursor-interactive);
}
/* Field-level help text (siblings of the button), indented to align under the label */
& > [data-slot="description"],
& > [data-slot="field-error"] {
@apply w-full min-w-0 cursor-default ps-7 text-xs text-wrap wrap-break-word text-muted select-none;
}
/* Disabled state */
&:disabled,
&[data-disabled="true"],
&[aria-disabled="true"] {
@apply status-disabled;
[data-slot="description"],
[data-slot="field-error"] {
@apply status-disabled;
}
}
}
/* Radio content — the clickable label (control + label text) */
.radio__content {
@apply relative inline-flex cursor-[inherit] items-center gap-3 text-sm font-medium text-foreground outline-none select-none no-highlight;
}
/* ==========================================================================
Radio control (the circular button)
========================================================================== */
.radio__control {
@apply relative inline-flex size-4 shrink-0 items-center justify-center rounded-lg border [border-width:var(--border-width-field)] border-field-border bg-field shadow-field outline-none no-highlight;
/**
* Transitions
* CRITICAL: motion-reduce must be AFTER transition for correct override specificity
*/
transition:
background-color 200ms var(--ease-out),
border-color 200ms var(--ease-out),
transform 100ms var(--ease-out);
@apply motion-reduce:transition-none;
cursor: var(--cursor-interactive);
/* Interactive states */
/* Focus */
.radio:has([data-slot="radio-content"][data-focus-visible="true"]) &,
.radio [data-slot="radio-content"][data-focus-visible="true"] & {
@apply status-focused;
}
/* Hover */
.radio:has([data-slot="radio-content"][data-hovered="true"]) &,
.radio [data-slot="radio-content"][data-hovered="true"] & {
@apply border-field-border-hover;
}
/* Hover - only when not selected */
.radio:has([data-slot="radio-content"][data-hovered="true"]):not([data-selected]):not(
:has(input:checked)
)
& {
.radio__indicator:empty::before {
@apply bg-field-hover;
}
}
/* Pressed */
.radio:has([data-slot="radio-content"][data-pressed="true"]) &,
.radio [data-slot="radio-content"][data-pressed="true"] & {
@apply scale-95;
}
/* Selection states */
/* Selected */
.radio[data-selected] &,
.radio:has([data-slot="radio-content"][aria-checked="true"]) &,
.radio:has(input:checked) & {
@apply border-transparent bg-accent;
}
/* Selected + Pressed */
.radio[data-selected]:has([data-slot="radio-content"][data-pressed="true"]) &,
.radio:has([data-slot="radio-content"][data-pressed="true"][aria-checked="true"]) &,
.radio:has(input:checked):has([data-slot="radio-content"][data-pressed="true"]) & {
@apply bg-accent-hover;
}
/* Validation states */
/* Invalid */
.radio[data-invalid="true"] &,
.radio[aria-invalid="true"] & {
@apply status-invalid-field;
}
/* Invalid + Selected */
.radio[data-invalid="true"][data-selected] &,
.radio[aria-invalid="true"][data-selected] &,
.radio[data-invalid="true"]:has(input:checked) &,
.radio[aria-invalid="true"]:has(input:checked) & {
@apply status-invalid-field;
}
}
/* ==========================================================================
Radio indicator (the inner dot)
========================================================================== */
.radio__indicator {
@apply absolute inset-0 flex items-center justify-center;
pointer-events: none;
}
/* :empty pseudo-class ensures these styles only apply when the indicator has no custom children
(e.g., custom icon). When custom content is present, these default dot styles are not applied. */
.radio__indicator:empty::before {
content: "";
@apply rounded-lg bg-field;
width: 100%;
height: 100%;
scale: 1;
/**
* Transitions
* CRITICAL: motion-reduce must be AFTER transition for correct override specificity
*/
transition:
scale 200ms var(--ease-out),
background-color 200ms var(--ease-out);
@apply motion-reduce:transition-none;
}
/* Selected state */
.radio[data-selected] .radio__indicator:empty::before,
.radio:has([data-slot="radio-content"][aria-checked="true"]) .radio__indicator:empty::before,
.radio:has(input:checked) .radio__indicator:empty::before {
@apply bg-accent-foreground;
scale: 0.4286; /* 6px */
}
/* Selected + Pressed state */
.radio[data-selected]:has([data-slot="radio-content"][data-pressed="true"])
.radio__indicator:empty::before,
.radio:has([data-slot="radio-content"][data-pressed="true"][aria-checked="true"])
.radio__indicator:empty::before,
.radio:has(input:checked):has([data-slot="radio-content"][data-pressed="true"])
.radio__indicator:empty::before {
scale: 0.5714; /* 8px */
}
/* ==========================================================================
Disabled modifier
========================================================================== */
.radio--disabled {
@apply status-disabled;
}