Skip to content

Theming: Storybook Appearance Integration #4269

Description

@JasonVMo

Summary

Extend StorybookTheme.tsx to support 'dynamic' system appearance and a
Flex-authored theme, replace hard-coded hex values in the toolbar chrome with
token-derived values, and add deterministic tests that exercise theme selection
and validate toolbar behavior on both macOS and Windows.

The Storybook toolbar is the only runtime surface that exercises the full
theme-switching flow. Its current four choices all use createDefaultTheme, it
cannot exercise 'dynamic' appearance or the Flex-value context, and the
toolbar styles are literal hex values that are inconsistent with the theming
patterns the workstream is establishing.

Goal

After this task, the Storybook toolbar adds a 'dynamic' system-appearance
option, a Flex-authored-theme option, and token-sourced toolbar chrome. Tests
confirm that each toolbar choice produces the expected context value, that the
toolbar is functional on macOS and Windows, and that CI does not depend on the
host machine's system appearance.

Stage

Stage 2 - Beta delivery.

Why it matters

  • Observed. The toolbar hard-codes four createDefaultTheme calls and has
    no 'dynamic' option, so a developer running the Storybook cannot trigger the
    system-appearance code path without modifying source
    (apps/storybook/src/StorybookTheme.tsx).
  • Observed. No toolbar option exercises the Flex-value context. Every choice
    is a ThemeReference built from a Fluent Theme, so a Flex-authored theme
    has no integration exercise in the Storybook today.
  • Observed. The toolbar StyleSheet contains eleven literal hex values
    (#f5f5f5, #d1d1d1, #242424, #ffffff, #b3b3b3, #0f6cbd) that
    duplicate design token values rather than sourcing them from the token system
    (StorybookTheme.tsx styles block).
  • Observed. The Windows smoke test
    (apps/storybook/windows-tests/storybook-smoke.test.cjs) does not exercise
    any theme button, so the toolbar is not covered by automated validation on
    Windows.
  • Inferred. Without a 'dynamic' integration test, a regression in
    appearance-change notification or ThemeState invalidation on macOS or
    Windows could ship silently.
  • Inferred. Toolbar colors derived from literals are inconsistent with
    themedStyleSheetFactory usage across agentic components and create a
    maintenance burden when brand or neutral token values change.

Observed current state

  • Observed. themeChoiceNames is ['none', 'light', 'dark', 'highContrast'].
    themeChoices builds each entry with createDefaultTheme from
    @fluentui-react-native/default-theme; 'none' omits the theme entirely
    (StorybookTheme.tsx lines 12-20).
  • Observed. StorybookThemeProvider (used as a story decorator in
    preview.tsx)
    reads the ThemeReference from StorybookThemeContext and conditionally
    renders <ThemeProvider theme={theme}>. No Flex-value context provider is
    present.
  • Observed. StorybookThemeHost in
    StorybookApp.tsx
    wraps the entire Storybook UI and is the root that renders the toolbar.
    The toolbar sits outside the story-level ThemeProvider, so it currently has
    no access to the Flex tokens of the selected theme.
  • Observed. Toolbar testID values are already stable:
    agentic-storybook-theme-toolbar on the containing View and
    agentic-storybook-theme-${name} on each button, providing reliable
    automation targets for both macOS accessibility IDs and Windows UIA selectors.
  • Observed. The Windows smoke test locates story elements by testID using
    @react-native-windows/automation but makes no reference to any theme button
    (storybook-smoke.test.cjs).
  • Observed. useFlexTokens currently ignores context and always returns
    defaultFlexTokens
    (packages/agentic/design/src/tokens/useFlexTokens.ts);
    this is noted as temporary in
    PR #4156
    and is the subject of Dynamic Theme Building.
  • Observed. System appearance handling is split across four platformUtils
    files and ThemeState does not expose the resolved scheme; this is the
    subject of System Appearance Handling.

Scope

  • Add a 'dynamic' toolbar option that constructs a theme using the
    'dynamic' appearance value and relies on the platform subscription defined
    in System Appearance Handling to re-render when the
    system appearance changes.
  • Add a Flex theme toolbar option that wraps stories with the Flex-value context
    provider defined in Dynamic Theme Building,
    supplying a minimal Flex-authored token set so the full context path is
    exercised without a Fluent Theme present.
  • Replace the literal hex values in the toolbar StyleSheet with values sourced
    from the Flex token set. The toolbar chrome must remain functional when no
    theme is selected (the 'none' case), so it requires a stable base token set
    that does not depend on a story-level ThemeProvider.
  • Add unit tests (Jest, host environment) that render StorybookThemeHost and
    assert: each button sets the expected context value, StorybookThemeProvider
    wraps with ThemeProvider only when a theme is selected, and selecting the
    Flex option activates the Flex-value context instead of ThemeProvider.
  • Extend the Windows smoke test to locate and activate at least the light and
    dark toolbar buttons by their stable testID values and confirm the toolbar
    is present and interactive after each selection.
  • Document the macOS validation steps: verifying that selecting 'dynamic'
    with macOS system appearance toggled between light and dark re-renders the
    story with updated tokens.
  • Provide a deterministic CI strategy so 'dynamic' appearance selections do
    not make snapshot or token-equality tests dependent on the host machine's
    system appearance setting.

Out of scope

  • Implementing 'dynamic' appearance resolution and the platform subscription
    mechanism; see System Appearance Handling.
  • Defining the Flex-value context type and making useFlexTokens context-aware;
    see Dynamic Theme Building.
  • Generating default Flex token values or adding appearance-specific token sets;
    see Default Values Codegen.
  • Building a macOS or Windows platform theme against the Flex contract; see
    Apple Theme.

Deliverables

  1. An extended themeChoiceNames tuple that includes 'dynamic' and a Flex
    theme option, with corresponding themeChoices entries.
  2. A token-sourced toolbar StyleSheet that reads chrome colors from the Flex
    token set, with a documented fallback for the 'none' case.
  3. A Flex-value context provider path in StorybookThemeProvider that activates
    when the Flex theme option is selected.
  4. Unit tests covering all toolbar choices, context wrapping, and the Flex
    context path.
  5. An extended Windows smoke test that exercises the theme toolbar buttons.
  6. A macOS validation note documenting the manual appearance-toggle verification
    steps and the expected re-render behavior.
  7. A CI determinism note or implementation specifying how 'dynamic' is handled
    in tests (for example, forcing a fixed system appearance in Jest or skipping
    appearance-dependent assertions).
  8. Changesets.

Acceptance criteria

  • The toolbar exposes a 'dynamic' option that activates a theme constructed
    with appearance: 'dynamic', and a system appearance change on macOS
    re-renders the story without a crash.
  • The toolbar exposes a Flex theme option that supplies tokens through the
    Flex-value context defined in Dynamic Theme Building
    without constructing a Fluent Theme.
  • The toolbar StyleSheet contains no hex literals; all chrome colors are
    derived from the Flex token set, and the 'none' case renders the toolbar
    with a stable default without accessing an undefined token.
  • Existing testID values (agentic-storybook-theme-toolbar,
    agentic-storybook-theme-none, agentic-storybook-theme-light,
    agentic-storybook-theme-dark, agentic-storybook-theme-highContrast,
    plus any new entries) remain stable and are findable by Windows UIA and
    macOS accessibility.
  • Unit tests pass: each toolbar choice produces the correct context value,
    StorybookThemeProvider wraps with ThemeProvider for Fluent choices and
    with the Flex context provider for the Flex choice, and 'none' renders
    children without either.
  • The Windows smoke test locates the theme toolbar, activates the light and
    dark buttons, and confirms the toolbar element remains displayed after each
    selection.
  • CI test runs do not fail or produce different results depending on the host
    machine's system appearance setting.
  • yarn build, yarn lage test, and yarn lage lint pass at the
    repository root, and changesets are present.

Dependencies and ordering

Depends on Dynamic Theme Building (Stage 1) for
the Flex-value context type and the context-aware useFlexTokens. The Flex
theme option cannot be wired until that context exists and a minimal Flex token
set can be supplied.

Depends on System Appearance Handling (Stage 2) for
the 'dynamic' appearance value to be reliably resolved into a concrete scheme.
The dynamic toolbar option can be scaffolded before that task lands, but its
subscription behavior and the macOS and Windows validation steps cannot be
completed until the structured appearance model is in place.

Should follow Package Consolidation so the Flex
context type and the toolbar's token imports resolve from their final submodule
paths in @fluentui-react-native/design.

Risks and open decisions

  • Open decision. Which Flex token set the toolbar chrome uses when 'none'
    is selected. Observed: defaultFlexTokens is always available as the
    module-level default in defaultTokens.ts, so it is the natural fallback.
    The owner should confirm whether the chrome should always render with the
    default Flex tokens or should follow whatever the selected theme supplies.
  • Open decision. Whether the Flex toolbar option uses the generated default
    Flex tokens or a small authored override set. Inferred: using the generated
    defaults keeps the option stateless but exercises less of the override path;
    a small authored set with a visible brand color change would make the Flex
    context visually distinguishable from the existing 'light' choice. This
    interacts with Default Values Codegen.
  • Open decision. Whether 'dynamic' should be disabled or hidden on
    platforms where the system appearance subscription is not available.
    Observed: getCurrentAppearance on the default platform falls back to
    the defaultAppearance value, so a 'dynamic' theme is safe to construct on
    all platforms but may not respond to system changes everywhere.
  • Open decision. How to make CI deterministic for the 'dynamic' option.
    Options include: (a) mock Appearance.getColorScheme to return 'light' in
    Jest, (b) exclude 'dynamic' from snapshot tests and cover it only with
    interaction tests that do not assert token values, or (c) use a controlled
    ThemeReference that forces a fixed appearance in test environments. The
    chosen approach must be documented in the test file.
  • Risk. Token-sourced chrome requires useFlexTokens (or useThemeState)
    to be callable outside the story-level ThemeProvider. If the chrome hook is
    called from StorybookThemeHost before any ThemeProvider is mounted, it
    must safely return the default token set; the current context-free
    implementation in useFlexTokens.ts already does this, but the updated
    context-aware version must preserve that fallback.
  • Risk. The toolbar sits outside StorybookThemeProvider, which is the
    story-level decorator. Adding a Flex context provider there does not affect the
    toolbar, so the two providers are structurally separate. A design where the
    chrome uses story-level tokens (the Flex choice) would require re-ordering the
    provider hierarchy, which may affect story rendering.

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