# Project theme contract

This guide describes the current theme factory, composition, and compatibility contract. Its filename is retained for existing links. For provider setup, see [Applying an ASUI theme](./theme-provider.md); for every component slot and default, see the [theme property reference](./theme-reference.md). All styled component families now consume semantic component styles, including ChoiceBar, charts, and SVG icons. Glyph icons retain their font-specific API.

## Create a project theme

The [Theme property reference](./theme-reference.md) lists every modern token and component style property with descriptions and resolved classic/neutral defaults, including size-specific values.

```tsx
import { createASTheme, ASThemeDefault, ASThemeProvider, ASButton } from '@eleventheye/asui';

const projectTheme = createASTheme({
  preset: 'neutral',
  tokens: {
    color: {
      accent: '#175cd3', onAccent: '#ffffff',
      accentHover: '#1849a9', accentPressed: '#194185',
      surface: '#ffffff', text: '#101828', focus: '#175cd3',
    },
    radius: { control: '6px' },
    typography: { fontFamily: 'system-ui, sans-serif' },
  },
  components: {
    button: { rest: { shadow: 'none' } },
    textfield: { inputBackground: '#fff', inputFocusShadow: 'none' },
  },
});

<ASThemeProvider theme={projectTheme}>
  <ASButton text="Save project" />
</ASThemeProvider>;
```

Create reusable themes outside render, or memoize them when their inputs change. `createASTheme()` selects the classic preset; `preset: 'neutral'` explicitly selects flat surfaces, ordinary casing, and no decorative button scaling or inset shadows. Dark mode is a neutral theme with a dark palette, not a separate styling engine. Hover, pressed, focus, and foreground colors are deliberate tokens: changing `accent` does not guess accessible colors for those other states.

The classic recipe retains historical variations in geometry, font weights, colors, and effects until the corresponding token is explicitly supplied. For example, a typography weight token changes dropdown and text-field weights as well as button weights; omitting it preserves each classic component's weight. Component style overrides apply last.

## Replacement and composition

- `theme` replaces the nearest theme completely.
- `themeOverrides` composes with the nearest inherited theme. Without a provider it starts from classic.
- Both inputs on the same provider or component are rejected at runtime. Provider types also reject this combination. Component props remain ordinary, extendable object types for consumers using `interface ... extends ASButtonProps`.
- A component's composed theme reaches its descendants, including an ASButton's loader.
- Explicit existing icon colors and loader section themes retain precedence.

```tsx
<ASThemeProvider theme={projectTheme}>
  <ASThemeProvider themeOverrides={{ tokens: { radius: { control: '2px' } } }}>
    <ASButton text="Inherited square corners" />
    <ASButton
      text="Local exception"
      themeOverrides={{ components: { button: { rest: { background: 'transparent' } } } }}
    />
    <ASButton text="Classic replacement" theme={ASThemeDefault} />
  </ASThemeProvider>
</ASThemeProvider>
```

`extendASTheme(projectTheme, overrides)` performs the same composition outside React. The factory retains the explicit inputs separately from derived styles: changing tokens recalculates dependent styles while preserving earlier component exceptions. `undefined` is ignored, including in nested exceptions; valid `0`, `''`, and `'none'` are retained. Unknown slots, nulls, malformed subtrees, incorrect primitive types, and non-finite numbers are rejected. Arrays, if present, replace rather than concatenate.

The public types are `ASThemeInput`, `ASThemeOverrides`, `ASThemeTokens`, `ASThemeComponents`, and `ASResolvedTheme`. Resolved themes are detached from caller inputs and frozen. Treat them as immutable and use `extendASTheme` to change them. Serialize the **input**, then rebuild with `createASTheme`; do not JSON-round-trip a resolved theme. The retained input definition uses symbol metadata, which survives an ordinary shallow copy but not JSON serialization. Editing a spread copy's computed fields is not a supported override mechanism.

## Complete-theme compatibility

`ASTheme`, `ASThemeDefault`, `useASTheme(theme?)`, and complete theme props remain available. `useASTheme()` retains the identity and existing shape of inherited complete themes. Migrated components resolve complete-theme values through one adapter rather than reading fallback keys individually.

Custom complete themes are treated as explicit component values. Token overrides do not reinterpret their old gradients or infer their originating palette; use `components` overrides to modify those styles, or start a token-driven theme with the factory. The actual `ASThemeDefault` singleton is the classic factory base, so ordinary provider-free token overrides work.

Resolved themes retain the original top-level sections for compatibility. Those sections are **not** synchronized mirrors of semantic styles: read `theme.components` for resolved component appearance. Use `extendASTheme` to change an existing theme; do not mutate either representation.

ASLoader keeps `theme?: ASLoaderTheme` as a complete loader-section replacement. Its `themeOverrides` prop composes with the inherited full theme and uses the resulting loader styles; pass one or the other. The previously unused legacy `loaderSpinBorderAfterColor` remains unused by the compatibility adapter to preserve existing rendering. New themes can set `components.loader.spinAfterColor` explicitly.

## Styling and behavior

Styles cover backgrounds, foregrounds, borders, focus rings, disabled and invalid states, typography, corners, sizing, and effects. Background slots accept complete CSS values, including flat colors and gradients. The classic per-size button shadows are recipe inputs referenced through internal CSS variables; explicit state `shadow` values, including `'none'`, take precedence.

Focus-visible outlines remain independent of selected, active, and hover decoration. Disabled/loading controls suppress active/hover decoration. Loaders honor reduced motion with static, still-visible indicators. Existing callbacks, controlled/uncontrolled text input behavior, native select behavior, and ASTappable handling are retained. ASTappable does not supply native button keyboard semantics. Use [native button mode](./native-buttons.md), `as="button"`, for native keyboard and form behavior.

The text-field autofill rules now use their intended semantic foreground/background slots consistently. Disabled inputs and icons use disabled colors, and `aria-invalid="true"` uses the invalid border slot. These are intentional visual corrections rather than changes to input behavior.

### Size-specific text-field shadows

`components.textfield.sizes[ComponentSize.Small].inputFocusShadow` changes the small field's hover/focus shadow independently. For example:

```tsx
const projectTheme = createASTheme({
  components: {
    textfield: {
      sizes: {
        [ComponentSize.Small]: { inputFocusShadow: 'inset 0 1px 2px 0 #0003' },
      },
    },
  },
});
```

The shared `components.textfield.inputFocusShadow` defaults to the active size's shadow through an internal CSS variable. An explicit shared value (including `'none'`) takes precedence for every size. Both classic and neutral support per-size overrides; neutral defaults to no shadow.

## Theme examples and verification

Storybook's **ASUI THEMES → Project themes → Gallery And Interactions** compares classic, neutral light, and neutral dark. **ASUI THEMES → Inheritance** demonstrates replacement, scoped overrides, and portals. **ASUI THEMES → Eleventheye → Gallery and Interactions** demonstrates a project-owned visual identity. Individual component stories cover their own sizes and states.

Follow the [component workflow](./component-workflow.md#validation-and-review-handoff) for automated and browser checks. Historical phase acceptance does not establish that a later package has been verified in consumers; release and consumer checks remain tracked in [issue #94](https://github.com/Andrey11/asui/issues/94).

## Text-field customization

### Outer text-field glow

`components.textfield.containerHoverShadow` and `containerFocusShadow` accept complete CSS box-shadow values. Both default to `none` in classic, neutral, and legacy adaptation. Hover applies only on hover-capable devices when the enabled container does not contain focus. Focus uses `:focus-within`, stays active when the pointer leaves, and includes focus on field accessories. Disabled containers receive neither effect. These slots are independent of the inner `inputFocusShadow` and keyboard `focusRing`.

The Eleventheye preview uses a soft hover glow and a stronger focus glow, with the inner shadow and ring disabled for that project. Other themes retain their existing inner focus decoration.

### Single-line fields without icons

Classic (yellow) single-line fields retain an inset on each side without an icon: `inputMarginInlineWithoutIcons` uses the per-size inset (12px small, 16px medium, 20px large), with `containerPaddingWithoutIcons` at `'0'`. Neutral uses `'4px'` horizontal input margins to match its default vertical inset, with zero outer padding. Complete themes, including ASThemeDefault and themes derived from it, use the same size-based insets through the compatibility adapter. Explicit component overrides still take precedence. Text padding is configured separately through `inputPaddingInline` (default `'12px'`); vertical margins, spacing on sides with icons, and multiline geometry are unchanged. For a wider inset, set outer padding to `'0 12px'` and input margins to `'8px'`. Storybook includes **Without Icons** and **Custom Inset Spacing** examples.

### Autofill and field accessories

Autofill styling now matches hover/focus selector specificity and excludes disabled
fields, so interaction shadows cannot erase the autofill fill. Control
`outlineOffset` defaults to `0px` independently of `focusRingOffset`. Text fields
expose `inputMarginInline` (`8px`, for icon-bearing inputs) and
`trailingIconMarginRight` (`-2px`). Icon-free margins still use their own override.
The clear action accepts `clearButtonLabel`, defaulting to “Clear input”.
Search inputs hide the browser's native clear button without reserving space for it, keeping text padding consistent with other input types. ASUI supplies the visible clear action.

## Switch and snackbar themes

ASSwitch and ASSnackbar extend semantic theming with `components.switch` and `components.snackbar`. Both use the same resolution boundary, provider inheritance, replacement and override rules. They do not require additions to existing complete themes. Their APIs and behavior are documented in [switch.md](./switch.md) and [snackbar.md](./snackbar.md).

### Text-field error example

Storybook’s **Error State** example uses `errorMessage` on the standard ASTextField. A non-empty message renders below the control, sets `aria-invalid`, and is automatically associated through `aria-describedby` while preserving consumer description IDs. Empty or whitespace-only messages remove the feedback. Consumers can still use `aria-invalid` independently and supply their own message. Validation remains consumer-owned. The same API works in multiline mode and preserves the character-counter association.

Invalid fields use `invalidBorderColor` and `invalidFocusOutlineWidth`. Classic retains its outer background and uses `invalidColor: #982900`; neutral uses `invalidContainerBackground: corral40`. The message uses `invalidColor`. Disabled control styling takes precedence.

Each side without an icon removes its icon padding and uses `inputMarginInlineWithoutIcons`: 12px/16px/20px for small/medium/large classic fields and 4px for neutral fields. Sides containing icons keep their existing spacing. The **Trailing Icon Only** and **Leading Icon Only** stories demonstrate these layouts.
