Skip to content

Architecture Cheat Sheet

Cindy Zhang edited this page Aug 26, 2026 · 3 revisions

Astryx Architecture Cheat Sheet

Operational routing guide, not an audit rubric. Verified against origin/main at baa01b1c25355a85f26c7363f5c98b4d6e806c6b (2026-08-25). Use the linked owner API and route detailed checks to the existing docs.

Golden template: a simple component

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.

Choose only what applies

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

Tier 1 — always check when applicable

Layer protocol suite

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.

Field/input anatomy

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.

Theming / visual state

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.

Accessibility primitives

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.

Collection/selection

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.

Container padding/bleed

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.

Text/i18n/direction

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.

Link/router

Resolve explicit asLinkProvider → 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.

Public DOM/style/event/ref composition

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.

Tier 2 — check when triggered

Size cascade

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.

Semantic icons / indicator maps

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.

Status/feedback

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.

Behavior units

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.

Global guardrails

  • 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.

Clone this wiki locally