Skip to content

Theming: Dynamic Theme Building #4264

Description

@JasonVMo

Summary

Support both Flex-first and Fluent-theme authoring without eagerly constructing
both token models. Add a Flex-value context contract, resolve Flex tokens only
for ThemeState consumers, and construct the legacy Fluent token set only when
legacy hooks request it.

Today every theme must start as a full Fluent Theme and useFlexTokens
ignores context. This task adds the second authoring path while preserving lazy,
shared theme state.

Goal

Let a theme be defined either in Flex tokens or in Fluent tokens, construct the
Fluent token set only when a legacy theme hook is used, ensure Flex tokens only
when the new ThemeState routines are used, and add a context type for
supplying Flex values directly.

Stage

Stage 1 - Beta foundations.

Why it matters

  • Observed. useFlexTokens currently ignores context entirely and always
    returns defaultFlexTokens
    (useFlexTokens.ts),
    which was landed as an explicitly temporary implementation in
    PR #4156.
    It is imported by 16 component test files under
    packages/agentic/components/src/components
    as the expected-value oracle, so those tests currently assert against defaults
    rather than against the theme in context.
  • Observed. A theme author has no way to supply Flex values. The only theme
    context is React.createContext<Theme>(undefined) in
    theming/context.ts,
    and Theme requires the full Fluent shape: colors, typography,
    components, shadows, spacing, and host
    (Theme.types.ts).
  • Inferred. Because ThemeState is derived from a Theme, any Flex-first
    theme must currently be expressed as a Fluent theme first, which forces the
    Fluent token set to be constructed even for consumers that only read Flex
    tokens.

Observed current state

  • Observed. useThemeState reads Theme from context and lazily builds one
    ThemeState per Theme object, storing it on the theme under a
    non-enumerable symbol so all consumers of that theme share one identity
    (useThemeState.ts).
    When context is empty it returns a module-level defaultThemeState.
  • Observed. ThemeState is { tokens: FlexTokens; highContrast: boolean; themeStyles: Record<symbol, unknown> }, and themedStyleSheetFactory caches
    one StyleSheet.create result per component per ThemeState.
  • Observed. flexTokensFromTheme(theme) merges a projection of the theme
    through flex-from-theme.json
    over nonFluentFlexTokens
    (flexTokensFromTheme.ts),
    landed in PR #4186.
  • Observed. FlexTokens is a nine-group object type -- color, shadow,
    fontWeight, fontFamily, fontSize, lineHeight, borderRadius,
    spacing, strokeWidth -- where color is SemanticColorTokenValues plus
    hover and pressed override maps
    (flex.types.ts).
  • Observed. ThemeProvider accepts a ThemeReference, subscribes to its
    change notifications, and pushes the resolved Theme into context
    (ThemeProvider.tsx);
    ThemeReference resolves recipes lazily and invalidates listeners
    (themeReference.ts).
  • Observed. The legacy hooks that consume the Fluent Theme live in
    packages/framework/use-tokens and
    packages/framework/use-styling,
    and platform themes such as
    createAppleTheme.macos.ts
    construct a full Fluent Theme eagerly inside a ThemeReference recipe.
  • Observed. flex-token-map.yaml records that flex-from-theme.json and
    nonFluentFlexTokens must be kept synchronized by hand, and that unmapped
    hover or pressed fallbacks are only included when their rest destination is
    also unmapped
    (flex-token-map.yaml).

Upstream Flex authoring model (x3-design/fluent-design at d334acf)

  • Observed. x3 already has the Flex-first authoring model this task adds.
    createTheme(overrides?) in dev/web/flex-themes/createTheme.d.ts accepts
    { brand, primitives, generics } and returns a ThemeResult carrying both
    resolved modes plus a toCss(mode?, selector?) serializer. Prebuilt
    lightTheme and darkTheme constants are exported from the same module.
  • Observed. Override granularity is per token with an optional per-mode
    shape: a scalar applies to both modes, and { light, dark } sets them
    separately with either side omittable. Overrides are layered in a documented
    order -- brand, then primitives, then generics on top of resolved values.
  • Observed. A theme is a flat Record<string, string | number> keyed by
    --gnrc-* token name, not a nested object.
  • Observed. Brand input is a small structured record --
    { color, step?, preset?, darkMode? } -- from which a full ramp is generated,
    rather than a full palette the author must supply.
  • Inferred. The FURN Flex-value context type has a directly comparable
    upstream precedent for the partial-override question below: upstream accepts
    sparse overrides layered over resolved defaults, and expresses per-mode values
    inline rather than requiring two complete theme objects.

Scope

  • Define the Flex-value context type: a context that carries Flex token values
    (in full or as a partial layered over generated defaults) without requiring a
    Fluent Theme.
  • Define how the two authoring models compose when both are present, including
    precedence and provider nesting.
  • Make useFlexTokens resolve from context, preserving the identity guarantee
    that useThemeState already provides so themedStyleSheetFactory caching
    stays valid.
  • Make ThemeState construction lazy per token set, so a Flex-authored theme
    does not build the Fluent token set and a Fluent-authored theme does not build
    Flex tokens until a ThemeState routine asks for them.
  • Keep the legacy hooks working against a Fluent Theme, constructing it only
    when those hooks are used.
  • Update the 16 component test files that use useFlexTokens as an oracle so
    they assert against the theme under test.

Out of scope

Deliverables

  1. A Flex-value context type and its provider, exported from the design package.
  2. A context-aware useFlexTokens that replaces the temporary implementation.
  3. Lazy construction of the Fluent and Flex token sets, wired through
    useThemeState.
  4. A documented precedence model for Flex-authored and Fluent-authored themes,
    including nesting.
  5. Tests covering: Flex-only theme, Fluent-only theme, both present, neither
    present, and ThemeState identity stability across re-renders.
  6. Updated component tests and changesets.

Acceptance criteria

  • A theme can be supplied as Flex token values and consumed by
    useThemeState and useFlexTokens without constructing a Fluent Theme.
  • useFlexTokens returns values from context when a theme is present and
    falls back to the generated defaults only when no theme is in context.
  • The Fluent token set is not constructed for a Flex-authored theme unless a
    legacy hook consumes it, verified by a test that observes construction.
  • Flex tokens are not constructed until a ThemeState routine requests
    them, verified by a test.
  • ThemeState identity remains stable for a given theme across re-renders,
    and themedStyleSheetFactory still creates each style sheet once, as
    asserted in useThemeState.test.ts.
  • Precedence between a Flex provider and a Fluent ThemeProvider, including
    nesting, is covered by tests.
  • Existing agentic components render unchanged under an unchanged Fluent
    theme; component snapshots and assertions still pass.
  • yarn build, yarn lage test, and yarn lage lint pass at the
    repository root, and changesets are present.

Dependencies and ordering

Risks and open decisions

  • Open decision. Whether the Flex context carries a complete FlexTokens
    object or a deep partial merged over generated defaults. Observed:
    FlexTokens requires every group and every token, and defaultTokens.ts
    already layers nonFluentFlexTokens under a fuller default set, so a partial
    model has precedent in the repository. Observed: upstream createTheme
    also takes sparse overrides layered over resolved defaults.
  • Open decision. Whether a FURN Flex theme carries one appearance or both,
    as upstream's ThemeResult does. Observed: ThemeState today is derived
    from a single already-resolved Theme, so carrying both modes would be a new
    shape; this interacts directly with
    System Appearance Handling.
  • Open decision. Whether per-mode override values use upstream's inline
    { light, dark } shape or FURN's existing pattern of separate resolved themes.
  • Open decision. Whether Flex and Fluent providers may be nested, and which
    wins. Inferred: without a rule, a component tree could read Flex tokens
    from one ancestor and Fluent-derived tokens from another and render
    inconsistently.
  • Open decision. Whether the caching mechanism for a Flex-authored theme
    reuses the symbol-on-theme-object approach in useThemeState or a different
    mechanism, since a plain Flex value object may be frozen or shared.
  • Risk. The 16 component test files that call useFlexTokens as an oracle
    will change behavior once it becomes context-aware; they must be updated in
    the same change to avoid masking regressions.
  • Risk. Laziness that is implemented with mutable memo state on a shared
    theme object can leak across themes. useThemeState already guards this by
    giving each ThemeState its own themeStyles registry.

Evidence and references

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions