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 e3259c94696e9730d11d4c97e0adca5af1b14cf3 (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

Floating and modal UI shares one protocol for host/portal placement, anchoring, dismissal ordering, nesting depth, focus, and lifecycle. Prefer an existing layer family; a new primitive must join all applicable Layer owners rather than rebuilding one concern locally.

import {useLayer, useLayerDismissal, LayerDepthProvider} from '@astryxdesign/core/Layer';
import {useFocusTrap} from '@astryxdesign/core/hooks';

Route: packages/core/src/Layer/; Component Audit Rubric §1 and Q10.

Field/input anatomy

A form control uses the shared label, description, required/optional, status, clear-control, group-ID, and input-chrome contracts. Do not assemble a private field wrapper.

import {
  Field,
  FieldLabel,
  FieldStatus,
  InputClearButton,
  inputWrapperStyles,
} from '@astryxdesign/core/Field';
import {useInputContainer} from '@astryxdesign/core/hooks';
import {getInputARIA} from '@astryxdesign/core/utils';

Route: API Conventions — Input Component Props and audit §§1, 3, 4.

Theming / visual state

Paint with semantic tokens, put themeProps only on stable painting elements, and reflect style-driving props/state through that target. Theme targets are public API; structural wrappers stay private.

<span
  {...mergeProps(
    themeProps('component-name', {
      variant,
      disabled: isDisabled ? 'disabled' : null,
    }),
    stylex.props(styles.root, variants[variant], xstyle),
    className,
    style,
  )}
/>

Route: Theming Infrastructure — Component Theming and audit §2.

Accessibility primitives

Compose the shared primitive for the interaction class. useAnnounce owns transient announcements; VisuallyHidden owns persistent hidden content—it is not a replacement live-region implementation.

import {
  useAnnounce,
  useClickableContainer,
  useContainerReveal,
  useFocusTrap,
  useInputContainer,
} from '@astryxdesign/core/hooks';
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';

const announce = useAnnounce();
// In the real transition handler: announce(translatedMessage, 'polite');

Route: audit A6/A7/A16/A17 and the component/hook docs emitted by astryx component / astryx docs.

Collection/selection

Use one semantic/focus owner per row, Item anatomy where it fits, the focus hook matching the collection shape, and shared Indicator primitives. Do not add a local arrow-key switch or private selection row.

import {Item} from '@astryxdesign/core/Item';
import {
  useGridFocus,
  useListFocus,
  useTreeFocus,
  useTypeahead,
} from '@astryxdesign/core/hooks';
import {CheckIndicator, useIndicator} from '@astryxdesign/core/Indicator';

Route: packages/core/src/Item/, hooks/use*Focus.ts, Indicator/; audit A17.

Container padding/bleed

Containers publish the padding they actually paint; bleed consumers read the matching logical edge with a 0px fallback, and nested container owners reset then republish. Use the owner utility instead of hardcoded negative spacing.

import {container, edgeCompSlot, EDGE_COMP_ATTR} from '@astryxdesign/core/Layout';

const containerVars = [
  '--container-padding-inline-start',
  '--container-padding-inline-end',
  '--container-padding-block-start',
  '--container-padding-block-end',
] as const;

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. Visible and assistive text share the catalog/provider locale; direction comes from the i18n system and CSS logical properties.

import {useDirection, useTranslator} from '@astryxdesign/core/i18n';
import {useAnnounce} from '@astryxdesign/core/hooks';

const t = useTranslator();
const direction = useDirection();

Route: audit §9 plus A7/A16; API Conventions.

Link/router

Every link-capable component resolves explicit asLinkProvider → native anchor and preserves router props/ref plus safe target/rel handling. Never hardcode <a> for a public navigation path.

import {
  LinkProvider,
  useLinkComponent,
  type LinkComponentType,
} from '@astryxdesign/core/Link';

const LinkComponent = useLinkComponent(as);

Route: packages/core/src/Link/ and API Conventions — Use the System.

Public DOM/style/event/ref composition

BaseProps, the React 19 ref prop, mergeProps, and composeEventHandlers preserve the public DOM contract. Combine styles, forward neutral props, place component-owned semantics after rest, and compose collisions explicitly—normally consumer first so preventDefault() cancels built-in behavior.

<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

When a component accepts size, resolve explicit prop → inherited context → default. A grouping component that owns density provides the resolved size to eligible descendants.

import {SizeProvider, useSize} from '@astryxdesign/core/SizeContext';

const size = useSize(sizeProp, 'md');
// <SizeProvider value={size}>{children}</SizeProvider>

Route: SizeContext/SizeContext.ts; API Conventions — Size Variants.

Semantic icons / indicator maps

Component-owned glyphs resolve by semantic name through the Icon registry; shared selection/control visuals resolve through Indicator. Use a narrow content slot only for consumer-owned content, not as a component-replacement registry.

import {Icon, useIcon} from '@astryxdesign/core/Icon';
import {CheckIndicator, RadioIndicator, useIndicator} from '@astryxdesign/core/Indicator';

Route: packages/core/src/Icon/, Indicator/; audit Q4/T17/I10.

Status/feedback

When adding status, use the canonical feedback values info | success | warning | error. Input validation intentionally uses the subset success | warning | error; busy / loading is operational state, not another status. Field-like controls use Field status anatomy; general feedback uses Banner; transition announcements use translated text through useAnnounce.

import type {BannerStatus} from '@astryxdesign/core/Banner';
import type {InputStatus, InputStatusType} from '@astryxdesign/core/Field';
import {useAnnounce} from '@astryxdesign/core/hooks';
import {useTranslator} from '@astryxdesign/core/i18n';

const t = useTranslator();
const announce = useAnnounce();
const feedback: BannerStatus = 'info';
const validationType: InputStatusType = 'error';
const validation: InputStatus = {
  type: validationType,
  // Placeholder: add this key to your component's catalog before use.
  message: t('@astryx.yourComponent.invalid'),
};

// In the transition handler: announce(validation.message ?? '', 'polite');

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.

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.
  • Data attributes are rare external contracts: never use them for component communication; theming state flows through themeProps.

Clone this wiki locally