Files
AeroFetch/src/renderer/src/components/Select.tsx
T
debont80 bd7ba8726e feat(audit): Batch 6-7 — design tokens + type/color roles, full migration
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>
2026-07-01 15:24:00 -04:00

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>
)
}