-
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
Operations Release Process Night Watch Overview