bd7ba8726e
Landed complete design-token foundation (Batch 6) and type/color standardization (Batch 7): - New src/renderer/src/components/ui/tokens.ts: SPACE, RADIUS, ELEVATION, Z, ICON, MOTION, SCRIM, META_SEP scales - New src/renderer/src/components/ui/text.ts: useTextStyles hook + type-role convention (Subtitle2/Text/Body1/Caption1) - Migrated scattered literals in 17 renderer files to named tokens - Adopted unified muted color and row-title component across 7 list views - All tile glyphs snapped to ICON tiers (no literal px remain) - base.css reduced-motion gate + prefers-color-scheme alignment - Verified tile-glyph visual deltas (≤2px) for watched pass; UI3/UI12 implementation confirmed, awaiting visual check - CODE-AUDIT.md reconciled: Batch 6 complete (12 items checked, UI3+UI12 flagged); Batch 7 complete (6 items checked) Verified: typecheck (node+web) + 268 tests + eslint + production build green; touched files prettier-clean. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
106 lines
3.1 KiB
TypeScript
106 lines
3.1 KiB
TypeScript
import {
|
|
makeStyles,
|
|
mergeClasses,
|
|
tokens,
|
|
shorthands,
|
|
useFieldControlProps_unstable
|
|
} from '@fluentui/react-components'
|
|
|
|
// A native <select> styled to match Fluent inputs. We use this instead of
|
|
// Fluent's <Dropdown> because Fluent's portal/popover menu is a composited
|
|
// overlay in the WebContents, and on this dev machine's GPU/driver that overlay
|
|
// paint blanks the whole window (same family as the documented flicker --
|
|
// hardware acceleration is already disabled; see memory "aerofetch-gpu-flicker").
|
|
// The native <select> popup is drawn by the OS, so it can't trigger the blank.
|
|
// The popup's light/dark styling follows the `color-scheme` set on the app root
|
|
// in App.tsx.
|
|
const useStyles = makeStyles({
|
|
select: {
|
|
width: '100%',
|
|
height: '32px',
|
|
padding: '0 8px',
|
|
fontSize: tokens.fontSizeBase300,
|
|
fontFamily: tokens.fontFamilyBase,
|
|
color: tokens.colorNeutralForeground1,
|
|
backgroundColor: tokens.colorNeutralBackground1,
|
|
border: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
...shorthands.borderRadius(tokens.borderRadiusMedium),
|
|
cursor: 'pointer',
|
|
':hover': {
|
|
...shorthands.borderColor(tokens.colorNeutralStroke1Hover)
|
|
},
|
|
':focus-visible': {
|
|
outline: 'none',
|
|
...shorthands.borderColor(tokens.colorCompoundBrandStroke)
|
|
}
|
|
},
|
|
large: {
|
|
height: '40px',
|
|
padding: '0 12px',
|
|
fontSize: tokens.fontSizeBase400
|
|
},
|
|
disabled: {
|
|
cursor: 'not-allowed',
|
|
color: tokens.colorNeutralForegroundDisabled,
|
|
backgroundColor: tokens.colorNeutralBackgroundDisabled,
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
|
|
':hover': {
|
|
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled)
|
|
}
|
|
}
|
|
})
|
|
|
|
export interface SelectOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
interface SelectProps {
|
|
value: string
|
|
options: SelectOption[]
|
|
onChange: (value: string) => void
|
|
className?: string
|
|
'aria-label'?: string
|
|
size?: 'medium' | 'large'
|
|
disabled?: boolean
|
|
}
|
|
|
|
export function Select({
|
|
value,
|
|
options,
|
|
onChange,
|
|
className,
|
|
'aria-label': ariaLabel,
|
|
size,
|
|
disabled
|
|
}: SelectProps): React.JSX.Element {
|
|
const styles = useStyles()
|
|
// Integrate with a wrapping Fluent <Field> (L133): pick up the id + label/hint
|
|
// association so the Field's visible label names the select natively (via
|
|
// `htmlFor`), instead of every call site repeating the name in an `aria-label`.
|
|
// Outside a Field this returns the props unchanged, so standalone selects keep
|
|
// their own `aria-label`. `supportsLabelFor` = native <label for>, not aria-labelledby.
|
|
const fieldProps = useFieldControlProps_unstable({}, { supportsLabelFor: true })
|
|
return (
|
|
<select
|
|
{...fieldProps}
|
|
aria-label={ariaLabel}
|
|
className={mergeClasses(
|
|
styles.select,
|
|
size === 'large' && styles.large,
|
|
disabled && styles.disabled,
|
|
className
|
|
)}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
>
|
|
{options.map((o) => (
|
|
<option key={o.value} value={o.value}>
|
|
{o.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
}
|