Skip to content

Night Watch Theme Auditor

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

Night Watch — Theme Auditor (Deprecated)

⚠️ This role has been merged into Night Watch Component Auditor. The Component Auditor combines theming, API, accessibility, and export checks into a single nightly pass per component. This page is kept for historical reference only.

Assigned to: Cindy's Navi (cixzhang)

Goal: Ensure all components are fully compatible with theming — using CSS variables, reusing primitives correctly for swizzle, and applying xdsThemeProps on the right elements.


Why This Role Exists

Theming compatibility regresses silently. A hardcoded color, a missing xdsThemeProps, a component that renders its own close button instead of using Button — these don't break tests or CI but they break the theming contract. When a consumer applies a theme, they expect it to cascade consistently. This role catches the gaps.


Scope

1. CSS Variable Usage

All visual properties must use CSS variables (design tokens), not hardcoded values.

Check for:

  • Colors: no hardcoded hex, rgb, hsl values in StyleX styles — must use colorVars
  • Shadows: no raw boxShadow strings — must use elevation token variables
  • Spacing: no raw px values for margins, paddings, gaps — must use spacingVars
  • Radii: no hardcoded border-radius — must use radiusVars
  • Typography: font sizes, weights, line heights should come from token variables
  • Font sizes must use semantic type scale tokens (typeScaleVars) instead of raw text size tokens (textSizeVars). Components should express intent ("this is a label", "this is supporting text") not implementation ("this is 14px"). See the mapping table below.

Semantic type scale migration (PR #661):

Raw token (textSizeVars) Semantic token (typeScaleVars) Used for
--text-base --text-label-size Interactive controls: buttons, tabs, selectors, menu items, form labels, nav items
--text-base --text-body-size Content: text inputs, links, descriptions, table cells
--text-sm, --text-xsm --text-supporting-size Secondary: descriptions, breadcrumbs, captions, placeholders, badges, helper text
--text-lg --text-large-size Emphasis: empty state titles, nav headings
--text-2xs --text-supporting-size Micro: compact density descriptions

When auditing, any textSizeVars['--text-*'] usage for font sizes should be flagged and replaced with the appropriate typeScaleVars semantic token. Use the semantic meaning of the text (is it a label? body content? supporting info?) to pick the right token — don't just mechanically map based on the raw size.

Exceptions:

  • 0, 'none', 'transparent', 'inherit', 'currentColor' are fine
  • Layout values that aren't semantically spacing (e.g. width: '100%', flex: 1) are fine
  • Large layout constraints (maxHeight, maxWidth, minWidth) are acceptable as hardcoded values. These are component-specific design decisions, not theme tokens (e.g. dropdown maxHeight: '300px', empty state maxWidth: '360px').
  • Animation values (durations, easing) have their own token story — flag but don't block

2. Component Reuse for Swizzle & Theming

Components should compose from core primitives so theming cascades correctly.

Check for:

  • Close buttons in dialogs, popovers, sheets should use Button (ghost variant) — so theming ghost buttons affects close buttons too
  • Dividers should use Divider — not raw <hr> or styled divs
  • Icons should use Icon with the registry — not inline SVGs
  • List items in menus, selectors, command palettes should share the same base primitive so theming is consistent

Nuance:

  • Not every button-like thing should be the same. Nav items, tab items, and toolbar buttons have distinct theming needs — they shouldn't be ghost buttons just because they look similar. The test is: would a theme author expect changing ghost button styles to affect this element?
  • Prefer borders over extra DOM elements for dividers. When a component needs visual separators between items (e.g. list dividers), use borderBlockEnd with colorVars['--color-border'] on the existing elements rather than injecting <hr>, <Divider>, or any additional DOM nodes. Use :last-child to suppress the final border. This keeps the DOM minimal while still using theme tokens. Only use <Divider> when the divider is a standalone structural element (e.g. separating sections in a dropdown menu), not when separating repeated siblings.
  • Skip compositions that wrap other themed components. If a component is primarily a composition of other Astryx components (e.g. PowerSearch wraps Tokenizer), adding xdsThemeProps to the wrapper creates specificity conflicts with the inner component's own theming. The rational target for styling is the inner component, not the wrapper. When in doubt, skip and flag for human review.
  • When in doubt, flag it for human review rather than filing a fix

3. xdsThemeProps Application

Every component and visual variant must apply xdsThemeProps on the correct DOM element so themes can target them with @scope. xdsThemeProps is the primary mechanism for per-component theming — it is how theme authors reach into components to override visual properties. Component CSS vars (--card-radius, etc.) are only used when values participate in calc() or are referenced by multiple elements.

Check for:

  • The element receiving visual styles has the correct xdsThemeProps — this is not always the root element. The main visual styles (background, border-radius, shadow, typography) determine which element gets the className. For example, a dialog's xdsThemeProps goes on the panel element where the visual styling lives, not the backdrop wrapper. A tooltip's xdsThemeProps goes on the container div with the background and border-radius, not the positioning wrapper.
  • Visual variants that need distinct theming get variant-specific classNames (e.g. astryx-button-ghost, astryx-button-secondary)
  • Props that drive visual styles must be included in xdsThemeProps. If a prop selects between different StyleX style objects (e.g. statusStyles[status], colorStyles[color]), that prop must appear in the xdsThemeProps call on the same element. Otherwise themes can't target those visual states. Look for patterns like fooStyles[prop] in stylex.props() and verify prop is in the corresponding xdsThemeProps() call.
  • Composed sub-components (e.g. dialog close button) get their own component's className, not the parent's

Common mistakes from past audits:

  • Placing xdsThemeProps on the wrong element. Layer-based components (Tooltip, HoverCard, Popover) have a positioning wrapper and a visual container — the className goes on the visual container where background/shadow/radius live, not the positioning wrapper (#749, #744).
  • Using hardcoded shadow strings with only the color tokenized. A shadow like `0 4px 12px ${colorVars['--color-shadow']}` is only half-themed — the offsets and blur are hardcoded. Use the full shadow token (e.g. shadowVars['--shadow-menu']) so the entire shadow is themeable (#746, #670).
  • Adding xdsThemeProps to composition wrappers. If a component wraps other themed Astryx components (e.g. Tokenizer wraps Token, MoreMenuToggle wraps Button), adding xdsThemeProps to the wrapper creates specificity conflicts with the inner component's theming. Skip it and flag for review (#749, #672).
  • Visual props missing from xdsThemeProps. A prop that selects between different StyleX style objects must be in the xdsThemeProps call, or themes can't target those states. Example: Banner's status prop drove statusStyles[status] (per-status background colors) but wasn't included in xdsThemeProps('banner', {variant}) — themes couldn't override per-status backgrounds without a CSS custom property escape hatch. Fixed in #808 by adding status to the call: xdsThemeProps('banner', {variant, status}). The detection pattern: find fooStyles[prop] in any stylex.props() call and check if prop appears in the nearest xdsThemeProps() call on the same element or a parent wrapper.

4. Sub-Element Targeting

Visually distinct sub-elements within a component need their own xdsThemeProps so theme authors can target them directly. This is the equivalent of CSS ::part() (which requires Shadow DOM and is not available to us).

A sub-element needs an xdsThemeProps when:

  • It has its own color, background, or border distinct from the parent (e.g. a switch thumb, a radio dot, a progress bar fill)
  • It cannot be styled via a CSS descendant selector from the root class due to specificity or structural reasons

A sub-element does NOT need an xdsThemeProps when:

  • It is structural only (wrapper divs for layout, flex containers)
  • It is text content that inherits from the parent (labels, descriptions)
  • Its appearance is fully controlled by global tokens with no per-component override need

Standard: prefer xdsThemeProps targeting over component CSS vars. Theme authors write plain CSS property overrides on class targets — no per-component token vocabulary to learn, no API surface to maintain, and components can refactor internals without breaking a token contract. State overrides use CSS pseudo-selectors on the targets (:hover, :checked, [aria-selected]), not state-specific vars.

Reference — current sub-element targets:

Component Root Sub-elements Notes
Avatar avatar avatar-status-dot
Banner banner banner-icon Icon color varies per status
Calendar calendar calendar-day Day buttons have selected/today states
CheckboxInput checkbox-input checkbox Visual checkbox box
DropdownMenu dropdown-menu-item
Field field field-label, field-status
Layout layout layout-content, layout-header, layout-footer, layout-panel
ProgressBar progressbar progressbar-fill Fill bar has distinct color
RadioList radio-list, radio-list-item radio, radio-dot Circle and inner dot
Selector selector selector-option
SideNav side-nav side-nav-heading, side-nav-item, side-nav-section
Slider slider slider-track, slider-thumb Track bg, thumb color
Switch switch switch-thumb, switch-field Track targetable via root
Table base-table table-row, table-cell, table-header-cell
TopNav top-nav top-nav-heading, top-nav-item, top-nav-mega-menu

Scale guideline: Most components need 0-1 sub-element targets. Compound components (Layout, Table, Navigation) may need 3-5. If a component seems to need more than 5 targets, flag for human review — it may indicate the component should be decomposed.

When to flag vs fix: If unsure whether a sub-element is "visually distinct" enough to warrant a target, flag it in the audit notes for human review. The bar is: would a theme author plausibly want to override this element's appearance independently of its parent?


Nightly Checklist

This role runs once per night and produces one PR covering all findings. Only one PR per calendar day.

Step 1: Check if Already Run Today

Check memory/xds-night-watch-state.json for themeAuditor.lastRunAt. If it's from today (PST), skip and NO_REPLY.

Step 2: Determine Which Components to Audit

Check memory/xds-night-watch-state.json for themeAuditor.auditQueue.

  • If the queue is empty, scan packages/core/src/ for all component directories and build a fresh queue
  • Pick the next 5 components from the queue (or fewer if less than 5 remain)

Step 3: Audit the Components

For each of the selected components:

  1. Read the source files — main component file, any sub-components, style definitions
  2. Check CSS variable usage — scan StyleX stylex.create() calls for hardcoded visual values
  3. Check component reuse — look for raw HTML elements that should be Astryx primitives
  4. Check xdsThemeProps — verify it's applied on the right element for all variants

Step 4: Report Findings

For each issue found:

  • Log it to memory/xds-night-watch/{date}.md under a ## Theme Audit section
  • Categorize as: hardcoded-value, missing-primitive, missing-classname, classname-wrong-element
  • Include the file path, line number, and what the fix should be

Step 5: Fix Findings — One PR Per Night

Batch all findings from this run into a single PR:

  • Branch naming: navi/theme-audit/YYYY-MM-DD
  • Fix hardcoded values → token variables, missing primitives → Astryx components, missing/misplaced classNames
  • Run pnpm test and pnpm build before creating the PR. Fix any failures. Do not send a PR with broken tests or build.
  • Publish PRs ready for review (not draft) — label with hardening and theming
  • When a finding needs human judgment (e.g. whether a button-like element should reuse Button or stay independent), include it in the PR description under a Needs Review section rather than skipping it or deciding unilaterally
  • If no issues are found across all audited components, do not create a PR — just update state

Step 6: Update State

Update memory/xds-night-watch-state.json:

{
  "themeAuditor": {
    "lastAuditedComponent": "Button",
    "auditQueue": ["Card", "Dialog", ...],
    "prsFiled": ["#650"],
    "lastRunAt": "2026-03-17T04:00:00Z"
  }
}

When the queue is empty (all components audited), clear completedComponents and rebuild the queue from scratch. This ensures components are rechecked continuously — fixes from previous cycles may have regressed, and new code may have introduced new issues.


Does NOT Do

  • Subjective design decisions — only objective checks
  • Subjective design feedback — only objective checks (is this a CSS variable? is this using Button?)
  • Review PRs (that's Reviewer)
  • Fix CI (that's QA)
  • Triage issues (that's PM)

State Schema

{
  "themeAuditor": {
    "lastAuditedComponent": "string — last component fully audited",
    "auditQueue": ["string[] — remaining components to audit"],
    "prsFiled": ["string[] — PR numbers filed this cycle"],
    "lastRunAt": "ISO timestamp",
    "completedComponents": ["string[] — all components audited this cycle"]
  }
}

Examples

Hardcoded color (bad)

const styles = stylex.create({
  error: { color: '#E53935' },  // ❌ hardcoded
});

Token usage (good)

const styles = stylex.create({
  error: { color: colorVars['--color-negative'] },  // ✅ token
});

Missing primitive (bad)

// Dialog close button as raw HTML
<button onClick={onClose} className="close-btn">
  <svg>...</svg>  {/* ❌ inline SVG, raw button */}
</button>

Correct primitive reuse (good)

// Dialog close button using Button
<Button variant="ghost" size="sm" onPress={onClose}>
  <Icon name="close" />  {/* ✅ themed, swizzlable */}
</Button>

Divider via extra DOM elements (bad)

// Injecting <hr> or <Divider> between siblings
{items.map((item, i) => (
  <>
    <li>{item}</li>
    {i < items.length - 1 && <hr style={{...}} />}  {/* ❌ extra DOM nodes */}
  </>
))}

Raw text size token (bad)

const styles = stylex.create({
  label: { fontSize: textSizeVars['--text-base'] },  // ❌ raw size token
});

Semantic type scale token (good)

const styles = stylex.create({
  label: { fontSize: typeScaleVars['--text-label-size'] },  // ✅ semantic token — expresses intent
});

Divider via border token (good)

// Border on the existing element — zero extra DOM
const styles = stylex.create({
  withDivider: {
    borderBlockEnd: `1px solid ${colorVars['--color-border']}`,  // ✅ token, no extra DOM
    ':last-child': {
      borderBlockEnd: 'none',
    },
  },
});

Clone this wiki locally