feat(ui): refine gem-blue acrylic palette and focus borders
Derive the gem-blue palette with OKLCH tonal and complementary-color rules while preserving acrylic opacity, blur and surface hierarchy. Darker focus tokens exposed stacked field rings, fallback outlines and persistent basemap selection rings. Keep fallback focus styles in the base layer and use one themed field boundary or inset card stroke. Preserve keyboard focus, invalid states and native input semantics. Add browser regression coverage for pointer and keyboard focus, all basemap presentations, narrow layouts and rendered contrast. Validation in an isolated checkout: pnpm lint, pnpm typecheck, pnpm test (393 tests), focused browser suites and pnpm build.
This commit is contained in:
@@ -524,7 +524,7 @@ export const CodeBlockLanguageSelectorTrigger = ({
|
||||
}: CodeBlockLanguageSelectorTriggerProps) => (
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-7 border-none bg-transparent px-2 text-xs shadow-none",
|
||||
"form-field-inset h-7 border-none bg-transparent px-2 text-xs shadow-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1276,7 +1276,7 @@ export const PromptInputSelectTrigger = ({
|
||||
}: PromptInputSelectTriggerProps) => (
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
||||
"form-field-inset border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
||||
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -37,7 +37,7 @@ const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<div className="form-field flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
|
||||
@@ -202,10 +202,10 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
disabled={disabled}
|
||||
aria-labelledby={`${id}-label`}
|
||||
aria-describedby={`${id}-value`}
|
||||
className="flex h-10 w-full min-w-0 items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 text-left text-sm tabular-nums text-slate-800 shadow-xs transition-[border-color,box-shadow,scale] duration-150 [@media(hover:hover)]:hover:border-slate-300 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/30 active:scale-[0.99] motion-reduce:transition-none motion-reduce:active:scale-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="form-field flex h-10 w-full min-w-0 items-center gap-2 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)] px-3 text-left text-sm tabular-nums text-slate-800 shadow-xs transition-[border-color,box-shadow,scale] duration-150 [@media(hover:hover)]:hover:border-[var(--surface-border)] active:scale-[0.99] motion-reduce:transition-none motion-reduce:active:scale-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<CalendarDays className="size-4 shrink-0 text-slate-400" aria-hidden="true" />
|
||||
<span id={`${id}-value`} className={cn("min-w-0 truncate", !value && "text-slate-400")}>
|
||||
<CalendarDays className="size-4 shrink-0 text-[var(--ink-muted)]" aria-hidden="true" />
|
||||
<span id={`${id}-value`} className={cn("min-w-0 truncate", !value && "text-[var(--ink-muted)]")}>
|
||||
{displayValue(value)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -214,7 +214,7 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 z-[80] bg-slate-950/20 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<DialogPrimitive.Content
|
||||
aria-describedby={undefined}
|
||||
className="fixed left-1/2 top-1/2 z-[85] max-h-[calc(100dvh-1.5rem)] w-[352px] max-w-[calc(100vw-1.5rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-2xl border border-slate-200 bg-white p-4 shadow-[0_20px_60px_rgba(15,23,42,0.22)] outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
||||
className="fixed left-1/2 top-1/2 z-[85] max-h-[calc(100dvh-1.5rem)] w-[352px] max-w-[calc(100vw-1.5rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-2xl border border-[var(--surface-divider)] bg-[var(--surface-reading)] p-4 shadow-[0_20px_60px_rgba(15,23,42,0.22)] outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
||||
onOpenAutoFocus={(event) => {
|
||||
if (sourceInvalid) return;
|
||||
event.preventDefault();
|
||||
@@ -243,7 +243,7 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
inputMode="numeric"
|
||||
value={yearText}
|
||||
aria-invalid={!validYear}
|
||||
className="mt-1 h-10 rounded-lg border-slate-200 bg-white text-sm tabular-nums"
|
||||
className="mt-1 h-10 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)] text-sm tabular-nums"
|
||||
onChange={(event) => {
|
||||
const nextText = event.target.value.replace(/\D/g, "").slice(0, 4);
|
||||
setYearText(nextText);
|
||||
@@ -286,7 +286,7 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-2 grid grid-cols-7 text-center text-[11px] font-medium text-slate-400"
|
||||
className="mt-2 grid grid-cols-7 text-center text-[11px] font-medium text-[var(--ink-muted)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{WEEKDAYS.map((weekday) => (
|
||||
@@ -317,11 +317,11 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
aria-selected={selected}
|
||||
aria-label={`${draft.year}年${draft.month}月${day}日`}
|
||||
className={cn(
|
||||
"aspect-square w-full min-w-0 rounded-lg text-xs tabular-nums text-slate-700 transition-[background-color,color,scale] duration-150 [@media(hover:hover)]:hover:bg-blue-50 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/40 active:scale-90 motion-reduce:transition-none motion-reduce:active:scale-100",
|
||||
"aspect-square w-full min-w-0 rounded-lg text-xs tabular-nums text-slate-700 transition-[background-color,color,scale] duration-150 [@media(hover:hover)]:hover:bg-[var(--action-soft)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] active:scale-90 motion-reduce:transition-none motion-reduce:active:scale-100",
|
||||
isToday &&
|
||||
!selected &&
|
||||
"font-semibold text-blue-600 ring-1 ring-inset ring-blue-200",
|
||||
selected && "bg-blue-600 font-semibold text-white [@media(hover:hover)]:hover:bg-blue-700"
|
||||
"font-semibold text-[var(--action-blue-hover)] ring-1 ring-inset ring-[var(--surface-border)]",
|
||||
selected && "bg-[var(--action-blue)] font-semibold text-white [@media(hover:hover)]:hover:bg-[var(--action-blue-hover)]"
|
||||
)}
|
||||
onClick={() => updateDraft({ ...draft, day })}
|
||||
onKeyDown={(event) => {
|
||||
@@ -347,7 +347,7 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3">
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 border-t border-[var(--surface-divider)] pt-3">
|
||||
<FieldSelect
|
||||
label="小时"
|
||||
value={pad(draft.hour)}
|
||||
@@ -368,7 +368,7 @@ export function DateTimeField({ label, value, onChange, disabled = false }: Date
|
||||
/>
|
||||
</div>
|
||||
{sourceInvalid ? (
|
||||
<p role="alert" className="mt-2 text-xs text-amber-700">
|
||||
<p role="alert" className="mt-2 text-xs text-[var(--status-warning-foreground)]">
|
||||
当前值格式无效,请重新选择日期和时间。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function Disclosure({
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
data-disclosure-indicator="accordion"
|
||||
className="disclosure-accordion-indicator size-5 shrink-0 rounded-md bg-slate-100 p-1 text-slate-500 transition-transform duration-150 motion-reduce:transition-none"
|
||||
className="disclosure-accordion-indicator size-5 shrink-0 rounded-md bg-[var(--surface-control)] p-1 text-[var(--ink-muted)] transition-transform duration-150 motion-reduce:transition-none"
|
||||
/>
|
||||
</summary>
|
||||
{children}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function FieldSelect({
|
||||
>
|
||||
<SelectTrigger
|
||||
id={triggerId}
|
||||
className="h-auto min-h-10 w-full min-w-0 gap-2 rounded-lg border-slate-200 bg-white px-3 text-sm shadow-xs transition-[border-color,box-shadow,background-color] [@media(hover:hover)]:hover:border-slate-300 focus:ring-2 focus:ring-[var(--action-focus-ring)] motion-reduce:transition-none [&>span]:line-clamp-none [&>span]:whitespace-normal [&>span]:break-words [&>span]:text-left [&>svg]:shrink-0"
|
||||
className="h-auto min-h-10 w-full min-w-0 gap-2 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)] px-3 text-sm shadow-xs transition-[border-color,box-shadow,background-color] [@media(hover:hover)]:hover:border-[var(--surface-border)] motion-reduce:transition-none [&>span]:line-clamp-none [&>span]:whitespace-normal [&>span]:break-words [&>span]:text-left [&>svg]:shrink-0"
|
||||
>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -12,7 +12,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-hidden transition-[color,box-shadow]",
|
||||
"form-field group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-hidden transition-[color,border-color,box-shadow]",
|
||||
"h-9 has-[>textarea]:h-auto",
|
||||
|
||||
// Variants based on alignment.
|
||||
@@ -21,12 +21,6 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||
|
||||
// Focus state.
|
||||
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
|
||||
|
||||
// Error state.
|
||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -134,7 +128,7 @@ function InputGroupInput({
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none dark:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -150,7 +144,7 @@ function InputGroupTextarea({
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none dark:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"form-field flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { forwardRef, type ComponentProps } from "react";
|
||||
import { cn } from "./cn";
|
||||
|
||||
export const Radio = forwardRef<HTMLInputElement, Omit<ComponentProps<"input">, "type">>(
|
||||
function Radio({ className, ...props }, ref) {
|
||||
return (
|
||||
<span className={cn("relative inline-flex size-5 shrink-0 align-middle", props.disabled && "opacity-50")}>
|
||||
<input
|
||||
{...props}
|
||||
ref={ref}
|
||||
type="radio"
|
||||
className={cn(
|
||||
"peer m-0 size-5 cursor-pointer appearance-none rounded-full border border-[var(--action-border)] bg-[var(--surface-reading)] shadow-xs transition-colors checked:border-[var(--action-blue)] checked:bg-[var(--action-blue)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] focus-visible:ring-offset-2 disabled:cursor-not-allowed motion-reduce:transition-none",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
<span aria-hidden className="pointer-events-none absolute inset-0 m-auto size-2 rounded-full bg-white opacity-0 peer-checked:opacity-100" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"form-field flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-colors data-[placeholder]:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"form-field flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-colors placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
Reference in New Issue
Block a user