feat(react-windmod-preview): a Tailwind v4 + CSS Modules styling layer for Fluent's headless components - #36656
Conversation
…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
…omments 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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
|
Updated in 937888a after merging current upstream and the reviewed fixes from #36666, #36667, and #36670.
Validation on Node 22.12.0:
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. |
|
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:
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. |
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-componentsconsumers.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
@fluentui/react-tailwind-theme-preview@fluentui/react-windmod-preview@fluentui/postcss-tailwind-css-modulesThe 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
Omitstable both-edgesgutters after reviewThis 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
TeachingPopoverCarouselFooterdrops thelayoutprop and the slot order it drives, so the offset footer's DOM and tab order cannot be reproduced through its renderer #36684 asks whether the headless Carousel layout omission was intentional. The question remains open; the current documented Windmod behavior is retained.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.