From f098e6c6cb5290768bc8d721bfc6923669cd85c2 Mon Sep 17 00:00:00 2001 From: cixzhang Date: Tue, 4 Aug 2026 21:14:20 +0000 Subject: [PATCH 1/2] feat(theme): add control icon renderers --- .changeset/control-icons.md | 7 + packages/core/src/Icon/controlIconRegistry.ts | 33 +++ packages/core/src/Icon/controlIcons.tsx | 205 ++++++++++++++++++ packages/core/src/Icon/index.ts | 14 ++ .../core/src/Icon/useControlIcon.test.tsx | 38 ++++ packages/core/src/Icon/useControlIcon.ts | 18 ++ packages/core/src/theme/defineTheme.test.ts | 14 ++ packages/core/src/theme/defineTheme.ts | 11 + packages/core/src/theme/index.ts | 5 + 9 files changed, 345 insertions(+) create mode 100644 .changeset/control-icons.md create mode 100644 packages/core/src/Icon/controlIconRegistry.ts create mode 100644 packages/core/src/Icon/controlIcons.tsx create mode 100644 packages/core/src/Icon/useControlIcon.test.tsx create mode 100644 packages/core/src/Icon/useControlIcon.ts diff --git a/.changeset/control-icons.md b/.changeset/control-icons.md new file mode 100644 index 000000000000..db56c8bf68b1 --- /dev/null +++ b/.changeset/control-icons.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/core': patch +--- + +[feature] Add themeable control icon renderers for stateful checkbox and radio visuals. + +@cixzhang diff --git a/packages/core/src/Icon/controlIconRegistry.ts b/packages/core/src/Icon/controlIconRegistry.ts new file mode 100644 index 000000000000..684992194c62 --- /dev/null +++ b/packages/core/src/Icon/controlIconRegistry.ts @@ -0,0 +1,33 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file controlIconRegistry.ts + * @input Theme source and control icon name + * @output Exports getControlIcon/useControlIcon backing helpers + * @position Server-safe control icon resolver for stateful checkbox/radio visuals + */ + +import type {DefinedTheme} from '../theme/defineTheme'; +import {getRegisteredTheme} from '../theme/themeRegistry'; +import { + defaultControlIcons, + type ControlIconName, + type ControlIconRenderer, +} from './controlIcons'; + +export type ControlIconRegistrySource = + DefinedTheme | string | null | undefined; + +function getTheme(source: ControlIconRegistrySource): DefinedTheme | null { + if (source == null) { + return null; + } + return typeof source === 'string' ? getRegisteredTheme(source) : source; +} + +export function getControlIcon( + name: ControlIconName, + source?: ControlIconRegistrySource, +): ControlIconRenderer { + return getTheme(source)?.controlIcons?.[name] ?? defaultControlIcons[name]; +} diff --git a/packages/core/src/Icon/controlIcons.tsx b/packages/core/src/Icon/controlIcons.tsx new file mode 100644 index 000000000000..e7a2eb0558ed --- /dev/null +++ b/packages/core/src/Icon/controlIcons.tsx @@ -0,0 +1,205 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file controlIcons.tsx + * @input Control icon state props + * @output Default checkbox/radio control icon renderers and control icon types + * @position Stateful control-icon layer used for checkbox/radio visuals + */ + +import type {ReactNode} from 'react'; + +export interface ControlIconRenderArgs { + state: 'unchecked' | 'checked' | 'indeterminate'; + size?: 'sm' | 'md' | (string & {}); + isDisabled?: boolean; + isHovered?: boolean; + isPressed?: boolean; +} + +export type ControlIconRenderer = (props: ControlIconRenderArgs) => ReactNode; + +export interface ControlIconMap { + checkbox: true; + radio: true; +} + +export type ControlIconName = keyof ControlIconMap & string; +export type ControlIconRegistry = Partial< + Record +>; + +const controlSize = (size: ControlIconRenderArgs['size']): number => + size === 'sm' ? 20 : 24; + +const boxStyle = { + boxSizing: 'border-box', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, +} as const; + +function checkboxColor( + state: ControlIconRenderArgs['state'], + isDisabled?: boolean, +) { + if (isDisabled && state === 'unchecked') { + return { + borderColor: 'var(--color-border)', + backgroundColor: 'var(--color-background-muted)', + color: 'var(--color-on-accent)', + opacity: 0.5, + }; + } + if (isDisabled) { + return { + borderColor: 'var(--color-border)', + backgroundColor: 'var(--color-accent)', + color: 'var(--color-on-accent)', + opacity: 0.5, + }; + } + if (state === 'unchecked') { + return { + borderColor: 'var(--color-border-emphasized)', + backgroundColor: 'var(--color-background-surface)', + color: 'var(--color-accent)', + opacity: 1, + }; + } + return { + borderColor: 'var(--color-accent)', + backgroundColor: 'var(--color-accent)', + color: 'var(--color-on-accent)', + opacity: 1, + }; +} + +export function defaultCheckboxControlIcon({ + state, + size = 'md', + isDisabled, +}: ControlIconRenderArgs): ReactNode { + const px = controlSize(size); + const colors = checkboxColor(state, isDisabled); + + return ( +