resolveFluentSpinnerStyle function

FluentSpinnerStyle resolveFluentSpinnerStyle(
  1. FluentSpinnerState state,
  2. FluentThemeData theme
)

Resolves the default style for state against theme.

The second of the three-function recomposition contract, and the only one that reads the design axes. Every colour comes from a Fluent token; nothing here computes one.

Token sources are the Figma Spinner and .SpinnerBase component sets, extracted into test/fixtures/spinner.json and test/fixtures/spinner_base.json and asserted variant-by-variant in the tests.

Implementation

FluentSpinnerStyle resolveFluentSpinnerStyle(
  FluentSpinnerState state,
  FluentThemeData theme,
) {
  final c = theme.colors;

  // Figma binds these three on the Track ellipse, the Tail ellipse and the
  // label TEXT of each Spinner variant.
  final (track, indicator, label) = switch (state.appearance) {
    FluentSpinnerAppearance.primary => (
      c.brandStroke2Contrast,
      c.brandStroke1,
      c.neutralForeground1,
    ),
    FluentSpinnerAppearance.subtle => (
      c.neutralStrokeAlpha2,
      c.neutralStrokeOnBrand2,
      c.neutralForegroundStaticInverted,
    ),
  };

  // Geometry, verbatim from the Figma .SpinnerBase set. Thickness there is
  // authored as an integer-percent `innerRadius`, so the rendered values land
  // 0.04 either side of the stroke ramp; the ramp stop is what ships. See
  // doc/token-divergences.md for extraTiny, the one that misses by more.
  final (diameter, strokeWidth, textStyle) = switch (state.size) {
    FluentSpinnerSize.extraTiny => (
      16.0,
      FluentStroke.thick,
      theme.typography.body1,
    ),
    FluentSpinnerSize.tiny => (
      20.0,
      FluentStroke.thick,
      theme.typography.body1,
    ),
    FluentSpinnerSize.extraSmall => (
      24.0,
      FluentStroke.thick,
      theme.typography.body1,
    ),
    FluentSpinnerSize.small => (
      28.0,
      FluentStroke.thick,
      theme.typography.body1,
    ),
    FluentSpinnerSize.medium => (
      32.0,
      FluentStroke.thicker,
      theme.typography.subtitle2,
    ),
    FluentSpinnerSize.large => (
      36.0,
      FluentStroke.thicker,
      theme.typography.subtitle2,
    ),
    FluentSpinnerSize.extraLarge => (
      40.0,
      FluentStroke.thicker,
      theme.typography.subtitle2,
    ),
    FluentSpinnerSize.huge => (
      44.0,
      FluentStroke.thickest,
      theme.typography.subtitle1,
    ),
  };

  return FluentSpinnerStyle(
    trackColor: WidgetStatePropertyAll<Color?>(track),
    indicatorColor: WidgetStatePropertyAll<Color?>(indicator),
    labelColor: WidgetStatePropertyAll<Color?>(label),
    strokeWidth: WidgetStatePropertyAll<double?>(strokeWidth),
    diameter: WidgetStatePropertyAll<double?>(diameter),
    textStyle: WidgetStatePropertyAll<TextStyle?>(textStyle),
    // Figma expresses the gap as padding on the Label frame rather than as
    // auto-layout item spacing, bound to `Spacing/Horizontal/S` on every one of
    // the 64 variants — the vertical layouts included.
    gap: const WidgetStatePropertyAll<double?>(FluentSpacing.s),
  );
}