Skip to content

feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components - #36656

Draft
Ray Knight (ArrayKnight) wants to merge 323 commits into
microsoft:masterfrom
ArrayKnight:styling/react-windmod
Draft

feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components#36656
Ray Knight (ArrayKnight) wants to merge 323 commits into
microsoft:masterfrom
ArrayKnight:styling/react-windmod

Conversation

@ArrayKnight

@ArrayKnight Ray Knight (ArrayKnight) commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What this PR proposes

A Tailwind v4 + CSS Modules styling layer for Fluent's headless components, offered for official support and discussion of a sustainable landing plan. It adds preview packages without changing the styling system shipped by existing @fluentui/react-components consumers.

Following the maintainer feedback about review size, the independent fixes have been split into their own PRs. The remaining decision is whether the styling layer should be submitted in smaller in-tree pieces or maintained as a community package. The follow-up comments describe those options.

What ships

Package Role
@fluentui/react-tailwind-theme-preview Fluent tokens, seven theme stylesheets, Tailwind variants, cascade layers, and density scaling
@fluentui/react-windmod-preview Styled headless components across 52 family subpaths; 133 authored CSS Modules; per-component CSS delivery
@fluentui/postcss-tailwind-css-modules Public PostCSS plugin that preserves Tailwind group/peer markers under CSS Modules; used by this build pipeline and Storybook

The styling packages include a 63-entry migration guide and a shipped authoring skill. Checkbox, Radio, and Switch use a grid to align the indicator with the first label line, including wrapping labels. The CSS layer and scaling contracts are documented with the packages.

Independent fixes and current status

PR Fix Status
#36663 Breadcrumb distributive Omit Merged
#36664 ColorPicker zero-channel fallback Merged
#36665 Headless popover attribute spread order Merged
#36666 Dialog scrollbar gutter Open; preserves host stable both-edges gutters after review
#36667 TagPicker aside-width animation frame Open; ref-detach cleanup, valid zero handles, cancellation-aware React 18 verification
#36668 Tooltip arrow class Merged
#36669 MenuSplitGroup context values Merged
#36670 Headless context exports Open; merge conflict resolved and API reports regenerated
#36671 Storybook sandbox registration on Windows Open
#36672 API subpath generation on Windows Open
#36673 Preservation of declared static export subpaths Open
#36690 NavCategoryItem decorative icon accessibility Merged

This branch incorporates current upstream and the reviewed Dialog, TagPicker, and context-export fixes. The four StackShim compiler tests were leftovers from the earlier whole-library conversion: they referenced a CSS module that is absent from this PR. That obsolete test group is removed. The existing v8-to-v9 StackShim implementation remains intact. The PostCSS declarations now match the callable CommonJS export, with type fixtures for CommonJS and ESM consumers.

Verification and evidence

The September 7 full sweep passes all 93 scenes: 67 strict-zero / 26 within individually ratified allowances / 0 failures. The registry retains its existing 27 allowance rows; one of those rows rendered at strict zero in this run. Pixelmatch uses threshold zero with its antialiasing classifier enabled; this is not a claim of byte-identical screenshots. All 163 Windmod test suites / 3,642 tests pass. Build, lint, type-check, and detailed verification limits are recorded in the update comment below.

The three TagPicker frame regressions pass on React/ReactDOM 18.3.1 and React 19.2.0. On React 18, reverting to passive-effect cleanup fails the StrictMode test; reverting the explicit null check fails the zero-handle unmount test. Dialog preservation tests cover inline and stylesheet-provided gutters.

The bundle and runtime tables in the deep-dive comment are historical measurements, not fresh measurements of this head. The runtime benchmark predates per-component CSS delivery and used the monolithic stylesheet; its heap metric covers JavaScript, excluding CSSOM. Griffel was faster on the measured re-render case. The deep dive retains the methodology and rationale alongside those limits.

Remaining dependencies and decisions

Related issues

#36645, #36646, #36647, #36648, #36649, #36650, #36651, #36652, #36653, #36654, #36655, #36685.

The individual fix PRs own issue closure. The larger argument, historical measurements, and detailed findings remain in the linked deep dive.

Ray Knight (ArrayKnight) and others added 30 commits August 23, 2026 21:04
…avDivider (headless + Tailwind, pixel-identical to Griffel)

Nav's root reduces to Griffel's verbatim flex column now that FluentProvider
authors the shared typography and colour globals. The row presentation that
NavItem shares with the category components ships as a plain pure function of
state rather than a hook, so future consumers compose it without hook-order
constraints. The selected indicator is pure CSS. Density folds into state and
reaches descendants through the headless context; the small-density selector
lives in the library's variant catalog.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1,010,880 px), 13 planned + 28 review mutations all accounted for, and
byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…mbDivider and BreadcrumbButton (headless + Tailwind, pixel-identical to Griffel)

The root ships classless now that FluentProvider authors the shared
typography and colour globals; the divider's chevron inherits its colour from
the provider. The button composes the shipped Button and swaps the current
item's glyph back with a plain layered rule — possible only because the icons
package no longer marks its declarations important. The theme catalog gains a
disabled-native variant: the current-item override keys the native :disabled
pseudo-class alone, which anchors can never match, while the broad disabled
variant would also match the data-disabled attribute stamped on anchors.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate across nine bands including RTL dividers and the anchor path, plus six
composed-button seam scenes at zero. 20 of 21 planned mutations killed (the
survivor proven a semantic no-op), 9 of 11 review mutations killed with both
survivors type-check-owned, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…Item and NavSubItemGroup (headless + Tailwind, pixel-identical to Griffel)

The category components complete the Nav family, reusing the shared row
presentation and identity marker the first four components established. The
expand chevron rotates through plain CSS declarations on the icon-slot class,
so consumer-supplied icons rotate identically, and the open group ships
Griffel's compiled overflow verbatim. The collapse enter/exit motion is not
ported; its end state leaves a Griffel group able to scroll overflowing
content where this implementation clips — recorded with the motion delta in
the migration notes, invisible while content fits.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x2015 scene, Griffel-vs-Griffel control zero), 23 planned + 12 CSS
mutations accounted for, and byte-idempotent API reports across all 60 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ch, ImageSwatch and SwatchPickerRow (headless + Tailwind, pixel-identical to Griffel)

The five-component family ships with the root unpinned — the provider already
authors a superset of the typography and colour the plan expected to pin. The
theme catalog gains a layout-grid variant, and the library catalog drops five
entries no stylesheet ever referenced, proven inert in both compiled sheets.
The disabled swatch's glyph honours consumer children and slot-null removal.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x684), with the full-catalog sweep as the shared-variant guard.
75 of 76 mutations killed across the planned and review tables (the survivor
a proven semantic no-op pinned by an invariant test), and byte-idempotent API
reports. Consumer style is silently discarded by the headless picker and row
base hooks on both libraries; the specs assert that parity rather than mask it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
… sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…t sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…t sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ilies to flat sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…lat sibling directories

Part of mirroring the Griffel packages' flat component layout; no public
subpath, export name, or pixel changes (API reports byte-identical, full
visual sweep held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…el-identical to Griffel)

The increment and decrement glyphs follow the uniform slot-fallback rule —
both slots always materialise, so consumer children and slot-null removal
compose without a special case. The active-step visual selects on a
presence-based data attribute stamped per button, derived locally because the
headless hook only exposes the keyboard half of its spin state. The root
ships without a typography pin, matching a Griffel reset that authors no font
declarations, while the small size restates its full caption set so nested
typography scopes inherit identically.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x848, Griffel-vs-Griffel control zero), 35 mutations killed across
the planned and review tables, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…l-identical to Griffel)

SearchBox composes the shipped Input conventions: the magnifier and dismiss
glyphs follow the uniform slot-fallback rule, restored together in one
immutable state object since both slots always materialise, with consumer
children and slot-null removal independent per slot. Content-presence stamps
follow the ratified spelling and remain Input's consumer contract even where
the composed block re-writes the padding they gate. The dismiss interaction
clears through a single change event and returns focus to the input, matching
the reference behaviour exactly.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (0 of 1,457,664 pixels across 74 cells including the focused underline),
30 mutations killed across the planned and review tables with one proven
equivalent, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…r and ColorArea (headless + Tailwind, pixel-identical to Griffel)

The transparency checkerboard ships as an inlined data URI proven
byte-identical to the CDN asset the Griffel implementation fetches, so the
alpha rail and area paint without a network request — including offline and
under restrictive content-security policies, a divergence the migration notes
record. Channel-specific styling selects on an enumerated data attribute pair
added to the library catalog; the sliders' gradients, thumb geometry and
right-to-left mirroring reproduce the compiled reference buckets, with the
mirrored rails covered by their own visual band.

Verified pixel-identical to the Griffel implementation at a strict zero-diff
gate (1248x2320 over 58 cells), 42 mutations killed across the planned and
review tables with a single-bit checkerboard flip proven caught by the visual
gate alone, and byte-idempotent API reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng for Button and ToggleButton

The reference stylesheet emits every forced-colors media rule after all other
buckets, so those declarations win any equal-specificity contest. This
package carries the same cascade with source order inside a layer, and its
forced-colors blocks sat early enough for later appearance, state and checked
blocks to beat them — leaving checked-subtle surfaces on authored greys where
the reference shows Highlight on HighlightText, and diverging under hover,
press and focus. Every forced-colors block now trails its class and restates
what it must win back, in cascade order only: no importance, no new variants.

Verified by computed-style probes under forced-colors emulation across 457
button and toggle-button cells in four interaction states: 610 divergences
before, zero after, zero newly-broken, with a reference-vs-reference control
of zero and the baseline reproduced exactly on revert. The normal-mode
visual sweep holds every scene at its gate, so the change is invisible
outside forced-colors. Seventeen residual cells belong to the toolbar layer's
own modules and are recorded for their own fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…e containers

A component rendered inside a providing container now reads the same look
contexts the reference implementation does: Button, CompoundButton,
MenuButton and ToggleButton take their size from ButtonContext, Link its
inline flag from LinkContext, Avatar its shape and size from AvatarContext,
and the field controls their size from FieldContext — narrowed to the look
key, since the base hooks already apply the aria half. Tag derives the
avatar shape and size its children consume. The contexts re-export from the
headless package so the provider instances connect; one shared helper folds
context into props ahead of destructuring, keeping local values authoritative
and letting context beat only the defaults.

Verified by container scenes that previously pinned these values and now
adjudicate them — message-bar and tag hold strict zero against the reference
and fail measurably when any fold is severed. 29 mutations caught, the full
visual sweep holds every scene, and the API report deltas are additive only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng in the toolbar button variants

The toolbar's layer sits above the shared button layers, so its plain
declarations beat the forced-colors blocks below: the checked-subtle glyph
kept a brand hover colour the system palette replaces arbitrarily, and the
checked rest border swallowed the focus border colour. Both variants gain
trailing forced-colors re-asserts — the glyph takes the system Highlight
under hover and press, the checked root takes HighlightText border colour
under focus — repeating only what their resting blocks win back.

Verified by computed-style probes under forced-colors emulation across all
75 toolbar cells in four interaction states, including a widened focus walk
that reaches past the reference toolbar's roving tabindex: 22 divergences
before, zero after, zero newly broken, with the shared button layers
unmoved, a reference-vs-reference control at zero, and the baseline
reproduced exactly on revert. The normal-mode visual sweep holds every
scene, so the change is invisible outside forced-colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ng for MenuButton and SplitButton

The open menu button's selected background, its focus border, and its
disabled colours all sat in blocks that outrank the shared button layer's
forced-colors rules, and the split button's divider edge declared a logical
border longhand that beats the physical focus shorthand below it — four
faces of the same cascade contest the reference resolves by emitting every
forced-colors rule last. Both modules gain trailing forced-colors re-asserts
inside the winning blocks, including the disabled colour crossing whose text
colour also restores the chevron glyph fill through currentColor.

Verified by computed-style probes under forced-colors emulation across all
250 menu-button and split-button cells with a widened focus walk: 106
divergences before, zero after, zero newly broken across every probed scene
and phase, with a reference-vs-reference control at zero and the baseline
reproduced exactly on revert. The survey now covers every module in the
package's upper layers; the two remaining divergences trace to reference
bugs and are recorded, not copied. The normal-mode visual sweep holds every
scene, so the change is invisible outside forced-colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…lass-map keys to camelCase

The named font-size tokens across all seven themes become calc products of
the shared base-scale ratio, so text now scales with the root font size the
way spacing and stroke widths already do — identical at the default 16px
root. Icon glyph sizes gain their own named tokens. The CSS-module class-map
serializer and the storybook loader both export each kebab-case local under
a camelCase alias so styles hooks keep dot access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ponents A-C

Arbitrary-property utilities become plain declarations, class-map locals go
kebab-case behind the camelCase aliases, multi-branch ternaries flatten to
if-returns, single-use props destructure in the parameter list with state
literals inlined, and transition-property adopts the reference shorthands.
No public API, ident-independent pixel, or behaviour change; the full visual
sweep holds every scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…g needs re-ratification)

Same grid-first-line model as Checkbox/Radio, extended to Switch's third
label position (above, in addition to before/after) and its size axis
(medium/small, which also changes the label's own font metrics -- unlike
Checkbox/Radio, Switch's 1lh row is genuinely size-dependent).

- Root mirrors the label's typography per size: base `text-base-300
  leading-20/14`, `@variant size-small { text-base-200 leading-16/12; }` --
  needed for real, not cosmetic: 1lh must equal 20px at medium and 16px at
  small, matching the indicator's own h-20/h-16 exactly at both sizes (no
  escalation case at either size).
- `px-horizontal-s` and `gap-x-horizontal-m` are constant across size AND
  position (measured: indicator's own mx and the label's own near-side
  padding never had a size-conditional value, so the 12px gap and 8px
  outer footprint hold everywhere). `py-vertical-s` (8px) is likewise
  constant across size for before/after -- unlike Checkbox, indicator's
  own vertical margin (vertical-s) was ALREADY the exact half-height
  delta at both sizes by construction, so no per-size override is needed.
- `label-above` is a documented per-variant override, structurally
  different from Radio's label-below: label takes row 1 alone (its
  `row-[1/span_2]` base narrows to `row-[1]`), the indicator moves to
  row 2 via its existing `.label-above` conditional class (already
  applied only in that case per useSwitchStyles -- repurposed from a
  `mt-0` margin reset to a `row-[2]` placement), asymmetric root padding
  (`pt-vertical-xs` / `pb-vertical-s`, matching the old label's own
  distinct top-padding value vs the indicator's own unchanged bottom
  margin), and `gap-y-vertical-xs` (4px, the old label's own bottom
  padding plus the indicator's now-zeroed top margin). No justify-items
  override -- explicit-width children default to start-aligned in a
  stretch track, reproducing the old `items-start` for free.
- `.input`/`.anchored` (the absolutely-positioned hit target) needed no
  changes: inset properties resolve against the padding box, which does
  not move when a padding VALUE changes, so the anchoring math is
  unaffected by root's flex-to-grid switch.

This deletes `--fui-switch-label-offset`... except that offset was
already deleted from a prior session (the comment explaining why
items-center didn't need it); this change removes the `items-center`/
`flex-col items-start` cross-axis logic it referred to.

MEASURED, NOT YET RATIFIED: `run.mjs --scene switch` reports 3132
strict-diff pixels (stable across 3 runs) against the currently-ratified
`allowedStrictDiff: 1664` (Decision X, layoutunit-1/64 class), and needs
`heightTolerance: 1` (1771px vs Griffel's 1772px, the same mechanism as
the ratified `persona` row) to get past the dimension check at all.

Root-caused with a control: swapping in the pre-grid CSS against the
same scene reproduces the ratified 1664 EXACTLY, with matching
dimensions and no height tolerance needed -- proving the pre-grid root's
height (flex-computed from the indicator's own integer h-20/h-16, never
touching a leading value) carried NONE of this residue at the root
level. The grid root's own height computation, mandated by spec
("Row 1 = 1lh"), newly runs `1lh` through `leading-20/14`'s already-
documented "N - 1/64px, any directly-evaluated spelling" behavior
(authoring-conventions.md), compounding with the label's own pre-existing
line-box residue rather than replacing it -- the same "compounding"
language already used for several Decision X ceiling raises in
scenes.json (menu, menu-rtl, menu-sequel, teaching-popover-placements).

Fully decomposed, zero unattributed pixels: two switch-thumb pill
silhouettes (1062px medium, 828px small -- edge-AA from a sub-pixel
vertical shift of the whole rounded shape, tapering to near-zero at the
flat mid-sides and peaking at the rounded caps, the signature of a pure
sub-pixel translation) + one 42px cluster + one 1200px near-full-width
single-row diff (a thin element boundary at a device-pixel-different
row, the same "row-edge, thin border-line" mechanism the `divider` row
already carries this ceiling class for). 1062+828+42+1200 = 3132 exactly.
Direct measurement confirms each instance carries exactly one 1/64px
shortfall in its own height (19.984375/15.984375 against the 20/16
nominal 1lh; the indicator's own integer-valued second grid row is
untouched), consistent with compounding across the many stacked
instances in this scene rather than a per-instance logic error.

Per the allowance protocol (authoring-conventions.md), a ceiling raise is
a reviewer's call, not the implementer's -- this is reported, not
self-granted. The .scratch/windmod-vr/scenes.json harness config (a
never-committed local file) still carries the 1664 ceiling; a
heightTolerance:1 + allowedStrictDiff>=3132 update there is the operator
action this finding recommends.

Gates: build, type-check, lint, test (3605/3605) all green for
react-windmod-preview (all four are independent of the VR ceiling).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
… grid-hosted control indicators

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…io and Switch — spacing owned by the grid, no offset math

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…corated with Badge's visual contract

Composes windmod's useBadgeStyles (Button-family pattern, l2-over-l1 layering) and adds the dot
(forces a 6px circle regardless of size) and hidden (empty, no-dot) presentations. Look props
(appearance/color/shape/size) live in windmod with Griffel's defaults; count/dot/overflowCount/
showZero stay headless state, already stamped as data-count/-dot/-hidden/-overflowed. VR scene
(sizes x appearances x colors x dot/overflow) verified strict-zero against Griffel's CounterBadge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…th per-size Fluent glyphs

Standalone component (not composed over Badge's styles — Griffel's own PresenceBadge is a
bespoke visual, not a Badge-appearance skin). Look prop is size only; status/outOfOffice stay
headless state, already stamped as data-status/-out-of-office. windmod restores the default
glyph itself (headless ships an empty icon slot) by mirroring @fluentui/react-badge's
presenceIcons status/outOfOffice/size selection with the matching react-icons/headless assets,
including its two reused-pixel-size cases (tiny reuses the 10px asset, extra-large the 20px
one) so the CSS forces only those two to their own footprint. VR scene (statuses x outOfOffice
x sizes) verified strict-zero against Griffel's PresenceBadge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…d, drop session labels and local-tooling pointers

Linkage: SKILL.md now routes line-height questions to tokens-and-scale.md
(NEVER bullet, token routing block, description triggers); README drops
the nonexistent AGENTS.md and lists failure-modes.md.

Facts re-derived from the tree: 63 deltas (was "fifty-nine"; MIGRATION.md's
own summary line too), 133 components, two l3 modules, 68 component
variants; griffel-deltas row 59 described --leading-* reads of tokens that
no longer exist and gains rows 60/63; troubleshooting defers its token
total to css-var-values.md's table.

Stale mentions: every Tailwind-contrast and Griffel-contrast note verified
current and kept. Eight passages carried this campaign's working-session
labels (AR2, O-23, phase-2A, Decision X, lane/landing, "campaign
contract") — rewritten in present tense. Every pointer at gitignored local
tooling (guard.mjs, lineheight-diff2.mjs, scenes.json, report.json,
MORNING-DECISIONS.md) is replaced by the requirement it enforced, so the
contributor half of the skill holds for a clone without that harness. The
"merge drops new VR scenes" row moves back inside the failure-modes table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ent-compatible and how to opt in

The package README gets a "Coding agents" section up front: the skill ships in
the tarball, the package carries the tanstack-intent keyword, and two Intent
commands bring the skill along with the package. The skill README's
Installation section lists Intent as the first of three opt-in routes (Intent,
config pointer, symlink), with the hooks step as optional.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…dation

Intent caps SKILL.md at 500 lines; it was 557. Three sections whose full
content already lives in references collapse to routed pointers — imports and
subpaths (setup.md), the CSS-var hook (css-var-values.md), the two worked
"common issues" (troubleshooting.md) — and the setup section drops its
duplicate checklist and compresses the import-order explanation.

Frontmatter gains Intent's optional fields: metadata.type "core" (a framework
type would require a core skill to depend on) and top-level sources listing
MIGRATION.md, the component modules, the variant catalog, and the theme
package's CSS and generator, so `intent stale` can flag drift.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…-list, setup, routing table

SKILL.md drops from 483 to 118 lines. It now carries only what every load
needs: the model in six lines, the never-list at one line each, the minimal
two-stylesheets-one-provider setup, and a table of which reference to load for
which task with the trigger words for each. Everything explanatory moved to
the reference it belongs to, verified fact by fact against the previous root:
overriding.md gains the per-layer stack, the !important rule, the Button
data-attribute example and the class-name-record exception; tokens-and-scale.md
gains the theme class-name list and the leading pairing rule; griffel-deltas.md
gains the parity-contract statement. TanStack Intent validation passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
… DevTools-first diagnosis

Evaluation of the lean root against the pre-rewrite snapshot (three consumer
tasks, deterministic assertions) tied on correctness but showed one routing
regression: the JSX form of ScaleRegion had dropped out of the root and no
reference showed it, so the scale task hedged on the prop name and loaded six
files where the old root's run loaded two. The root bullet now reads
`<ScaleRegion scale={1.25}>` and tokens-and-scale.md carries the import and
usage. overriding.md gains the DevTools-first paragraph for "override not
applying", which both old and new runs failed to surface from the checklist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…e, p-4 gotcha, after-porting checklist

setup.md gains the section a consumer authoring CSS Modules against windmod
needs: a composed reference target and the #theme subpath-import recipe the
library itself uses, why named group/peer markers must be :global()-wrapped
between Tailwind and postcss-modules (with a minimal plugin), prettier's
tailwindStylesheet for class sorting, and a pre-flight list. tokens-and-scale.md
gets the pixel values of the named spacing steps; troubleshooting.md the
"p-4 is 4px" symptom (numeric axis is pixels on the density knob);
griffel-deltas.md an after-porting checklist and self-check matrix keyed to
delta numbers. Root routing row names the new triggers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
… not only the aggregate

The root's setup block and setup.md's pre-flight list named only the all-seven
styles.css; setup.md's own recommended path (and the package README) is the
theme-less base.css plus one themes/<name>.css per theme shipped, with the
aggregate as the fallback. Both now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ins the single-@apply rule

The consumer CSS Modules section no longer prescribes prettier: any Tailwind
class sorter (prettier-plugin-tailwindcss, oxfmt, Biome) works, pointed at the
same reference target the modules use. The paragraph now carries the why — a
sorter guarantees canonical order only within one @apply list, so two lists in
a rule hide which declaration wins — which is the reason for one @apply per
block position.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
…ilwind group/peer markers under CSS Modules

Extracts scripts/css-modules/globalize-group-markers.js into
@fluentui/postcss-tailwind-css-modules (packages/react-components, plain
CommonJS with a hand-written .d.ts, no build step) so consumers who author CSS
Modules against windmod can install the same step the library's own build
runs: Tailwind emits `.group\/name` / `.peer\/name`, postcss-modules would
hash them into selectors the DOM never matches, and this plugin wraps them in
:global() in between. Adds an `include` filter (default `.module.css`; `true`
to disable; an undefined `from` is left untouched unless disabled), keeps the
text-level idempotence, `onRewrite` and `globalizeSelector` exports, ships a
README with the ordering rule and the Vite note, and a 24-case spec.

The build executor and the storybook rules now require the package; the old
script is deleted. The windmod skill's setup reference recommends the package
(with @accelint/postcss-tailwind-css-modules noted as the equivalent
third-party plugin) instead of an inline copy.

Verified: package jest 24/24; react-windmod-preview build green with no
marker-leak assertion; lint green; static VR storybook build and the Button
scene at 0 strict-diff pixels through the new require path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated in 937888a after merging current upstream and the reviewed fixes from #36666, #36667, and #36670.

  • Resolved the main and fix(react-headless-components-preview): publish context readers from the button and link subpaths and context providers from avatar and avatar-group #36670 merge conflicts. The Avatar report retains upstream state changes and the requested context exports; MenuSplitGroup uses the upstream exported context-values hook.
  • Addressed the new TagPicker review: the mock flushes only uncancelled frames and tests handle 0. The three frame regressions pass with React/ReactDOM 18.3.1 and 19.2.0. On React 18, reverting to passive-effect cleanup fails the StrictMode test; reverting the explicit null check fails the zero-handle unmount test. fix(react-tag-picker): tie aside-width frame cancellation to observer detach #36667 includes the React 18 integration-test config. Local Windows verification required corrections to the generated RIT scaffold's paths and Jest launcher, documented in that PR's reply.
  • Addressed the new Dialog review: preserve an existing computed stable or stable both-edges gutter, including stylesheet-provided values, during locking and after unlocking. All 18 Dialog tests pass; restoring the unconditional fallback fails the preservation tests.
  • Removed four obsolete StackShim compiler tests. They came from the earlier whole-library CSS conversion and entered this PR with the build-infrastructure port, but their StackShim.module.css never did. The existing compatibility component and its Griffel StackShim.styles.ts remain intact. These four failures were introduced by this PR, rather than being upstream test debt.
  • Corrected the PostCSS plugin's declarations to match its callable CommonJS export. Strict NodeNext fixtures exercise CommonJS and ESM consumers; the previous declaration fails those call-site checks. The Storybook type boundary also accounts for the two resolved PostCSS versions. Updated the migration guide to describe the current first-line grid and existing visual allowances.

Validation on Node 22.12.0:

Check Result
Theme generation/verifiers; headless and Windmod builds Pass
Windmod and stories lint/type-check Pass, with an existing unrelated stories lint warning
Windmod unit tests 163 suites / 3,642 tests pass
Headless unit tests on #36670 79 suites / 1,083 tests pass
PostCSS runtime tests and CommonJS/ESM type fixtures 24 tests pass; type-check passes
Remaining CSS Modules compiler tests 34 pass
Static Storybook and full visual sweep 93 scenes: 67 strict-zero, 26 within ratified allowances, 0 failures
Workspace sync, immutable install, formatting, change-file check Pass

The visual registry still has 27 allowance rows; one rendered at strict zero in this run. No ceilings were raised for this follow-up. Historical bundle/runtime measurements in the deep dive have not been rerun.

Audit follow-up: the broader workspace-plugin suite still has 65 local failures, including Windows path assertions and output/snapshot mismatches. A controlled comparison with the original StackShim test group gives 69 failures; removing it gives 65, with exactly those four missing-file failures disappearing and all other failures unchanged. This establishes the cleanup's effect, not that the whole build-tool suite is green or that all 65 failures predate this PR. Those remaining failures need separate triage. The broader TagPicker default run also encountered an unrelated icon snapshot difference with the local icons dependency; no snapshot was updated.

The PR remains draft pending microsoft/fluentui-system-icons#1228 and a published dependency containing the icon stamp. The Carousel question in #36684 remains held for maintainer direction.

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated status and reposted below the commit history for visibility.

Thanks Dmytro Kirpa (@dmytrokirpa) — completely fair on size, and I'll take you up on the fixes offer immediately: each in-tree fix from this PR is now its own isolated PR, one per linked issue:

On the rest — before I move it to a community repo, I'd like to understand which cost is the actual blocker, because they point to different paths:

  1. If it's this PR's review surface, I can decompose much further — theme package alone first, then per-family batches, each pixel-verified independently.
  2. If it's the prospect of maintaining two styling systems indefinitely, that's the conversation I'd most value having, because the design goal was the opposite: the layer holds pixel-parity with Griffel across 93 VR scenes precisely so it can serve as a migration path — one that would let Griffel be deprecated over time and shrink the maintenance surface rather than double it. We proposed the big-bang version of this before and understood the appetite wasn't there; the preview-package shape was meant as the incremental route to the same destination. If there's any appetite for that direction, I'd gladly shape the work to whatever on-ramp the team could sustain.

If neither lands, I'll host it as a community package — in that case: would the team be open to a discoverability pointer (docs/community listing), and is the headless package's public API + DOM structure a contract you'd consider stable enough for an external styling layer to build against?

@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Updated status and reposted below the commit history for visibility.

Dmytro Kirpa (@dmytrokirpa) Following up with a proposed submission order if review size is the main concern:

  1. Theme package. Fluent tokens, seven theme stylesheets, the generator, and its checks. This can be useful independently of the component layer.
  2. PostCSS plugin and build infrastructure, with Button as the first complete example. The plugin is now a separately publishable package used by the build and Storybook paths. It could be reviewed separately from the infrastructure if that makes the first component PR easier to assess.
  3. Component-family batches. Order each batch after the families whose state selectors it depends on. Each submission would carry its build/type/lint results, visual comparisons, and relevant migration-guide entries.
  4. Remaining documentation and migration guidance. Include shared documentation with the change it explains, then finish the cross-package guidance.

The earlier sizing exercise estimated roughly 16 PRs and a five-PR critical path. That estimate predates the separately publishable PostCSS plugin and is a starting point, not a final count. I would recut the batches and sizes against the current tree once there is agreement on the approach and a review cadence the team can sustain.

The independent fixes are already separate (#36663#36673, plus #36690), and six have merged. The remaining fixes can proceed independently of the decision on Windmod.

If the main concern is maintaining a second styling system over time, the longer-term direction in my earlier comment is the decision to settle first; the community-package route remains available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants