-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Architecture Cheat Sheet
Operational routing guide, not an audit rubric. Verified against
origin/mainatbaa01b1c25355a85f26c7363f5c98b4d6e806c6b(2026年08月25日). Use the linked owner API and route detailed checks to the existing docs.
Rename NewComponent and new-component; choose the intrinsic root that matches the component's semantics.
// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file NewComponent.tsx * @input React content, BaseProps DOM passthrough, StyleX overrides * @output Exports NewComponent and NewComponentProps * @position Core leaf component; consumed by index.ts * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/NewComponent/NewComponent.doc.mjs * - /packages/core/src/NewComponent/NewComponent.test.tsx * - /packages/core/src/NewComponent/index.ts * - /apps/storybook/stories/NewComponent.stories.tsx */ import type {ReactNode} from 'react'; import * as stylex from '@stylexjs/stylex'; import type {BaseProps} from '../BaseProps'; import {colorVars, spacingVars} from '../theme/tokens.stylex'; import {mergeProps, themeProps} from '../utils'; const styles = stylex.create({ root: { alignItems: 'center', color: colorVars['--color-text-primary'], display: 'inline-flex', gap: spacingVars['--spacing-1'], }, }); export interface NewComponentProps extends BaseProps<HTMLSpanElement> { children: ReactNode; ref?: React.Ref<HTMLSpanElement>; } export function NewComponent({ children, xstyle, className, style, ref, ...rest }: NewComponentProps) { return ( <span ref={ref} {...mergeProps( themeProps('new-component'), stylex.props(styles.root, xstyle), className, style, )} {...rest}> {children} </span> ); } NewComponent.displayName = 'NewComponent';
This order is the current Badge pattern: themeProps → one stylex.props(base, xstyle) call → consumer className → consumer style → neutral ...rest. If the component owns a role, ARIA value, or handler, apply the collision rules in Public DOM/style/event/ref composition below.
| Trigger | Compose this owner API |
|---|---|
| Announce a state transition |
useAnnounce + useTranslator; audit A6/A7/A16 |
| Trap/restore focus |
useFocusTrap; layer participants also join dismissal |
| Enlarge an interactive surface |
useClickableContainer; input chrome uses useInputContainer
|
| Reveal controls on hover/focus/touch | useContainerReveal |
| Render navigation |
useLinkComponent + the shared target/rel handling used by Link / Item
|
| Navigate a collection |
Item + useListFocus / useGridFocus / useTreeFocus; add useTypeahead when required |
| Open floating/modal UI | Existing Dialog/Popover/Tooltip family first; otherwise useLayer + dismissal/depth owners |
| Accept or provide size |
useSize; container owners use SizeProvider
|
| Collect form data |
Field anatomy + getInputARIA + useInputContainer
|
| Publish or consume container geometry |
container() and the four directional padding variables; edge owners use edgeCompSlot
|
Prefer an existing layer family: it already owns portal placement, dismissal, nesting, focus, and lifecycle. useLayer is low-level component-author infrastructure, not the consumer starting point.
import {Button, Popover} from '@astryxdesign/core'; <Popover label="Settings" content={<div>Settings</div>}> <Button label="Settings" /> </Popover>
Route: packages/core/src/Layer/; Component Audit Rubric §1 and Q10.
Consumers start with a finished input and its standard controlled API. Choose one of isRequired or isOptional; TextInput also supports isDisabled, isReadOnly, and isLoading—there is no busy prop.
import {TextInput, type TextInputStatus} from '@astryxdesign/core/TextInput'; const status: TextInputStatus | undefined = isInvalid ? {type: 'error', message: validationMessage} : undefined; <TextInput label="Email" description="Used for account notifications" value={email} onChange={setEmail} changeAction={saveEmail} status={status} isRequired isDisabled={isDisabled} disabledMessage={disabledMessage} isReadOnly={isReadOnly} isLoading={isSaving} hasClear />
When building a system input, compose the public author primitives rather than rebuilding the anatomy:
import {Field} from '@astryxdesign/core/Field'; import {getInputARIA} from '@astryxdesign/core/utils'; const {ariaLabelledBy, ariaDescribedBy} = getInputARIA(labelID, [ description ? descriptionID : null, ]); <Field label={label} inputID={inputID} labelID={labelID} description={description} descriptionID={description ? descriptionID : undefined}> <input id={inputID} aria-labelledby={ariaLabelledBy} aria-describedby={ariaDescribedBy} /> </Field>
Route: API Conventions — Input Component Props and audit §§1, 3, 4.
Theme authors configure stable targets; component authors put themeProps on the element that paints and reflect style-driving state there. Structural wrappers stay private.
import {defineTheme} from '@astryxdesign/core/theme'; import {mergeProps, themeProps} from '@astryxdesign/core/utils'; const brandTheme = defineTheme({ name: 'brand', components: { button: {'variant:secondary': {fontWeight: '600'}}, }, }); <span {...mergeProps( themeProps('choice', {selected: isSelected ? 'selected' : null}), stylex.props(styles.root, xstyle), )} />
Route: Theming Infrastructure — Component Theming and audit §2.
Compose the shared primitive for the interaction class. Announce translated text in the real transition handler; use the focus primitive only when the surface owns a trap.
import {useAnnounce, useFocusTrap} from '@astryxdesign/core/hooks'; const announce = useAnnounce(); const {containerRef} = useFocusTrap<HTMLDivElement>({ isActive: isOpen, onEscape: close, }); const handleSave = () => { onSave(); announce(savedMessage); // Pass a translated message. }; <div ref={containerRef}><button onClick={handleSave}>Save</button></div>
Route: audit A6/A7/A16/A17 and the hook docs emitted by astryx docs.
The collection owns keyboard navigation; each row owns its semantic role, tab stop, and activation. Use the focus hook that matches the collection shape instead of a local key switch.
import {Item} from '@astryxdesign/core/Item'; import {useListFocus} from '@astryxdesign/core/hooks'; const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({ itemSelector: '[role="menuitem"]', }); <div ref={listRef} role="menu" onKeyDown={handleKeyDown}> {items.map((item, index) => ( <Item key={item.id} role="menuitem" tabIndex={index === 0 ? 0 : -1} label={item.label} onClick={item.select} /> ))} </div>
Route: packages/core/src/Item/, hooks/use*Focus.ts, Indicator/; audit A17.
A container publishes the padding it paints; a bleed consumer reads the matching logical edge with a 0px fallback. These per-edge variables are component-author internals—themes set normal padding properties.
import * as stylex from '@stylexjs/stylex'; import {container} from '@astryxdesign/core/Layout'; const bleed = stylex.create({ root: { marginInlineStart: 'calc(-1 * var(--container-padding-inline-start, 0px))', marginInlineEnd: 'calc(-1 * var(--container-padding-inline-end, 0px))', }, }); <div {...stylex.props(...container({padding: 'spacing3'}))}> <div {...stylex.props(bleed.root)}>{content}</div> </div>
Route: Layout/container.stylex.ts, Layout/padding.stylex.ts, and Container Padding System.
One owner supplies the whole utterance or the semantic values needed to translate it. Use provider direction only when JavaScript needs it; layout uses CSS logical properties.
import {useDirection, useTranslator} from '@astryxdesign/core/i18n'; const t = useTranslator(); const direction = useDirection(); <output dir={direction}> {t('@astryx.pagination.pageAnnounce', {current})} </output>
Route: audit §9 plus A7/A16; API Conventions.
Resolve explicit as → LinkProvider → native anchor through useLinkComponent. Render href; the adapter keeps it for native links and also maps it to to for custom router components.
import { useLinkComponent, type LinkComponentType, } from '@astryxdesign/core/Link'; function RoutedLink({as, href}: {as?: LinkComponentType; href: string}) { const LinkComponent = useLinkComponent(as); return <LinkComponent href={href}>Open</LinkComponent>; }
Route: packages/core/src/Link/ and API Conventions — Use the System.
BaseProps, React 19 refs, mergeProps, and composeEventHandlers preserve the public DOM contract. Consumer handlers normally run first so preventDefault() can cancel built-in behavior.
import {composeEventHandlers, mergeProps, themeProps} from '@astryxdesign/core/utils'; <button ref={ref} {...mergeProps( themeProps('choice'), stylex.props(styles.root, xstyle), className, style, )} {...rest} role="radio" aria-checked={isSelected} onClick={composeEventHandlers(onClickProp, handleSelect)} />
Route: API Conventions — BaseProps and precedence and audit P2/P15.
Resolve explicit prop → inherited context → default through useSize. A grouping component that owns density provides its resolved size to eligible descendants.
import { SizeProvider, useSize, type ElementSize, } from '@astryxdesign/core/SizeContext'; function Control({size}: {size?: ElementSize}) { const resolvedSize = useSize(size, 'md'); return <button data-size={resolvedSize}>Action</button>; } <SizeProvider value="sm"><Control /><Control size="lg" /></SizeProvider>
Route: SizeContext/SizeContext.ts; API Conventions — Size Variants.
Consumers render semantic glyph names through Icon; component authors resolve shared control visuals through Indicator. Indicators stay decorative—the owning control keeps role, state, focus, and keyboard behavior.
import {Icon} from '@astryxdesign/core/Icon'; import {useIndicator} from '@astryxdesign/core/Indicator'; const CheckboxIndicator = useIndicator('checkbox'); <> <Icon icon="success" label="Completed" /> <CheckboxIndicator state={checked ? 'checked' : 'unchecked'} size="sm" /> </>
Route: packages/core/src/Icon/, Indicator/; audit Q4/T17/I10.
General feedback uses info | success | warning | error; input validation uses success | warning | error. Busy/loading is separate operational state.
import {Banner, type BannerStatus} from '@astryxdesign/core/Banner'; import {TextInput, type TextInputStatus} from '@astryxdesign/core/TextInput'; const feedback: BannerStatus = 'info'; const validation: TextInputStatus = { type: 'error', message: validationMessage, }; <> <Banner status={feedback} title="Profile saved" /> <TextInput label="Email" value={email} onChange={setEmail} status={validation} /> </>
Do not introduce parallel values such as danger, critical, positive, negative, severity, or tone. Components select the status; the status system owns the matching surface, text, border, and icon token family.
Route: API Conventions — Status / Validation, audit field behavior plus A3/A7/A11/A12/A16/I1, and Theming Infrastructure for semantic token roles.
Give a separately nameable state machine, gesture, timing protocol, or interaction algorithm a focused test boundary. useMenuHover is a shipped source precedent but component-author internal, with no supported package import.
import {useMenuHover} from '../hooks/useMenuHover'; // Core source only. const hover = useMenuHover({ show: layer.show, hide: layer.hide, isOpen: layer.isOpen, isEnabled: true, popoverId: layer.id, }); <> <button ref={hover.setTriggerEl} {...hover.triggerProps}>Menu</button> <div ref={hover.menuRef} {...hover.contentProps} role="menu" tabIndex={-1}> {items} </div> </>
Extraction does not prove placement: the hook must run at the lifecycle owner and survive every documented composition seam. Add a behavior component only when it owns necessary semantic DOM, provider lifetime, a portal/top-layer host, descendant registration/order, or a rendered affordance.
Route: audit C22 and the ARCHITECTURE slot's BEHAVIOR UNIT / SEAMS lines.
- React first: escape to the DOM only for a named performance/accessibility requirement; never read data React already knows.
- Reuse contracts: compose the existing component or owner primitive instead of rebuilding its behavior and appearance.
- Behavior defaults to a hook/utility: add a component only when it owns a necessary semantic/provider/portal/registration/rendered boundary.
-
Data attributes are rare external contracts: never use them for component communication; theming state flows through
themeProps.
Start here Astryx Philosophy Contributing with AI Assistants Contributing
Architecture System Architecture Architecture Cheat Sheet Theming Infrastructure Distribution
Building a component Component Lifecycle Component Authoring Guide API Conventions Design Conventions
Quality Component Audit Rubric Accessibility Checklist AST-009 assistive-technology verification
Operations Release Process Night Watch Overview