diff --git a/.agents/skills/review-pr/SKILL.md b/.agents/skills/review-pr/SKILL.md index 818d303a28f22..f7614bd340c41 100644 --- a/.agents/skills/review-pr/SKILL.md +++ b/.agents/skills/review-pr/SKILL.md @@ -63,13 +63,15 @@ Required if any published package source code changed (not just tests/stories/do Only for files in `packages/react-components/react-*/library/src/`: -| Check | Look for | Severity | -| ------------------- | --------------------------------------------------------------------------------------------------- | -------- | -| No `React.FC` | `React.FC`, `: FC<`, `React.FunctionComponent` in added lines | BLOCKER | -| No hardcoded styles | Hex colors `#[0-9a-fA-F]{3,8}`, hardcoded `px` values for spacing/radius/font in `.styles.ts` files | WARNING | -| Griffel usage | Style files must use `makeStyles` from `@griffel/react`, not inline styles | WARNING | -| mergeClasses order | User `className` must be the LAST argument in `mergeClasses()` | WARNING | -| Slot system | New components must use `slot.always`/`slot.optional` and `assertSlots` | WARNING | +| Check | Look for | Severity | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| No `React.FC` | `React.FC`, `: FC<`, `React.FunctionComponent` in added lines | BLOCKER | +| No hardcoded styles | Hex colors `#[0-9a-fA-F]{3,8}`, hardcoded `px` values for spacing/radius/font in `.module.css` files | WARNING | +| CSS Modules usage | Styles must live in a co-located `*.module.css`, not in `makeStyles`/`makeResetStyles` or inline styles | WARNING | +| Layer discipline | Library modules author into `fui.components.l1` (or `l2` when styling another component's output); `l3`–`l5` are consumer space and must stay empty | WARNING | +| clsx order | User `className` must be the LAST argument to `clsx()`; the unconditional module class must be first, so the `group/fui-*` marker is never `classList[0]` | WARNING | +| No state mutation | Styles hooks must return a new state object, not assign to `state..className` | WARNING | +| Slot system | New components must use `slot.always`/`slot.optional` and `assertSlots` | WARNING | Reference: [docs/architecture/component-patterns.md](../../../docs/architecture/component-patterns.md) @@ -79,7 +81,8 @@ For changes to `package.json` files or new imports in Tier 3 component packages: - **BLOCKER** if a Tier 3 package (`react-button`, `react-menu`, etc.) adds a dependency on another Tier 3 package - Allowed Tier 2 deps: `react-utilities`, `react-theme`, `react-shared-contexts`, `react-tabster`, `react-positioning`, `react-portal` -- Allowed Tier 1 deps: `@griffel/react`, `@fluentui/tokens`, `@fluentui/react-jsx-runtime` +- Allowed Tier 1 deps: `@fluentui/tokens`, `@fluentui/react-jsx-runtime`, `clsx`, and + `@fluentui/react-tailwind-theme` as a **devDependency** only (build-time `#theme` alias) Reference: [docs/architecture/layers.md](../../../docs/architecture/layers.md) diff --git a/.agents/skills/triage-issues/SKILL.md b/.agents/skills/triage-issues/SKILL.md index 487e9a6ebe79d..05f4461346363 100644 --- a/.agents/skills/triage-issues/SKILL.md +++ b/.agents/skills/triage-issues/SKILL.md @@ -110,7 +110,7 @@ For each issue, read the title + body + existing labels. If repro evidence is am - `packages/react-components/react-field/` — label/description/validation composition (if the ask is form-control adornments) - The component's stories for worked examples of the composition pattern -v9 defaults to composition over configuration. An ask like "add a `shouldFadeIn` prop" usually has an answer like "compose with ``" or "use `onLoad` + `makeStyles` keyframes". Surface the v9 pattern in the comment so the reporter isn't left hanging. +v9 defaults to composition over configuration. An ask like "add a `shouldFadeIn` prop" usually has an answer like "compose with ``" or "use `onLoad` plus a `@keyframes` rule in your own stylesheet". Surface the v9 pattern in the comment so the reporter isn't left hanging. When checking for duplicates, search first: diff --git a/.agents/skills/v9-component/SKILL.md b/.agents/skills/v9-component/SKILL.md index 08f3a93fc1854..5b11238460bee 100644 --- a/.agents/skills/v9-component/SKILL.md +++ b/.agents/skills/v9-component/SKILL.md @@ -41,11 +41,27 @@ yarn nx g @fluentui/workspace-plugin:react-component --name $ARGUMENTS --project 1. **Review generated files** against [docs/architecture/component-patterns.md](../../../docs/architecture/component-patterns.md) and fill in component-specific logic. -2. **Add styles** in `use${ARGUMENTS}Styles.styles.ts` using design tokens: +2. **Add styles** in a co-located `${ARGUMENTS}.module.css`, using token custom properties: + + ```css + @reference '#theme'; + + @layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + + @layer fui.components.l1 { + .root { + @apply flex items-center; + + color: var(--colorNeutralForeground1); + } + } + ``` + + Then compose the class names in `use${ARGUMENTS}Styles.styles.ts`: ```tsx - import { makeStyles } from '@griffel/react'; - import { tokens } from '@fluentui/react-theme'; + import { clsx } from 'clsx'; + import styles from './${ARGUMENTS}.module.css'; ``` 3. **Create a default story** at the appropriate stories package location if not generated. @@ -58,8 +74,11 @@ yarn nx g @fluentui/workspace-plugin:react-component --name $ARGUMENTS --project ## Critical Rules - Always use `ForwardRefComponent` with `React.forwardRef` — never `React.FC` -- Always use design tokens from `@fluentui/react-theme` — never hardcoded colors/spacing/typography -- Always preserve user `className` as the LAST argument in `mergeClasses()` +- Always use design tokens — `var(--colorNeutralForeground1)` in CSS, the `tokens` object from + `@fluentui/react-theme` in TS — never hardcoded colors/spacing/typography +- Always preserve user `className` as the LAST argument to `clsx()` (convention; the cascade layer, + not the argument position, decides which rule wins) +- Styles hooks must **return** the composed state, never mutate the state they are handed - Use `_unstable` suffix on exported hooks: `use$ARGUMENTS_unstable`, `use${ARGUMENTS}Styles_unstable`, `render${ARGUMENTS}_unstable` - Guard any `window`/`document`/`navigator` access with `canUseDOM()` from `@fluentui/react-utilities` - Do not add dependencies on other Tier 3 component packages (see [docs/architecture/layers.md](../../../docs/architecture/layers.md)) diff --git a/.devops/templates/build-test-lint.yml b/.devops/templates/build-test-lint.yml index 1ad5296fdba16..5684a9417447e 100644 --- a/.devops/templates/build-test-lint.yml +++ b/.devops/templates/build-test-lint.yml @@ -43,13 +43,19 @@ steps: env: DANGER_GITHUB_API_TOKEN: $(DANGER_GITHUB_API_TOKEN) + # `verify-tokens-css` exists on exactly one project (@fluentui/react-tailwind-theme). It asserts + # the checked-in css/tokens.css AND css/themes.css still match what the generator produces from + # @fluentui/tokens (tokens.ts + theme-values.json), so a token added or renamed upstream — or a + # theme value changed without regenerating — cannot silently leave the Tailwind theme layer stale. + # `verify-theme-values` (@fluentui/tokens) asserts the committed theme-values.json snapshot matches + # the built theme objects, closing the loop from the values side (theming Phase 2b). - script: | - yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging --nxBail $(sinceArg) + yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging verify-tokens-css verify-theme-values --nxBail $(sinceArg) displayName: build, test, lint, test-ssr (affected) condition: eq(variables.isPR, true) - script: | - yarn nx run-many -t build test lint type-check test-ssr test-integration verify-packaging --nxBail + yarn nx run-many -t build test lint type-check test-ssr test-integration verify-packaging verify-tokens-css verify-theme-values --nxBail displayName: build, test, lint, test-ssr (all) condition: eq(variables.isPR, false) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca6ae6b05d575..390b55ace7c11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -153,7 +153,7 @@ packages/react-components/react-aria @microsoft/teams-prg packages/react-components/react-aria/library @microsoft/teams-prg packages/react-components/react-aria/stories @microsoft/teams-prg packages/react-cards @microsoft/cxe-red @khmakoto -packages/react-components/react-conformance-griffel @microsoft/teams-prg +packages/react-components/deprecated/react-conformance-griffel @microsoft/teams-prg packages/react-components/react-context-selector @microsoft/teams-prg packages/react-date-time @microsoft/cxe-red packages/react-docsite-components @microsoft/fluentui-v8-website diff --git a/.github/instructions/copilot.instructions.md b/.github/instructions/copilot.instructions.md index 0cea69b329340..8a13ee9cd2faf 100644 --- a/.github/instructions/copilot.instructions.md +++ b/.github/instructions/copilot.instructions.md @@ -24,7 +24,7 @@ This is a large Nx monorepo with the following key characteristics: - Location: `packages/react-components/` - Nx Project Tags: `vNext` - - Features: Current stable version, actively developed. Tree-shakeable, atomic CSS classes + - Features: Current stable version, actively developed. Tree-shakeable, precompiled CSS shipped as `dist/styles.css` per package 2. **Fluent UI v8** (`@fluentui/react`) - **MAINTENANCE ONLY** @@ -145,7 +145,8 @@ packages/react-components/react-component-name/ │ │ ├── index.ts # Local exports │ │ ├── renderComponentName.tsx # JSX rendering │ │ ├── useComponentName.ts # State management -│ │ └── useComponentNameStyles.styles.ts # Griffel styling +│ │ ├── ComponentName.module.css # Authored styles (CSS Module) +│ │ └── useComponentNameStyles.styles.ts # Class composition (clsx) │ ├── testing/ # Test utilities │ └── utils # Reusable utils (if needed) └── stories/src/ # Storybook documentation @@ -164,7 +165,8 @@ react-component-name/ │ │ ├── ComponentName.tsx # Main component export │ │ ├── ComponentName.types.ts # Props, State, Slots types │ │ ├── useComponentName.ts # State management hook -│ │ ├── useComponentNameStyles.styles.ts # Griffel styling +│ │ ├── ComponentName.module.css # Authored styles (CSS Module) +│ │ ├── useComponentNameStyles.styles.ts # Class composition (clsx) │ │ └── renderComponentName.tsx # JSX rendering logic │ └── index.ts # Package exports └── stories/ # Storybook stories @@ -175,7 +177,7 @@ react-component-name/ Components use three core hooks: 1. **`useComponent_unstable()`** - Processes props, slots and main component logic into normalized state -2. **`useComponentStyles_unstable()`** - Creates Griffel CSS-in-JS styling +2. **`useComponentStyles_unstable()`** - Composes the CSS-Module class names onto the state and returns the new state (never mutates it) 3. **`renderComponent_unstable()`** - Pure JSX rendering from state ### Slot System @@ -207,54 +209,81 @@ export const renderButton_unstable = (state: ButtonState) => { }; ``` -### Build-Time CSS-in-JS with Atomic Classes +### Precompiled CSS with Tailwind-flavored CSS Modules -**Critical**: v9 uses Griffel for compile-time CSS generation - styles are extracted into atomic CSS classes at build time, not runtime: +**Critical**: v9 authors styles as plain CSS in a co-located `*.module.css`, compiled at build time +into the package's `dist/styles.css`. There is no runtime style engine and no atomic-class merge +step — every conflict is settled by the CSS cascade, using one `@layer` family: + +```css +/* Button.module.css */ +@reference '#theme'; + +/* `@reference` emits nothing, so the layer order has to be restated in every module — + otherwise first-appearance order would decide the ranking. Restating it is a no-op. */ +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l1 { + .root { + @apply inline-flex items-center; + + /* Use token custom properties, not hardcoded values */ + color: var(--colorNeutralForeground1); + background-color: var(--colorNeutralBackground1); + padding: var(--spacingVerticalS) var(--spacingHorizontalM); + } + + .root:hover { + background-color: var(--colorNeutralBackground1Hover); + } + + /* Size variations */ + .small { + padding: var(--spacingVerticalXS); + } + + .large { + padding: var(--spacingVerticalL); + } +} +``` ```tsx // useButtonStyles.styles.ts -import { makeStyles } from '@griffel/react'; -import { tokens } from '@fluentui/react-theme'; +import { clsx } from 'clsx'; +import styles from './Button.module.css'; -export const useButtonStyles = makeStyles({ +/** The component's public identity class — a named group marker, not a BEM static. */ +export const buttonClassNames: { root: string } = { + root: 'group/fui-button', +}; + +export const useButtonStyles_unstable = (state: ButtonState): ButtonState => ({ + ...state, root: { - // Use design tokens, not hardcoded values - color: tokens.colorNeutralForeground1, - backgroundColor: tokens.colorNeutralBackground1, - padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, - - // Pseudo-selectors and media queries supported - ':hover': { - backgroundColor: tokens.colorNeutralBackground1Hover, - }, - - ':focus-visible': { - outline: `${tokens.strokeWidthThick} solid ${tokens.colorStrokeFocus2}`, - }, + ...state.root, + className: clsx( + // Unconditional module class first: the `group/fui-*` marker must never be classList[0]. + styles.root, + 'group/fui-button', + state.size === 'small' && styles.small, + state.size === 'large' && styles.large, + // Always preserve the user's className, last by convention. + state.root.className, + ), }, - - // Size variations - small: { padding: tokens.spacingVerticalXS }, - large: { padding: tokens.spacingVerticalL }, }); +``` -// Apply in component hook -export const useButton_unstable = (props, ref) => { - const classes = useButtonStyles(); - const state = { - /* ... */ - }; - - state.root.className = mergeClasses( - classes.root, - props.size === 'small' && classes.small, - props.size === 'large' && classes.large, - state.root.className, // Always preserve user className - ); +Three rules follow from the cascade-first model: - return state; -}; -``` +1. **`clsx` argument order carries no cascade meaning.** It is a plain string join. Which rule wins + is decided by layer, then specificity, then source order — record intended winners in the + module's layer assignment. +2. **Styles hooks return state, they do not mutate it** (`react-hooks/immutability` enforces this). + Call sites rebind: `state = useButtonStyles_unstable(state);`. +3. **Library modules author into `fui.components.l1`**, or `l2` when deliberately styling another + component's output. `l3`–`l5` are consumer space; unlayered consumer CSS beats all of it. ### Design Tokens System @@ -274,22 +303,30 @@ borderRadius: '4px'; ### Theme Architecture -Themes define CSS custom properties consumed by components: +Themes define CSS custom properties consumed by components. They ship as static CSS classes in +`@fluentui/react-tailwind-theme` (import `@fluentui/react-tailwind-theme/styles.css` once per +document; web-light values are the `:root` defaults): ```tsx -// FluentProvider injects CSS variables into DOM - +// FluentProvider applies a static theme class (and propagates it to portals) + -; + +``` -// Components reference tokens which resolve to CSS variables -makeStyles({ - root: { - color: tokens.colorNeutralForeground1, // becomes 'var(--colorNeutralForeground1)' - }, -}); +Components reference the custom properties directly in their `*.module.css`: + +```css +@layer fui.components.l1 { + .root { + color: var(--color-neutral-foreground-1); + } +} ``` +From TypeScript, use the `tokens` object — it maps each token name to the same reference +(`tokens.colorNeutralForeground1 === 'var(--color-neutral-foreground-1)'`). + ### TypeScript Patterns (v9) **Strict typing with consistent interfaces:** @@ -345,7 +382,7 @@ export const Component: ForwardRefComponent = React.forwardRef(( - Comprehensive prop interfaces with JSDoc comments - Consistent naming conventions (PascalCase for components, camelCase for props) - Use React hooks and modern patterns for v9 components -- Focus on the slot system, Griffel styling, and hook-based architecture for v9 +- Focus on the slot system, CSS-Modules styling with the `@layer fui.*` family, and hook-based architecture for v9 ### Testing Requirements diff --git a/.nxignore b/.nxignore index ef00542fa5b1e..8b2fba971f64e 100644 --- a/.nxignore +++ b/.nxignore @@ -15,3 +15,8 @@ plop-templates-_ ; TODO: temporary ignore - remove once https://github.com/microsoft/fluentui/pull/32371 is merged scripts/\*/just.config.ts + +; local scratch workspace (gitignored) — contains copies of the repo tree from measurement +; runs; without this nx discovers them as duplicate projects and the project graph fails to build +.scratch +**/.scratch/** diff --git a/.prettierignore b/.prettierignore index 36acacc7b5f17..fbc8e122dbea9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,9 @@ # generated or imported files package-deps.json +# generated theme CSS — byte-exact against packages/tokens values (generator --check); +# prettier would rewrite value bytes (rgba spacing, trailing zeros, line wraps) +packages/react-components/react-tailwind-theme/css/tokens.css +packages/react-components/react-tailwind-theme/css/themes.css CHANGELOG.* # generated from `web-features` via `yarn generate-browser-support` **/browser-support-data.generated.json diff --git a/.storybook/main.js b/.storybook/main.js index b1fd67c435302..1270e4519b01d 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -47,11 +47,29 @@ module.exports = /** @type {import('./types').StorybookConfig} */ ({ test: /\.stories\.tsx$/, include: /stories/, }, + /** + * Converted packages import `*.module.css`; without this the sandbox export drops + * those imports and every exported story opens unstyled. The addon side has supported + * this since the CSS-modules work landed — only the option was never passed. + */ + cssModules: true, }, }), ], webpackFinal: config => { - registerRules({ config, rules: [rules.swcRule] }); + /** + * Narrow storybook's own implicit `/\.css$/` rule FIRST, then add ours — webpack applies + * every matching rule, so the builder's plain style-loader/css-loader pair has to stop + * matching `*.module.css` (it would hand back an empty class map) and the Tailwind theme + * entry (it would emit `@import … source(none)` verbatim) before these are registered. + * + * Every converted package's `*.module.css` opens with `@reference '#theme'` and uses + * `@apply`, which are not valid CSS until Tailwind's PostCSS pass has run. Without this + * wiring this storybook — and the ~76 package storybooks and the public docsite that + * compose it — render every converted package unstyled. + */ + rules.excludeTailwindCssFromDefaultCssRule(config); + registerRules({ config, rules: [rules.swcRule, rules.cssModulesRule, rules.tailwindThemeRule] }); registerTsPaths({ config, configFile: tsConfigPath }); if ((process.env.CI || process.env.TF_BUILD) && config.plugins) { diff --git a/.storybook/preview-head-template.html b/.storybook/preview-head-template.html index 9f813c11b2ee6..00194ffee5a7f 100644 --- a/.storybook/preview-head-template.html +++ b/.storybook/preview-head-template.html @@ -55,10 +55,10 @@ H = function () {}, W = { height: function () { - return re('Custom height calculation function not defined'), document.documentElement.offsetHeight; + return (re('Custom height calculation function not defined'), document.documentElement.offsetHeight); }, width: function () { - return re('Custom width calculation function not defined'), document.body.scrollWidth; + return (re('Custom width calculation function not defined'), document.body.scrollWidth); }, }, B = {}, @@ -74,7 +74,7 @@ }, } ); - window.addEventListener('test', ee, U), window.removeEventListener('test', ee, U); + (window.addEventListener('test', ee, U), window.removeEventListener('test', ee, U)); } catch (e) {} var V, X, @@ -165,16 +165,16 @@ K ); }); - te(window, 'message', function (t) { + (te(window, 'message', function (t) { var n = { init: function () { - (y = t.data), + ((y = t.data), (z = t.source), ae(), (h = !1), setTimeout(function () { v = !1; - }, m); + }, m)); }, reset: function () { v ? ie('Page reset ignored by init') : (ie('Page size reset by host page'), Me('resetPage')); @@ -190,11 +190,11 @@ }, pageInfo: function () { var e = i(); - ie('PageInfoFromParent called from parent: ' + e), H(JSON.parse(e)), ie(' --'); + (ie('PageInfoFromParent called from parent: ' + e), H(JSON.parse(e)), ie(' --')); }, message: function () { var e = i(); - ie('onMessage called from parent: ' + e), j(JSON.parse(e)), ie(' --'); + (ie('onMessage called from parent: ' + e), j(JSON.parse(e)), ie(' --')); }, }; function o() { @@ -219,11 +219,11 @@ (!1 === h ? e() : r() - ? n.init() - : ie('Ignored message of type "' + o() + '". Received before initialization.')); + ? n.init() + : ie('Ignored message of type "' + o() + '". Received before initialization.')); }), te(window, 'readystatechange', Ae), - Ae(); + Ae()); } function ee() {} function te(e, t, n, o) { @@ -250,9 +250,9 @@ Ne(0, 0, e.type, e.screenY + ':' + e.screenX); } function o(e, t) { - ie('Add event listener: ' + t), te(window.document, e, n); + (ie('Add event listener: ' + t), te(window.document, e, n)); } - (t = y.substr(M).split(':')), + ((t = y.substr(M).split(':')), (I = t[0]), (a = c !== t[1] ? Number(t[1]) : a), (l = c !== t[2] ? e(t[2]) : l), @@ -271,19 +271,19 @@ ie('Initialising iFrame (' + window.location.href + ')'), (function () { function e(e, t) { - return 'function' == typeof e && (ie('Setup custom ' + t + 'CalcMethod'), (W[t] = e), (e = 'custom')), e; + return ('function' == typeof e && (ie('Setup custom ' + t + 'CalcMethod'), (W[t] = e), (e = 'custom')), e); } 'iFrameResizer' in window && Object === window.iFrameResizer.constructor && ((function () { var e = window.iFrameResizer; - ie('Reading data from page: ' + JSON.stringify(e)), + (ie('Reading data from page: ' + JSON.stringify(e)), Object.keys(e).forEach(ue, e), (j = 'onMessage' in e ? e.onMessage : j), (q = 'onReady' in e ? e.onReady : q), (k = 'targetOrigin' in e ? e.targetOrigin : k), (p = 'heightCalculationMethod' in e ? e.heightCalculationMethod : p), - (P = 'widthCalculationMethod' in e ? e.widthCalculationMethod : P); + (P = 'widthCalculationMethod' in e ? e.widthCalculationMethod : P)); })(), (p = e(p, 'height')), (P = e(P, 'width'))); @@ -346,13 +346,13 @@ Ne(0, 0, 'message', JSON.stringify(e), t); }, setHeightCalculationMethod: function (e) { - (p = e), fe(); + ((p = e), fe()); }, setWidthCalculationMethod: function (e) { - (P = e), me(); + ((P = e), me()); }, setTargetOrigin: function (e) { - ie('Set targetOrigin: ' + e), (k = e); + (ie('Set targetOrigin: ' + e), (k = e)); }, size: function (e, t) { Oe('size', 'parentIFrame.size(' + ((e || '') + (t ? ',' + t : '')) + ')', e, t); @@ -390,7 +390,7 @@ Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'), function (e) { '#' !== e.getAttribute('href') && te(e, 'click', function (e) { - e.preventDefault(), n(this.getAttribute('href')); + (e.preventDefault(), n(this.getAttribute('href'))); }); }); } @@ -403,7 +403,7 @@ return { findTarget: n }; })()), Oe('init', 'Init message from host page'), - q(); + q()); } function ue(e) { var t = e.split('Callback'); @@ -427,21 +427,21 @@ function t() { Oe(n.eventName, n.eventType); } - (B[e] = t), te(window, e, t, { passive: !0 }); + ((B[e] = t), te(window, e, t, { passive: !0 })); }, remove: function (e) { var t, n = B[e]; - delete B[e], (t = window), (e = e), (n = n), t.removeEventListener(e, n, !1); + (delete B[e], (t = window), (e = e), (n = n), t.removeEventListener(e, n, !1)); }, }; - n.eventNames && Array.prototype.map + (n.eventNames && Array.prototype.map ? ((n.eventName = n.eventNames[0]), n.eventNames.map(e[n.method])) : e[n.method](n.eventName), - ie(ne(n.method) + ' event listener: ' + n.eventType); + ie(ne(n.method) + ' event listener: ' + n.eventType)); } function de(e) { - se({ method: e, eventType: 'Animation Start', eventNames: ['animationstart', 'webkitAnimationStart'] }), + (se({ method: e, eventType: 'Animation Start', eventNames: ['animationstart', 'webkitAnimationStart'] }), se({ method: e, eventType: 'Animation Iteration', @@ -484,7 +484,7 @@ eventType: 'Transition End', eventNames: ['transitionend', 'webkitTransitionEnd', 'MSTransitionEnd', 'oTransitionEnd', 'otransitionend'], }), - 'child' === A && se({ method: e, eventType: 'IFrame Resized', eventName: 'resize' }); + 'child' === A && se({ method: e, eventType: 'IFrame Resized', eventName: 'resize' })); } function le(e, t, n, o) { return ( @@ -522,14 +522,14 @@ : 'childList' === e.type && Array.prototype.forEach.call(e.target.querySelectorAll('img'), t); } function o(e) { - ie('Remove listeners from ' + e.src), + (ie('Remove listeners from ' + e.src), e.removeEventListener('load', i, !1), e.removeEventListener('error', r, !1), (e = e), - u.splice(u.indexOf(e), 1); + u.splice(u.indexOf(e), 1)); } function n(e, t, n) { - o(e.target), Oe(t, n + ': ' + e.target.src); + (o(e.target), Oe(t, n + ': ' + e.target.src)); } function i(e) { n(e, 'imageLoad', 'Image loaded'); @@ -538,7 +538,7 @@ n(e, 'imageLoadFailed', 'Image load failed'); } function a(e) { - Oe('mutationObserver', 'mutationObserver: ' + e[0].target + ' ' + e[0].type), e.forEach(t); + (Oe('mutationObserver', 'mutationObserver: ' + e[0].target + ' ' + e[0].type), e.forEach(t)); } var u = [], c = window.MutationObserver || window.WebKitMutationObserver, @@ -616,12 +616,12 @@ function u(e, t) { return !(Math.abs(e - t) <= R); } - (r = c !== n ? n : Z[p]()), + ((r = c !== n ? n : Z[p]()), (a = c !== o ? o : $[P]()), - u(g, r) || (l && u(F, a)) || 'init' === e ? (Se(), Ne((g = r), (F = a), e)) : i(); + u(g, r) || (l && u(F, a)) || 'init' === e ? (Se(), Ne((g = r), (F = a), e)) : i()); } function Ee() { - (G = Date.now()), (Q = null), (K = V.apply(X, Y)), Q || (X = Y = null); + ((G = Date.now()), (Q = null), (K = V.apply(X, Y)), Q || (X = Y = null)); } function Oe(e, t, n, o) { x && e in f @@ -629,18 +629,18 @@ : (e in { reset: 1, resetPage: 1, init: 1 } || ie('Trigger event: ' + t), ('init' === e ? Te : _)(e, t, n, o)); } function Se() { - x || ((x = !0), ie('Trigger event lock on')), + (x || ((x = !0), ie('Trigger event lock on')), clearTimeout(e), (e = setTimeout(function () { - (x = !1), ie('Trigger event lock off'), ie('--'); - }, m)); + ((x = !1), ie('Trigger event lock off'), ie('--')); + }, m))); } function Me(e) { - (g = Z[p]()), (F = $[P]()), Ne(g, F, e); + ((g = Z[p]()), (F = $[P]()), Ne(g, F, e)); } function Ie(e) { var t = p; - (p = n), ie('Reset trigger event: ' + e), Se(), Me('reset'), (p = t); + ((p = n), ie('Reset trigger event: ' + e), Se(), Me('reset'), (p = t)); } function Ne(e, t, n, o, i) { var r; diff --git a/.storybook/preview.js b/.storybook/preview.js index 04fd8067731b1..1c8f7ca13534b 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,8 @@ -import '../packages/react-components/react-storybook-addon/src/styles.css'; +// Emits the shared Tailwind theme layer once per document (see the file's header). +// Required by every converted package's `*.module.css`. Package/app previews import this +// module, so the side effect reaches all of them. +import '../scripts/storybook/src/tailwind-theme.css'; +import '../packages/react-components/react-storybook-addon/src/docs-chrome.css'; import '../packages/react-components/react-storybook-addon-export-to-sandbox/src/styles.css'; import { withLinks } from '@storybook/addon-links'; diff --git a/AGENTS.md b/AGENTS.md index ba571e028485b..0fe073b3abe3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,10 @@ from existing code without verifying they match these instructions. ## Critical Rules (never violate) -1. **Never hardcode colors, spacing, or typography values.** Always use design tokens from `@fluentui/react-theme`. See [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md). +1. **Never hardcode colors, spacing, or typography values.** Always use design tokens — as + `var(--colorNeutralForeground1)` in a `.module.css`, or the `tokens` object from + `@fluentui/react-theme` in TS (it resolves to the same custom property). See + [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md). 2. **Never use `React.FC`.** Always use `ForwardRefComponent` with `React.forwardRef`. 3. **Never access `window`, `document`, or `navigator` directly.** In v9 components, use `useFluent_unstable()` to get `targetDocument` and `targetDocument.defaultView` instead of `document`/`window`. For non-component code, use `canUseDOM()` from `@fluentui/react-utilities`. 4. **Never add dependencies between component packages.** `react-button` must not depend on `react-menu`. Shared logic goes in `react-utilities` or `react-shared-contexts`. See [docs/architecture/layers.md](docs/architecture/layers.md). @@ -29,35 +32,78 @@ from existing code without verifying they match these instructions. ```tsx // ComponentName.tsx — always ForwardRefComponent, never React.FC export const ComponentName: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useComponentName_unstable(props, ref); - useComponentNameStyles_unstable(state); + let state = useComponentName_unstable(props, ref); + + // Styles hooks RETURN the composed state — they no longer mutate the argument. + state = useComponentNameStyles_unstable(state); + state = useCustomStyleHook_unstable('useComponentNameStyles_unstable')(state); + return renderComponentName_unstable(state); }); +``` -// Styles — always use tokens, never hardcoded values -import { makeStyles } from '@griffel/react'; -import { tokens } from '@fluentui/react-theme'; +```css +/* ComponentName.module.css — Tailwind-flavored CSS Modules, co-located with the component */ +@reference '#theme'; + +/* Repeated in every module: `@reference` emits nothing, so without this the layer ranking + would be decided by whichever Fluent stylesheet happened to load first. */ +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +/* Library components author into `fui.components.l1`. `l2` is for a component styling + another component's output; `l3`–`l5` belong to consumers and must stay empty here. */ +@layer fui.components.l1 { + .root { + @apply flex items-center; + + /* Tokens are CSS custom properties — never hardcoded values. */ + color: var(--colorNeutralForeground1); + padding: var(--spacingVerticalS) var(--spacingHorizontalM); + } +} +``` -export const useComponentNameStyles = makeStyles({ +```tsx +// ComponentName.styles.ts — compose with clsx; consumer className LAST by convention +import { clsx } from 'clsx'; +import styles from './ComponentName.module.css'; + +export const componentNameClassNames: { root: string } = { + // The root's public identity class is the named group marker, not a BEM static. + root: 'group/fui-component-name', +}; + +export const useComponentNameStyles_unstable = (state: ComponentNameState): ComponentNameState => ({ + ...state, root: { - color: tokens.colorNeutralForeground1, - padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + ...state.root, + // Unconditional module class FIRST (the marker must never be classList[0]), + // then the marker, then conditionals, then the consumer's className. + className: clsx(styles.root, 'group/fui-component-name', state.root.className), }, }); - -// mergeClasses — always preserve user className LAST -state.root.className = mergeClasses( - classes.root, - state.root.className, // always last -); ``` +Ordering note: `clsx` argument order carries **no** cascade meaning — it is a plain string join with +no merge or de-duplication. Which rule wins is decided by the `@layer` the rules live in. Put the +consumer's `className` last by convention, and record the winner in the module's layer assignment. + ## Legacy Anti-Patterns (never copy these) - **DO NOT copy patterns from `packages/react/` (v8).** That's maintenance-only legacy code using runtime styling, class components, and different APIs. - **DO NOT use `@fluentui/react` imports for new v9 work.** Use `@fluentui/react-components`. -- **DO NOT use `mergeStyles` or `mergeStyleSets`.** Use Griffel `makeStyles` with design tokens. -- **DO NOT use `IStyle` or `IStyleFunctionOrObject`.** Use Griffel's `GriffelStyle` type. +- **DO NOT use `mergeStyles` or `mergeStyleSets`.** Author a co-located `*.module.css` and reference + token custom properties (`var(--colorNeutralForeground1)`). +- **DO NOT use `makeStyles`, `makeResetStyles`, `mergeClasses`, `shorthands` or `GriffelStyle` in + library code.** Those symbols are still re-exported from `@fluentui/react-components` so existing + consumer apps keep compiling, but the library no longer authors styles with them. New library + styles are CSS Modules; class names are composed with `clsx`. +- **DO NOT mutate `state` in a styles hook.** Return a new state object (`react-hooks/immutability` + enforces this). +- **DO NOT hand-write a selector for a `group/fui-*` marker.** The `/` is legal in a class _token_ + but terminates the name in a _selector_, so use `fuiSelector(xClassNames.root)`. +- **DO NOT select a component's internal classes.** They are hashed CSS-Module identifiers + (`fuicm-…`) and are not public API — use the slot `className` props. - **DO NOT use `initializeIcons()`.** V9 uses `@fluentui/react-icons` with tree-shaking. ## Exploration Guidance @@ -68,11 +114,11 @@ state.root.className = mergeClasses( ## Architecture (deep dives) -| Topic | Location | -| --------------------------------------------- | ---------------------------------------------------------------------------------- | -| V9 component patterns (hooks, slots, Griffel) | [docs/architecture/component-patterns.md](docs/architecture/component-patterns.md) | -| Design tokens and theming | [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md) | -| Package dependency layers | [docs/architecture/layers.md](docs/architecture/layers.md) | +| Topic | Location | +| ------------------------------------------------- | ---------------------------------------------------------------------------------- | +| V9 component patterns (hooks, slots, CSS Modules) | [docs/architecture/component-patterns.md](docs/architecture/component-patterns.md) | +| Design tokens and theming | [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md) | +| Package dependency layers | [docs/architecture/layers.md](docs/architecture/layers.md) | ## Workflows diff --git a/apps/perf-test-react-components/eslint.config.js b/apps/perf-test-react-components/eslint.config.js index 0884d266211dc..2c22956fdc6cf 100644 --- a/apps/perf-test-react-components/eslint.config.js +++ b/apps/perf-test-react-components/eslint.config.js @@ -6,7 +6,6 @@ module.exports = [ ...fluentPlugin.configs['flat/react'], { rules: { - '@griffel/styles-file': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', 'no-console': 'off', '@nx/workspace-no-restricted-globals': 'off', diff --git a/apps/perf-test-react-components/index.html b/apps/perf-test-react-components/index.html index 8b76e973d1fd9..2747a5ce484c3 100644 --- a/apps/perf-test-react-components/index.html +++ b/apps/perf-test-react-components/index.html @@ -1,4 +1,4 @@ - + diff --git a/apps/perf-test-react-components/package.json b/apps/perf-test-react-components/package.json index 84663493f88e1..2e9b8ef7d464f 100644 --- a/apps/perf-test-react-components/package.json +++ b/apps/perf-test-react-components/package.json @@ -22,7 +22,6 @@ "@fluentui/react-spinbutton": "*", "@fluentui/react-theme": "*", "@fluentui/scripts-perf-test-flamegrill": "*", - "@griffel/core": "^1.16.0", "@microsoft/load-themed-styles": "^1.10.26", "react": "19.2.0", "react-dom": "19.2.0", diff --git a/apps/perf-test-react-components/src/scenarios/Avatar.tsx b/apps/perf-test-react-components/src/scenarios/Avatar.tsx index b6125037d2357..e230516aabf13 100644 --- a/apps/perf-test-react-components/src/scenarios/Avatar.tsx +++ b/apps/perf-test-react-components/src/scenarios/Avatar.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { Avatar } from '@fluentui/react-avatar'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ; Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/Button.tsx b/apps/perf-test-react-components/src/scenarios/Button.tsx index d3bc4f5e8245e..0db94180b646b 100644 --- a/apps/perf-test-react-components/src/scenarios/Button.tsx +++ b/apps/perf-test-react-components/src/scenarios/Button.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { Button } from '@fluentui/react-button'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ; Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/Field.tsx b/apps/perf-test-react-components/src/scenarios/Field.tsx index dc19a247fade2..21f5b13fcfb76 100644 --- a/apps/perf-test-react-components/src/scenarios/Field.tsx +++ b/apps/perf-test-react-components/src/scenarios/Field.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Field } from '@fluentui/react-field'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ( @@ -10,7 +10,7 @@ const Scenario = () => ( ); Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/FluentProvider.tsx b/apps/perf-test-react-components/src/scenarios/FluentProvider.tsx index 90016e5379c9d..b995d40575b6d 100644 --- a/apps/perf-test-react-components/src/scenarios/FluentProvider.tsx +++ b/apps/perf-test-react-components/src/scenarios/FluentProvider.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; import { FluentProvider } from '@fluentui/react-provider'; -const Scenario = () => ; +const Scenario = () => ; export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx b/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx index e492df4960652..8dbe160b7cbd3 100644 --- a/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx +++ b/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { FluentProvider } from '@fluentui/react-provider'; -import { teamsLightTheme } from '@fluentui/react-theme'; +import { teamsLightThemeClassName } from '@fluentui/react-theme'; const LayoutShift: React.FunctionComponent<{ children?: React.ReactNode }> = ({ children }) => { // eslint-disable-next-line no-restricted-properties @@ -18,7 +18,7 @@ const LayoutShift: React.FunctionComponent<{ children?: React.ReactNode }> = ({ const FluentProviderWithTheme = () => ( {Array.from({ length: 20 }, (n, i) => ( - + FluentProvider ))} diff --git a/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx b/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx index 5375ef92490bf..ead08d9aa441b 100644 --- a/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx +++ b/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx @@ -1,47 +1,28 @@ -import { mergeClasses, makeStyles, createDOMRenderer } from '@griffel/core'; import * as React from 'react'; -const renderer = createDOMRenderer(document); - -const useStyles = makeStyles({ - view: { - alignItems: 'stretch', - borderLeftWidth: 0, - borderLeftStyle: 'solid', - boxSizing: 'border-box', - display: 'flex', - flexBasis: 'auto', - flexDirection: 'column', - flexShrink: 0, - marginLeft: 0, - paddingLeft: 0, - position: 'relative', - minHeight: 0, - minWidth: 0, - }, - boxOuter: { alignSelf: 'flex-start', paddingLeft: '4px' }, - boxRow: { flexDirection: 'row' }, - boxColor0: { backgroundColor: '#14171A' }, - boxColor1: { backgroundColor: '#AAB8C2' }, - boxColor2: { backgroundColor: '#E6ECF0' }, - boxColor3: { backgroundColor: '#FFAD1F' }, - boxColor4: { backgroundColor: '#F45D22' }, - boxColor5: { backgroundColor: '#E0245E' }, - boxFixed: { width: '6px', height: '6px' }, -}); +import styles from './MakeStylesStyles.module.css'; + +/** + * Historically this scenario benchmarked `@griffel/core` (`makeStyles` + `mergeClasses` with an + * explicit renderer). With the Griffel -> Tailwind + CSS Modules migration the Griffel path no + * longer ships, so the scenario is repointed at the replacement styling path: static CSS Modules + * classes composed with plain string concatenation. The rendered DOM and computed styles are + * unchanged (see MakeStylesStyles.module.css); what is measured is now the mount cost of the + * shipped styling approach. + * + * The file keeps its historical name so the scenario id stays stable in perf-test output. + */ const View: React.FunctionComponent<{ className?: string }> = props => { const { className } = props; - const styles = useStyles({ dir: 'ltr', renderer }); - const classes = mergeClasses(styles.view, className); + const classes = className ? `${styles.view} ${className}` : styles.view; return
; }; const Box: React.FunctionComponent = () => { - const styles = useStyles({ dir: 'ltr', renderer }); - const classes = mergeClasses(styles.boxOuter, styles.boxRow, styles.boxFixed, styles.boxColor3); + const classes = `${styles.boxOuter} ${styles.boxRow} ${styles.boxFixed} ${styles.boxColor3}`; return ; }; diff --git a/apps/perf-test-react-components/src/scenarios/MakeStylesStyles.module.css b/apps/perf-test-react-components/src/scenarios/MakeStylesStyles.module.css new file mode 100644 index 0000000000000..5dde8dc04a6eb --- /dev/null +++ b/apps/perf-test-react-components/src/scenarios/MakeStylesStyles.module.css @@ -0,0 +1,58 @@ +/* Styles for the MakeStyles perf scenario, repointed from `@griffel/core` at the shipped + * CSS Modules path (S-E harness batch; griffel-elimination-evaluation.md §7 called for + * "retire it or repoint it at CSS Modules" — repointed so the suite keeps a styling-path + * mount scenario). Declarations are byte-for-byte the longhands the Griffel version used. */ + +.view { + align-items: stretch; + border-left-width: 0; + border-left-style: solid; + box-sizing: border-box; + display: flex; + flex-basis: auto; + flex-direction: column; + flex-shrink: 0; + margin-left: 0; + padding-left: 0; + position: relative; + min-height: 0; + min-width: 0; +} + +.boxOuter { + align-self: flex-start; + padding-left: 4px; +} + +.boxRow { + flex-direction: row; +} + +.boxColor0 { + background-color: #14171a; +} + +.boxColor1 { + background-color: #aab8c2; +} + +.boxColor2 { + background-color: #e6ecf0; +} + +.boxColor3 { + background-color: #ffad1f; +} + +.boxColor4 { + background-color: #f45d22; +} + +.boxColor5 { + background-color: #e0245e; +} + +.boxFixed { + width: 6px; + height: 6px; +} diff --git a/apps/perf-test-react-components/src/scenarios/Persona.tsx b/apps/perf-test-react-components/src/scenarios/Persona.tsx index 08119d937cef7..18ddc4497f63b 100644 --- a/apps/perf-test-react-components/src/scenarios/Persona.tsx +++ b/apps/perf-test-react-components/src/scenarios/Persona.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Persona } from '@fluentui/react-persona'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ( ( ); Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/SpinButton.tsx b/apps/perf-test-react-components/src/scenarios/SpinButton.tsx index 9ebf8d8a71cbe..ad696d75d4f2a 100644 --- a/apps/perf-test-react-components/src/scenarios/SpinButton.tsx +++ b/apps/perf-test-react-components/src/scenarios/SpinButton.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { SpinButton } from '@fluentui/react-spinbutton'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ; Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/src/scenarios/SwatchPicker.tsx b/apps/perf-test-react-components/src/scenarios/SwatchPicker.tsx index 0c87e712589ad..e41a250fd34e8 100644 --- a/apps/perf-test-react-components/src/scenarios/SwatchPicker.tsx +++ b/apps/perf-test-react-components/src/scenarios/SwatchPicker.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SwatchPicker, ColorSwatch, ImageSwatch, EmptySwatch } from '@fluentui/react-components'; import { FluentProvider } from '@fluentui/react-provider'; -import { webLightTheme } from '@fluentui/react-theme'; +import { webLightThemeClassName } from '@fluentui/react-theme'; const Scenario = () => ( @@ -12,7 +12,7 @@ const Scenario = () => ( ); Scenario.decorator = (props: { children: React.ReactNode }) => ( - {props.children} + {props.children} ); export default Scenario; diff --git a/apps/perf-test-react-components/tsconfig.app.json b/apps/perf-test-react-components/tsconfig.app.json index 71de2514af514..a711aa139d21d 100644 --- a/apps/perf-test-react-components/tsconfig.app.json +++ b/apps/perf-test-react-components/tsconfig.app.json @@ -6,7 +6,7 @@ "outDir": "lib", "jsx": "react", "lib": ["ES2019", "DOM"], - "types": ["webpack-env"] + "types": ["webpack-env", "static-assets"] }, "include": ["./src/**/*.ts", "./src/**/*.tsx"] } diff --git a/apps/perf-test-react-components/webpack.config.js b/apps/perf-test-react-components/webpack.config.js index 6d7158e78b518..f7b5a49d80598 100644 --- a/apps/perf-test-react-components/webpack.config.js +++ b/apps/perf-test-react-components/webpack.config.js @@ -13,7 +13,12 @@ const tsConfigPath = path.resolve(__dirname, '../../tsconfig.base.json'); const config = /** @type {import('webpack').Configuration}*/ ({ mode: 'production', target: ['web', 'es5'], - entry: './src/app.tsx', + // The shared Tailwind theme emission loads first: compiled `*.module.css` utilities + // reference `var(--spacing, calc(1px * var(--base-scale)))`, so without this entry every + // numeric utility is invalid at computed-value time and converted components render + // unstyled in perf scenarios. `rules.tailwindThemeRule` below routes it through the same + // Tailwind PostCSS pass the storybooks use. + entry: [rules.tailwindThemeEntry, './src/app.tsx'], output: { filename: 'perf-test.js', libraryTarget: 'umd', @@ -24,7 +29,18 @@ const config = /** @type {import('webpack').Configuration}*/ ({ }, devtool: 'eval', module: { - rules: [rules.cssRule, rules.scssRule, rules.tsRule], + // `cssModulesRule` resolves `*.module.css` (converted components and the MakeStyles + // scenario) with the same getLocalIdent/PostCSS chain as every storybook in the repo; + // `tailwindThemeRule` owns the theme entry added above. The plain `cssRule` must stop + // matching both so they are not double-processed as global CSS (which would also emit + // the theme entry's `@import … source(none)` verbatim). + rules: [ + { ...rules.cssRule, exclude: [/\.module\.css$/, /[\\/]tailwind-theme\.css$/] }, + rules.cssModulesRule, + rules.tailwindThemeRule, + rules.scssRule, + rules.tsRule, + ], }, }); diff --git a/apps/perf-test/index.html b/apps/perf-test/index.html index 8b76e973d1fd9..2747a5ce484c3 100644 --- a/apps/perf-test/index.html +++ b/apps/perf-test/index.html @@ -1,4 +1,4 @@ - + diff --git a/apps/perf-test/src/scenarios/GroupedList.tsx b/apps/perf-test/src/scenarios/GroupedList.tsx index 7c2ab01c04f28..8aef533418613 100644 --- a/apps/perf-test/src/scenarios/GroupedList.tsx +++ b/apps/perf-test/src/scenarios/GroupedList.tsx @@ -9,14 +9,12 @@ const groups = createGroups(groupCount, groupDepth, 0, groupCount); const columns = Object.keys(items[0]) .slice(0, 3) - .map( - (key: string): IColumn => ({ - key, - name: key, - fieldName: key, - minWidth: 300, - }), - ); + .map((key: string): IColumn => ({ + key, + name: key, + fieldName: key, + minWidth: 300, + })); const selection = new Selection(); selection.setItems(items); diff --git a/apps/perf-test/src/scenarios/GroupedListV2.tsx b/apps/perf-test/src/scenarios/GroupedListV2.tsx index 51d1b7a4e18b5..418718e35e3d1 100644 --- a/apps/perf-test/src/scenarios/GroupedListV2.tsx +++ b/apps/perf-test/src/scenarios/GroupedListV2.tsx @@ -16,14 +16,12 @@ const groups = createGroups(groupCount, groupDepth, 0, groupCount); const columns = Object.keys(items[0]) .slice(0, 3) - .map( - (key: string): IColumn => ({ - key, - name: key, - fieldName: key, - minWidth: 300, - }), - ); + .map((key: string): IColumn => ({ + key, + name: key, + fieldName: key, + minWidth: 300, + })); const selection = new Selection(); selection.setItems(items); diff --git a/apps/pr-deploy-site/chiclet-test.html b/apps/pr-deploy-site/chiclet-test.html index 8d4c379d0202a..b17c24288c966 100644 --- a/apps/pr-deploy-site/chiclet-test.html +++ b/apps/pr-deploy-site/chiclet-test.html @@ -1,4 +1,4 @@ - + diff --git a/apps/pr-deploy-site/index.html b/apps/pr-deploy-site/index.html index 31def61dc74de..a3ed6b26b3824 100644 --- a/apps/pr-deploy-site/index.html +++ b/apps/pr-deploy-site/index.html @@ -1,4 +1,4 @@ - + diff --git a/apps/pr-deploy-site/pr-deploy-site.css b/apps/pr-deploy-site/pr-deploy-site.css index 65d02e1239e81..ce6abf77267d9 100644 --- a/apps/pr-deploy-site/pr-deploy-site.css +++ b/apps/pr-deploy-site/pr-deploy-site.css @@ -33,14 +33,18 @@ body { .Tile { background-color: white; border-radius: 2px; - box-shadow: 0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108); + box-shadow: + 0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), + 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108); margin: 0 12px 12px 0; opacity: 0.96; transition: all 0.15s linear; } .Tile:not(.Tile--intro):hover { - box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108); + box-shadow: + 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), + 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108); opacity: 1; } diff --git a/apps/pr-deploy-site/pr-deploy-site.js b/apps/pr-deploy-site/pr-deploy-site.js index eb75f70ce0364..f08ebc90ac234 100644 --- a/apps/pr-deploy-site/pr-deploy-site.js +++ b/apps/pr-deploy-site/pr-deploy-site.js @@ -10,9 +10,7 @@ function main() { * NOTE: A build step will replace this with the list of actual built packages * @type {string[]} */ - var packages = [ - /* __PACKAGES_LIST_PLACEHOLDER__ */ - ]; + var packages = [/* __PACKAGES_LIST_PLACEHOLDER__ */]; /** * @type {SiteInfo[]} diff --git a/apps/public-docsite-v9-headless/.storybook/manager-head.html b/apps/public-docsite-v9-headless/.storybook/manager-head.html index 730b487ec19bf..30562832d95b4 100644 --- a/apps/public-docsite-v9-headless/.storybook/manager-head.html +++ b/apps/public-docsite-v9-headless/.storybook/manager-head.html @@ -27,7 +27,14 @@ - - - - +style-src 'self'; ``` -For mobile-first methodology, consider using [`sort-css-media-queries`](https://github.com/dutchenkoOleg/sort-css-media-queries): +`FluentProvider` creates no ` - - - - -``` - -### Content Security Policies (CSP) - -To add `nonce` attribute need for [Content Security Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), please use `styleElementAttributes` to specify it: - -```jsx -import { createDOMRenderer, FluentProvider, RendererProvider } from '@fluentui/react-components'; -import * as React from 'react'; - -function MyComponent(props) { - const { children } = props; - const renderer = React.useMemo( - () => createDOMRenderer(document, { styleElementAttributes: { nonce: 'random' } }), - [], - ); - - return ( - - {children} - - ); -} -``` +> **Migrating from an older setup?** Two mechanisms are gone. The Griffel-era +> `RendererProvider` + `createDOMRenderer(document, { styleElementAttributes: { nonce } })` wrapper +> and the `nonce` prop that briefly replaced it were both there to sign a style element that no +> longer exists. Delete them and make sure `style-src` allows the origin serving your CSS bundle. ### IdPrefixProvider diff --git a/apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx b/apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx index a9e11b1582632..d9cb8a6f092ab 100644 --- a/apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx +++ b/apps/public-docsite-v9/src/Concepts/AdvancedStylingTechniques.mdx @@ -35,13 +35,18 @@ The default styles are defined by `useButtonStyles_unstable` and are packaged wi For example, an `App.tsx` might look like: ```tsx -import { Button, FluentProvider, webLightTheme, CustomStyleHooksProvider_unstable } from '@fluentui/react-components'; +import { + Button, + FluentProvider, + webLightThemeClassName, + CustomStyleHooksProvider_unstable, +} from '@fluentui/react-components'; import { AlertRegular } from '@fluentui/react-icons'; import { FANCY_CUSTOM_STYLE_HOOKS } from './FancyAppCustomStyleHooksValue.ts'; export function App() { return ( - + @@ -53,43 +58,61 @@ export function App() { A little scaffolding is required to get here first. Define a `useFancyButtonStyles.ts`, and calculate the style similar to how you would for any other Fluent component. -> ⚠️ Custom style hooks must also append the slot's original className prop as returned by `getSlotClassNameProp_unstable`, after the custom styles. This ensures that the className prop added by the user will take precedence over custom styles. See [microsoft/fluentui#34166](https://github.com/microsoft/fluentui/pull/34166) for a detailed explanation. +Write the rules in your own stylesheet. Because your rules are unlayered (or in `fui.components.l3` +if you prefer to keep app styling inside the layer family), they beat the component's own +`fui.components.l1` rules with no ordering care at all: + +```css +/* FancyButton.module.css */ +.root { + border: 2px solid green; + background-color: pink; + border-radius: 64px; +} + +.icon { + color: blue; + background-color: white; +} +``` + +> ⚠️ A custom style hook must **return** the state it wants rendered — it must not mutate the state +> it is handed. It must also re-append the slot's original `className` prop as returned by +> `getSlotClassNameProp_unstable`, so the user's own `className` stays present on the element. See +> [microsoft/fluentui#34166](https://github.com/microsoft/fluentui/pull/34166) for the background. +> +> Hooks written against the older `(state) => void` shape still work — the component normalizes a +> `undefined` return back to the state it passed in — but new hooks should return. ```ts -import { getSlotClassNameProp_unstable, makeStyles, type ButtonState } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { - // These are all unique to Fancy theme. - border: '2px solid green', - backgroundColor: 'pink', - borderRadius: '64px', - }, - icon: { - color: 'blue', - backgroundColor: 'white', - }, -}); - -export const useFancyButtonStyles = (state: unknown) => { +import { clsx } from 'clsx'; +import { getSlotClassNameProp_unstable, type ButtonState } from '@fluentui/react-components'; + +import styles from './FancyButton.module.css'; + +export const useFancyButtonStyles = (state: unknown): ButtonState => { const buttonState = state as ButtonState; - const styles = useStyles(); - buttonState.root.className = mergeClasses( - buttonState.root.className, - styles.root, - getSlotClassNameProp_unstable(buttonState.root), - ); - if (buttonState.icon) { - buttonState.icon.className = mergeClasses( - buttonState.icon.className, - styles.icon, - getSlotClassNameProp_unstable(buttonState.icon), - ); - } + return { + ...buttonState, + root: { + ...buttonState.root, + className: clsx(buttonState.root.className, styles.root, getSlotClassNameProp_unstable(buttonState.root)), + }, + ...(buttonState.icon && { + icon: { + ...buttonState.icon, + className: clsx(buttonState.icon.className, styles.icon, getSlotClassNameProp_unstable(buttonState.icon)), + }, + }), + }; }; ``` +`clsx` here is a plain string join — the argument order decides the order of tokens in the rendered +`class` attribute and nothing else. Your rules win because of where they live in the cascade, not +because of where they sit in this call. + Define the value for `CustomStyleHooksProvider_unstable` in `FancyAppCustomStyleHooksValue.ts` that consumes custom style hooks: ```ts @@ -108,7 +131,7 @@ Finally use the `CustomStyleHooksProvider_unstable` in your app: // App.tsx + import { FANCY_CUSTOM_STYLE_HOOKS } from './FancyAppCustomStyleHooksValue'; - + + {/* application code ... */} + @@ -122,7 +145,7 @@ One caveat is that `CustomStyleHooksProvider` does not automatically merge conte ```tsx export function App() { return ( - + { - const buttonState = state as ButtonState; + let buttonState = state as ButtonState; // "Fancy" comes first - useFancyButtonStyles(buttonState); + buttonState = useFancyButtonStyles(buttonState); // "Smart" comes second, so it will win where there are conflicts - useSmartButtonStyles(buttonState); + buttonState = useSmartButtonStyles(buttonState); + + // A custom style hook returns the state it wants rendered — see the note above. + return buttonState; }; export const SMANCY_CUSTOM_STYLE_HOOKS: CustomStyleHooksContextValue = { - useButtonStyles_unstable: useAppCustomButtonStyles, + useButtonStyles_unstable: useSmancyCustomButtonStyles, // ... more component style overrides }; ``` @@ -171,12 +197,9 @@ And finally the App code can be simplified: ```tsx export function App() { return ( - - - {/* application code ... */} - + + + {/* application code ... */} ); diff --git a/apps/public-docsite-v9/src/Concepts/BuildTimeStyles.mdx b/apps/public-docsite-v9/src/Concepts/BuildTimeStyles.mdx deleted file mode 100644 index 4eebcf95d14c5..0000000000000 --- a/apps/public-docsite-v9/src/Concepts/BuildTimeStyles.mdx +++ /dev/null @@ -1,238 +0,0 @@ -import { Meta, Source, Link } from '@storybook/addon-docs/blocks'; -import imageFile from '../../public/style-resolution.png'; - - - -## Optimize runtime cost of styles - -Fluent UI uses the open source [Griffel.js](https://griffel.js.org/) CSS-in-JS styling engine. -Make sure to take a look at the following docs to get started with styling in Fluent UI: - -- [Quick start](?path=/docs/concepts-developer-quick-start--docs) -- [Styling components](?path=/story/concepts-developer-styling-components--docs) - -While there is nothing wrong with the associated runtime costs of a CSS-in-JS engine, larger and more complex -applications might want to optimize for performance. In Fluent UI, the expensive runtime work only -happens on the first render of the component. This one-time work can be further optimized at build time by -pre-computing and transforming styles. You can refer to the [last section](#what-is-being-optimized) of this page for more details -about what the build time optimization does. - -## Setup build time style transforms - -- For application developers please use the [Webpack loader](#webpack) -- For library developers please use the [Babel preset](#babel) - -### Webpack loader - -```sh -yarn add -D @griffel/webpack-loader -``` - -In your Webpack config, simply add the `@griffel/webpack-loader` to the list of rules. - -```js -// webpack.config.js -module.exports = { - module: { - rules: [ - // 💡 We recommend adding the Griffel webpack loader after `babel-loader`, `ts-loader`, etc. - // - // The underlying Babel transforms used by the Webpack loader are configured by default to use - // the most basic language features. Therefore, to avoid extra configuration or parsing errors, - // using the Webpack loader after compilation is the required approach. - { - test: /\.(ts|tsx)$/, - exclude: /node_modules/, - use: { - loader: '@griffel/webpack-loader', - options: { - // see https://github.com/microsoft/griffel/tree/main/packages/webpack-loader#configuring-babel-settings - babelOptions: { - presets: ['@babel/preset-typescript'], - }, - }, - }, - }, - { - test: /\.(ts|tsx)$/, - exclude: /node_modules/, - use: { - // or 'ts-loader' - loader: 'babel-loader', - }, - }, - ], - }, -}; -``` - -> ℹ️ **Note**: A chain of webpack loaders is executed in reverse order -> [https://webpack.js.org/concepts/loaders/#loader-features](https://webpack.js.org/concepts/loaders/#loader-features) - -#### Typescript (and additional compilation) support - -The Webpack loader [evaluate styles at runtime](#build-time-style-evaluation). For typescript -it's necessary to install `@babel/preset-typescript` and configure it in the [babelOptions](https://github.com/microsoft/griffel/tree/main/packages/webpack-loader#configuring-babel-settings) of the loader. Similarly -any other language features that might be required can also be configured here. While the order of loaders will -ensure that the webpack loader will run on transpiled code (for a specific module), runtime evaluation can potentially require other -modules that are not transpiled. Any additional language features can also be configured in the same way. - -### Babel preset - -The Babel preset is used internally by the Webpack loader. - -> For applications using [Webpack loader](#webpack-loader) is encouraged and recommended - -However, library devs which might prefer to not bundle their code should use our Babel preset. - -```sh -yarn add -D @griffel/babel-preset -``` - -Simply create a Babel configuration file such as `.babelrc.json` and add the preset. - -```json -{ - "presets": [ - [ - "@griffel", - { - "babelOptions": { - "presets": ["@babel/preset-typescript"], - }, - }, - ], - ], -}; -``` - -#### Typescript (and additional transpilation) support - -[Similar to above](#typescript-and-additional-transpilation-support) - -#### Importing from a third party package - -If Griffel is re-exported from a third party package, it's necessary to configure the preset to use a different -module source. By default, the preset is hard coded to handle imports from `@fluentui/react-components`. - -```js -import { makeStyles } from 'custom-package'; -``` - -```json -{ - "presets": [ - [ - "@griffel/babel", - { - "modules": [{ "moduleSource": "custom-package", "importName": "makeStyles" }] - } - ] - ] -} -``` - -### What is being optimized - -> ⚠️ Style resolution only needs to happen on the initial render of a component. Therefore,without build time -> optimization the performance is comparable with the 2nd and consecutive renders. It is perfectly reasonable to follow the -> quickstart guide and introduce build time optimization if/when it is required. - -Let's start with a simple example: - -```tsx -import { makeStyles } from '@fluentui/react-components'; - -// 1. -const useStyles = makeStyles({ - root: { paddingLeft: '1px', display: 'flex' }, -}); - -function Component() { - // 2. - const classes = useStyles(); - - return
; -} -``` - -What's happening in that example? - -1. Invocation of makeStyles creates a styling hook that will be used inside a component -2. The `useStyles` hook call resolves styles which are injected into the document. - -You can look at the image below which describes what work is done during style resolution. - -> 💡 NOTE: this work only happens once, during first render. - -{'Illustration - -The final result before the CSS rules are inserted into DOM can be compiled ahead of time during build time through -the methods described above. Once the styles of our simple example are transformed at build time the resulting bundle contains -a result similar to what is in our diagram. The actual runtime code of `makeStyles` is completely stripped from the -bundle and replaced with a lightweight function (`__styles`) that simply concatenates the CSS classes and inserts them to -DOM. - -**Result of ahead of time compilation:** - -```tsx -const useStyles = __styles( - { - root: { - mc9l5x: 'f22iagw', - uwmqm3: ['f10xn8zz', 'f136y8j8'], - }, - }, - { - d: [ - '.f22iagw{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}', - '.f10xn8zz{padding-left:1px;}', - '.f136y8j8{padding-right:1px;}', - ], - }, -); - -function Component() { - const classes = useStyles(); - return
; -} -``` - -### Build time style evaluation - -Let's consider the following scenario: - -```ts -// constants.ts -export const PADDING_TOKEN = '1px'; - -// common.ts -export const commonStyles = () => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); - -// styles.ts -import { makeStyles } from '@fluentui/react-components'; -import { PADDING_TOKEN } from './constants'; -import { commonStyles } from './common'; - -const useStyles = makeStyles({ - root: { paddingLeft: PADDING_TOKEN, ...commonStyles() }, -}); -``` - -It's perfectly fine, and even recommended to reuse common tokens and create style helpers across an application. It's -one of the main benefits of using CSS-in-JS. However, this means that the build time transforms which are described -above are not trivial to compute because code needs to be evaluated to know what styles to transform. In the example -above, in order to transform the `styles.ts` file, the code needs to be executed/evaluated by importing the extra -modules that it depends on (`constants.ts` and `common.ts`). - -Griffel uses -[style evaluation from Linaria](https://github.com/callstack/linaria). -The build-time evaluation happens as a part of the Babel transforms in Griffel. All styles that require evaluation will be -batched and done in single evaluation context. **Linaria's Babel config is separate to any config used by the application**. -Therefore, additional language features -may require extra configuration. How to do this configuration is described in the sections above. One common -language feature that will require extra configuration is Typescript. diff --git a/apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx b/apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx index dde5bccb6f4db..a55197e68efdf 100644 --- a/apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx +++ b/apps/public-docsite-v9/src/Concepts/BuildingCustomControls.mdx @@ -33,7 +33,7 @@ Base state hooks are not a replacement for composition hooks in most cases. If y ### What base state hooks exclude - Design props (for example `appearance`, `size`, `shape`) -- Style logic (Griffel styles, design token styling) +- Style logic (CSS-Module class composition, design token styling) - Motion and transitions - Default slot content @@ -75,7 +75,8 @@ The example below shows the complete pattern for building a custom component on ```tsx import * as React from 'react'; -import { assertSlots, mergeClasses, slot, type Slot } from '@fluentui/react-components'; +import { clsx } from 'clsx'; +import { assertSlots, slot, type Slot } from '@fluentui/react-components'; import { useButtonBase_unstable } from '@fluentui/react-button'; import type { ButtonBaseProps, ButtonBaseState } from '@fluentui/react-button'; @@ -131,20 +132,23 @@ const useLoadingButtonState = ( }; // --- Styles hook --- -// Apply class names using mergeClasses. Always append the existing slot className last -// so that consumer class names take precedence over your internal ones. - -const useLoadingButtonStyles = (state: LoadingButtonState): void => { - state.root.className = mergeClasses( - 'loadingButton', - state.isLoading && 'loadingButton--busy', - state.root.className, // consumer class names win - ); - - if (state.loadingIndicator) { - state.loadingIndicator.className = mergeClasses('loadingButton__indicator', state.loadingIndicator.className); - } -}; +// Join class names with clsx and RETURN the new state — never mutate the state you were handed. +// Put the existing slot className last by convention; which rule wins is decided by the cascade +// (your rules are unlayered or in fui.components.l3-l5, so they already beat the library's). + +const useLoadingButtonStyles = (state: LoadingButtonState): LoadingButtonState => ({ + ...state, + root: { + ...state.root, + className: clsx('loadingButton', state.isLoading && 'loadingButton--busy', state.root.className), + }, + ...(state.loadingIndicator && { + loadingIndicator: { + ...state.loadingIndicator, + className: clsx('loadingButton__indicator', state.loadingIndicator.className), + }, + }), +}); // --- Render function --- // Controls what gets rendered and when. Use assertSlots to get type-safe slot @@ -171,8 +175,8 @@ const renderLoadingButton = (state: LoadingButtonState) => { export const LoadingButton = React.forwardRef( (props, ref) => { - const state = useLoadingButtonState(props, ref); - useLoadingButtonStyles(state); + let state = useLoadingButtonState(props, ref); + state = useLoadingButtonStyles(state); return renderLoadingButton(state); }, ); @@ -181,7 +185,8 @@ export const LoadingButton = React.forwardRef { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - customAvatar: { - ':focus': { - outlineColor: tokens.colorNeutralBackground3Pressed, - }, - }, -}); ``` ## Migrate `image` prop diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Box.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Box.mdx index 6a7835ba55c5a..c1356e83db663 100755 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Box.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Box.mdx @@ -77,22 +77,25 @@ export const boxStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .wide-box { + width: 100%; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Primitive } from '@fluentui/react-migration-v0-v9'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - wideBox: { - width: '100%', - }, -}); ``` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Button.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Button.mdx index 2750ec180eb3c..a0b60fb8510dc 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Button.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Button.mdx @@ -6,8 +6,6 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Overview: -To help with the migration we also offer a mixin that [can be checked here](/docs/migration-shims-v0-buttonmixins--disabled-cursor) - Before: ```tsx @@ -95,24 +93,27 @@ export const buttonStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .action-button { + color: var(--color-neutral-foreground-1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Button } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - actionButton: { - color: colors.colorNeutralForeground1, - }, -}); ``` ### Example for migrate namespaced styles, with conditional styles via `variableProps`: @@ -156,31 +157,34 @@ export default { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .chat-list-group-title-as-header { + height: 32px; + width: 32px; + min-width: 32px; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx -import { Button, mergeClasses } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import { clsx } from 'clsx'; +import { Button } from '@fluentui/react-components'; +import styles from './COMPONENT_NAME.module.css'; export const Component = props => { - const classes = useStyles(); - return ( ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - chatListGroupTitleAsHeader: { - height: '32px', - width: '32px', - minWidth: '32px', - }, -}); ``` ### Migrate `iconOnly` prop @@ -221,7 +225,6 @@ For usage v9 Icon with v0 Button it's required to add additional styles to keep ```tsx import { CalendarMonth } from '@fluentui/react-components-icons'; -import { buttonMigrationStyles } from '@fluentui/react-components'; import { Button, useUIProviderContext } from '@fluentui/react-northstar'; const Component = () => { @@ -230,12 +233,26 @@ const Component = () => { }; // in button-namespace-flyout.ts -import { buttonMigrationStyles } from '@fluentui/react-components'; +import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons'; export default { root: { filterButton: () => ({ - ...buttonMigrationStyles.V9Icon(), + // Keep a v9 SVG icon sized/centered consistently with the v0 icon metrics. + '& svg': { + width: '100%', + paddingBottom: '100%', + // negative margin centers the v9 icon inside the v0 button (the svg carries extra space) + margin: '-4px 0 0 -4px', + overflow: 'visible', + }, + // Swap regular → filled on hover, matching the v0 icon behavior. + [`& .${iconFilledClassName}`]: { display: 'none' }, + [`& .${iconRegularClassName}`]: { display: 'inline' }, + '&:hover': { + [`& .${iconFilledClassName}`]: { display: 'inline' }, + [`& .${iconRegularClassName}`]: { display: 'none' }, + }, }), }, }; @@ -245,25 +262,35 @@ export default { ```tsx import { CalendarIcon } from '@fluentui/react-icons-northstar'; -import { Button, makeStyles } from '@fluentui/react-components'; -import { buttonMigrationStyles } from '@fluentui/react-components'; - -const useButtonStyles = makeStyles({ - v0IconStyle: { - ...buttonMigrationStyles.v0Icon(), - }, -}); +import { Button } from '@fluentui/react-components'; +import styles from './Component.module.css'; const Component = () => { - const classes = useButtonStyles(); return ( - ); }; ``` +```css +/* Component.module.css — v0 icon inside a v9 Button: outline → filled swap on hover. + `ui-icon__outline` / `ui-icon__filled` are the v0 (react-northstar) icon class names. */ +.v0Icon :global(.ui-icon__outline) { + display: block; +} +.v0Icon :global(.ui-icon__filled) { + display: none; +} +.v0Icon:hover :global(.ui-icon__outline) { + display: none; +} +.v0Icon:hover :global(.ui-icon__filled) { + display: block; +} +``` + Live example is here: https://codesandbox.io/s/button-icon-migration-lkt6o5?file=/example.tsx ### Migrate `loading` and `loader` props diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Card/Card.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Card/Card.mdx index 3071763a02516..c6a976ad15aa6 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Card/Card.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Card/Card.mdx @@ -63,25 +63,27 @@ export const CardStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .action-card { + color: var(--color-neutral-foreground-1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Card } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - actionCard: { - color: colors.colorNeutralForeground1, - }, -}); ``` ### Example for migrate namespaced styles, with conditional styles via `variableProps`: @@ -125,31 +127,34 @@ export default { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .chat-list-group-title-as-header { + height: 3rem; + width: 8rem; + min-width: 8rem; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx -import { Card, mergeClasses } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import { clsx } from 'clsx'; +import { Card } from '@fluentui/react-components'; +import styles from './COMPONENT_NAME.module.css'; export const Component = props => { - const classes = useStyles(); - return ( ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - chatListGroupTitleAsHeader: { - height: '3rem', - width: '8rem', - minWidth: '8rem', - }, -}); ``` ## Migrate `centered` prop @@ -166,22 +171,28 @@ const Component = () => Lorem ipsum, dolor sit amet consectetur a After: +```css +/* Card.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .centered-card { + justify-items: center; + } +} +``` + ```tsx import * as React from 'react'; -import { makeStyles } from '@fluentui/react-components'; import { Card } from '@fluentui/react-components'; -const useStyles = makeStyles({ - centeredCard: { - justifyItems: 'center', - }, -}); +import styles from './Card.module.css'; export const CenteredCard = () => { - const styles = useStyles(); - return ( - +

Lorem ipsum dolor sit amet.

); @@ -202,20 +213,26 @@ const Component = () => Lorem ipsum, dolor sit amet consectet After: +```css +/* Card.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .parent { + width: 500px; + } +} +``` + ```jsx import * as React from 'react'; -import { makeStyles } from '@fluentui/react-components'; import { Card } from '@fluentui/react-components'; -const useStyles = makeStyles({ - parent: { - width: '500px', - }, -}); +import styles from './Card.module.css'; export const SizedCard = () => { - const styles = useStyles(); - return (
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Divider.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Divider.mdx index 974f9f3014590..c9586fb0c32f9 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Divider.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Divider.mdx @@ -27,17 +27,17 @@ To add paddings on v9 Divider to match v0, please see style overrides in this [c ## How to migrate props: -| Divider props | migrate guide | -| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| as, className, key, ref | keep it as is | -| vertical | keep it as is | -| fitted | simply remove it | -| content | see Migrate content prop in this document | -| variables, styles | see Migrate style overrides in this document | -| accessibility | see \[migrate-custom-accessibility.md\](?path=/docs/concepts-migration-from-v0-custom-accessibility--docs) | -| important | see Migrate important prop in this document | -| size | see Migrate size prop in this document | -| color | replace \`color="brand"\` with \`appearance="brand"\`. For other color value, add style override with makeStyles and color tokens | +| Divider props | migrate guide | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| as, className, key, ref | keep it as is | +| vertical | keep it as is | +| fitted | simply remove it | +| content | see Migrate content prop in this document | +| variables, styles | see Migrate style overrides in this document | +| accessibility | see \[migrate-custom-accessibility.md\](?path=/docs/concepts-migration-from-v0-custom-accessibility--docs) | +| important | see Migrate important prop in this document | +| size | see Migrate size prop in this document | +| color | replace \`color="brand"\` with \`appearance="brand"\`. For other color value, add style override with a CSS Module and color tokens | See this [codesandbox](https://codesandbox.io/s/divider-migration-88ws6u?file=/example.js) comparing v9 Divider with v0, including guides on adding v9 style overrides to match v0. @@ -98,22 +98,25 @@ export const dividerStyles = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .chat-entity-header-divider { + height: 32px; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Divider } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - chatEntityHeaderDivider: { - height: '32px', - }, -}); ``` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Flex.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Flex.mdx index a62ec5e35138a..21b1319ba98f6 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Flex.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Flex.mdx @@ -6,7 +6,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Overview: -To help with the migration we also offer a mixin and a shim that [can be checked here](/docs/migration-shims-v0-flexshim--default) +To help with the migration we also offer a shim that [can be checked here](/docs/migration-shims-v0-flexshim--default) Before: @@ -87,22 +87,18 @@ After: ```tsx // in COMPONENT_NAME.tsx import { Flex } from '@fluentui/react-migration-v0-v9'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - - return ; + return ; }; ``` -```tsx -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-migration-v0-v9'; - -export const useStyles = makeStyles({ - root: { color: 'red' }, -}); +```css +/* in COMPONENT_NAME.module.css */ +.root { + color: red; +} ``` ## Migrate flex items diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/FlexItem.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/FlexItem.mdx index 02f09bcc09c83..9c2089a5d9a0b 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/FlexItem.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/FlexItem.mdx @@ -6,9 +6,9 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Overview: -FlexItem component is replaced with matching style functions. This allows the code to be more robust, customizable and performant. +FlexItem component is replaced with plain CSS on the child element. This allows the code to be more robust, customizable and performant. -`flexItem` mixins are provided to make the transition easier. The mixins have very similar API as the `FlexItem` react-northstar component. +Each `FlexItem` prop maps to one or two standard flexbox declarations (table below). Before: @@ -26,42 +26,37 @@ After: ```tsx // in COMPONENT_NAME.tsx import { Flex, Button } from "@fluentui/react-components"; -import { useStyles } from "./COMPONENT_NAME.styles.ts"; +import styles from "./COMPONENT_NAME.module.css"; -export const Component = () => { - const classes = useStyles(); +export const Component = () => + ; ``` -```tsx -// in COMPONENT_NAME.styles.ts -import { makeStyles, flexItem } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - flexItemPushColumn: { - ...flexItem.grow(true), - ...flexItem.pushColumn(), - }, -}); +```css +/* in COMPONENT_NAME.module.css */ +.flexItemPushColumn { + flex-grow: 1; + margin-top: auto; +} ``` -If you are using `Flex` with horizontal direction, please use `pushRow` instead of `pushColumn`. +If you are using `Flex` with horizontal direction, please use `margin-left: auto` instead of `margin-top: auto`. ## How to migrate props: -| \`FlexItem\` props | migrate guide | -| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| className | apply on the child component | -| variables, design, styles | apply on the child component (see corresponding migration guide for the particular component | -| align | use \`flexItem.align()\` mixin in child component's style file | -| flexDirection | ignore | -| grow | use \`flexItem.grow()\` mixin in child component's style file | -| push | use \`flexItem.pushRow()\` or \`flexItem.pushColumn()\` mixin in child component's style file depending on the parent Flex direction | -| shrink | use \`flexItem.shrink()\` mixin in child component's style file | -| size | use \`flexItem.size()\` mixin in child component's style file. For arbitrary (pixel or rem) values, use style overrides as described in \[Migrate style overrides\](#migrate-style-overrides). | +| \`FlexItem\` props | migrate guide | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| className | apply on the child component | +| variables, design, styles | apply on the child component (see corresponding migration guide for the particular component | +| align | \`align-self\` on the child (\`start\` → \`flex-start\`, \`end\` → \`flex-end\`; \`auto\` / \`center\` / \`baseline\` / \`stretch\` as-is) | +| flexDirection | ignore | +| grow | \`flex-grow: 1\` (or the numeric value) on the child | +| push | \`margin-left: auto\` (row direction) or \`margin-top: auto\` (column direction) on the child | +| shrink | \`flex-shrink: 0\` for \`shrink={false}\`, or the numeric value | +| size | \`flex-basis\` on the child (\`half\` → \`50%\`, \`quarter\` → \`25%\`, \`small\` → \`150px\`, \`medium\` → \`200px\`, \`large\` → \`300px\`). For arbitrary (pixel or rem) values, use style overrides as described in \[Migrate style overrides\](#migrate-style-overrides). | --- @@ -100,29 +95,23 @@ After: ```tsx // in COMPONENT_NAME.tsx import { Button, Flex } from "@fluentui/react-components"; -import { useStyles } from "./COMPONENT_NAME.styles.ts"; +import styles from "./COMPONENT_NAME.module.css"; export const Component = () => { - const classes = useStyles(); - return ( - + ); }; ``` -```tsx -// in COMPONENT_NAME.styles.ts -import { flexItem, makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - root: { - ...flexItem.grow(true), - color: 'red', - }, -}); +```css +/* in COMPONENT_NAME.module.css */ +.root { + flex-grow: 1; + color: red; +} ``` ## Migrate flex items diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Grid.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Grid.mdx index 038251bce671d..040d2d947c593 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Grid.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Grid.mdx @@ -89,13 +89,21 @@ export const Component = () => { }; ``` +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .root { + color: red; + } +} +``` + ```tsx -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; -export const useStyles = makeStyles({ - root: { color: 'red' }, -}); ``` ## Migrate columns and rows @@ -136,23 +144,17 @@ After: ```tsx // in COMPONENT_NAME.tsx import { Grid } from '@fluentui/react-migration-v0-v9'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - - return ; + return ; }; ``` -```tsx -// in COMPONENT_NAME.styles.ts -import { makeStyles, grid } from '@fluentui/react-migration-v0-v9'; - -export const useStyles = makeStyles({ - root: { - ...grid.columns('5'), - ...grid.rows('2fr repeat(2, 1fr)'), - }, -}); +```css +/* in COMPONENT_NAME.module.css — a numeric v0 template `N` maps to `repeat(N, 1fr)` */ +.root { + grid-template-columns: repeat(5, 1fr); + grid-template-rows: 2fr repeat(2, 1fr); +} ``` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Header.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Header.mdx index 6beb94bf01028..ed03c0aafd3a6 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Header.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Header.mdx @@ -120,24 +120,27 @@ export const headerStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .main-header { + color: var(--color-neutral-foreground-1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Text } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - mainHeader: { - color: tokens.colorNeutralForeground1, - }, -}); ``` You can replace certain styles with supported `Text` props in V9. Boolean props: `wrap`, `truncate`, `block`, `italic`, `underline`, `strikethrough`. Other props for font styling: `size`, `font`, `weight`. diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.module.css new file mode 100644 index 0000000000000..51de134d7a179 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.module.css @@ -0,0 +1,66 @@ +/* Story styles for ComparisonTile. + * Token references are the Fluent theme's CSS variables. */ + +.root { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + max-height: 105px; + padding: 5px 0px; + gap: 10px; + border-color: var(--color-neutral-stroke-1); + border-style: solid; + border-width: var(--stroke-width-thin); +} + +.root:hover { + background-color: var(--color-neutral-background-2-hover); +} + +.badge { + position: absolute; + top: 5px; + right: 10px; +} + +.warning { + color: var(--color-palette-dark-orange-background-3); +} + +.success { + color: var(--color-palette-green-background-3); +} + +.v0 { + width: 16px; + height: 16px; +} + +.v9 { + width: 20px; + height: 20px; +} + +.tile { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 2px; +} + +.buttonReset { + resize: horizontal; + box-sizing: content-box; + background-color: inherit; + color: inherit; + font-family: inherit; + font-size: inherit; + line-height: normal; + -webkit-appearance: button; + text-align: unset; + padding: 0; + overflow: visible; + border-style: none; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.styles.ts b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.styles.ts index 476e75c2aadf4..1f50de235539b 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.styles.ts +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.styles.ts @@ -1,64 +1,5 @@ -import { makeStyles, shorthands, tokens } from '@fluentui/react-components'; +import styles from './ComparisonTile.module.css'; -export const useComparisonTileStyles = makeStyles({ - root: { - position: 'relative', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - ...shorthands.padding('5px', '0px'), - ...shorthands.gap('10px'), - ...shorthands.border(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke1), - maxHeight: '105px', - ':hover': { - backgroundColor: tokens.colorNeutralBackground2Hover, - }, - }, - - badge: { - position: 'absolute', - top: '5px', - right: '10px', - }, - - warning: { - color: tokens.colorPaletteDarkOrangeBackground3, - }, - - success: { - color: tokens.colorPaletteGreenBackground3, - }, - - v0: { - width: '16px', - height: '16px', - }, - - v9: { - width: '20px', - height: '20px', - }, - - tile: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - ...shorthands.gap('2px'), - }, - - buttonReset: { - resize: 'horizontal', - boxSizing: 'content-box', - backgroundColor: 'inherit', - color: 'inherit', - fontFamily: 'inherit', - fontSize: 'inherit', - lineHeight: 'normal', - ...shorthands.overflow('visible'), - ...shorthands.padding(0), - ...shorthands.borderStyle('none'), - WebkitAppearance: 'button', - textAlign: 'unset', - }, -}); +/** Story-scaffolding classes (see the colocated .module.css). Kept hook-shaped so consuming + * components are untouched by the Griffel -> CSS Modules conversion. */ +export const useComparisonTileStyles = (): typeof styles => styles; diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.tsx index c36f7dba5eaf5..a74efc586a494 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/ComparisonTile.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { DismissRegular, InfoRegular } from '@fluentui/react-icons'; -import { Button, mergeClasses, Tooltip } from '@fluentui/react-components'; +import { Button, Tooltip } from '@fluentui/react-components'; import { V0IconComponent, V9IconComponent } from './types'; import { useComparisonTileStyles } from './ComparisonTile.styles'; @@ -21,7 +21,7 @@ export const ComparisonTile: React.FC = ({ V0Icon, V9Icon } @@ -112,15 +124,6 @@ export const Component = () => { ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - breakoutRoomsAssignmentMenuButton: { - width: '100%', - }, -}); ``` ## Migrate `menu` and `trigger` props diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Popup.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Popup.mdx index 881dc81a0537c..3c886b9461aa2 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Popup.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Popup.mdx @@ -70,7 +70,7 @@ When `content` value is shorthand object, for example content = { content: 'hi', variables: { someVariable: true } }; ``` -Migrate variables to use `makeStyles` API. Before: +Migrate variables to a CSS Module class. Before: ```tsx // in COMPONENT_NAME.tsx @@ -98,29 +98,32 @@ export const popupContentStyles = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .custom-popup { + background-color: var(--color-brand-background); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); return ( {props.trigger} - {props.content} + {props.content} ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - customPopup: { - backgroundColor: tokens.colorBrandBackground, - }, -}); ``` ⚠️ **If this is your first time migrating style overrides**, please read [the general guide on how to migrate styles](?path=/docs/concepts-migration-from-v0-custom-style-overrides--docs). diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Segment.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Segment.mdx index 5ea3165ae17f1..4be18207c8426 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Segment.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Segment.mdx @@ -70,23 +70,26 @@ export const segmentStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .speed-dial-contact-card { + background-color: inherit; + box-shadow: none; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; import { Segment } from '@fluentui/react-migration-v0-v9'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - speedDialContactCard: { - backgroundColor: 'inherit', - boxShadow: 'none', - }, -}); ``` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Slider.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Slider.mdx index 6086ec5e23601..ba7177df85821 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Slider.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Slider.mdx @@ -6,8 +6,6 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Overview: -To help with the migration we also offer a mixin that [can be checked here](/docs/migration-shims-v0-slidermixin--docs) - Before: ```tsx @@ -35,7 +33,7 @@ Slider upgrade doc in V9: [Slider upgrade to V9](https://react.fluentui.dev/?pat | defaultValue | keep it as is | | disabled | keep it as is | | getA11yValueMessageOnChange | create your own message for a11y attr \`aria-valuetext\` by listening \`onChange\` and updating state value | -| fluid | use \`slider.fluid()\` mixin in Slider component's style file | +| fluid | add \`width: 100%\` to the Slider's class in your stylesheet | | input | keep it as is | | max, min | keep it as is | | onChange | keep it as is | @@ -74,24 +72,27 @@ export const sliderStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .is-volume-control { + color: var(--color-neutral-foreground-1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Slider } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - isVolumeControl: { - color: tokens.colorNeutralForeground1, - }, -}); ``` Find more examples here: [Sandbox](https://codesandbox.io/s/slider-migration-fluentui-forked-e3zdj5?file=/example.tsx) diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/SplitButton.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/SplitButton.mdx index c9b437c595f20..886adda0e0ab3 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/SplitButton.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/SplitButton.mdx @@ -128,6 +128,19 @@ export const splitButtonStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .action-button { + color: var(--color-neutral-foreground-1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { @@ -140,16 +153,15 @@ import { MenuTrigger, SplitButton, } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); return ( {(triggerProps: MenuButtonProps) => ( alert('button clicked') }} > @@ -168,15 +180,6 @@ export const Component = () => { ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - actionButton: { - color: tokens.colorNeutralForeground1, - }, -}); ``` ### Others diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/TabList.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/TabList.mdx index e1ff97306e232..800d9c826c1ef 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/TabList.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/TabList.mdx @@ -128,15 +128,27 @@ export const menuStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .chat-tab-list { + width: 100%; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Tab, TabList } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = ({ items }) => { - const classes = useStyles(); return ( - + {items.map(({ id, content }) => ( {content} @@ -145,13 +157,4 @@ export const Component = ({ items }) => { ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - chatTabList: { - width: '100%', - }, -}); ``` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Text.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Text.mdx index b2ea7f3440366..7d82a35cba40a 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Text.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Text.mdx @@ -83,24 +83,27 @@ export const textStyles1 = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .main-window-chat-header-title { + max-width: 11rem; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { StyledText } from '@fluentui/react-migration-v0-v9'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); - return ; + return ; }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - mainWindowChatHeaderTitle: { - maxWidth: '11rem', - }, -}); ``` ## Migrate size `large` diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Toolbar.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Toolbar.mdx index 4814b02c01ece..b8e60909a747e 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Toolbar.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Toolbar.mdx @@ -186,12 +186,7 @@ V0 only allows to set an item active in a controlled way through `active` proper V9 Controlled: ```javascript -import { - Toolbar, - ToolbarToggleButton, - ToolbarDivider, - ToolbarButton -} from '@fluentui/react-components'; +import { Toolbar, ToolbarToggleButton, ToolbarDivider, ToolbarButton } from '@fluentui/react-components'; @@ -303,28 +298,31 @@ export const toolbarStyles = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .breakout-rooms-assignment-toolbar { + width: 100%; + } +} +``` + ```tsx // in COMPONENT_NAME.tsx -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; import { Toolbar, ToolbarButton } from '@fluentui/react-components'; export const Component = () => { - const classes = useStyles(); return ( - + Italic ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - breakoutRoomsAssignmentToolbar: { - width: '100%', - }, -}); ``` ### Example for migrate namespaced styles, with conditional styles via `variableProps`: @@ -355,35 +353,40 @@ export default { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .tab-item-toolbar { + height: 100%; + display: inline-flex; + align-items: center; + color: inherit; + } + + .live-tab-item-toolbar { + color: var(--colorPaletteSilverForeground1); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx -import { useStyles } from './COMPONENT_NAME.styles.ts'; -import { Toolbar, Button, mergeClasses } from '@fluentui/react-components'; +import { clsx } from 'clsx'; +import styles from './COMPONENT_NAME.module.css'; +import { Toolbar, Button } from '@fluentui/react-components'; export const Component = props => { - const classes = useStyles(); const { isLive } = props; return ( - + Italic ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, shorthands, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - tabItemToolbar: { - height: '100%', - display: 'inline-flex', - alignItems: 'center', - color: 'inherit', - }, - liveTabItemToolbar: { - color: tokens.colorPaletteSilverForeground1, - }, -}); ``` ## Migrate `overflow` props diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Tooltip.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Tooltip.mdx index c29565bf81d84..cbf32d7088d61 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Tooltip.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/Tooltip.mdx @@ -62,7 +62,7 @@ When `content` value is shorthand object, for example content = { content: 'hi', variables: { someVariable: true } }; ``` -Migrate variables to use `makeStyles` API. Before: +Migrate variables to a CSS Module class. Before: ```tsx // in COMPONENT_NAME.tsx @@ -90,33 +90,36 @@ export const tooltipContentStyles = { After: +```css +/* COMPONENT_NAME.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .custom-tooltip { + background-color: var(--color-brand-background); + } +} +``` + ```tsx // in COMPONENT_NAME.tsx import { Tooltip } from '@fluentui/react-components'; -import { useStyles } from './COMPONENT_NAME.styles.ts'; +import styles from './COMPONENT_NAME.module.css'; export const Component = () => { - const classes = useStyles(); return ( {props.trigger} ); }; - -// in COMPONENT_NAME.styles.ts -import { makeStyles, tokens } from '@fluentui/react-components'; - -export const useStyles = makeStyles({ - customTooltip: { - backgroundColor: tokens.colorBrandBackground, - }, -}); ``` ⚠️ **If this is your first time migrating style overrides**, please read [the general guide on how to migrate styles](?path=/docs/concepts-migration-from-v0-custom-style-overrides--docs). diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-custom-accessibility.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-custom-accessibility.mdx index c15be8e71ea30..03de90ee7a9e7 100755 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-custom-accessibility.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-custom-accessibility.mdx @@ -77,17 +77,19 @@ const Component = () => ( After: +```css +/* Component.module.css */ +.grid { + display: grid; + justify-content: space-evenly; +} +``` + ```tsx -import { makeStyles } from '@fluentui/react-components'; import { useTabsterAttributes } from '@fluentui/react-tabster'; import { useFocusFinders } from '@fluentui/react-tabster'; -const useStyles = makeStyles({ - grid: { - display: 'grid', - justifyContent: 'space-evenly', - }, -}); +import styles from './Component.module.css'; const arrowKeyNavigationAttributes = useTabsterAttributes({ mover: { @@ -98,7 +100,6 @@ const arrowKeyNavigationAttributes = useTabsterAttributes({ const Component = () => { const { findFirstFocusable } = useFocusFinders(); const gridRef = React.useRef(null); - const classes = useStyles(); React.useEffect(() => { if (gridRef.current) { @@ -108,7 +109,7 @@ const Component = () => { }, [findFirstFocusable, gridRef]); return ( -
+
...
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-styles.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-styles.mdx index 42d7dc05afc48..09379abb835a1 100755 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-styles.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/migrate-styles.mdx @@ -6,10 +6,21 @@ import { Meta } from '@storybook/addon-docs/blocks'; This guide provides instructions for migrate style overrides on react-northstar (v0) components to v9 components. -React-northstar provides a flexible styling and theming infrastructure and different consumers might have built their own way of adjusting the styles based on it. The component migration guides provide abstracted examples that show how to migrate one of the approaches used with v0 components to the v9 standardized `makeStyles` approach. +React-northstar provides a flexible styling and theming infrastructure and different consumers might have built their own way of adjusting the styles based on it. The component migration guides provide abstracted examples that show how to migrate one of the approaches used with v0 components to v9, where styling is ordinary CSS applied through the `className` prop. ## Overview -1. create/modify _COMPONENT_NAME.styles.ts_ file (COMPONENT_NAME is the name of your component file) -2. in _COMPONENT_NAME.styles.ts_, move your custom styles into hooks from `makeStyles` - > if this is your first time using `makeStyles`, please check [this guide on how to style components](http://aka.ms/fluentui-storybook/?path=/docs/concepts-developer-styling-components--docs) on fluentui docmentation site. +Create or modify a _COMPONENT_NAME.module.css_ file next to your component and move your custom +styles into it as plain CSS rules, referencing theme tokens as custom properties such as +`var(--color-neutral-foreground-1)`. Then import the generated class map and pass the class names +through the `className` prop, which is available on the component itself and on each of its slots. + +If this is your first time styling a v9 component, please check [this guide on how to style components](http://aka.ms/fluentui-storybook/?path=/docs/concepts-developer-styling-components--docs) on fluentui docmentation site. + +Your rules are unlayered and the component's live inside `@layer fui.*`, so yours win by default — +there is no merge helper to call and no argument order to get right. + +> The Griffel-based migration mixins (`flexItem`, `grid`, `input`, `slider`, `spinner` and the +> button icon mixins) were removed together with the `makeStyles` re-export in the same major +> version. The component migration guides now show the equivalent plain CSS for each former mixin; +> the CSS Module form above is what new code should use. diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Button.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Button.mdx index 46d9ac9774a7b..4ba431137b767 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Button.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Button.mdx @@ -65,7 +65,7 @@ To migrate a button replace the v8 usage with the corresponding v9 button varian | split | (use SplitButton) | | splitButtonAriaLabel | HTML aria-label | | splitButtonMenuProps | (use Menu and SplitButton) | -| styles | (use makeStyles and className) | +| styles | (use a CSS Module class via className) | | text | children | | theme | (use FluentProvider) | | toggle | (use ToggleButton) | diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Card/CardFooter.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Card/CardFooter.mdx index 61ad0904e5bd0..c7a81cc415566 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Card/CardFooter.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Card/CardFooter.mdx @@ -50,28 +50,35 @@ export const DocumentCardCompleteExample: React.FunctionComponent = () => ( After: +```css +/* CardFooter.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .card { + width: 300px; + } + + .actions { + display: flex; + align-items: center; + padding: 4px; + gap: 4px; + } +} +``` + ```jsx import * as React from 'react'; -import { Button, shorthands, makeStyles } from '@fluentui/react-components'; +import { Button } from '@fluentui/react-components'; import { Pin20Regular, Share20Regular, ServiceBell20Regular, Eye20Regular } from '@fluentui/react-icons'; import { Card, CardFooter } from '@fluentui/react-card/unstable'; -const useStyles = makeStyles({ - card: { - width: '300px', - }, - - actions: { - ...shorthands.gap('4px'), - ...shorthands.padding('4px'), - display: 'flex', - alignItems: 'center', - }, -}); +import styles from './CardFooter.module.css'; export const Default = () => { - const styles = useStyles(); - return ( +
{getDayName(calloutData.xAxisPoint)}
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Divider.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Divider.mdx index 2a958987cb99b..8cf672dab9286 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Divider.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Divider.mdx @@ -8,7 +8,7 @@ The v8 `Separator` component is called `Divider` in v9. Migration should be a straightforward name change of the component. v9 `Divider` supports the same `alignContent`, `children` and `vertical` props. -If you custom styled `Separator` then use `makeStyles` to set a `className` with `Divider`. +If you custom styled `Separator` then use a CSS Module class to set a `className` with `Divider`. Here is a vertical `Separator` with the content aligned at the end. @@ -28,11 +28,11 @@ Here is an equivalent `Divider`. ## Divider Props Mapping -| v8 | v9 | -| ------------ | ------------------------------ | -| alignContent | alignContent | -| | appearance | -| | inset | -| styles | (use makeStyles and className) | -| theme | (use FluentProvider) | -| vertical | vertical | +| v8 | v9 | +| ------------ | -------------------------------------- | +| alignContent | alignContent | +| | appearance | +| | inset | +| styles | (use a CSS Module class via className) | +| theme | (use FluentProvider) | +| vertical | vertical | diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.Stack.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.Stack.mdx index e04fd1917a2b5..faa3865bfb597 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.Stack.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.Stack.mdx @@ -17,47 +17,10 @@ Stack's approach to layout is slightly different from Northstar's Flex, and CSS ``` - - - ```js - makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - flexWrap: 'nowrap', - width: 'auto', - height: 'auto', - boxSizing: 'border-box', - '> *': { - textOverflow: 'ellipsis', - }, - '> :not(:first-child)': { - marginTop: '0px', - }, - '> *:not(.ms-StackItem)': { - flexShrink: 1, - }, - }, - }) - ``` - - - - - - - - ```html -
- ... -
- ``` - -
- + ```css - .flex { + .root { display: flex; flex-direction: column; flex-wrap: nowrap; @@ -65,13 +28,13 @@ Stack's approach to layout is slightly different from Northstar's Flex, and CSS height: auto; box-sizing: border-box; } - .flex > * { + .root > * { text-overflow: ellipsis; } - .flex > :not(:first-child) { + .root > :not(:first-child) { margin-top: 0px; } - .flex > *:not(.ms-StackItem) { + .root > *:not(.ms-StackItem) { flex-shrink: 1; } ``` @@ -102,19 +65,6 @@ The `as` property strictly replaces which element type will be rendered as the r ## disableShrink - - - ```js - makeStyles({ - root: { - '> *': { - flexShrink: 0, - } - }, - }) - ``` - - ```css @@ -137,19 +87,6 @@ MDN documentation: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - '> *': { - margin: '10px', - } - }, - }) - ``` - - ```css @@ -172,21 +109,6 @@ Heres an example with an horizontal layout and margin-right: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - display: 'flex', - flexDirection: 'row', - '> :not(:last-child)': { - marginRight: '10px', - } - }, - }) - ``` - - ```css @@ -208,19 +130,6 @@ If you do not have this limitation, you can use the following CSS: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - display: 'flex', - flexDirection: 'row', - gap: '10px', - }, - }) - ``` - - ```css @@ -246,19 +155,6 @@ MDN documentation: ## grow - - - ```js - makeStyles({ - root: { - '> *': { - flexGrow: 1, - } - }, - }) - ``` - - ```css @@ -279,17 +175,6 @@ MDN documentation: ## horizontal - - - ```js - makeStyles({ - root: { - flexDirection: 'row', - }, - }) - ``` - - ```css @@ -333,19 +218,6 @@ MDN documentation: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - '> *': { - maxHeight: '10px', - } - }, - }) - ``` - - ```css @@ -368,19 +240,6 @@ MDN documentation: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - '> *': { - maxWidth: '10px', - } - }, - }) - ``` - - ```css @@ -403,17 +262,6 @@ MDN documentation: _`10px` is used below as an example value_ - - - ```js - makeStyles({ - root: { - padding: '10px', - }, - }) - ``` - - ```css @@ -443,17 +291,6 @@ MDN documentation: Example usage for an horizontal layout: - - - ```js - makeStyles({ - root: { - flexDirection: 'row-reverse', - }, - }) - ``` - - ```css @@ -474,17 +311,6 @@ MDN documentation: ## verticalFill - - - ```js - makeStyles({ - root: { - height: '100%', - }, - }) - ``` - - ```css @@ -518,44 +344,10 @@ The `wrap` prop will also change the rendering. As such find below the styles an ``` - - - ```js - makeStyles({ - root: { - flexWrap: 'wrap', - overflow: 'visible', - height: '100%', - }, - inner: { - display: 'flex', - flexWrap: 'wrap', - overflow: 'visible', - boxSizing: 'border-box', - width: '100%', - maxWidth: '100vw', - } - }) - ``` - - - - - - - ```html -
-
- ... -
-
- ``` - -
- + ```css - .flex { + .root { flex-wrap: wrap; overflow: visible; height: 100%; diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.StackItem.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.StackItem.mdx index e6250aa4d5dba..2660b99cb67ed 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.StackItem.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/Flex.StackItem.mdx @@ -17,36 +17,10 @@ import { CodeComparison, CodeExample } from './utils.stories'; ``` - - - ```js - makeStyles({ - root: {...}, // Flex container - item: { - height: 'auto', - width: 'auto', - flexShrink: 1, - }, - }) - ``` - - -
- - - - - ```html -
-
Item
-
- ``` - -
- + ```css - .flex-item { + .root-item { height: auto; width: auto; flex-shrink: 1; @@ -71,17 +45,6 @@ MDN documentation: _`center` is used below as an example value_ - - - ```js - makeStyles({ - root: { - alignSelf: 'center' - }, - }) - ``` - - ```css @@ -108,17 +71,6 @@ This is just an override to apply the class name to the rendered node. ## disableShrink - - - ```js - makeStyles({ - root: { - flexShrink: 0, - }, - }) - ``` - - ```css @@ -139,17 +91,6 @@ MDN documentation: ## grow - - - ```js - makeStyles({ - root: { - flexGrow: 1, - }, - }) - ``` - - ```css @@ -172,17 +113,6 @@ MDN documentation: _`1` is used below as an example value_ - - - ```js - makeStyles({ - root: { - order: 1, - }, - }) - ``` - - ```css @@ -205,17 +135,6 @@ MDN documentation: _`1` is used below as an example value_ - - - ```js - makeStyles({ - root: { - flexShrink: 1, - }, - }) - ``` - - ```css @@ -236,17 +155,6 @@ MDN documentation: ## verticalFill - - - ```js - makeStyles({ - root: { - height: "100%", - }, - }) - ``` - - ```css diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.module.css new file mode 100644 index 0000000000000..cd540792933a1 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.module.css @@ -0,0 +1,15 @@ +/* Story styles for utils. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + justify-content: space-between; +} + +.root > * { + width: 100%; +} + +.root > :not(:last-child) { + margin-inline-end: 5%; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.stories.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.stories.tsx index 099437c9b3001..afb6d6c540a68 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Flex/utils.stories.tsx @@ -1,21 +1,9 @@ import * as React from 'react'; import { Source } from '@storybook/addon-docs/blocks'; -import { makeStyles } from '@fluentui/react-components'; - -const useCodeComparisonStyles = makeStyles({ - root: { - display: 'flex', - justifyContent: 'space-between', - - '> *': { - width: '100%', - }, - - '> :not(:last-child)': { - marginRight: '5%', - }, - }, -}); + +import styles from './utils.module.css'; + +const useCodeComparisonStyles = () => styles; export const CodeComparison = (props: { children: React.ReactElement[] }) => { const { children } = props; diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.module.css new file mode 100644 index 0000000000000..ad5e9905b1db1 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.module.css @@ -0,0 +1,65 @@ +/* Story styles for FitDifferences. + * Token references are the Fluent theme's CSS variables. */ + +.flexColumn { + display: flex; + flex-direction: column; +} + +.gridWrapper { + display: grid; + margin: 60px 0; + grid-template-columns: repeat(2, 150px); + justify-content: center; + gap: 20px; +} + +.box { + height: 150px; + width: 150px; + box-sizing: border-box; + border: 1px solid green; +} + +.imageBox { + border: 1px dashed red; +} + +.imageWrapperV9 { + display: flex; + flex-direction: column; +} + +.imageWrapperV8 { + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.imageV8 { + display: block; + position: absolute; + border-radius: var(--radius-circular); + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.flexCenter { + display: flex; + justify-content: center; + align-items: center; +} + +.auto { + height: auto; + width: auto; +} + +.caption { + font-size: 12px !important; + line-height: var(--leading-base-200); + margin-top: 8px !important; + text-align: center; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.styles.ts b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.styles.ts index f3c1b6d1b18c9..4508ba8cc331a 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.styles.ts +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.styles.ts @@ -1,57 +1,5 @@ -import { makeStyles, tokens } from '@fluentui/react-components'; +import styles from './FitDifferences.module.css'; -export const useStyles = makeStyles({ - flexColumn: { - display: 'flex', - flexDirection: 'column', - }, - gridWrapper: { - display: 'grid', - margin: '60px 0', - gridTemplateColumns: 'repeat(2, 150px)', - justifyContent: 'center', - gap: '20px', - }, - box: { - height: '150px', - width: '150px', - boxSizing: 'border-box', - border: '1px solid green', - }, - imageBox: { - border: '1px dashed red', - }, - imageWrapperV9: { - display: 'flex', - flexDirection: 'column', - }, - imageWrapperV8: { - display: 'flex', - flexDirection: 'column', - position: 'relative', - overflow: 'hidden', - }, - imageV8: { - display: 'block', - position: 'absolute', - borderRadius: `${tokens.borderRadiusCircular}`, - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - }, - flexCenter: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, - auto: { - height: 'auto', - width: 'auto', - }, - caption: { - fontSize: '12px !important', - lineHeight: tokens.lineHeightBase200, - marginTop: '8px !important', - textAlign: 'center', - }, -}); +/** Story-scaffolding classes (see the colocated .module.css). Kept hook-shaped so consuming + * components are untouched by the Griffel -> CSS Modules conversion. */ +export const useStyles = (): typeof styles => styles; diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.tsx index cac63917fcf11..ba3a55a5c7337 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Image/FitDifferences.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { Image as ImageV9 } from '@fluentui/react-components'; -import { mergeClasses } from '@fluentui/react-components'; import { useStyles } from './FitDifferences.styles'; export const FitDifferences = () => { @@ -9,9 +8,9 @@ export const FitDifferences = () => { return (
-
+
v8-example @@ -19,7 +18,7 @@ export const FitDifferences = () => {

100x100 v8 Image with border-radius style and imageFit.center

-
+
{

100x100 v9 Image with shape="circular" and fit="center"

-
+
v8-example @@ -41,7 +40,7 @@ export const FitDifferences = () => {

500x500 v8 Image with border-radius style and imageFit.center

-
+
({ - fadeIn400: { - animationName: { - from: { - opacity: 0, - }, - to: { - opacity: 1, - }, - }, - animationIterationCount: '1', - animationDuration: '0.367s', - }, -})); + +import styles from './Image.module.css'; const Component = () => { const [isLoaded, setLoaded] = useState(false); - const styles = useStyles(); return ( Example image setLoaded(true)} - className={!isLoaded ? styles.fadeIn400 : ''} + className={!isLoaded ? styles['fade-in'] : ''} /> ); }; @@ -96,20 +105,11 @@ Below is an example showcasing this: ```jsx import { useState } from 'react'; -import { makeStyles } from '@griffel/react'; import { Image } from '@fluentui/react-image'; -const useStyles = makeStyles({ - root: { - display: 'none', - }, -}); - const Component = () => { const [isLoaded, setLoaded] = useState(false); - const styles = useStyles(); - return ( { ### styles -For v9, this feature is no longer supported. The alternative is to apply styles through `makeStyles`. Below is an example of a migration: +For v9, this feature is no longer supported. The alternative is to apply styles through a CSS Module and `className`. Below is an example of a migration: #### v8 implementation @@ -143,18 +143,25 @@ const Component = () => { #### v9 implementation +```css +/* Image.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .bordered { + border: var(--stroke-width-thin) solid var(--color-neutral-stroke-2); + } +} +``` + ```jsx -import { Image, makeStyles, tokens } from '@fluentui/react-components'; +import { Image } from '@fluentui/react-components'; -const useStyles = makeStyles({ - bordered: { - border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke2}`, - }, -}); +import styles from './Image.module.css'; const Component = () => { - const styles = useStyles(); - return ; }; ``` @@ -163,20 +170,20 @@ const Component = () => { The table below presents a mapping of props between the v8 and v9 `Image` components in order to clarify which properties require changes. -| v8 | v9 | -| ------------------------ | -------------------------------------------------------------------------- | -| \`height\` | \`height\` | -| \`width\` | \`width\` | -| \`className\` | \`className\` | -| \`coverStyle\` | \`fit="cover"\` | -| \`imageFit\` | \`fit\` | -| \`maximizeFrame\` | \`block\`. | -| \`styles\` | \`className\` | -| \`loading\` | Not supported -> use onLoad and onError events | -| \`onLoadingStateChange\` | Not supported -> use onLoad and onError events | -| \`shouldFadeIn\` | Not supported -> implement animation via makeStyles and className + onLoad | -| \`shouldStartVisible\` | Not supported -> use onLoad and onError events | -| \`theme\` | Use \`FluentProvider\` to customize the theme | +| v8 | v9 | +| ------------------------ | -------------------------------------------------------------------- | +| \`height\` | \`height\` | +| \`width\` | \`width\` | +| \`className\` | \`className\` | +| \`coverStyle\` | \`fit="cover"\` | +| \`imageFit\` | \`fit\` | +| \`maximizeFrame\` | \`block\`. | +| \`styles\` | \`className\` | +| \`loading\` | Not supported -> use onLoad and onError events | +| \`onLoadingStateChange\` | Not supported -> use onLoad and onError events | +| \`shouldFadeIn\` | Not supported -> implement animation via a CSS Module class + onLoad | +| \`shouldStartVisible\` | Not supported -> use onLoad and onError events | +| \`theme\` | Use \`FluentProvider\` to customize the theme | In v9, the boolean props from [IImageStyleProps](https://developer.microsoft.com/en-us/fluentui#/controls/web/image#IImageStyleProps) — such as `isCenter`, `isCenterCover`, `isCenterContain`, `isContain`, `isLandscape`, `isNone`, `isError`, `isLoaded`, and `isNotImageFit` — are no longer supported. diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Input.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Input.mdx index 229120bcfaee7..606b965f0e77a 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Input.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Input.mdx @@ -38,30 +38,38 @@ const TextFieldV8BasicExample = () => { An equivalent `Input` usage is: +```css +/* Input.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .root { + max-width: 300px; + display: flex; + flex-direction: column; + } + + .root > * + * { + margin-top: 20px; + } + + .root > div { + display: flex; + flex-direction: column; + } +} +``` + ```tsx import * as React from 'react'; -import { makeStyles, Text, Label, Input } from '@fluentui/react-components'; +import { Text, Label, Input } from '@fluentui/react-components'; import { useId } from '@fluentui/react-utilities'; -const useStackStyles = makeStyles({ - root: { - maxWidth: '300px', - display: 'flex', - flexDirection: 'column', - - '> * + *': { - marginTop: '20px', - }, - - '> div': { - display: 'flex', - flexDirection: 'column', - }, - }, -}); +import styles from './Input.module.css'; const InputV9BasicExample = () => { - const stackStyles = useStackStyles(); const standardId = useId('standard'); const disabledId = useId('disabled'); const readonlyId = useId('readonly'); @@ -70,7 +78,7 @@ const InputV9BasicExample = () => { const errorMessageId = useId('error-message'); return ( -
+
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Menu.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Menu.mdx index b0c10cedd5035..038ad2ac9e993 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Menu.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Menu.mdx @@ -94,7 +94,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; | href | | | iconProps | icon (slot) | | inactive | (removed) | -| itemProps | (use makeStyles and className) | +| itemProps | (use a CSS Module class via className) | | items | children | | itemType | children (MenuDivider, MenuGroup, MenuGroupHeader, MenuSplitGroup) | | key | | @@ -112,7 +112,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; | secondaryText | secondaryContent (slot) | | sectionProps | (MenuGroup, MenuGroupHeader) | | split | children (MenuSplitGroup) | -| style | (use makeStyles and className) | +| style | (use a CSS Module class via className) | | subMenuIconProps | | | subMenuProps | children (Menu), hasSubmenu | | target | | diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/RadioGroup.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/RadioGroup.mdx index 875e1be0e5f29..edfe8382280e7 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/RadioGroup.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/RadioGroup.mdx @@ -100,38 +100,40 @@ const ChoiceGroupCustomOptionExample = () => { An equivalent `RadioGroup` implementation: +```css +/* RadioGroupCustomOption.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .icon-option { + display: flex; + align-items: center; + gap: 5px; + } + + .label { + display: flex; + gap: 5px; + } + + .green-border-option { + border: 1px solid green; + } +} +``` + ```tsx import * as React from 'react'; -import { makeStyles, shorthands, Label, Radio, RadioGroup } from '@fluentui/react-components'; +import { Label, Radio, RadioGroup } from '@fluentui/react-components'; import { useId } from '@fluentui/react-utilities'; import { AnimalCat24Regular } from '@fluentui/react-icons'; -const useIconOptionStyles = makeStyles({ - root: { - display: 'flex', - alignItems: 'center', - ...shorthands.gap('5px'), - }, -}); - -const useLabelStyles = makeStyles({ - root: { - display: 'flex', - ...shorthands.gap('5px'), - }, -}); - -const useGreenBorderOptionStyles = makeStyles({ - root: { - ...shorthands.border('1px', 'solid', 'green'), - }, -}); +import styles from './RadioGroupCustomOption.module.css'; const RadioGroupCustomOptionExample = () => { const labelId = useId('label'); - const iconOptionStyles = useIconOptionStyles(); - const labelStyles = useLabelStyles(); - const greenBorderOptionStyles = useGreenBorderOptionStyles(); return ( <> @@ -139,11 +141,11 @@ const RadioGroupCustomOptionExample = () => { Pick One -
+
A @@ -152,7 +154,7 @@ const RadioGroupCustomOptionExample = () => { }} />
- + diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Slider/Slider.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Slider/Slider.mdx index b3ad42a09c926..9cec1635bedc8 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Slider/Slider.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Slider/Slider.mdx @@ -46,9 +46,27 @@ export const V8FormattedValueExample = () => { An equivalent `Slider` in v9 is +```css +/* Slider.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .wrapper { + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + column-gap: var(--spacing-horizontal-l); + } +} +``` + ```tsx import * as React from 'react'; -import { Slider, SliderProps, Label, useId, makeStyles, tokens } from '@fluentui/react-components'; +import { Slider, SliderProps, Label, useId, tokens } from '@fluentui/react-components'; + +import styles from './Slider.module.css'; export const V9BasicExample = () => { const basicId = useId(); @@ -82,17 +100,7 @@ export const V9ControlledExample = () => { ); }; -const useGetFormattedExampleStyles = makeStyles({ - wrapper: { - display: 'grid', - alignItems: 'center', - gridTemplateColumns: '1fr auto', - columnGap: tokens.spacingHorizontalL, - }, -}); - export const V9FormattedValueExample = () => { - const styles = useGetFormattedExampleStyles(); const [sliderValue, setSliderValue] = React.useState(0); const sliderOnChange: SliderProps['onChange'] = (ev, data) => setSliderValue(data.value); const formattedId = useId(); diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/SpinButton.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/SpinButton.mdx index 428f3f50d4f7c..168116246c14f 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/SpinButton.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/SpinButton.mdx @@ -45,23 +45,32 @@ const SpinButtonV8BasicExample: React.FunctionComponent = () => { An equivalent `SpinButton` v9 usage is +```css +/* SpinButton.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .root { + display: flex; + flex-direction: column; + max-width: 300px; + } + + .root > label { + margin-bottom: 5px; + } +} +``` + ```tsx import * as React from 'react'; -import { makeStyles, Label, SpinButton } from '@fluentui/react-components'; +import { Label, SpinButton } from '@fluentui/react-components'; import type { SpinButtonChangeEvent, SpinButtonOnChangeData } from '@fluentui/react-components'; import { useId } from '@fluentui/react-utilities'; -const useLayoutStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - maxWidth: '300px', - - '> label': { - marginBottom: '5px', - }, - }, -}); +import styles from './SpinButton.module.css'; const getNumericPart = (value: string): number | undefined => { const valueRegex = /^(\d+(\.\d+)?).*/; @@ -74,7 +83,6 @@ const getNumericPart = (value: string): number | undefined => { const SpinButtonV9BasicExample = () => { const spinButtonId = useId('spinbutton'); - const layoutStyles = useLayoutStyles(); const [value, setValue] = React.useState(5); @@ -95,7 +103,7 @@ const SpinButtonV9BasicExample = () => { }; return ( -
+
@@ -182,23 +190,32 @@ const SpinButtonV8CustomSuffixBasicExample: React.FunctionComponent = () => { `SpinButton` v9 introduces a new prop called `displayValue` that may be used in conjunction with `value` to display a formatted value in `SpinButton`. To display a value with a custom suffix (or prefix or an entirely different name) just provide the `displayValue` prop to your `SpinButton`: +```css +/* SpinButton.module.css */ +@reference '#theme'; + +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +@layer fui.components.l4 { + .root { + display: flex; + flex-direction: column; + max-width: 300px; + } + + .root > label { + margin-bottom: 5px; + } +} +``` + ```tsx import * as React from 'react'; -import { makeStyles, Label, SpinButton } from '@fluentui/react-components'; +import { Label, SpinButton } from '@fluentui/react-components'; import type { SpinButtonChangeEvent, SpinButtonOnChangeData } from '@fluentui/react-components'; import { useId } from '@fluentui/react-utilities'; -const useLayoutStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - maxWidth: '300px', - - '> label': { - marginBottom: '5px', - }, - }, -}); +import styles from './SpinButton.module.css'; const suffix = 'cm'; const getNumericPart = (value: string): number | undefined => { @@ -212,7 +229,6 @@ const getNumericPart = (value: string): number | undefined => { const SpinButtonV9CustomSuffixBasicExample = () => { const spinButtonId = useId('spinbutton'); - const layoutStyles = useLayoutStyles(); const [value, setValue] = React.useState(7); const [displayValue, setDisplayValue] = React.useState(`7 ${suffix}`); @@ -240,7 +256,7 @@ const SpinButtonV9CustomSuffixBasicExample = () => { }; return ( -
+
{ ## Pivot -> TabList Props Mapping -| v8 | v9 | -| ------------------ | ------------------------------ | -| | appearance | -| componentRef | ref | -| className | className | -| defaultSelectedKey | defaultSelectedValue | -| focusZoneProps | (removed) | -| getTabId | (removed) | -| headersOnly | (removed) | -| linkFormat | (removed) | -| linkSize | size | -| onLinkClick | onTabSelect | -| overflowAriaLabel | (removed) | -| overflowBehavior | (removed) | -| selectedKey | selectedValue | -| styles | (use makeStyles and className) | -| theme | (use FluentProvider) | -| | vertical | +| v8 | v9 | +| ------------------ | -------------------------------------- | +| | appearance | +| componentRef | ref | +| className | className | +| defaultSelectedKey | defaultSelectedValue | +| focusZoneProps | (removed) | +| getTabId | (removed) | +| headersOnly | (removed) | +| linkFormat | (removed) | +| linkSize | size | +| onLinkClick | onTabSelect | +| overflowAriaLabel | (removed) | +| overflowBehavior | (removed) | +| selectedKey | selectedValue | +| styles | (use a CSS Module class via className) | +| theme | (use FluentProvider) | +| | vertical | ## PivotItem -> Tab Props Mapping diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Theme.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Theme.mdx index df6836e15364c..17763dfbc7cc3 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Theme.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Components/Theme.mdx @@ -14,7 +14,8 @@ You may decide you want components to all look like v8, all look like v9, or be ### ThemeProvider -> FluentProvider ThemeProvider sets theme values on the React context for use in CSS-in-JS. -FluentProvider applies theme values as CSS variables. +FluentProvider applies a theme class (`themeClassName`) — a static CSS class shipped by +`@fluentui/react-tailwind-theme` that defines every theme value as a CSS variable. This change from CSS-in-JS themes to CSS variable themes is the biggest difference between v8 and v9 themes and styles. Beyond consistency with the latest Fluent Design system, this change provides significant performance improvements and reduction of bundle sizes. @@ -48,21 +49,26 @@ The alias tokens define general purpose color, layout, and effect values useful For example, 'colorNeutralBackground1Hover'. A component like button would use this token when styling the background in the hover state. -### CSS-in-JS style objects -> makeStyles +### CSS-in-JS style objects -> CSS In v8, custom styles were created by defining style objects in Javascript. The types of style objects mapped closely to the structural hierarchy of the particular component. Style objects could be built by parameterized functions taking theme object or other state to produce the result at runtime. -In v9, makeStyles helps define static styles that can be optimized at build time. -The makeStyles function returns a hook that can be used during component render. +In v9, styles are just CSS. Write them wherever your application already writes CSS — a stylesheet, +a CSS Module, Tailwind — and reference theme values as custom properties +(`var(--color-neutral-background-1-hover)`). Nothing is computed at render time. -### mergeStyles -> mergeClasses +### mergeStyles -> the cascade In v8, mergeStyles combines a set of style objects to return a merged style object. The ordering of the parameters determines which styles overwrote preceding styles. -In v9, mergeClasses takes a list of class names from makeStyles and returns the class names optimized for de-duping and minimal bundle size. +In v9 there is no merge step at all. Class names are inert identifiers, and every property conflict +is resolved by the CSS cascade: layer first, then specificity, then source order. Fluent's own rules +live inside the `@layer fui.*` family, and unlayered author CSS beats every layer — so your rules +win without you doing anything. If you prefer to keep app styling inside the family, the +`fui.components.l3`–`l5` layers are reserved for consumers and are empty in the library. ### styles -> className @@ -70,18 +76,18 @@ In v8, the styles prop was provided on each Fluent UI component. Callers could pass a style object to customize the style of a component. In v9, there is no custom prop for styling. The React className prop is used. -Callers pass class names just like they would in any React code. -Class names are built using makeStyles and mergeClasses. +Callers pass class names just like they would in any React code — on the component, or on any of its +slots, each of which takes its own `className`. ## Creating compatible themes When you have v8 and v9 components running side-by-side, you will need to apply both a v8 theme and a v9 theme. Add a v9 `FluentProvider` each place you have a v8 `ThemeProvider`. -Pass each their respective theme instances. +Pass the `ThemeProvider` its v8 theme instance and the `FluentProvider` a v9 theme class via `themeClassName`. ### Default themes -If you are using the default light or dark theme from v8, you can use the v9 `webLightTheme` or `webDarkTheme`. +If you are using the default light or dark theme from v8, you can use the v9 `webLightThemeClassName` or `webDarkThemeClassName`. This will give you pretty close consistency especially if you have moved to v9 for all of one component type. For example, if all your buttons are v9 buttons then users likely won't notice small color differences between button and input components. @@ -89,7 +95,7 @@ If you have a mixture of v8 and v9 buttons then the differences are more noticea ### Custom themes -Both v8 and v9 have helper methods for creating custom themes. +Both v8 and v9 have helper methods for creating custom theme objects (in v9 they live in `@fluentui/tokens` and are build-time/tooling input). If you have custom v8 themes, you can define v9 themes with similar color sets. You can do this by mapping the theme colors from the `IPalette` to the `BrandVariants`. @@ -97,6 +103,10 @@ There are 7 more values in the `BrandVariants`, so you will need to expand the c If this is too tedious a process, you can use shim methods to more easily create compatible themes. +In v9 the resulting theme object is applied by converting it into a custom CSS theme class — a class +containing only custom-property declarations with the canonical kebab-case token names — and passing +that class to `FluentProvider`'s `themeClassName`. See [Theming](/docs/concepts-developer-theming--docs). + ### Compatible themes If you want to minimize visual differences, you can make the themes more alike. diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.module.css new file mode 100644 index 0000000000000..dba577a0fd7a8 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.module.css @@ -0,0 +1,19 @@ +/* Story styles for BrandColors. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.section { + padding: 25px; +} + +.comparisons { + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto; + gap: 10px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.stories.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.stories.tsx index c9eccc059f26d..8370efbd8e306 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/BrandColors.stories.tsx @@ -2,29 +2,9 @@ import * as React from 'react'; import { ColorCompare } from './ColorCompare'; import { darkThemeBrandMap, lightThemeBrandMap } from './themeMap'; -import { makeStyles, shorthands } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - }, - section: { - ...shorthands.padding('25px'), - }, - comparisons: { - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto', - columnGap: '10px', - rowGap: '10px', - }, -}); +import styles from './BrandColors.module.css'; export const BrandColors = () => { - const styles = useStyles(); - return (
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.module.css new file mode 100644 index 0000000000000..51e73653bd394 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.module.css @@ -0,0 +1,62 @@ +/* Story styles for ColorBlock. + * Token references are the Fluent theme's CSS variables. */ + +.root { + border-top-color: #aaa; + border-bottom-color: #aaa; + border-left-color: transparent; + border-right-color: #aaa; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto; + column-gap: 10px; + padding: 0 5px 0 0; + border-style: solid; + border-width: 1px; +} + +.flipAlign { + grid-template-columns: 1fr auto; + padding: 0 0 0 5px; + border-right: 1px solid transparent; + border-left: 1px solid #aaa; +} + +.color { + background-color: var(--ColorBlock__background-color); + width: 20px; + align-self: stretch; + border-right: 1px solid #aaa; + border-left: 1px solid #aaa; +} + +.names { + align-self: flex-start; + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto; + column-gap: 5px; + justify-self: flex-start; +} + +.names label { + text-align: right; + color: #aaa; + padding: 0; + margin: 0; +} + +.blockName { + font-weight: 700; +} + +.colorName { +} + +.colorValue { +} + +.comment { + color: green; + max-width: 250px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.styles.ts b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.styles.ts index 3c9611fd2aac5..3070aab271377 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.styles.ts +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.styles.ts @@ -1,51 +1,5 @@ -import { makeStyles, shorthands } from '@fluentui/react-components'; +import styles from './ColorBlock.module.css'; -export const useColorBlockStyles = makeStyles({ - root: { - ...shorthands.borderTop('1px', 'solid', '#aaa'), - ...shorthands.borderBottom('1px', 'solid', '#aaa'), - ...shorthands.borderLeft('1px', 'solid', 'transparent'), - ...shorthands.borderRight('1px', 'solid', '#aaa'), - display: 'grid', - gridTemplateColumns: 'auto 1fr', - gridTemplateRows: 'auto', - columnGap: '10px', - ...shorthands.padding('0', '5px', '0', '0'), - }, - flipAlign: { - ...shorthands.borderLeft('1px', 'solid', '#aaa'), - ...shorthands.borderRight('1px', 'solid', 'transparent'), - gridTemplateColumns: '1fr auto', - ...shorthands.padding('0', '0', '0', '5px'), - }, - color: { - backgroundColor: 'var(--ColorBlock__background-color)', - ...shorthands.borderLeft('1px', 'solid', '#aaa'), - ...shorthands.borderRight('1px', 'solid', '#aaa'), - width: '20px', - alignSelf: 'stretch', - }, - names: { - alignSelf: 'flex-start', - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto', - columnGap: '5px', - justifySelf: 'flex-start', - '& label': { - textAlign: 'right', - color: '#aaa', - ...shorthands.margin(0), - ...shorthands.padding(0), - }, - }, - blockName: { - fontWeight: '700', - }, - colorName: {}, - colorValue: {}, - comment: { - color: 'green', - maxWidth: '250px', - }, -}); +/** Story-scaffolding classes (see the colocated .module.css). Kept hook-shaped so consuming + * components are untouched by the Griffel -> CSS Modules conversion. */ +export const useColorBlockStyles = (): typeof styles => styles; diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.tsx index c551935e5ee07..b53a2d448f471 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/ColorBlock.tsx @@ -1,4 +1,3 @@ -import { mergeClasses } from '@fluentui/react-components'; import * as React from 'react'; import { ColorInfo } from './types'; @@ -50,7 +49,7 @@ export const ColorBlock = (props: Props) => { const styles = useColorBlockStyles(); const labels = getLabels(kind); - const className = mergeClasses(styles.root, flipAlign && styles.flipAlign); + const className = [styles.root, flipAlign && styles.flipAlign].filter(Boolean).join(' '); const blockColorStyle = { ['--ColorBlock__background-color' as any]: `${colorValue || 'tranparent'}`, diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.module.css new file mode 100644 index 0000000000000..574c1336a7a51 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.module.css @@ -0,0 +1,17 @@ +/* Story styles for NamedColors. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 25px; +} + +.comparisons { + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto; + column-gap: 10px; + row-gap: 5px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.stories.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.stories.tsx index 4697aef1f51f2..e37c67aaed1e2 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NamedColors.stories.tsx @@ -2,27 +2,9 @@ import * as React from 'react'; import { ColorCompare } from './ColorCompare'; import { namedColorMap } from './themeMap'; -import { makeStyles, shorthands } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { - ...shorthands.padding('25px'), - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - }, - comparisons: { - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto', - columnGap: '10px', - rowGap: '5px', - }, -}); +import styles from './NamedColors.module.css'; export const NamedColors = () => { - const styles = useStyles(); - return (

Named Colors

diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.module.css new file mode 100644 index 0000000000000..189cc31afe0bc --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.module.css @@ -0,0 +1,20 @@ +/* Story styles for NeutralColors. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.section { + padding: 25px; +} + +.comparisons { + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto; + column-gap: 10px; + row-gap: 5px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.stories.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.stories.tsx index 6707dc54a53f0..7d2c08448f172 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/NeutralColors.stories.tsx @@ -2,29 +2,9 @@ import * as React from 'react'; import { ColorCompare } from './ColorCompare'; import { lightThemeNeutralMap, darkThemeNeutralMap } from './themeMap'; -import { makeStyles, shorthands } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - }, - section: { - ...shorthands.padding('25px'), - }, - comparisons: { - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto', - columnGap: '10px', - rowGap: '5px', - }, -}); +import styles from './NeutralColors.module.css'; export const NeutralColors = () => { - const styles = useStyles(); - return (
diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.module.css b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.module.css new file mode 100644 index 0000000000000..72e8974594250 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.module.css @@ -0,0 +1,16 @@ +/* Story styles for SemanticColors. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 25px; +} + +.comparisons { + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto; + gap: 10px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.stories.tsx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.stories.tsx index 7e01479510568..015a797b4af46 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/ThemeColors/SemanticColors.stories.tsx @@ -2,27 +2,9 @@ import * as React from 'react'; import { ColorCompare } from './ColorCompare'; import { semanticToAliasMap } from './themeMap'; -import { makeStyles, shorthands } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { - ...shorthands.padding('25px'), - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - }, - comparisons: { - display: 'grid', - gridTemplateColumns: 'auto auto', - gridTemplateRows: 'auto', - columnGap: '10px', - rowGap: '10px', - }, -}); +import styles from './SemanticColors.module.css'; export const SemanticColors = () => { - const styles = useStyles(); - return (

Semantic Colors

diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Troubleshooting.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Troubleshooting.mdx index 6bddd64625a27..3dbd5fe479a98 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Troubleshooting.mdx +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV8/Troubleshooting.mdx @@ -5,20 +5,21 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## "I tried using the new components but there does not seem to be any styling applied to them. Am I doing something wrong?" Components in version 8 could be used in isolation and still retain their default styling. -For components to be styled in version 9, they must be wrapped with `FluentProvider` with a theme passed to it. +In version 9, styling comes from CSS: import `@fluentui/react-tailwind-theme/styles.css` once per document. The stylesheet applies the Web Light theme by default; to use another theme, wrap your app with `FluentProvider` and pass a theme class to `themeClassName`. -We recommend placing the `FluentProvider` at the root of your app so that everything gets styled appropriately. The theme to use if you are coming from version 8 is `webLightTheme`. +We recommend placing the `FluentProvider` at the root of your app so that everything gets styled appropriately (including portals). The theme class to use if you are coming from version 8 is `webLightThemeClassName`. ```jsx import React from 'react'; import ReactDOMClient from 'react-dom/client'; -import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import '@fluentui/react-tailwind-theme/styles.css'; +import { FluentProvider, webLightThemeClassName } from '@fluentui/react-components'; import App from './App'; const root = ReactDOMClient.createRoot(document.getElementById('root')); root.render( - + , ); @@ -74,7 +75,7 @@ We are modernizing our components in version 9 to adhere to the latest guideline The styling story in version 9 is very different to what existed in version 8. For one, we are moving away from providing a `styles` prop and are mostly focusing of providing styling via the `className` prop (if you are worried about how could you style every piece of a component via `className`, do not fret and keep reading). -We are also moving away from `mergeStyles` in favor of `makeStyles` from [`@griffel/react`](https://github.com/microsoft/griffel), a new in-house CSS-in-JS solution that allows for things like atomic classes and build-time optimization of styles. +We are also moving away from `mergeStyles`. There is no replacement API to learn: v9 components ship precompiled CSS inside the `@layer fui.*` family, so a rule you write in your own stylesheet already outranks the component's without any merging step, ordering trick or `!important`. If you want to learn more, read our [guide on how to style components](?path=/story/concepts-developer-styling-components--docs). diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV9/TokenRename.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV9/TokenRename.mdx new file mode 100644 index 0000000000000..6646b03db0a72 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV9/TokenRename.mdx @@ -0,0 +1,591 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Token rename: camelCase → kebab-case + +All **467** Fluent design tokens changed their **CSS custom property** name in this +major release. The JavaScript `tokens` object is unaffected — same key names, updated values. + +## Read this first: it fails silently + +A `var()` that references an undefined custom property does not throw, does not warn, and does not +log. The declaration becomes invalid at computed-value time and the property falls back to its +inherited or initial value. Your app keeps running and renders something plausible. + +```css +/* Your stylesheet, unchanged. No error anywhere. */ +.my-card { + color: var(--colorNeutralForeground1); /* ← undefined now: this line does nothing */ + padding: var(--spacingHorizontalM); /* ← undefined now: padding collapses to 0 */ +} +``` + +That is why this page exists as a mechanical find/replace rather than a "look out for" note. Run the +script below over your sources; do not rely on visual review. + +## What did not change + +```js +import { tokens } from '@fluentui/react-components'; + +tokens.colorNeutralForeground1; // key unchanged; now returns 'var(--color-neutral-foreground-1)' +``` + +Anything reading tokens through the `tokens` object — inline `style` props, canvas fills, +third-party colour APIs — needs **no change at all**. Only literal `--name` text needs rewriting: +hand-written `.css` / `.scss`, `styled`/template-literal CSS, and anywhere you built a variable +name as a string. + +## The naming rule + +Names were aligned to Tailwind v4's theme namespaces, which is why some families also changed prefix +(`--borderRadius*` → `--radius-*`, `--fontSize*` → `--text-*`, +`--lineHeight*` → `--leading-*`, `--curve*` → `--ease-*`). + +| Tailwind namespace | Tokens | Example | +| ------------------ | -----: | ----------------------------------------------------------------- | +| `--color-*` | 366 | `--colorNeutralForeground1` → `--color-neutral-foreground-1` | +| `--spacing-*` | 22 | `--spacingHorizontalNone` → `--spacing-horizontal-none` | +| `--shadow-*` | 12 | `--shadow2` → `--shadow-2` | +| `--radius-*` | 11 | `--borderRadiusNone` → `--radius-none` | +| `--text-*` | 10 | `--fontSizeBase100` → `--text-base-100` | +| `--leading-*` | 10 | `--lineHeightBase100` → `--leading-base-100` | +| `--ease-*` | 9 | `--curveAccelerateMax` → `--ease-accelerate-max` | +| `--duration-*` | 8 | `--durationUltraFast` → `--duration-ultra-fast` | +| `--z-*` | 8 | `--zIndexBackground` → `--z-index-background` | +| `--font-*` | 7 | `--fontFamilyBase` → `--font-base` | +| `--stroke-*` | 4 | `--strokeWidthThin` → `--stroke-width-thin` | + +## Find and replace + +The complete map ships in this repository as machine-readable JSON at +`migration/griffel-to-tailwind/reports/token-rename-map.json`. This script applies it to a source +tree. Two details matter and both are in the map's own description: a `(?![A-Za-z0-9-])` boundary, +so `--colorNeutralForeground1` never rewrites the prefix of `--colorNeutralForeground1Hover`; and +longest-name-first ordering as a second guard on the same hazard. + +```js +// rename-fluent-tokens.mjs — node rename-fluent-tokens.mjs ./src +import fs from 'node:fs'; +import path from 'node:path'; + +const map = JSON.parse(fs.readFileSync('./token-rename-map.json', 'utf8')); +const pairs = map.tokens.map(t => [t.oldVar, t.newVar]).sort((a, b) => b[0].length - a[0].length); // longest first + +const EXTS = new Set(['.css', '.scss', '.sass', '.less', '.ts', '.tsx', '.js', '.jsx', '.mdx', '.html']); + +let changedFiles = 0; +let changedRefs = 0; + +const walk = dir => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; + const p = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(p); + continue; + } + if (!EXTS.has(path.extname(entry.name))) continue; + + const before = fs.readFileSync(p, 'utf8'); + let after = before; + for (const [oldVar, newVar] of pairs) { + const re = new RegExp(oldVar + '(?![A-Za-z0-9-])', 'g'); + after = after.replace(re, m => (changedRefs++, newVar)); + } + if (after !== before) { + fs.writeFileSync(p, after); + changedFiles++; + } + } +}; + +walk(process.argv[2] ?? './src'); +console.log(`rewrote ${changedRefs} references across ${changedFiles} files`); +``` + +**Check for leftovers afterwards.** Any surviving `--` name in camelCase is either a Fluent token the +script missed or one of your own: + +```sh +grep -rnE -- '--[a-z]+[A-Z]' src/ +``` + +> **One exception you may hit:** `@fluentui/web-components` and `@fluentui/chart-web-components` +> are a separate system that defines its **own** camelCase variables. Those are not Fluent React +> tokens and must be left alone — exclude those paths if your app uses both. + +## The complete map + +467 tokens, alphabetical by JS name. + +| Token (JS name) | Old CSS variable | New CSS variable | +| -------------------------------------------- | ---------------------------------------------- | --------------------------------------------------- | +| `borderRadius2XLarge` | `--borderRadius2XLarge` | `--radius-2-x-large` | +| `borderRadius3XLarge` | `--borderRadius3XLarge` | `--radius-3-x-large` | +| `borderRadius4XLarge` | `--borderRadius4XLarge` | `--radius-4-x-large` | +| `borderRadius5XLarge` | `--borderRadius5XLarge` | `--radius-5-x-large` | +| `borderRadius6XLarge` | `--borderRadius6XLarge` | `--radius-6-x-large` | +| `borderRadiusCircular` | `--borderRadiusCircular` | `--radius-circular` | +| `borderRadiusLarge` | `--borderRadiusLarge` | `--radius-large` | +| `borderRadiusMedium` | `--borderRadiusMedium` | `--radius-medium` | +| `borderRadiusNone` | `--borderRadiusNone` | `--radius-none` | +| `borderRadiusSmall` | `--borderRadiusSmall` | `--radius-small` | +| `borderRadiusXLarge` | `--borderRadiusXLarge` | `--radius-x-large` | +| `colorBackgroundOverlay` | `--colorBackgroundOverlay` | `--color-background-overlay` | +| `colorBrandBackground` | `--colorBrandBackground` | `--color-brand-background` | +| `colorBrandBackground2` | `--colorBrandBackground2` | `--color-brand-background-2` | +| `colorBrandBackground2Hover` | `--colorBrandBackground2Hover` | `--color-brand-background-2-hover` | +| `colorBrandBackground2Pressed` | `--colorBrandBackground2Pressed` | `--color-brand-background-2-pressed` | +| `colorBrandBackground3Static` | `--colorBrandBackground3Static` | `--color-brand-background-3-static` | +| `colorBrandBackground4Static` | `--colorBrandBackground4Static` | `--color-brand-background-4-static` | +| `colorBrandBackgroundHover` | `--colorBrandBackgroundHover` | `--color-brand-background-hover` | +| `colorBrandBackgroundInverted` | `--colorBrandBackgroundInverted` | `--color-brand-background-inverted` | +| `colorBrandBackgroundInvertedHover` | `--colorBrandBackgroundInvertedHover` | `--color-brand-background-inverted-hover` | +| `colorBrandBackgroundInvertedPressed` | `--colorBrandBackgroundInvertedPressed` | `--color-brand-background-inverted-pressed` | +| `colorBrandBackgroundInvertedSelected` | `--colorBrandBackgroundInvertedSelected` | `--color-brand-background-inverted-selected` | +| `colorBrandBackgroundPressed` | `--colorBrandBackgroundPressed` | `--color-brand-background-pressed` | +| `colorBrandBackgroundSelected` | `--colorBrandBackgroundSelected` | `--color-brand-background-selected` | +| `colorBrandBackgroundStatic` | `--colorBrandBackgroundStatic` | `--color-brand-background-static` | +| `colorBrandForeground1` | `--colorBrandForeground1` | `--color-brand-foreground-1` | +| `colorBrandForeground2` | `--colorBrandForeground2` | `--color-brand-foreground-2` | +| `colorBrandForeground2Hover` | `--colorBrandForeground2Hover` | `--color-brand-foreground-2-hover` | +| `colorBrandForeground2Pressed` | `--colorBrandForeground2Pressed` | `--color-brand-foreground-2-pressed` | +| `colorBrandForegroundInverted` | `--colorBrandForegroundInverted` | `--color-brand-foreground-inverted` | +| `colorBrandForegroundInvertedHover` | `--colorBrandForegroundInvertedHover` | `--color-brand-foreground-inverted-hover` | +| `colorBrandForegroundInvertedPressed` | `--colorBrandForegroundInvertedPressed` | `--color-brand-foreground-inverted-pressed` | +| `colorBrandForegroundLink` | `--colorBrandForegroundLink` | `--color-brand-foreground-link` | +| `colorBrandForegroundLinkHover` | `--colorBrandForegroundLinkHover` | `--color-brand-foreground-link-hover` | +| `colorBrandForegroundLinkPressed` | `--colorBrandForegroundLinkPressed` | `--color-brand-foreground-link-pressed` | +| `colorBrandForegroundLinkSelected` | `--colorBrandForegroundLinkSelected` | `--color-brand-foreground-link-selected` | +| `colorBrandForegroundOnLight` | `--colorBrandForegroundOnLight` | `--color-brand-foreground-on-light` | +| `colorBrandForegroundOnLightHover` | `--colorBrandForegroundOnLightHover` | `--color-brand-foreground-on-light-hover` | +| `colorBrandForegroundOnLightPressed` | `--colorBrandForegroundOnLightPressed` | `--color-brand-foreground-on-light-pressed` | +| `colorBrandForegroundOnLightSelected` | `--colorBrandForegroundOnLightSelected` | `--color-brand-foreground-on-light-selected` | +| `colorBrandShadowAmbient` | `--colorBrandShadowAmbient` | `--color-brand-shadow-ambient` | +| `colorBrandShadowKey` | `--colorBrandShadowKey` | `--color-brand-shadow-key` | +| `colorBrandStroke1` | `--colorBrandStroke1` | `--color-brand-stroke-1` | +| `colorBrandStroke2` | `--colorBrandStroke2` | `--color-brand-stroke-2` | +| `colorBrandStroke2Contrast` | `--colorBrandStroke2Contrast` | `--color-brand-stroke-2-contrast` | +| `colorBrandStroke2Hover` | `--colorBrandStroke2Hover` | `--color-brand-stroke-2-hover` | +| `colorBrandStroke2Pressed` | `--colorBrandStroke2Pressed` | `--color-brand-stroke-2-pressed` | +| `colorCompoundBrandBackground` | `--colorCompoundBrandBackground` | `--color-compound-brand-background` | +| `colorCompoundBrandBackgroundHover` | `--colorCompoundBrandBackgroundHover` | `--color-compound-brand-background-hover` | +| `colorCompoundBrandBackgroundPressed` | `--colorCompoundBrandBackgroundPressed` | `--color-compound-brand-background-pressed` | +| `colorCompoundBrandForeground1` | `--colorCompoundBrandForeground1` | `--color-compound-brand-foreground-1` | +| `colorCompoundBrandForeground1Hover` | `--colorCompoundBrandForeground1Hover` | `--color-compound-brand-foreground-1-hover` | +| `colorCompoundBrandForeground1Pressed` | `--colorCompoundBrandForeground1Pressed` | `--color-compound-brand-foreground-1-pressed` | +| `colorCompoundBrandStroke` | `--colorCompoundBrandStroke` | `--color-compound-brand-stroke` | +| `colorCompoundBrandStrokeHover` | `--colorCompoundBrandStrokeHover` | `--color-compound-brand-stroke-hover` | +| `colorCompoundBrandStrokePressed` | `--colorCompoundBrandStrokePressed` | `--color-compound-brand-stroke-pressed` | +| `colorNeutralBackground1` | `--colorNeutralBackground1` | `--color-neutral-background-1` | +| `colorNeutralBackground1Hover` | `--colorNeutralBackground1Hover` | `--color-neutral-background-1-hover` | +| `colorNeutralBackground1Pressed` | `--colorNeutralBackground1Pressed` | `--color-neutral-background-1-pressed` | +| `colorNeutralBackground1Selected` | `--colorNeutralBackground1Selected` | `--color-neutral-background-1-selected` | +| `colorNeutralBackground2` | `--colorNeutralBackground2` | `--color-neutral-background-2` | +| `colorNeutralBackground2Hover` | `--colorNeutralBackground2Hover` | `--color-neutral-background-2-hover` | +| `colorNeutralBackground2Pressed` | `--colorNeutralBackground2Pressed` | `--color-neutral-background-2-pressed` | +| `colorNeutralBackground2Selected` | `--colorNeutralBackground2Selected` | `--color-neutral-background-2-selected` | +| `colorNeutralBackground3` | `--colorNeutralBackground3` | `--color-neutral-background-3` | +| `colorNeutralBackground3Hover` | `--colorNeutralBackground3Hover` | `--color-neutral-background-3-hover` | +| `colorNeutralBackground3Pressed` | `--colorNeutralBackground3Pressed` | `--color-neutral-background-3-pressed` | +| `colorNeutralBackground3Selected` | `--colorNeutralBackground3Selected` | `--color-neutral-background-3-selected` | +| `colorNeutralBackground4` | `--colorNeutralBackground4` | `--color-neutral-background-4` | +| `colorNeutralBackground4Hover` | `--colorNeutralBackground4Hover` | `--color-neutral-background-4-hover` | +| `colorNeutralBackground4Pressed` | `--colorNeutralBackground4Pressed` | `--color-neutral-background-4-pressed` | +| `colorNeutralBackground4Selected` | `--colorNeutralBackground4Selected` | `--color-neutral-background-4-selected` | +| `colorNeutralBackground5` | `--colorNeutralBackground5` | `--color-neutral-background-5` | +| `colorNeutralBackground5Hover` | `--colorNeutralBackground5Hover` | `--color-neutral-background-5-hover` | +| `colorNeutralBackground5Pressed` | `--colorNeutralBackground5Pressed` | `--color-neutral-background-5-pressed` | +| `colorNeutralBackground5Selected` | `--colorNeutralBackground5Selected` | `--color-neutral-background-5-selected` | +| `colorNeutralBackground6` | `--colorNeutralBackground6` | `--color-neutral-background-6` | +| `colorNeutralBackground7` | `--colorNeutralBackground7` | `--color-neutral-background-7` | +| `colorNeutralBackground7Hover` | `--colorNeutralBackground7Hover` | `--color-neutral-background-7-hover` | +| `colorNeutralBackground7Pressed` | `--colorNeutralBackground7Pressed` | `--color-neutral-background-7-pressed` | +| `colorNeutralBackground7Selected` | `--colorNeutralBackground7Selected` | `--color-neutral-background-7-selected` | +| `colorNeutralBackground8` | `--colorNeutralBackground8` | `--color-neutral-background-8` | +| `colorNeutralBackgroundAlpha` | `--colorNeutralBackgroundAlpha` | `--color-neutral-background-alpha` | +| `colorNeutralBackgroundAlpha2` | `--colorNeutralBackgroundAlpha2` | `--color-neutral-background-alpha-2` | +| `colorNeutralBackgroundDisabled` | `--colorNeutralBackgroundDisabled` | `--color-neutral-background-disabled` | +| `colorNeutralBackgroundDisabled2` | `--colorNeutralBackgroundDisabled2` | `--color-neutral-background-disabled-2` | +| `colorNeutralBackgroundInverted` | `--colorNeutralBackgroundInverted` | `--color-neutral-background-inverted` | +| `colorNeutralBackgroundInvertedDisabled` | `--colorNeutralBackgroundInvertedDisabled` | `--color-neutral-background-inverted-disabled` | +| `colorNeutralBackgroundInvertedHover` | `--colorNeutralBackgroundInvertedHover` | `--color-neutral-background-inverted-hover` | +| `colorNeutralBackgroundInvertedPressed` | `--colorNeutralBackgroundInvertedPressed` | `--color-neutral-background-inverted-pressed` | +| `colorNeutralBackgroundInvertedSelected` | `--colorNeutralBackgroundInvertedSelected` | `--color-neutral-background-inverted-selected` | +| `colorNeutralBackgroundStatic` | `--colorNeutralBackgroundStatic` | `--color-neutral-background-static` | +| `colorNeutralCardBackground` | `--colorNeutralCardBackground` | `--color-neutral-card-background` | +| `colorNeutralCardBackgroundDisabled` | `--colorNeutralCardBackgroundDisabled` | `--color-neutral-card-background-disabled` | +| `colorNeutralCardBackgroundHover` | `--colorNeutralCardBackgroundHover` | `--color-neutral-card-background-hover` | +| `colorNeutralCardBackgroundPressed` | `--colorNeutralCardBackgroundPressed` | `--color-neutral-card-background-pressed` | +| `colorNeutralCardBackgroundSelected` | `--colorNeutralCardBackgroundSelected` | `--color-neutral-card-background-selected` | +| `colorNeutralForeground1` | `--colorNeutralForeground1` | `--color-neutral-foreground-1` | +| `colorNeutralForeground1Hover` | `--colorNeutralForeground1Hover` | `--color-neutral-foreground-1-hover` | +| `colorNeutralForeground1Pressed` | `--colorNeutralForeground1Pressed` | `--color-neutral-foreground-1-pressed` | +| `colorNeutralForeground1Selected` | `--colorNeutralForeground1Selected` | `--color-neutral-foreground-1-selected` | +| `colorNeutralForeground1Static` | `--colorNeutralForeground1Static` | `--color-neutral-foreground-1-static` | +| `colorNeutralForeground2` | `--colorNeutralForeground2` | `--color-neutral-foreground-2` | +| `colorNeutralForeground2BrandHover` | `--colorNeutralForeground2BrandHover` | `--color-neutral-foreground-2-brand-hover` | +| `colorNeutralForeground2BrandPressed` | `--colorNeutralForeground2BrandPressed` | `--color-neutral-foreground-2-brand-pressed` | +| `colorNeutralForeground2BrandSelected` | `--colorNeutralForeground2BrandSelected` | `--color-neutral-foreground-2-brand-selected` | +| `colorNeutralForeground2Hover` | `--colorNeutralForeground2Hover` | `--color-neutral-foreground-2-hover` | +| `colorNeutralForeground2Link` | `--colorNeutralForeground2Link` | `--color-neutral-foreground-2-link` | +| `colorNeutralForeground2LinkHover` | `--colorNeutralForeground2LinkHover` | `--color-neutral-foreground-2-link-hover` | +| `colorNeutralForeground2LinkPressed` | `--colorNeutralForeground2LinkPressed` | `--color-neutral-foreground-2-link-pressed` | +| `colorNeutralForeground2LinkSelected` | `--colorNeutralForeground2LinkSelected` | `--color-neutral-foreground-2-link-selected` | +| `colorNeutralForeground2Pressed` | `--colorNeutralForeground2Pressed` | `--color-neutral-foreground-2-pressed` | +| `colorNeutralForeground2Selected` | `--colorNeutralForeground2Selected` | `--color-neutral-foreground-2-selected` | +| `colorNeutralForeground3` | `--colorNeutralForeground3` | `--color-neutral-foreground-3` | +| `colorNeutralForeground3BrandHover` | `--colorNeutralForeground3BrandHover` | `--color-neutral-foreground-3-brand-hover` | +| `colorNeutralForeground3BrandPressed` | `--colorNeutralForeground3BrandPressed` | `--color-neutral-foreground-3-brand-pressed` | +| `colorNeutralForeground3BrandSelected` | `--colorNeutralForeground3BrandSelected` | `--color-neutral-foreground-3-brand-selected` | +| `colorNeutralForeground3Hover` | `--colorNeutralForeground3Hover` | `--color-neutral-foreground-3-hover` | +| `colorNeutralForeground3Pressed` | `--colorNeutralForeground3Pressed` | `--color-neutral-foreground-3-pressed` | +| `colorNeutralForeground3Selected` | `--colorNeutralForeground3Selected` | `--color-neutral-foreground-3-selected` | +| `colorNeutralForeground4` | `--colorNeutralForeground4` | `--color-neutral-foreground-4` | +| `colorNeutralForeground5` | `--colorNeutralForeground5` | `--color-neutral-foreground-5` | +| `colorNeutralForeground5Hover` | `--colorNeutralForeground5Hover` | `--color-neutral-foreground-5-hover` | +| `colorNeutralForeground5Pressed` | `--colorNeutralForeground5Pressed` | `--color-neutral-foreground-5-pressed` | +| `colorNeutralForeground5Selected` | `--colorNeutralForeground5Selected` | `--color-neutral-foreground-5-selected` | +| `colorNeutralForegroundDisabled` | `--colorNeutralForegroundDisabled` | `--color-neutral-foreground-disabled` | +| `colorNeutralForegroundInverted` | `--colorNeutralForegroundInverted` | `--color-neutral-foreground-inverted` | +| `colorNeutralForegroundInverted2` | `--colorNeutralForegroundInverted2` | `--color-neutral-foreground-inverted-2` | +| `colorNeutralForegroundInvertedDisabled` | `--colorNeutralForegroundInvertedDisabled` | `--color-neutral-foreground-inverted-disabled` | +| `colorNeutralForegroundInvertedHover` | `--colorNeutralForegroundInvertedHover` | `--color-neutral-foreground-inverted-hover` | +| `colorNeutralForegroundInvertedLink` | `--colorNeutralForegroundInvertedLink` | `--color-neutral-foreground-inverted-link` | +| `colorNeutralForegroundInvertedLinkHover` | `--colorNeutralForegroundInvertedLinkHover` | `--color-neutral-foreground-inverted-link-hover` | +| `colorNeutralForegroundInvertedLinkPressed` | `--colorNeutralForegroundInvertedLinkPressed` | `--color-neutral-foreground-inverted-link-pressed` | +| `colorNeutralForegroundInvertedLinkSelected` | `--colorNeutralForegroundInvertedLinkSelected` | `--color-neutral-foreground-inverted-link-selected` | +| `colorNeutralForegroundInvertedPressed` | `--colorNeutralForegroundInvertedPressed` | `--color-neutral-foreground-inverted-pressed` | +| `colorNeutralForegroundInvertedSelected` | `--colorNeutralForegroundInvertedSelected` | `--color-neutral-foreground-inverted-selected` | +| `colorNeutralForegroundOnBrand` | `--colorNeutralForegroundOnBrand` | `--color-neutral-foreground-on-brand` | +| `colorNeutralForegroundStaticInverted` | `--colorNeutralForegroundStaticInverted` | `--color-neutral-foreground-static-inverted` | +| `colorNeutralShadowAmbient` | `--colorNeutralShadowAmbient` | `--color-neutral-shadow-ambient` | +| `colorNeutralShadowAmbientDarker` | `--colorNeutralShadowAmbientDarker` | `--color-neutral-shadow-ambient-darker` | +| `colorNeutralShadowAmbientLighter` | `--colorNeutralShadowAmbientLighter` | `--color-neutral-shadow-ambient-lighter` | +| `colorNeutralShadowKey` | `--colorNeutralShadowKey` | `--color-neutral-shadow-key` | +| `colorNeutralShadowKeyDarker` | `--colorNeutralShadowKeyDarker` | `--color-neutral-shadow-key-darker` | +| `colorNeutralShadowKeyLighter` | `--colorNeutralShadowKeyLighter` | `--color-neutral-shadow-key-lighter` | +| `colorNeutralStencil1` | `--colorNeutralStencil1` | `--color-neutral-stencil-1` | +| `colorNeutralStencil1Alpha` | `--colorNeutralStencil1Alpha` | `--color-neutral-stencil-1-alpha` | +| `colorNeutralStencil2` | `--colorNeutralStencil2` | `--color-neutral-stencil-2` | +| `colorNeutralStencil2Alpha` | `--colorNeutralStencil2Alpha` | `--color-neutral-stencil-2-alpha` | +| `colorNeutralStroke1` | `--colorNeutralStroke1` | `--color-neutral-stroke-1` | +| `colorNeutralStroke1Hover` | `--colorNeutralStroke1Hover` | `--color-neutral-stroke-1-hover` | +| `colorNeutralStroke1Pressed` | `--colorNeutralStroke1Pressed` | `--color-neutral-stroke-1-pressed` | +| `colorNeutralStroke1Selected` | `--colorNeutralStroke1Selected` | `--color-neutral-stroke-1-selected` | +| `colorNeutralStroke2` | `--colorNeutralStroke2` | `--color-neutral-stroke-2` | +| `colorNeutralStroke3` | `--colorNeutralStroke3` | `--color-neutral-stroke-3` | +| `colorNeutralStroke4` | `--colorNeutralStroke4` | `--color-neutral-stroke-4` | +| `colorNeutralStroke4Hover` | `--colorNeutralStroke4Hover` | `--color-neutral-stroke-4-hover` | +| `colorNeutralStroke4Pressed` | `--colorNeutralStroke4Pressed` | `--color-neutral-stroke-4-pressed` | +| `colorNeutralStroke4Selected` | `--colorNeutralStroke4Selected` | `--color-neutral-stroke-4-selected` | +| `colorNeutralStrokeAccessible` | `--colorNeutralStrokeAccessible` | `--color-neutral-stroke-accessible` | +| `colorNeutralStrokeAccessibleHover` | `--colorNeutralStrokeAccessibleHover` | `--color-neutral-stroke-accessible-hover` | +| `colorNeutralStrokeAccessiblePressed` | `--colorNeutralStrokeAccessiblePressed` | `--color-neutral-stroke-accessible-pressed` | +| `colorNeutralStrokeAccessibleSelected` | `--colorNeutralStrokeAccessibleSelected` | `--color-neutral-stroke-accessible-selected` | +| `colorNeutralStrokeAlpha` | `--colorNeutralStrokeAlpha` | `--color-neutral-stroke-alpha` | +| `colorNeutralStrokeAlpha2` | `--colorNeutralStrokeAlpha2` | `--color-neutral-stroke-alpha-2` | +| `colorNeutralStrokeDisabled` | `--colorNeutralStrokeDisabled` | `--color-neutral-stroke-disabled` | +| `colorNeutralStrokeDisabled2` | `--colorNeutralStrokeDisabled2` | `--color-neutral-stroke-disabled-2` | +| `colorNeutralStrokeInvertedDisabled` | `--colorNeutralStrokeInvertedDisabled` | `--color-neutral-stroke-inverted-disabled` | +| `colorNeutralStrokeOnBrand` | `--colorNeutralStrokeOnBrand` | `--color-neutral-stroke-on-brand` | +| `colorNeutralStrokeOnBrand2` | `--colorNeutralStrokeOnBrand2` | `--color-neutral-stroke-on-brand-2` | +| `colorNeutralStrokeOnBrand2Hover` | `--colorNeutralStrokeOnBrand2Hover` | `--color-neutral-stroke-on-brand-2-hover` | +| `colorNeutralStrokeOnBrand2Pressed` | `--colorNeutralStrokeOnBrand2Pressed` | `--color-neutral-stroke-on-brand-2-pressed` | +| `colorNeutralStrokeOnBrand2Selected` | `--colorNeutralStrokeOnBrand2Selected` | `--color-neutral-stroke-on-brand-2-selected` | +| `colorNeutralStrokeSubtle` | `--colorNeutralStrokeSubtle` | `--color-neutral-stroke-subtle` | +| `colorPaletteAnchorBackground2` | `--colorPaletteAnchorBackground2` | `--color-palette-anchor-background-2` | +| `colorPaletteAnchorBorderActive` | `--colorPaletteAnchorBorderActive` | `--color-palette-anchor-border-active` | +| `colorPaletteAnchorForeground2` | `--colorPaletteAnchorForeground2` | `--color-palette-anchor-foreground-2` | +| `colorPaletteBeigeBackground2` | `--colorPaletteBeigeBackground2` | `--color-palette-beige-background-2` | +| `colorPaletteBeigeBorderActive` | `--colorPaletteBeigeBorderActive` | `--color-palette-beige-border-active` | +| `colorPaletteBeigeForeground2` | `--colorPaletteBeigeForeground2` | `--color-palette-beige-foreground-2` | +| `colorPaletteBerryBackground1` | `--colorPaletteBerryBackground1` | `--color-palette-berry-background-1` | +| `colorPaletteBerryBackground2` | `--colorPaletteBerryBackground2` | `--color-palette-berry-background-2` | +| `colorPaletteBerryBackground3` | `--colorPaletteBerryBackground3` | `--color-palette-berry-background-3` | +| `colorPaletteBerryBorder1` | `--colorPaletteBerryBorder1` | `--color-palette-berry-border-1` | +| `colorPaletteBerryBorder2` | `--colorPaletteBerryBorder2` | `--color-palette-berry-border-2` | +| `colorPaletteBerryBorderActive` | `--colorPaletteBerryBorderActive` | `--color-palette-berry-border-active` | +| `colorPaletteBerryForeground1` | `--colorPaletteBerryForeground1` | `--color-palette-berry-foreground-1` | +| `colorPaletteBerryForeground2` | `--colorPaletteBerryForeground2` | `--color-palette-berry-foreground-2` | +| `colorPaletteBerryForeground3` | `--colorPaletteBerryForeground3` | `--color-palette-berry-foreground-3` | +| `colorPaletteBlueBackground2` | `--colorPaletteBlueBackground2` | `--color-palette-blue-background-2` | +| `colorPaletteBlueBorderActive` | `--colorPaletteBlueBorderActive` | `--color-palette-blue-border-active` | +| `colorPaletteBlueForeground2` | `--colorPaletteBlueForeground2` | `--color-palette-blue-foreground-2` | +| `colorPaletteBrassBackground2` | `--colorPaletteBrassBackground2` | `--color-palette-brass-background-2` | +| `colorPaletteBrassBorderActive` | `--colorPaletteBrassBorderActive` | `--color-palette-brass-border-active` | +| `colorPaletteBrassForeground2` | `--colorPaletteBrassForeground2` | `--color-palette-brass-foreground-2` | +| `colorPaletteBrownBackground2` | `--colorPaletteBrownBackground2` | `--color-palette-brown-background-2` | +| `colorPaletteBrownBorderActive` | `--colorPaletteBrownBorderActive` | `--color-palette-brown-border-active` | +| `colorPaletteBrownForeground2` | `--colorPaletteBrownForeground2` | `--color-palette-brown-foreground-2` | +| `colorPaletteCornflowerBackground2` | `--colorPaletteCornflowerBackground2` | `--color-palette-cornflower-background-2` | +| `colorPaletteCornflowerBorderActive` | `--colorPaletteCornflowerBorderActive` | `--color-palette-cornflower-border-active` | +| `colorPaletteCornflowerForeground2` | `--colorPaletteCornflowerForeground2` | `--color-palette-cornflower-foreground-2` | +| `colorPaletteCranberryBackground2` | `--colorPaletteCranberryBackground2` | `--color-palette-cranberry-background-2` | +| `colorPaletteCranberryBorderActive` | `--colorPaletteCranberryBorderActive` | `--color-palette-cranberry-border-active` | +| `colorPaletteCranberryForeground2` | `--colorPaletteCranberryForeground2` | `--color-palette-cranberry-foreground-2` | +| `colorPaletteDarkGreenBackground2` | `--colorPaletteDarkGreenBackground2` | `--color-palette-dark-green-background-2` | +| `colorPaletteDarkGreenBorderActive` | `--colorPaletteDarkGreenBorderActive` | `--color-palette-dark-green-border-active` | +| `colorPaletteDarkGreenForeground2` | `--colorPaletteDarkGreenForeground2` | `--color-palette-dark-green-foreground-2` | +| `colorPaletteDarkOrangeBackground1` | `--colorPaletteDarkOrangeBackground1` | `--color-palette-dark-orange-background-1` | +| `colorPaletteDarkOrangeBackground2` | `--colorPaletteDarkOrangeBackground2` | `--color-palette-dark-orange-background-2` | +| `colorPaletteDarkOrangeBackground3` | `--colorPaletteDarkOrangeBackground3` | `--color-palette-dark-orange-background-3` | +| `colorPaletteDarkOrangeBorder1` | `--colorPaletteDarkOrangeBorder1` | `--color-palette-dark-orange-border-1` | +| `colorPaletteDarkOrangeBorder2` | `--colorPaletteDarkOrangeBorder2` | `--color-palette-dark-orange-border-2` | +| `colorPaletteDarkOrangeBorderActive` | `--colorPaletteDarkOrangeBorderActive` | `--color-palette-dark-orange-border-active` | +| `colorPaletteDarkOrangeForeground1` | `--colorPaletteDarkOrangeForeground1` | `--color-palette-dark-orange-foreground-1` | +| `colorPaletteDarkOrangeForeground2` | `--colorPaletteDarkOrangeForeground2` | `--color-palette-dark-orange-foreground-2` | +| `colorPaletteDarkOrangeForeground3` | `--colorPaletteDarkOrangeForeground3` | `--color-palette-dark-orange-foreground-3` | +| `colorPaletteDarkRedBackground2` | `--colorPaletteDarkRedBackground2` | `--color-palette-dark-red-background-2` | +| `colorPaletteDarkRedBorderActive` | `--colorPaletteDarkRedBorderActive` | `--color-palette-dark-red-border-active` | +| `colorPaletteDarkRedForeground2` | `--colorPaletteDarkRedForeground2` | `--color-palette-dark-red-foreground-2` | +| `colorPaletteForestBackground2` | `--colorPaletteForestBackground2` | `--color-palette-forest-background-2` | +| `colorPaletteForestBorderActive` | `--colorPaletteForestBorderActive` | `--color-palette-forest-border-active` | +| `colorPaletteForestForeground2` | `--colorPaletteForestForeground2` | `--color-palette-forest-foreground-2` | +| `colorPaletteGoldBackground2` | `--colorPaletteGoldBackground2` | `--color-palette-gold-background-2` | +| `colorPaletteGoldBorderActive` | `--colorPaletteGoldBorderActive` | `--color-palette-gold-border-active` | +| `colorPaletteGoldForeground2` | `--colorPaletteGoldForeground2` | `--color-palette-gold-foreground-2` | +| `colorPaletteGrapeBackground2` | `--colorPaletteGrapeBackground2` | `--color-palette-grape-background-2` | +| `colorPaletteGrapeBorderActive` | `--colorPaletteGrapeBorderActive` | `--color-palette-grape-border-active` | +| `colorPaletteGrapeForeground2` | `--colorPaletteGrapeForeground2` | `--color-palette-grape-foreground-2` | +| `colorPaletteGreenBackground1` | `--colorPaletteGreenBackground1` | `--color-palette-green-background-1` | +| `colorPaletteGreenBackground2` | `--colorPaletteGreenBackground2` | `--color-palette-green-background-2` | +| `colorPaletteGreenBackground3` | `--colorPaletteGreenBackground3` | `--color-palette-green-background-3` | +| `colorPaletteGreenBorder1` | `--colorPaletteGreenBorder1` | `--color-palette-green-border-1` | +| `colorPaletteGreenBorder2` | `--colorPaletteGreenBorder2` | `--color-palette-green-border-2` | +| `colorPaletteGreenBorderActive` | `--colorPaletteGreenBorderActive` | `--color-palette-green-border-active` | +| `colorPaletteGreenForeground1` | `--colorPaletteGreenForeground1` | `--color-palette-green-foreground-1` | +| `colorPaletteGreenForeground2` | `--colorPaletteGreenForeground2` | `--color-palette-green-foreground-2` | +| `colorPaletteGreenForeground3` | `--colorPaletteGreenForeground3` | `--color-palette-green-foreground-3` | +| `colorPaletteGreenForegroundInverted` | `--colorPaletteGreenForegroundInverted` | `--color-palette-green-foreground-inverted` | +| `colorPaletteLavenderBackground2` | `--colorPaletteLavenderBackground2` | `--color-palette-lavender-background-2` | +| `colorPaletteLavenderBorderActive` | `--colorPaletteLavenderBorderActive` | `--color-palette-lavender-border-active` | +| `colorPaletteLavenderForeground2` | `--colorPaletteLavenderForeground2` | `--color-palette-lavender-foreground-2` | +| `colorPaletteLightGreenBackground1` | `--colorPaletteLightGreenBackground1` | `--color-palette-light-green-background-1` | +| `colorPaletteLightGreenBackground2` | `--colorPaletteLightGreenBackground2` | `--color-palette-light-green-background-2` | +| `colorPaletteLightGreenBackground3` | `--colorPaletteLightGreenBackground3` | `--color-palette-light-green-background-3` | +| `colorPaletteLightGreenBorder1` | `--colorPaletteLightGreenBorder1` | `--color-palette-light-green-border-1` | +| `colorPaletteLightGreenBorder2` | `--colorPaletteLightGreenBorder2` | `--color-palette-light-green-border-2` | +| `colorPaletteLightGreenBorderActive` | `--colorPaletteLightGreenBorderActive` | `--color-palette-light-green-border-active` | +| `colorPaletteLightGreenForeground1` | `--colorPaletteLightGreenForeground1` | `--color-palette-light-green-foreground-1` | +| `colorPaletteLightGreenForeground2` | `--colorPaletteLightGreenForeground2` | `--color-palette-light-green-foreground-2` | +| `colorPaletteLightGreenForeground3` | `--colorPaletteLightGreenForeground3` | `--color-palette-light-green-foreground-3` | +| `colorPaletteLightTealBackground2` | `--colorPaletteLightTealBackground2` | `--color-palette-light-teal-background-2` | +| `colorPaletteLightTealBorderActive` | `--colorPaletteLightTealBorderActive` | `--color-palette-light-teal-border-active` | +| `colorPaletteLightTealForeground2` | `--colorPaletteLightTealForeground2` | `--color-palette-light-teal-foreground-2` | +| `colorPaletteLilacBackground2` | `--colorPaletteLilacBackground2` | `--color-palette-lilac-background-2` | +| `colorPaletteLilacBorderActive` | `--colorPaletteLilacBorderActive` | `--color-palette-lilac-border-active` | +| `colorPaletteLilacForeground2` | `--colorPaletteLilacForeground2` | `--color-palette-lilac-foreground-2` | +| `colorPaletteMagentaBackground2` | `--colorPaletteMagentaBackground2` | `--color-palette-magenta-background-2` | +| `colorPaletteMagentaBorderActive` | `--colorPaletteMagentaBorderActive` | `--color-palette-magenta-border-active` | +| `colorPaletteMagentaForeground2` | `--colorPaletteMagentaForeground2` | `--color-palette-magenta-foreground-2` | +| `colorPaletteMarigoldBackground1` | `--colorPaletteMarigoldBackground1` | `--color-palette-marigold-background-1` | +| `colorPaletteMarigoldBackground2` | `--colorPaletteMarigoldBackground2` | `--color-palette-marigold-background-2` | +| `colorPaletteMarigoldBackground3` | `--colorPaletteMarigoldBackground3` | `--color-palette-marigold-background-3` | +| `colorPaletteMarigoldBorder1` | `--colorPaletteMarigoldBorder1` | `--color-palette-marigold-border-1` | +| `colorPaletteMarigoldBorder2` | `--colorPaletteMarigoldBorder2` | `--color-palette-marigold-border-2` | +| `colorPaletteMarigoldBorderActive` | `--colorPaletteMarigoldBorderActive` | `--color-palette-marigold-border-active` | +| `colorPaletteMarigoldForeground1` | `--colorPaletteMarigoldForeground1` | `--color-palette-marigold-foreground-1` | +| `colorPaletteMarigoldForeground2` | `--colorPaletteMarigoldForeground2` | `--color-palette-marigold-foreground-2` | +| `colorPaletteMarigoldForeground3` | `--colorPaletteMarigoldForeground3` | `--color-palette-marigold-foreground-3` | +| `colorPaletteMinkBackground2` | `--colorPaletteMinkBackground2` | `--color-palette-mink-background-2` | +| `colorPaletteMinkBorderActive` | `--colorPaletteMinkBorderActive` | `--color-palette-mink-border-active` | +| `colorPaletteMinkForeground2` | `--colorPaletteMinkForeground2` | `--color-palette-mink-foreground-2` | +| `colorPaletteNavyBackground2` | `--colorPaletteNavyBackground2` | `--color-palette-navy-background-2` | +| `colorPaletteNavyBorderActive` | `--colorPaletteNavyBorderActive` | `--color-palette-navy-border-active` | +| `colorPaletteNavyForeground2` | `--colorPaletteNavyForeground2` | `--color-palette-navy-foreground-2` | +| `colorPalettePeachBackground2` | `--colorPalettePeachBackground2` | `--color-palette-peach-background-2` | +| `colorPalettePeachBorderActive` | `--colorPalettePeachBorderActive` | `--color-palette-peach-border-active` | +| `colorPalettePeachForeground2` | `--colorPalettePeachForeground2` | `--color-palette-peach-foreground-2` | +| `colorPalettePinkBackground2` | `--colorPalettePinkBackground2` | `--color-palette-pink-background-2` | +| `colorPalettePinkBorderActive` | `--colorPalettePinkBorderActive` | `--color-palette-pink-border-active` | +| `colorPalettePinkForeground2` | `--colorPalettePinkForeground2` | `--color-palette-pink-foreground-2` | +| `colorPalettePlatinumBackground2` | `--colorPalettePlatinumBackground2` | `--color-palette-platinum-background-2` | +| `colorPalettePlatinumBorderActive` | `--colorPalettePlatinumBorderActive` | `--color-palette-platinum-border-active` | +| `colorPalettePlatinumForeground2` | `--colorPalettePlatinumForeground2` | `--color-palette-platinum-foreground-2` | +| `colorPalettePlumBackground2` | `--colorPalettePlumBackground2` | `--color-palette-plum-background-2` | +| `colorPalettePlumBorderActive` | `--colorPalettePlumBorderActive` | `--color-palette-plum-border-active` | +| `colorPalettePlumForeground2` | `--colorPalettePlumForeground2` | `--color-palette-plum-foreground-2` | +| `colorPalettePumpkinBackground2` | `--colorPalettePumpkinBackground2` | `--color-palette-pumpkin-background-2` | +| `colorPalettePumpkinBorderActive` | `--colorPalettePumpkinBorderActive` | `--color-palette-pumpkin-border-active` | +| `colorPalettePumpkinForeground2` | `--colorPalettePumpkinForeground2` | `--color-palette-pumpkin-foreground-2` | +| `colorPalettePurpleBackground2` | `--colorPalettePurpleBackground2` | `--color-palette-purple-background-2` | +| `colorPalettePurpleBorderActive` | `--colorPalettePurpleBorderActive` | `--color-palette-purple-border-active` | +| `colorPalettePurpleForeground2` | `--colorPalettePurpleForeground2` | `--color-palette-purple-foreground-2` | +| `colorPaletteRedBackground1` | `--colorPaletteRedBackground1` | `--color-palette-red-background-1` | +| `colorPaletteRedBackground2` | `--colorPaletteRedBackground2` | `--color-palette-red-background-2` | +| `colorPaletteRedBackground3` | `--colorPaletteRedBackground3` | `--color-palette-red-background-3` | +| `colorPaletteRedBorder1` | `--colorPaletteRedBorder1` | `--color-palette-red-border-1` | +| `colorPaletteRedBorder2` | `--colorPaletteRedBorder2` | `--color-palette-red-border-2` | +| `colorPaletteRedBorderActive` | `--colorPaletteRedBorderActive` | `--color-palette-red-border-active` | +| `colorPaletteRedForeground1` | `--colorPaletteRedForeground1` | `--color-palette-red-foreground-1` | +| `colorPaletteRedForeground2` | `--colorPaletteRedForeground2` | `--color-palette-red-foreground-2` | +| `colorPaletteRedForeground3` | `--colorPaletteRedForeground3` | `--color-palette-red-foreground-3` | +| `colorPaletteRedForegroundInverted` | `--colorPaletteRedForegroundInverted` | `--color-palette-red-foreground-inverted` | +| `colorPaletteRoyalBlueBackground2` | `--colorPaletteRoyalBlueBackground2` | `--color-palette-royal-blue-background-2` | +| `colorPaletteRoyalBlueBorderActive` | `--colorPaletteRoyalBlueBorderActive` | `--color-palette-royal-blue-border-active` | +| `colorPaletteRoyalBlueForeground2` | `--colorPaletteRoyalBlueForeground2` | `--color-palette-royal-blue-foreground-2` | +| `colorPaletteSeafoamBackground2` | `--colorPaletteSeafoamBackground2` | `--color-palette-seafoam-background-2` | +| `colorPaletteSeafoamBorderActive` | `--colorPaletteSeafoamBorderActive` | `--color-palette-seafoam-border-active` | +| `colorPaletteSeafoamForeground2` | `--colorPaletteSeafoamForeground2` | `--color-palette-seafoam-foreground-2` | +| `colorPaletteSteelBackground2` | `--colorPaletteSteelBackground2` | `--color-palette-steel-background-2` | +| `colorPaletteSteelBorderActive` | `--colorPaletteSteelBorderActive` | `--color-palette-steel-border-active` | +| `colorPaletteSteelForeground2` | `--colorPaletteSteelForeground2` | `--color-palette-steel-foreground-2` | +| `colorPaletteTealBackground2` | `--colorPaletteTealBackground2` | `--color-palette-teal-background-2` | +| `colorPaletteTealBorderActive` | `--colorPaletteTealBorderActive` | `--color-palette-teal-border-active` | +| `colorPaletteTealForeground2` | `--colorPaletteTealForeground2` | `--color-palette-teal-foreground-2` | +| `colorPaletteYellowBackground1` | `--colorPaletteYellowBackground1` | `--color-palette-yellow-background-1` | +| `colorPaletteYellowBackground2` | `--colorPaletteYellowBackground2` | `--color-palette-yellow-background-2` | +| `colorPaletteYellowBackground3` | `--colorPaletteYellowBackground3` | `--color-palette-yellow-background-3` | +| `colorPaletteYellowBorder1` | `--colorPaletteYellowBorder1` | `--color-palette-yellow-border-1` | +| `colorPaletteYellowBorder2` | `--colorPaletteYellowBorder2` | `--color-palette-yellow-border-2` | +| `colorPaletteYellowBorderActive` | `--colorPaletteYellowBorderActive` | `--color-palette-yellow-border-active` | +| `colorPaletteYellowForeground1` | `--colorPaletteYellowForeground1` | `--color-palette-yellow-foreground-1` | +| `colorPaletteYellowForeground2` | `--colorPaletteYellowForeground2` | `--color-palette-yellow-foreground-2` | +| `colorPaletteYellowForeground3` | `--colorPaletteYellowForeground3` | `--color-palette-yellow-foreground-3` | +| `colorPaletteYellowForegroundInverted` | `--colorPaletteYellowForegroundInverted` | `--color-palette-yellow-foreground-inverted` | +| `colorScrollbarOverlay` | `--colorScrollbarOverlay` | `--color-scrollbar-overlay` | +| `colorStatusDangerBackground1` | `--colorStatusDangerBackground1` | `--color-status-danger-background-1` | +| `colorStatusDangerBackground2` | `--colorStatusDangerBackground2` | `--color-status-danger-background-2` | +| `colorStatusDangerBackground3` | `--colorStatusDangerBackground3` | `--color-status-danger-background-3` | +| `colorStatusDangerBackground3Hover` | `--colorStatusDangerBackground3Hover` | `--color-status-danger-background-3-hover` | +| `colorStatusDangerBackground3Pressed` | `--colorStatusDangerBackground3Pressed` | `--color-status-danger-background-3-pressed` | +| `colorStatusDangerBorder1` | `--colorStatusDangerBorder1` | `--color-status-danger-border-1` | +| `colorStatusDangerBorder2` | `--colorStatusDangerBorder2` | `--color-status-danger-border-2` | +| `colorStatusDangerBorderActive` | `--colorStatusDangerBorderActive` | `--color-status-danger-border-active` | +| `colorStatusDangerForeground1` | `--colorStatusDangerForeground1` | `--color-status-danger-foreground-1` | +| `colorStatusDangerForeground2` | `--colorStatusDangerForeground2` | `--color-status-danger-foreground-2` | +| `colorStatusDangerForeground3` | `--colorStatusDangerForeground3` | `--color-status-danger-foreground-3` | +| `colorStatusDangerForegroundInverted` | `--colorStatusDangerForegroundInverted` | `--color-status-danger-foreground-inverted` | +| `colorStatusSuccessBackground1` | `--colorStatusSuccessBackground1` | `--color-status-success-background-1` | +| `colorStatusSuccessBackground2` | `--colorStatusSuccessBackground2` | `--color-status-success-background-2` | +| `colorStatusSuccessBackground3` | `--colorStatusSuccessBackground3` | `--color-status-success-background-3` | +| `colorStatusSuccessBorder1` | `--colorStatusSuccessBorder1` | `--color-status-success-border-1` | +| `colorStatusSuccessBorder2` | `--colorStatusSuccessBorder2` | `--color-status-success-border-2` | +| `colorStatusSuccessBorderActive` | `--colorStatusSuccessBorderActive` | `--color-status-success-border-active` | +| `colorStatusSuccessForeground1` | `--colorStatusSuccessForeground1` | `--color-status-success-foreground-1` | +| `colorStatusSuccessForeground2` | `--colorStatusSuccessForeground2` | `--color-status-success-foreground-2` | +| `colorStatusSuccessForeground3` | `--colorStatusSuccessForeground3` | `--color-status-success-foreground-3` | +| `colorStatusSuccessForegroundInverted` | `--colorStatusSuccessForegroundInverted` | `--color-status-success-foreground-inverted` | +| `colorStatusWarningBackground1` | `--colorStatusWarningBackground1` | `--color-status-warning-background-1` | +| `colorStatusWarningBackground2` | `--colorStatusWarningBackground2` | `--color-status-warning-background-2` | +| `colorStatusWarningBackground3` | `--colorStatusWarningBackground3` | `--color-status-warning-background-3` | +| `colorStatusWarningBorder1` | `--colorStatusWarningBorder1` | `--color-status-warning-border-1` | +| `colorStatusWarningBorder2` | `--colorStatusWarningBorder2` | `--color-status-warning-border-2` | +| `colorStatusWarningBorderActive` | `--colorStatusWarningBorderActive` | `--color-status-warning-border-active` | +| `colorStatusWarningForeground1` | `--colorStatusWarningForeground1` | `--color-status-warning-foreground-1` | +| `colorStatusWarningForeground2` | `--colorStatusWarningForeground2` | `--color-status-warning-foreground-2` | +| `colorStatusWarningForeground3` | `--colorStatusWarningForeground3` | `--color-status-warning-foreground-3` | +| `colorStatusWarningForegroundInverted` | `--colorStatusWarningForegroundInverted` | `--color-status-warning-foreground-inverted` | +| `colorStrokeFocus1` | `--colorStrokeFocus1` | `--color-stroke-focus-1` | +| `colorStrokeFocus2` | `--colorStrokeFocus2` | `--color-stroke-focus-2` | +| `colorSubtleBackground` | `--colorSubtleBackground` | `--color-subtle-background` | +| `colorSubtleBackgroundHover` | `--colorSubtleBackgroundHover` | `--color-subtle-background-hover` | +| `colorSubtleBackgroundInverted` | `--colorSubtleBackgroundInverted` | `--color-subtle-background-inverted` | +| `colorSubtleBackgroundInvertedHover` | `--colorSubtleBackgroundInvertedHover` | `--color-subtle-background-inverted-hover` | +| `colorSubtleBackgroundInvertedPressed` | `--colorSubtleBackgroundInvertedPressed` | `--color-subtle-background-inverted-pressed` | +| `colorSubtleBackgroundInvertedSelected` | `--colorSubtleBackgroundInvertedSelected` | `--color-subtle-background-inverted-selected` | +| `colorSubtleBackgroundLightAlphaHover` | `--colorSubtleBackgroundLightAlphaHover` | `--color-subtle-background-light-alpha-hover` | +| `colorSubtleBackgroundLightAlphaPressed` | `--colorSubtleBackgroundLightAlphaPressed` | `--color-subtle-background-light-alpha-pressed` | +| `colorSubtleBackgroundLightAlphaSelected` | `--colorSubtleBackgroundLightAlphaSelected` | `--color-subtle-background-light-alpha-selected` | +| `colorSubtleBackgroundPressed` | `--colorSubtleBackgroundPressed` | `--color-subtle-background-pressed` | +| `colorSubtleBackgroundSelected` | `--colorSubtleBackgroundSelected` | `--color-subtle-background-selected` | +| `colorTransparentBackground` | `--colorTransparentBackground` | `--color-transparent-background` | +| `colorTransparentBackgroundHover` | `--colorTransparentBackgroundHover` | `--color-transparent-background-hover` | +| `colorTransparentBackgroundPressed` | `--colorTransparentBackgroundPressed` | `--color-transparent-background-pressed` | +| `colorTransparentBackgroundSelected` | `--colorTransparentBackgroundSelected` | `--color-transparent-background-selected` | +| `colorTransparentStroke` | `--colorTransparentStroke` | `--color-transparent-stroke` | +| `colorTransparentStrokeDisabled` | `--colorTransparentStrokeDisabled` | `--color-transparent-stroke-disabled` | +| `colorTransparentStrokeInteractive` | `--colorTransparentStrokeInteractive` | `--color-transparent-stroke-interactive` | +| `curveAccelerateMax` | `--curveAccelerateMax` | `--ease-accelerate-max` | +| `curveAccelerateMid` | `--curveAccelerateMid` | `--ease-accelerate-mid` | +| `curveAccelerateMin` | `--curveAccelerateMin` | `--ease-accelerate-min` | +| `curveDecelerateMax` | `--curveDecelerateMax` | `--ease-decelerate-max` | +| `curveDecelerateMid` | `--curveDecelerateMid` | `--ease-decelerate-mid` | +| `curveDecelerateMin` | `--curveDecelerateMin` | `--ease-decelerate-min` | +| `curveEasyEase` | `--curveEasyEase` | `--ease-easy-ease` | +| `curveEasyEaseMax` | `--curveEasyEaseMax` | `--ease-easy-ease-max` | +| `curveLinear` | `--curveLinear` | `--ease-linear` | +| `durationFast` | `--durationFast` | `--duration-fast` | +| `durationFaster` | `--durationFaster` | `--duration-faster` | +| `durationGentle` | `--durationGentle` | `--duration-gentle` | +| `durationNormal` | `--durationNormal` | `--duration-normal` | +| `durationSlow` | `--durationSlow` | `--duration-slow` | +| `durationSlower` | `--durationSlower` | `--duration-slower` | +| `durationUltraFast` | `--durationUltraFast` | `--duration-ultra-fast` | +| `durationUltraSlow` | `--durationUltraSlow` | `--duration-ultra-slow` | +| `fontFamilyBase` | `--fontFamilyBase` | `--font-base` | +| `fontFamilyMonospace` | `--fontFamilyMonospace` | `--font-monospace` | +| `fontFamilyNumeric` | `--fontFamilyNumeric` | `--font-numeric` | +| `fontSizeBase100` | `--fontSizeBase100` | `--text-base-100` | +| `fontSizeBase200` | `--fontSizeBase200` | `--text-base-200` | +| `fontSizeBase300` | `--fontSizeBase300` | `--text-base-300` | +| `fontSizeBase400` | `--fontSizeBase400` | `--text-base-400` | +| `fontSizeBase500` | `--fontSizeBase500` | `--text-base-500` | +| `fontSizeBase600` | `--fontSizeBase600` | `--text-base-600` | +| `fontSizeHero1000` | `--fontSizeHero1000` | `--text-hero-1000` | +| `fontSizeHero700` | `--fontSizeHero700` | `--text-hero-700` | +| `fontSizeHero800` | `--fontSizeHero800` | `--text-hero-800` | +| `fontSizeHero900` | `--fontSizeHero900` | `--text-hero-900` | +| `fontWeightBold` | `--fontWeightBold` | `--font-weight-bold` | +| `fontWeightMedium` | `--fontWeightMedium` | `--font-weight-medium` | +| `fontWeightRegular` | `--fontWeightRegular` | `--font-weight-regular` | +| `fontWeightSemibold` | `--fontWeightSemibold` | `--font-weight-semibold` | +| `lineHeightBase100` | `--lineHeightBase100` | `--leading-base-100` | +| `lineHeightBase200` | `--lineHeightBase200` | `--leading-base-200` | +| `lineHeightBase300` | `--lineHeightBase300` | `--leading-base-300` | +| `lineHeightBase400` | `--lineHeightBase400` | `--leading-base-400` | +| `lineHeightBase500` | `--lineHeightBase500` | `--leading-base-500` | +| `lineHeightBase600` | `--lineHeightBase600` | `--leading-base-600` | +| `lineHeightHero1000` | `--lineHeightHero1000` | `--leading-hero-1000` | +| `lineHeightHero700` | `--lineHeightHero700` | `--leading-hero-700` | +| `lineHeightHero800` | `--lineHeightHero800` | `--leading-hero-800` | +| `lineHeightHero900` | `--lineHeightHero900` | `--leading-hero-900` | +| `shadow16` | `--shadow16` | `--shadow-16` | +| `shadow16Brand` | `--shadow16Brand` | `--shadow-16-brand` | +| `shadow2` | `--shadow2` | `--shadow-2` | +| `shadow28` | `--shadow28` | `--shadow-28` | +| `shadow28Brand` | `--shadow28Brand` | `--shadow-28-brand` | +| `shadow2Brand` | `--shadow2Brand` | `--shadow-2-brand` | +| `shadow4` | `--shadow4` | `--shadow-4` | +| `shadow4Brand` | `--shadow4Brand` | `--shadow-4-brand` | +| `shadow64` | `--shadow64` | `--shadow-64` | +| `shadow64Brand` | `--shadow64Brand` | `--shadow-64-brand` | +| `shadow8` | `--shadow8` | `--shadow-8` | +| `shadow8Brand` | `--shadow8Brand` | `--shadow-8-brand` | +| `spacingHorizontalL` | `--spacingHorizontalL` | `--spacing-horizontal-l` | +| `spacingHorizontalM` | `--spacingHorizontalM` | `--spacing-horizontal-m` | +| `spacingHorizontalMNudge` | `--spacingHorizontalMNudge` | `--spacing-horizontal-m-nudge` | +| `spacingHorizontalNone` | `--spacingHorizontalNone` | `--spacing-horizontal-none` | +| `spacingHorizontalS` | `--spacingHorizontalS` | `--spacing-horizontal-s` | +| `spacingHorizontalSNudge` | `--spacingHorizontalSNudge` | `--spacing-horizontal-s-nudge` | +| `spacingHorizontalXL` | `--spacingHorizontalXL` | `--spacing-horizontal-xl` | +| `spacingHorizontalXS` | `--spacingHorizontalXS` | `--spacing-horizontal-xs` | +| `spacingHorizontalXXL` | `--spacingHorizontalXXL` | `--spacing-horizontal-xxl` | +| `spacingHorizontalXXS` | `--spacingHorizontalXXS` | `--spacing-horizontal-xxs` | +| `spacingHorizontalXXXL` | `--spacingHorizontalXXXL` | `--spacing-horizontal-xxxl` | +| `spacingVerticalL` | `--spacingVerticalL` | `--spacing-vertical-l` | +| `spacingVerticalM` | `--spacingVerticalM` | `--spacing-vertical-m` | +| `spacingVerticalMNudge` | `--spacingVerticalMNudge` | `--spacing-vertical-m-nudge` | +| `spacingVerticalNone` | `--spacingVerticalNone` | `--spacing-vertical-none` | +| `spacingVerticalS` | `--spacingVerticalS` | `--spacing-vertical-s` | +| `spacingVerticalSNudge` | `--spacingVerticalSNudge` | `--spacing-vertical-s-nudge` | +| `spacingVerticalXL` | `--spacingVerticalXL` | `--spacing-vertical-xl` | +| `spacingVerticalXS` | `--spacingVerticalXS` | `--spacing-vertical-xs` | +| `spacingVerticalXXL` | `--spacingVerticalXXL` | `--spacing-vertical-xxl` | +| `spacingVerticalXXS` | `--spacingVerticalXXS` | `--spacing-vertical-xxs` | +| `spacingVerticalXXXL` | `--spacingVerticalXXXL` | `--spacing-vertical-xxxl` | +| `strokeWidthThick` | `--strokeWidthThick` | `--stroke-width-thick` | +| `strokeWidthThicker` | `--strokeWidthThicker` | `--stroke-width-thicker` | +| `strokeWidthThickest` | `--strokeWidthThickest` | `--stroke-width-thickest` | +| `strokeWidthThin` | `--strokeWidthThin` | `--stroke-width-thin` | +| `zIndexBackground` | `--zIndexBackground` | `--z-index-background` | +| `zIndexContent` | `--zIndexContent` | `--z-index-content` | +| `zIndexDebug` | `--zIndexDebug` | `--z-index-debug` | +| `zIndexFloating` | `--zIndexFloating` | `--z-index-floating` | +| `zIndexMessages` | `--zIndexMessages` | `--z-index-messages` | +| `zIndexOverlay` | `--zIndexOverlay` | `--z-index-overlay` | +| `zIndexPopup` | `--zIndexPopup` | `--z-index-popup` | +| `zIndexPriority` | `--zIndexPriority` | `--z-index-priority` | diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV9/UpgradeGuide.mdx b/apps/public-docsite-v9/src/Concepts/Migration/FromV9/UpgradeGuide.mdx new file mode 100644 index 0000000000000..b477327a7193a --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV9/UpgradeGuide.mdx @@ -0,0 +1,259 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Upgrading from v9 + +This page is for teams **already using Fluent UI v9** who are moving to the major release that +replaces the runtime CSS-in-JS styling system (Griffel) with build-time CSS Modules + Tailwind v4. + +If you are coming from v8 or v0, start at +[Migration / Getting started](?path=/docs/concepts-migration-getting-started--docs) instead — those +guides cover a different jump. + +## What breaks, at a glance + +| # | What changed | Fails how? | +| ---------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------- | +| [1](#1-install-and-import-the-theme-package) | A theme stylesheet must be installed and imported | **Silently** — wrong metrics, unthemed values | +| [2](#2-token-css-variables-were-renamed) | All 467 token CSS custom properties renamed to kebab-case | **Silently** — CSS custom properties fail without error | +| [3](#3-theming-is-css-native-now) | `FluentProvider`'s `theme` prop and the JS theming path are removed | Compile error | +| [4](#4-class-name-targeting-of-internals) | BEM statics removed; `*ClassNames` narrowed to `{ root }` | Compile error, **or silently** for global CSS | +| [5](#5-griffel-apis-are-no-longer-re-exported) | `makeStyles` / `mergeClasses` / `shorthands` no longer come from the umbrella | Compile error | +| [6](#6-fluentuireact-icons) | Icons moved to a headless build with its own stylesheet | Missing icon styling | +| [7](#7-commonjs-and-ssr-stylesheet-loading) | CommonJS output does not `require` component CSS | **Silently** — unstyled components | + +Two of these fail **silently**, which is why they are first. CSS custom properties and missing +stylesheets produce no error, no warning and no console message — the declaration is simply dropped +and the browser renders something plausible-looking and wrong. + +--- + +## 1. Install and import the theme package + +`@fluentui/react-components` no longer carries a styling runtime that emits document-level values at +mount. Those values now live in a stylesheet you install and import once. + +```sh +yarn add @fluentui/react-components @fluentui/react-tailwind-theme +``` + +```js +// src/main.jsx — before your own styles +import '@fluentui/react-tailwind-theme/styles.css'; +``` + +That stylesheet carries the cascade `@layer` order, the token values (Web Light at `:root, :host`), +the spacing/stroke scales and the shipped theme classes. + +**Symptom if you skip it:** components render, but with collapsed padding and gaps, no radii and no +shadows — every `var(--…)` reference resolves to nothing. It does not throw. + +See [Quick start](?path=/docs/concepts-developer-quick-start--docs) for the full setup. + +## 2. Token CSS variables were renamed + +Every one of the 467 design-token custom properties was renamed from camelCase to kebab-case, and +grouped under Tailwind v4 namespaces: + +```diff +- color: var(--colorNeutralForeground1); ++ color: var(--color-neutral-foreground-1); + +- padding: var(--spacingHorizontalM); ++ padding: var(--spacing-horizontal-m); + +- border-width: var(--strokeWidthThin); ++ border-width: var(--stroke-width-thin); +``` + +**The old names no longer exist in any shipped CSS.** Nothing warns you: a `var()` referencing an +undefined custom property makes the whole declaration invalid at computed-value time, and the +element quietly falls back. + +The `tokens` object is unaffected — the JS names did not change, and each one now resolves to the +new variable: + +```js +import { tokens } from '@fluentui/react-components'; + +tokens.colorNeutralForeground1; // 'var(--color-neutral-foreground-1)' ← name unchanged, value updated +``` + +So the fix is confined to hand-written CSS/SCSS and to any place you interpolated a variable name as +a string. The complete 467-row map, plus a find/replace script, is on the +[Token rename](?path=/docs/concepts-migration-upgrading-from-v9-token-rename--docs) page. + +## 3. Theming is CSS-native now + +A theme is no longer a JavaScript object handed to a provider that builds and injects a rule at +runtime. It is a **CSS class containing only custom-property declarations**. + +```diff +- import { FluentProvider, webDarkTheme } from '@fluentui/react-components'; ++ import '@fluentui/react-tailwind-theme/styles.css'; ++ import { FluentProvider, webDarkThemeClassName } from '@fluentui/react-components'; + +- ++ + + +``` + +Every shipped theme has a `ThemeClassName` counterpart, and Web Light is the `:root` default, +so an app that used `webLightTheme` can drop the prop entirely. + +**Removed with no runtime replacement**, all of which are compile errors and therefore safe: + +| Removed | Replacement | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `FluentProvider`'s `theme` prop | `themeClassName` | +| `FluentProvider`'s `nonce` prop | Nothing to sign — no style element is created. Use CSP `style-src` for the `.css` assets | +| `createCSSRuleFromTheme`, `useFluentProviderThemeStyleTag` | Author a CSS class (see below) | +| `ThemeContext_unstable` | `useCssVarValue` from `@fluentui/react-utilities` | +| `webLightTheme` & co., `create*Theme`, `themeToTokensObject`, `Theme` / `PartialTheme` / `BrandVariants` types, from the umbrella and `@fluentui/react-theme` | Still exported by `@fluentui/tokens` as **build-time tooling input** | + +Full recipes — scoped theming, nested providers, portals, runtime dark-mode switching and a complete +custom-brand-ramp walkthrough — are on the +[Theming](?path=/docs/concepts-developer-theming--docs) page. Read the custom-theme section before +you port a `createDarkTheme()` call; there is a fall-through rule that differs between light-derived +and dark-derived themes. + +## 4. Class-name targeting of internals + +Components no longer render BEM static classes (`fui-Button`, `fui-Button__icon`). Each component +stamps a single public identity marker on its outermost slot — `group/fui-` — and the +`*ClassNames` constants are narrowed to `{ root: string }` with `root` pointing at that marker. + +This is a **type change, not a deprecation**: on `master` there are 215 `*ClassNames` constants typed +`SlotClassNames<…Slots>`; today there are 193 typed `{ root: string }`. Any reference to a non-root +slot key is a TypeScript error on the exact line that would otherwise have gone silently dead. + +```diff +-
// ❌ TS error: no such property +``` + +To style a slot, pass a `className` to the slot: + +```jsx + diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.module.css new file mode 100644 index 0000000000000..19b9869086f4e --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.module.css @@ -0,0 +1,10 @@ +/* Story styles for PositioningFallbackPositions. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: grid; + grid-template-rows: repeat(1fr); + justify-items: start; + gap: 2px; + max-width: 400px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx index f7e340e295fe4..475cadc7e0c25 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx @@ -1,21 +1,12 @@ import * as React from 'react'; -import { Combobox, makeStyles, Option, useId } from '@fluentui/react-components'; +import { Combobox, Option, useId } from '@fluentui/react-components'; import type { ComboboxProps } from '@fluentui/react-components'; -const useStyles = makeStyles({ - root: { - display: 'grid', - gridTemplateRows: 'repeat(1fr)', - justifyItems: 'start', - gap: '2px', - maxWidth: '400px', - }, -}); +import styles from './PositioningFallbackPositions.module.css'; export const FallbackPositions = (props: Partial) => { const comboId = useId('combo-default'); const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake']; - const styles = useStyles(); return (
diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.module.css new file mode 100644 index 0000000000000..de027964c305f --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.module.css @@ -0,0 +1,17 @@ +/* Story styles for PositioningFlipBoundary. + * Token references are the Fluent theme's CSS variables. */ + +.boundary { + border: 2px dashed red; + padding: 20px; + width: 300px; + height: 300px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.trigger { + display: block; + width: 150px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.stories.tsx index c0021184c0d3c..c97c725f81353 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningFlipBoundary.stories.tsx @@ -1,24 +1,9 @@ import * as React from 'react'; -import { Popover, PopoverTrigger, PopoverSurface, Button, makeStyles, Checkbox } from '@fluentui/react-components'; +import { Popover, PopoverTrigger, PopoverSurface, Button, Checkbox } from '@fluentui/react-components'; -const useStyles = makeStyles({ - boundary: { - border: '2px dashed red', - padding: '20px', - width: '300px', - height: '300px', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - }, - trigger: { - display: 'block', - width: '150px', - }, -}); +import styles from './PositioningFlipBoundary.module.css'; export const FlipBoundary = () => { - const styles = useStyles(); const [boundaryRef, setBoundaryRef] = React.useState(null); const [open, setOpen] = React.useState(false); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.module.css new file mode 100644 index 0000000000000..9ea858b90ffb6 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.module.css @@ -0,0 +1,14 @@ +/* Story styles for PositioningImperativePositionUpdate. + * Token references are the Fluent theme's CSS variables. */ + +.container { + position: relative; +} + +.button { + position: absolute; +} + +.slider { + margin-bottom: 10px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.stories.tsx index 6a1813ff26cf7..a657b0b9d8c1b 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningImperativePositionUpdate.stories.tsx @@ -1,23 +1,10 @@ import * as React from 'react'; -import { Button, Popover, PopoverSurface, PopoverTrigger, Slider, Field, makeStyles } from '@fluentui/react-components'; +import { Button, Popover, PopoverSurface, PopoverTrigger, Slider, Field } from '@fluentui/react-components'; import type { PositioningImperativeRef } from '@fluentui/react-components'; -const useStyles = makeStyles({ - container: { - position: 'relative', - }, - - button: { - position: 'absolute', - }, - - slider: { - marginBottom: '10px', - }, -}); +import styles from './PositioningImperativePositionUpdate.module.css'; export const ImperativePositionUpdate = () => { - const styles = useStyles(); const positioningRef = React.useRef(null); const [value, setValue] = React.useState(0); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.module.css new file mode 100644 index 0000000000000..1996c70a23651 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.module.css @@ -0,0 +1,35 @@ +/* Story styles for PositioningListenToUpdates. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + gap: 20px; +} + +.button { + display: block; + min-width: 120px; +} + +.logContainer { + display: flex; + flex-direction: column; +} + +.logLabel { + color: var(--color-neutral-foreground-on-brand); + background-color: var(--color-brand-background); + width: fit-content; + font-weight: var(--font-weight-bold); + padding: 2px 12px; +} + +.log { + overflow-y: auto; + box-shadow: var(--shadow-16); + position: relative; + min-width: 200px; + height: 200px; + border: 2px solid var(--color-brand-background); + padding: 12px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.stories.tsx index b6e587c3e4e66..f7721e8d029c2 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningListenToUpdates.stories.tsx @@ -1,57 +1,14 @@ import * as React from 'react'; import type { PositioningImperativeRef, PositioningProps } from '@fluentui/react-components'; -import { - useId, - Text, - makeStyles, - tokens, - Popover, - Button, - PopoverTrigger, - PopoverSurface, -} from '@fluentui/react-components'; +import { useId, Text, Popover, Button, PopoverTrigger, PopoverSurface } from '@fluentui/react-components'; + +import styles from './PositioningListenToUpdates.module.css'; // Derive the event type from PositioningProps instead of importing directly type OnPositioningEndEvent = Parameters>[0]; -const useStyles = makeStyles({ - root: { - display: 'flex', - gap: '20px', - }, - - button: { - display: 'block', - minWidth: '120px', - }, - - logContainer: { - display: 'flex', - flexDirection: 'column', - }, - - logLabel: { - color: tokens.colorNeutralForegroundOnBrand, - backgroundColor: tokens.colorBrandBackground, - width: 'fit-content', - fontWeight: tokens.fontWeightBold, - padding: '2px 12px', - }, - - log: { - overflowY: 'auto', - boxShadow: tokens.shadow16, - position: 'relative', - minWidth: '200px', - height: '200px', - border: `2px solid ${tokens.colorBrandBackground}`, - padding: '12px', - }, -}); - export const ListenToUpdates = () => { - const styles = useStyles(); const labelId = useId(); const [statusLog, setStatusLog] = React.useState>([]); const positioningRef = React.useRef(null); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.module.css new file mode 100644 index 0000000000000..745f9b94ae9f0 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.module.css @@ -0,0 +1,6 @@ +/* Story styles for PositioningMatchTargetSize. + * Token references are the Fluent theme's CSS variables. */ + +.target { + width: 350px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx index 52eb42e58c95d..b1a97cd7c7175 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx @@ -1,14 +1,9 @@ import * as React from 'react'; -import { Button, Popover, PopoverSurface, PopoverTrigger, makeStyles } from '@fluentui/react-components'; +import { Button, Popover, PopoverSurface, PopoverTrigger } from '@fluentui/react-components'; -const useStyles = makeStyles({ - target: { - width: '350px', - }, -}); +import styles from './PositioningMatchTargetSize.module.css'; export const MatchTargetSize = () => { - const styles = useStyles(); return ( diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.module.css new file mode 100644 index 0000000000000..ded961c506437 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.module.css @@ -0,0 +1,18 @@ +/* Story styles for PositioningOverflowBoundary. + * Token references are the Fluent theme's CSS variables. */ + +.boundary { + border: 2px dashed red; + padding: 20px; + width: 300px; + height: 300px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: end; +} + +.trigger { + display: block; + width: 150px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.stories.tsx index 8ca79dd8ea1fc..922ba584567b8 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundary.stories.tsx @@ -1,25 +1,9 @@ import * as React from 'react'; -import { Popover, PopoverTrigger, PopoverSurface, Button, makeStyles, Checkbox } from '@fluentui/react-components'; +import { Popover, PopoverTrigger, PopoverSurface, Button, Checkbox } from '@fluentui/react-components'; -const useStyles = makeStyles({ - boundary: { - border: '2px dashed red', - padding: '20px', - width: '300px', - height: '300px', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'end', - }, - trigger: { - display: 'block', - width: '150px', - }, -}); +import styles from './PositioningOverflowBoundary.module.css'; export const OverflowBoundary = () => { - const styles = useStyles(); const [boundaryRef, setBoundaryRef] = React.useState(null); const [open, setOpen] = React.useState(false); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.module.css new file mode 100644 index 0000000000000..332d691c03796 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.module.css @@ -0,0 +1,18 @@ +/* Story styles for PositioningOverflowBoundaryPadding. + * Token references are the Fluent theme's CSS variables. */ + +.boundary { + border: 2px dashed red; + padding: 20px; + width: 300px; + height: 300px; + display: flex; + flex-direction: column; + align-items: end; +} + +.trigger { + display: block; + width: 150px; + margin-top: 60px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.stories.tsx index 533adf94cc76e..f160989bb3bb8 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryPadding.stories.tsx @@ -4,31 +4,14 @@ import { PopoverTrigger, PopoverSurface, Button, - makeStyles, Checkbox, SpinButton, Label, } from '@fluentui/react-components'; -const useStyles = makeStyles({ - boundary: { - border: '2px dashed red', - padding: '20px', - width: '300px', - height: '300px', - display: 'flex', - flexDirection: 'column', - alignItems: 'end', - }, - trigger: { - display: 'block', - width: '150px', - marginTop: '60px', - }, -}); +import styles from './PositioningOverflowBoundaryPadding.module.css'; export const OverflowBoundaryPadding = () => { - const styles = useStyles(); const [boundaryRef, setBoundaryRef] = React.useState(null); const [open, setOpen] = React.useState(false); const [padding, setPadding] = React.useState(8); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.module.css new file mode 100644 index 0000000000000..dbc8f96e67475 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.module.css @@ -0,0 +1,44 @@ +/* Story styles for PositioningOverflowBoundaryRect. + * Token references are the Fluent theme's CSS variables. */ + +.area { + border: 2px solid var(--color-status-danger-background-3); + padding: 60px 20px 20px 20px; + width: 300px; + height: 300px; + display: flex; + flex-direction: column; + align-items: end; + justify-content: space-between; + position: relative; +} + +.area::before { + content: 'Container'; + position: absolute; + padding: var(--spacing-horizontal-m-nudge) var(--spacing-horizontal-s); + top: 0; + left: 0; + color: var(--color-status-danger-background-1); + background-color: var(--color-status-danger-background-3); +} + +.boundary { + width: 320px; + height: 320px; + outline: 2px solid var(--color-brand-background); + position: absolute; + top: 50px; + left: 10px; + pointer-events: none; +} + +.boundary::before { + content: 'Boundary'; + position: absolute; + padding: var(--spacing-horizontal-m-nudge) var(--spacing-horizontal-s); + top: 0; + left: 0; + color: var(--color-neutral-foreground-on-brand); + background-color: var(--color-brand-background); +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.stories.tsx index fa6b2f21fea84..d0c365e927d3a 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningOverflowBoundaryRect.stories.tsx @@ -4,60 +4,13 @@ import { PopoverTrigger, PopoverSurface, Button, - makeStyles, - tokens, type PositioningRect, useIsomorphicLayoutEffect, } from '@fluentui/react-components'; -const useClasses = makeStyles({ - area: { - border: `2px solid ${tokens.colorStatusDangerBackground3}`, - padding: '60px 20px 20px 20px', - width: '300px', - height: '300px', +import styles from './PositioningOverflowBoundaryRect.module.css'; - display: 'flex', - flexDirection: 'column', - alignItems: 'end', - justifyContent: 'space-between', - position: 'relative', - - '::before': { - content: '"Container"', - position: 'absolute', - padding: `${tokens.spacingHorizontalMNudge} ${tokens.spacingHorizontalS}`, - - top: 0, - left: 0, - - color: tokens.colorStatusDangerBackground1, - backgroundColor: tokens.colorStatusDangerBackground3, - }, - }, - boundary: { - width: '320px', - height: '320px', - outline: `2px solid ${tokens.colorBrandBackground}`, - - position: 'absolute', - top: '50px', - left: '10px', - pointerEvents: 'none', - - '::before': { - content: '"Boundary"', - position: 'absolute', - padding: `${tokens.spacingHorizontalMNudge} ${tokens.spacingHorizontalS}`, - - top: 0, - left: 0, - - color: tokens.colorNeutralForegroundOnBrand, - backgroundColor: tokens.colorBrandBackground, - }, - }, -}); +const useClasses = () => styles; export const OverflowBoundaryRect = () => { const classes = useClasses(); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.module.css new file mode 100644 index 0000000000000..dbd4d99143e26 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.module.css @@ -0,0 +1,16 @@ +/* Story styles for PositioningShiftToCoverTarget. + * Token references are the Fluent theme's CSS variables. */ + +.boundary { + border: 2px dashed red; + width: 300px; + height: 300px; + overflow: auto; + resize: both; +} + +.trigger { + display: block; + width: 150px; + margin: 200px auto; +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.stories.tsx index 4ce61533f73e4..a09bb4df0aa0a 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShiftToCoverTarget.stories.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Button, - makeStyles, SpinButton, Menu, MenuTrigger, @@ -17,20 +16,7 @@ import { PositioningProps, } from '@fluentui/react-components'; -const useStyles = makeStyles({ - boundary: { - border: '2px dashed red', - width: '300px', - height: '300px', - overflow: 'auto', - resize: 'both', - }, - trigger: { - display: 'block', - width: '150px', - margin: '200px auto', - }, -}); +import styles from './PositioningShiftToCoverTarget.module.css'; const ResizableBoundary = React.forwardRef< HTMLDivElement, @@ -52,8 +38,6 @@ const ResizableBoundary = React.forwardRef< } }, [onResize]); - const styles = useStyles(); - return (
{children} @@ -62,7 +46,6 @@ const ResizableBoundary = React.forwardRef< }); export const CoverTargetForSmallViewport = () => { - const styles = useStyles(); const [boundaryRef, setBoundaryRef] = React.useState(null); const [menuItemCount, setMenuItemCount] = React.useState(6); diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.module.css b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.module.css new file mode 100644 index 0000000000000..bd6e2149f8162 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.module.css @@ -0,0 +1,148 @@ +/* Story styles for PositioningShorthandPositions. + * Token references are the Fluent theme's CSS variables. */ + +.popoverSurface { + width: 150px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; +} + +.target { + height: 50px; + width: 140px; + display: flex; + justify-content: space-between; +} + +.wrapper { + display: flex; + flex-direction: column; + gap: 1em; + width: 100%; + overflow: scroll; +} + +.instructions { + text-align: center; +} + +.targetContainer { + display: inline-grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(5, 64px); + gap: 20px; + margin: 16px 128px; +} + +.aboveStart { + grid-row-start: 1; + grid-column-start: 1; + flex-direction: row-reverse; +} + +.above { + grid-row-start: 1; + grid-column-start: 2; + justify-content: center; +} + +.above div:nth-child(2) { + display: none; +} + +.aboveEnd { + grid-row-start: 1; + grid-column-start: 3; +} + +.beforeTop { + grid-row-start: 2; + grid-column-start: 1; + flex-direction: column-reverse; +} + +.beforeTop div:nth-child(2) { + transform: rotate(270deg); +} + +.before { + grid-row-start: 3; + grid-column-start: 1; + flex-direction: row-reverse; + justify-content: center; +} + +.before div:nth-child(2) { + display: none; +} + +.beforeBottom { + grid-row-start: 4; + grid-column-start: 1; + flex-direction: column; +} + +.beforeBottom div:nth-child(2) { + transform: rotate(270deg); +} + +.afterTop { + grid-row-start: 2; + grid-column-start: 3; + flex-direction: column-reverse; +} + +.afterTop div:nth-child(2) { + transform: rotate(90deg); +} + +.after { + grid-row-start: 3; + grid-column-start: 3; + justify-content: center; +} + +.after div:nth-child(2) { + display: none; +} + +.afterBottom { + grid-row-start: 4; + grid-column-start: 3; + flex-direction: column; +} + +.afterBottom div:nth-child(2) { + transform: rotate(90deg); +} + +.belowStart { + flex-direction: row-reverse; + grid-row-start: 5; + grid-column-start: 1; +} + +.belowStart div:nth-child(2) { + transform: rotate(180deg); +} + +.below { + grid-row-start: 5; + grid-column-start: 2; + justify-content: center; +} + +.below div:nth-child(2) { + display: none; +} + +.belowEnd { + grid-row-start: 5; + grid-column-start: 3; +} + +.belowEnd div:nth-child(2) { + transform: rotate(180deg); +} diff --git a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.stories.tsx b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.stories.tsx index 7883847a0dd88..bc92d6e43e909 100644 --- a/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Positioning/PositioningShorthandPositions.stories.tsx @@ -1,139 +1,10 @@ import * as React from 'react'; -import { makeStyles, mergeClasses, Button, Popover, PopoverSurface, PopoverTrigger } from '@fluentui/react-components'; +import { Button, Popover, PopoverSurface, PopoverTrigger } from '@fluentui/react-components'; import type { PositioningShorthand } from '@fluentui/react-components'; -const useExampleStyles = makeStyles({ - popoverSurface: { - width: '150px', - height: '50px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - - target: { - height: '50px', - width: '140px', - display: 'flex', - justifyContent: 'space-between', - }, -}); - -const useGridExampleStyles = makeStyles({ - wrapper: { - display: 'flex', - flexDirection: 'column', - gap: '1em', - width: '100%', - overflow: 'scroll', - }, - - instructions: { - textAlign: 'center', - }, - - targetContainer: { - display: 'inline-grid', - gridTemplateColumns: 'repeat(3, 1fr)', - gridTemplateRows: 'repeat(5, 64px)', - gap: '20px', - margin: '16px 128px', - }, - - aboveStart: { - gridRowStart: '1', - gridColumnStart: '1', - flexDirection: 'row-reverse', - }, - above: { - gridRowStart: '1', - gridColumnStart: '2', - justifyContent: 'center', - '& div:nth-child(2)': { - display: 'none', - }, - }, - aboveEnd: { - gridRowStart: '1', - gridColumnStart: '3', - }, - beforeTop: { - gridRowStart: '2', - gridColumnStart: '1', - flexDirection: 'column-reverse', - '& div:nth-child(2)': { - transform: 'rotate(270deg)', - }, - }, - before: { - gridRowStart: '3', - gridColumnStart: '1', - flexDirection: 'row-reverse', - justifyContent: 'center', - '& div:nth-child(2)': { - display: 'none', - }, - }, - beforeBottom: { - gridRowStart: '4', - gridColumnStart: '1', - flexDirection: 'column', - '& div:nth-child(2)': { - transform: 'rotate(270deg)', - }, - }, - afterTop: { - gridRowStart: '2', - gridColumnStart: '3', - flexDirection: 'column-reverse', - '& div:nth-child(2)': { - transform: 'rotate(90deg)', - }, - }, - after: { - gridRowStart: '3', - gridColumnStart: '3', - justifyContent: 'center', - '& div:nth-child(2)': { - display: 'none', - }, - }, - afterBottom: { - gridRowStart: '4', - gridColumnStart: '3', - flexDirection: 'column', - '& div:nth-child(2)': { - transform: 'rotate(90deg)', - }, - }, - belowStart: { - flexDirection: 'row-reverse', - gridRowStart: '5', - gridColumnStart: '1', - '& div:nth-child(2)': { - transform: 'rotate(180deg)', - }, - }, - below: { - gridRowStart: '5', - gridColumnStart: '2', - justifyContent: 'center', - '& div:nth-child(2)': { - display: 'none', - }, - }, - belowEnd: { - gridRowStart: '5', - gridColumnStart: '3', - '& div:nth-child(2)': { - transform: 'rotate(180deg)', - }, - }, -}); +import styles from './PositioningShorthandPositions.module.css'; export const ShorthandPositions = () => { - const styles = useGridExampleStyles(); - return (
Click each button to see its positioned element
@@ -201,11 +72,10 @@ const PositionedComponent = (props: { targetClassName?: string; }) => { const { positioning, targetContent = 'Click me', targetClassName } = props; - const styles = useExampleStyles(); return ( - diff --git a/apps/public-docsite-v9/src/Concepts/QuickStart.mdx b/apps/public-docsite-v9/src/Concepts/QuickStart.mdx index c51a7df67e0b5..1a79422d673f4 100644 --- a/apps/public-docsite-v9/src/Concepts/QuickStart.mdx +++ b/apps/public-docsite-v9/src/Concepts/QuickStart.mdx @@ -4,47 +4,75 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Install -Fluent UI should be installed as a `dependency` of your app. +Fluent UI should be installed as a `dependency` of your app, together with the shared theme layer its CSS +compiles against. ```sh -yarn add @fluentui/react-components +yarn add @fluentui/react-components @fluentui/react-tailwind-theme ``` ## Setup -Fluent UI components are styled using CSS in JS. This technique requires a style renderer which inserts CSS into DOM when needed. React context is used to provide the style renderer. +### 1. Import the theme stylesheet, once -Place a `` at the root of your app and pass theme as a prop. +Fluent UI components ship plain, precompiled CSS, pulled in automatically when you import a component. That +CSS references a small set of document-level custom properties — the cascade `@layer` order, the spacing +scale, the focus-outline registrations — and something has to emit those **once per document**. That is what +`@fluentui/react-tailwind-theme` is for. -### React 18 +Import it at your app root, before your own styles: + +```js +// src/main.jsx (or _app.tsx, root layout, etc.) +import '@fluentui/react-tailwind-theme/styles.css'; +``` + +> ⚠ Skip this and components render with the right colors and the wrong metrics — spacing utilities resolve +> against a missing variable and compute to `0px`. Collapsed padding and gaps means this import is missing. + +### 2. Add a provider + +The stylesheet you just imported already applies the **web light** theme at the document root, so components +are themed before you add anything. A `FluentProvider` is what lets you pick a _different_ theme, and what +carries that choice into portals (popups, menus, dialogs) that render outside its DOM subtree. + +Every theme ships as a static CSS class — `webDarkThemeClassName` is `.fui-theme-web-dark`, a block of custom +property declarations. Place a `` at the root of your app and pass the class name you want +through the `themeClassName` prop. + +#### React 18 ```jsx import React from 'react'; import { createRoot } from 'react-dom/client'; -import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { FluentProvider, webLightThemeClassName } from '@fluentui/react-components'; + +import '@fluentui/react-tailwind-theme/styles.css'; import App from './App'; const root = createRoot(document.getElementById('root')); root.render( - + , ); ``` -### React 17 +#### React 17 ```jsx import React from 'react'; import ReactDOM from 'react-dom'; -import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { FluentProvider, webLightThemeClassName } from '@fluentui/react-components'; + +import '@fluentui/react-tailwind-theme/styles.css'; import App from './App'; ReactDOM.render( - + , document.getElementById('root'), @@ -53,7 +81,9 @@ ReactDOM.render( ## Usage -That's it. You can now use Fluent UI components in your app. +That's it. You can now use Fluent UI components in your app. With a bundler consuming the ESM build — +Vite, webpack, Next.js, Remix, Parcel, esbuild — you never import a component stylesheet yourself: +importing the component brings its CSS with it. ```jsx import React from 'react'; @@ -62,6 +92,41 @@ import { Button } from '@fluentui/react-components'; export default () => ; ``` +To restyle a component, pass a class name to it or to one of its slots — your own CSS wins over Fluent's by +default. See [Styling components](?path=/docs/concepts-developer-styling-components--docs). + +### Loading stylesheets + +There are three delivery paths, and only the first is automatic. + +**1. The theme stylesheet — always, in every setup.** `@fluentui/react-tailwind-theme/styles.css`, +imported once per document (step 1 above). Nothing imports it for you. + +**2. Component CSS via a bundler (ESM).** Each package's `lib/**` modules side-effect-import their own +`dist/styles.css`, and every component package declares `"sideEffects": ["**/*.css"]` so bundlers +keep those imports through tree-shaking. Import the component, get the CSS. No action needed. + +**3. Component CSS in CommonJS / SSR — you must load it yourself.** The `lib-commonjs` output +deliberately contains **no** stylesheet `require`, because a Node `require()` of a `.css` file is not +meaningful. If your runtime resolves Fluent through `require` — most SSR setups, jest, some Node +bundling paths — load each package's stylesheet through its `./styles.css` export subpath: + +```js +// a in your document shell, or your bundler's client entry +import '@fluentui/react-button/styles.css'; +import '@fluentui/react-menu/styles.css'; +``` + +61 packages declare that subpath; import the ones you actually use. + +> **Known gap:** `@fluentui/react-components` itself does **not** expose a `./styles.css` subpath, so +> there is no single umbrella stylesheet import for a CommonJS consumer today. Use the per-package +> subpaths, or consume the ESM build. + +**Symptoms.** Missing theme stylesheet (path 1) → components keep their colors but lose spacing, +radii and shadows. Missing component CSS (path 3) → components render as unstyled markup. Neither +throws. + ### Strict mode We are aware of some strict mode bugs when using Fluent UI v9 in React 18. These bugs only show up in strict mode, and they will not stop the rest of your app from running. diff --git a/apps/public-docsite-v9/src/Concepts/SSR/NextJSAppDir.mdx b/apps/public-docsite-v9/src/Concepts/SSR/NextJSAppDir.mdx index 24d24ddde24a1..92d81922473a0 100644 --- a/apps/public-docsite-v9/src/Concepts/SSR/NextJSAppDir.mdx +++ b/apps/public-docsite-v9/src/Concepts/SSR/NextJSAppDir.mdx @@ -6,6 +6,10 @@ import { Meta } from '@storybook/addon-docs/blocks'; For basic instructions on getting Next.js set up, see [Getting Started](https://nextjs.org/docs/getting-started). +**There is no style-extraction step.** Component styles are precompiled CSS; the server renders +markup only. See +[Basic setup](?path=/docs/concepts-developer-server-side-rendering-basic-setup--docs). + 1. Get a basic next.js setup running, rendering a page from the `app` folder, as guided by the tutorial. 2. Add the Fluent UI dependencies: `@fluentui/react-components`. 3. Add the SWC plugin to add the client directive for our library: `fluentui-next-appdir-directive`. See @@ -27,45 +31,28 @@ npm install @fluentui/react-components fluentui-next-appdir-directive 'use client'; import * as React from 'react'; -import { - FluentProvider, - teamsDarkTheme, - SSRProvider, - RendererProvider, - createDOMRenderer, - renderToStyleElements, -} from '@fluentui/react-components'; -import { useServerInsertedHTML } from 'next/navigation'; +import { FluentProvider, teamsDarkThemeClassName, SSRProvider } from '@fluentui/react-components'; export function Providers({ children }: { children: React.ReactNode }) { - const [renderer] = React.useState(() => createDOMRenderer()); - const didRenderRef = React.useRef(false); - - useServerInsertedHTML(() => { - if (didRenderRef.current) { - return; - } - didRenderRef.current = true; - return <>{renderToStyleElements(renderer)}; - }); - return ( - - - {children} - - + + {children} + ); } ``` -2. Modify the `layout.tsx` file under your `app` folder to add our providers: +2. Modify the `layout.tsx` file under your `app` folder to add our providers and the theme + stylesheet: ```tsx import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; +// 👇 the theme's custom properties — without this stylesheet spacing collapses +import '@fluentui/react-tailwind-theme/styles.css'; + // 👇 import the providers import { Providers } from './providers'; @@ -88,13 +75,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } ``` -3. Add `fluentui-next-appdir-directive` plugin with the paths for `@griffel` and `@fluentui`: +3. Add `fluentui-next-appdir-directive` plugin with the path for `@fluentui`: ```js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { - swcPlugins: [['fluentui-next-appdir-directive', { paths: ['@griffel', '@fluentui'] }]], + swcPlugins: [['fluentui-next-appdir-directive', { paths: ['@fluentui'] }]], }, }; @@ -111,3 +98,8 @@ export default function Home() { return ; } ``` + +> **Migrating from an older setup?** The `useServerInsertedHTML` / `createDOMRenderer` / +> `renderToStyleElements` plumbing in `providers.tsx` existed to stream Griffel CSS-in-JS rules +> into the server-rendered HTML. Fluent's components no longer produce any, so it can simply be +> deleted, along with the `@griffel` entry in the SWC plugin's `paths`. diff --git a/apps/public-docsite-v9/src/Concepts/SSR/Nextjs.mdx b/apps/public-docsite-v9/src/Concepts/SSR/Nextjs.mdx index 15e86ec8075f1..35701e11a81ac 100644 --- a/apps/public-docsite-v9/src/Concepts/SSR/Nextjs.mdx +++ b/apps/public-docsite-v9/src/Concepts/SSR/Nextjs.mdx @@ -6,6 +6,11 @@ import { Meta } from '@storybook/addon-docs/blocks'; For basic instructions on getting Next.js set up, see [Getting Started](https://nextjs.org/docs/getting-started). +**Component CSS needs no SSR wiring.** Fluent's component styles and the theme stylesheet are +ordinary CSS — import them once and Next.js handles them like any other stylesheet. No custom +`_document.tsx` is required. See +[Basic setup](?path=/docs/concepts-developer-server-side-rendering-basic-setup--docs). + 1. Get a basic next.js setup running, rendering a page from the `pages` folder, as guided by the tutorial. 2. Add the Fluent UI dependencies: `@fluentui/react-components`. @@ -19,118 +24,53 @@ npm install @fluentui/react-components ### Setting up Fluent UI -1. Create a `_document.tsx` file under your `pages` folder with the following content: - -```tsx -import { createDOMRenderer, renderToStyleElements } from '@fluentui/react-components'; -import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'; - -class MyDocument extends Document { - static async getInitialProps(ctx: DocumentContext) { - // 👇 creates a renderer that will be used for SSR - const renderer = createDOMRenderer(); - const originalRenderPage = ctx.renderPage; - - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: App => - function EnhancedApp(props) { - const enhancedProps = { - ...props, - // 👇 this is required to provide a proper renderer instance - renderer, - }; - - return ; - }, - }); - - const initialProps = await Document.getInitialProps(ctx); - const styles = renderToStyleElements(renderer); - - return { - ...initialProps, - styles: ( - <> - {initialProps.styles} - {/* 👇 adding Fluent UI styles elements to output */} - {styles} - - ), - }; - } - - render() { - return ( - - - -
- - - - ); - } -} - -export default MyDocument; -``` - -2. Create or modify a `_app.tsx` file under your `pages` folder with the following content: +1. Create or modify a `_app.tsx` file under your `pages` folder with the following content: ```tsx -import { - createDOMRenderer, - FluentProvider, - GriffelRenderer, - SSRProvider, - RendererProvider, - webLightTheme, -} from '@fluentui/react-components'; +import { FluentProvider, SSRProvider, webLightThemeClassName } from '@fluentui/react-components'; import type { AppProps } from 'next/app'; -type EnhancedAppProps = AppProps & { renderer?: GriffelRenderer }; +// 👇 the theme's custom properties — without this stylesheet spacing collapses +import '@fluentui/react-tailwind-theme/styles.css'; -function MyApp({ Component, pageProps, renderer }: EnhancedAppProps) { +function MyApp({ Component, pageProps }: AppProps) { return ( - // 👇 Accepts a renderer from or creates a default one - // Also triggers rehydration a client - - - - - - - + + + + + ); } export default MyApp; ``` -3. You should now be able to server render Fluent UI React components in any of your pages: +2. You should now be able to server render Fluent UI React components in any of your pages. Styles + for the page itself are a CSS Module next to the page, referencing theme tokens as custom + properties: + +```css +/* Home.module.css */ +.container { + display: flex; + flex-direction: column; + width: 200px; + border: 2px dashed var(--color-palette-berry-border-2); + border-radius: var(--radius-medium); + gap: 5px; + padding: 10px; +} +``` ```tsx -import { Button, makeStyles, shorthands, Title1, tokens } from '@fluentui/react-components'; +import { Button, Title1 } from '@fluentui/react-components'; import type { NextPage } from 'next'; import Head from 'next/head'; -const useStyles = makeStyles({ - container: { - display: 'flex', - flexDirection: 'column', - width: '200px', - - ...shorthands.border('2px', 'dashed', tokens.colorPaletteBerryBorder2), - ...shorthands.borderRadius(tokens.borderRadiusMedium), - ...shorthands.gap('5px'), - ...shorthands.padding('10px'), - }, -}); +import styles from './Home.module.css'; const Home: NextPage = () => { - const styles = useStyles(); - return ( <> @@ -147,3 +87,8 @@ const Home: NextPage = () => { export default Home; ``` + +> **Migrating from an older setup?** The `_document.tsx` renderer plumbing +> (`createDOMRenderer` / `renderToStyleElements`) and the `RendererProvider` in `_app.tsx` existed +> to extract Griffel CSS-in-JS rules during SSR. Fluent's components no longer produce any, so both +> can simply be deleted. diff --git a/apps/public-docsite-v9/src/Concepts/SSR/Portals.mdx b/apps/public-docsite-v9/src/Concepts/SSR/Portals.mdx index cba11a55fbfee..34b0bda577b91 100644 --- a/apps/public-docsite-v9/src/Concepts/SSR/Portals.mdx +++ b/apps/public-docsite-v9/src/Concepts/SSR/Portals.mdx @@ -51,3 +51,39 @@ const DefaultOpenMenu = () => { ); }; ``` + +## Theming portals + +Portal content is rendered outside your React tree's DOM position, so it does not inherit anything +from the ancestors it appears under in JSX. Theming still works, because `FluentProvider` propagates +its resolved theme class onto the portal mount node. + +What that means in practice: + +- **A `` themes the popovers, menus, dialogs and tooltips opened + from inside it.** The mount node carries the provider's class, so the custom properties cascade + into the portal exactly as they do into the inline subtree. +- **Nested providers inherit.** A `` with no `themeClassName` resolves to its + closest ancestor provider's class, and portals opened from within it follow that. Pass a different + class to re-theme just that part of the tree, portals included. +- **A custom theme class propagates identically** — nothing about the mechanism is specific to the + shipped themes. It is a class name being copied onto the mount node. +- **Outside any provider**, there is no class to copy, and the portal resolves the `:root` Web Light + defaults from `@fluentui/react-tailwind-theme/styles.css`. That is a valid, themed result — not an + unstyled one. + +The case that needs care is a portal target **you** create and mount outside every `FluentProvider` +— a container appended to `document.body` by non-Fluent code, for example. Nothing copies a class +onto it, so it gets the `:root` defaults regardless of what theme the rest of your app is showing. +Put the theme class on the container yourself: + +```jsx +import { webDarkThemeClassName } from '@fluentui/react-components'; + +const container = document.createElement('div'); +container.classList.add(webDarkThemeClassName); +document.body.appendChild(container); +``` + +Or, more simply, apply your app's theme class to `` so there is no un-themed region of the +document at all. See [Theming](?path=/docs/concepts-developer-theming--docs). diff --git a/apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx b/apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx index f0b83854c2f06..df298e915962e 100644 --- a/apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx +++ b/apps/public-docsite-v9/src/Concepts/SSR/Remix.mdx @@ -4,6 +4,11 @@ import { Meta } from '@storybook/addon-docs/blocks'; # React Router 7/Remix setup +**Component CSS needs no build plugin and no style extraction.** Fluent's component styles are +precompiled — import each package's stylesheet (and `@fluentui/react-tailwind-theme/styles.css`) +once at your app root, and Vite serves them like any other CSS. See +[Basic setup](?path=/docs/concepts-developer-server-side-rendering-basic-setup--docs). + ## Installation 1. Create a new React Router 7/Remix project or skip this step if you already have one: @@ -21,8 +26,8 @@ npx create-react-router@latest fluentui-react-router # Install Fluent UI core packages npm i @fluentui/react-components @fluentui/react-icons -# Install required Vite plugins -npm i vite-plugin-cjs-interop @griffel/vite-plugin -D +# Install required Vite plugin +npm i vite-plugin-cjs-interop -D ``` ## Configuration @@ -32,9 +37,8 @@ npm i vite-plugin-cjs-interop @griffel/vite-plugin -D ```ts // Import Vite plugins import { cjsInterop } from 'vite-plugin-cjs-interop'; -import griffel from '@griffel/vite-plugin'; -export default defineConfig(({ command }) => ({ +export default defineConfig(() => ({ plugins: [ reactRouter(), // or remix(), tsconfigPaths(), @@ -43,8 +47,6 @@ export default defineConfig(({ command }) => ({ cjsInterop({ dependencies: ['@fluentui/react-components'], }), - // Add Griffel plugin for production optimization - command === 'build' && griffel(), ], // Required for Fluent UI icons in SSR ssr: { @@ -53,11 +55,14 @@ export default defineConfig(({ command }) => ({ })); ``` -2. Modify `app/root.tsx` to add Fluent UI providers: +2. Modify `app/root.tsx` to add Fluent UI providers and the theme stylesheet: ```tsx // 1. Import Fluent UI dependencies -import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { FluentProvider, webLightThemeClassName } from '@fluentui/react-components'; + +// 2. Import the theme's custom properties — without this stylesheet spacing collapses +import '@fluentui/react-tailwind-theme/styles.css'; export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -67,12 +72,10 @@ export function Layout({ children }: { children: React.ReactNode }) { - {/* 2. Add insertion point for Fluent UI styles before the . */} - {/* 3. Wrap app content with FluentProvider */} - {children} + {children} @@ -93,10 +96,11 @@ npx react-router reveal npx remix reveal ``` -- Update the `entry.client.tsx` to wrap the router with both `` and ``: +- Update the `entry.client.tsx` to wrap the router with `` so generated ids match the + server's: ```tsx -import { createDOMRenderer, RendererProvider, SSRProvider } from '@fluentui/react-components'; +import { SSRProvider } from '@fluentui/react-components'; import { startTransition, StrictMode } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; @@ -105,25 +109,18 @@ startTransition(() => { hydrateRoot( document, - - - - - + + + , ); }); ``` -- and then update the `entry.server.tsx`: +- and then wrap the server router in `entry.server.tsx` the same way: ```tsx -// 1. Import required Fluent UI SSR utilities -import { createDOMRenderer, RendererProvider, renderToStyleElements, SSRProvider } from '@fluentui/react-components'; - -// 2. Define constants for style injection -const FLUENT_UI_INSERTION_POINT_TAG = ``; -const FLUENT_UI_INSERTION_TAG_REGEX = new RegExp(FLUENT_UI_INSERTION_POINT_TAG.replaceAll(' ', '(\\s)*')); +import { SSRProvider } from '@fluentui/react-components'; export default function handleRequest( request: Request, @@ -131,48 +128,24 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext, ) { - // 3. Create Fluent UI renderer - const renderer = createDOMRenderer(); - - // ... - return new Promise((resolve, reject) => { - let shellRendered = false; - // 4. Track style extraction state - let isStyleExtracted = false; - const { pipe, abort } = renderToPipeableStream( - // 5. Wrap RemixServer with Fluent UI providers - - - - {/* or */} - - , + + + {/* or */} + , { - [callbackName]: () => { - shellRendered = true; - const body = new PassThrough({ - // 6. Transform stream to inject Fluent UI styles - transform(chunk, _, callback) { - const str = chunk.toString(); - const style = renderToStaticMarkup(<>{renderToStyleElements(renderer)}); - - if (!isStyleExtracted && FLUENT_UI_INSERTION_TAG_REGEX.test(str)) { - chunk = str.replace(FLUENT_UI_INSERTION_TAG_REGEX, `${FLUENT_UI_INSERTION_POINT_TAG}${style}`); - isStyleExtracted = true; - } - - callback(null, chunk); - }, - }); - // ... - } - } + // ... standard React Router / Remix streaming options, unchanged + }, + ); }); } ``` +No transform stream, insertion-point `` tag, or style injection is needed — the stylesheets +arrive through `` / the Vite bundle, and `FluentProvider` only stamps its static theme +class into the markup; the theme's custom properties come from the stylesheet. + ## Usage Example Create or update `app/routes/_index.tsx`: @@ -198,15 +171,7 @@ export default function Index() { ### Common Issues -1. **SSR Hydration Mismatch** - -``` -Text content does not match server-rendered HTML -``` - -Fix: Check style injection in `entry.server.tsx`. - -2. **Icons Not Rendering in SSR** +1. **Icons Not Rendering in SSR** ``` Error: No "exports" main defined in node_modules/@fluentui/react-icons/package.json @@ -220,7 +185,7 @@ ssr: { } ``` -3. **Module Resolution Errors** +2. **Module Resolution Errors** ``` Cannot use import statement outside a module @@ -234,7 +199,7 @@ cjsInterop({ }), ``` -4. **Development Mode Warning** +3. **Development Mode Warning** ``` @fluentui/react-provider: There are conflicting ids in your DOM. @@ -244,6 +209,11 @@ Configuration guide: https://aka.ms/fluentui-conflicting-ids This warning occurs in development due to [React's StrictMode double rendering](https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-double-rendering-in-development). It can be safely ignored as it doesn't affect production builds. -### Production Build Optimization - -For production builds, install and configure [`@griffel/vite-plugin`](https://griffel.js.org/react/ahead-of-time-compilation/with-vite) to enable build time pre-computing and transforming styles. +> **Migrating from an older setup?** The `RendererProvider` wrappers, the +> `fluentui-insertion-point` meta tag, the style-injecting transform stream in `entry.server.tsx`, +> and `@griffel/vite-plugin` all existed to extract Griffel CSS-in-JS rules. Fluent's components no +> longer produce any, so all of it can simply be deleted. If your own application still authors +> Griffel styles — which now requires a direct `@griffel/react` dependency, since +> `@fluentui/react-components` no longer re-exports any Griffel API — Griffel's +> [SSR guide](https://griffel.js.org/react/guides/ssr-usage) covers those rules until you migrate +> them to CSS. diff --git a/apps/public-docsite-v9/src/Concepts/SSR/SSR.mdx b/apps/public-docsite-v9/src/Concepts/SSR/SSR.mdx index d52431ee6f8d8..e6256294944d7 100644 --- a/apps/public-docsite-v9/src/Concepts/SSR/SSR.mdx +++ b/apps/public-docsite-v9/src/Concepts/SSR/SSR.mdx @@ -6,6 +6,22 @@ import { Meta } from '@storybook/addon-docs/blocks'; Fluent UI React v9 supports Server-Side Rendering. +**There is no style-extraction step.** Component styles are precompiled stylesheets shipped in each +package, and the theme is one stylesheet (`@fluentui/react-tailwind-theme/styles.css`). Both are +ordinary CSS: your bundler includes them in the page like any other stylesheet, so they are in the +document before React runs. There is nothing to collect, flush, or rehydrate on the styling side — +the server's only job is rendering markup. + +What SSR setup still consists of: + +- **`SSRProvider`** — makes generated ids stable between server and client so hydration matches. +- **`FluentProvider`** — provides the theme by applying a static theme class (`themeClassName`) to + its root and propagating it to portals. The class is plain markup, identical on the server and the + client, so it needs no special wiring; the theme's values come from the stylesheet. +- **Your stylesheets** — make sure the component CSS and `@fluentui/react-tailwind-theme/styles.css` + end up in the served page (a plain `import` at the app root is enough for every mainstream + bundler). Without the theme stylesheet the custom properties are missing and spacing collapses. + ### Basic setup Add `@fluentui/react-components` dependency: @@ -18,57 +34,31 @@ yarn add @fluentui/react-components npm install @fluentui/react-components ``` -For any setup using SSR, you need to provide a `RendererProvider`, `SSRProvider` and `FluentProvider` in the root of your app. If these providers are not added, there will be issues when hydrating. See the following example: +Wrap your app in `SSRProvider` and `FluentProvider` — identically on the server and the client: ```tsx import express from 'express'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; -import { - createDOMRenderer, - RendererProvider, - renderToStyleElements, - FluentProvider, - webLightTheme, - SSRProvider, -} from '@fluentui/react-components'; - -const useExampleStyles = makeStyles({ - root: { - color: 'red', - }, -}); - -const ExampleComponent: React.FC = () => { - const classes = useExampleStyles(); - - return
Hello world
; -}; +import { Button, FluentProvider, webLightThemeClassName, SSRProvider } from '@fluentui/react-components'; const server = express(); server.get('/', (req, res) => { - const renderer = createDOMRenderer(); - const html = ReactDOMServer.renderToString( - - - - - - - , + + + + + , ); - // Converting Fluent UI styles to style elements. 👇 - const style = ReactDOMServer.renderToStaticMarkup(<>{renderToStyleElements(renderer)}); - res.write(` - ${/* 👇 adding Fluent UI styles elements to output */} - ${style} + ${/* 👇 the app bundle's CSS — component styles and the theme stylesheet */ ''} +
${html}
@@ -80,3 +70,11 @@ server.get('/', (req, res) => { server.listen(3000, 'localhost'); ``` + +> **Migrating from an older setup?** `RendererProvider`, `createDOMRenderer` and +> `renderToStyleElements` existed to extract [Griffel](https://griffel.js.org) CSS-in-JS rules +> during SSR. Fluent's components no longer produce any, so that plumbing can simply be deleted. If +> your own application still authors Griffel styles — which now requires a direct +> `@griffel/react` dependency, since `@fluentui/react-components` no longer re-exports any Griffel +> API — Griffel's own [SSR guide](https://griffel.js.org/react/guides/ssr-usage) covers extracting +> those rules until you migrate them to CSS. diff --git a/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.module.css b/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.module.css new file mode 100644 index 0000000000000..4075324a7d1b5 --- /dev/null +++ b/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.module.css @@ -0,0 +1,110 @@ +/* Story styles for SlotSketch. + * Token references are the Fluent theme's CSS variables. */ + +.root { + display: flex; + align-items: center; +} + +.card { + max-width: 400px; + display: flex; + flex-direction: column; + align-items: stretch; + justify-items: flex-start; + margin: 5px; +} + +.header { + display: flex; + align-items: center; + justify-items: space-between; + background-color: var(--color-neutral-background-3); + padding: 5px 20px; +} + +.slot { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + justify-items: center; + margin: 5px; + border-color: var(--color-neutral-stroke-2); + border-style: dotted; + border-width: 3px; +} + +.imageSlot { + width: 30px; + height: 30px; + margin: 5px 20px 5px 5px; +} + +.image { + margin: 5px 20px 5px 5px; +} + +.title { + font-family: var(--font-base); + font-size: var(--text-base-400); + font-weight: var(--font-weight-semibold); + line-height: var(--leading-base-400); +} + +.content { + display: flex; + align-items: center; + justify-items: space-between; + flex-grow: 1; + font-family: var(--font-base); + font-size: var(--text-base-200); + font-weight: var(--font-weight-regular); + line-height: var(--leading-base-300); + padding: 20px; +} + +.component { + display: flex; + align-items: center; + justify-items: center; + border-radius: var(--radius-medium); + border-color: var(--color-neutral-stroke-2); + border-style: solid; + border-width: 1px; +} + +.inputSketch { + width: 100px; +} + +.contentSlot { + display: block; + border-color: var(--color-neutral-stroke-2); + border-style: dotted; + border-width: 3px; +} + +.input { + width: 210px; + min-height: 0; + padding: 0; +} + +.urlBefore { + background-color: var(--color-neutral-background-3); + font-family: var(--font-base); + font-size: var(--text-base-200); + font-weight: var(--font-weight-semibold); + line-height: var(--leading-base-200); + padding: 10px 5px; +} + +.urlAfter { + background-color: var(--color-neutral-background-3); + font-family: var(--font-base); + font-size: var(--text-base-200); + font-weight: var(--font-weight-semibold); + line-height: var(--leading-base-200); + padding: 10px 5px; +} diff --git a/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.stories.tsx b/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.stories.tsx index 0eab2d637bbd7..8bb7a9617467d 100644 --- a/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.stories.tsx +++ b/apps/public-docsite-v9/src/Concepts/Slots/SlotSketch.stories.tsx @@ -1,97 +1,17 @@ import * as React from 'react'; -import { makeStyles, mergeClasses, shorthands, Input, tokens, typographyStyles } from '@fluentui/react-components'; +import { Input } from '@fluentui/react-components'; -const useStyles = makeStyles({ - root: { - display: 'flex', - alignItems: 'center', - }, - card: { - maxWidth: '400px', - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', - justifyItems: 'flex-start', - ...shorthands.margin('5px'), - }, - header: { - display: 'flex', - alignItems: 'center', - justifyItems: 'space-between', - ...shorthands.padding('5px', '20px'), - backgroundColor: tokens.colorNeutralBackground3, - }, - slot: { - ...shorthands.border('3px', 'dotted', tokens.colorNeutralStroke2), - display: 'flex', - alignContent: 'center', - alignItems: 'center', - justifyContent: 'center', - justifyItems: 'center', - ...shorthands.margin('5px'), - }, - imageSlot: { - width: '30px', - height: '30px', - ...shorthands.margin('5px', '20px', '5px', '5px'), - }, - image: { - ...shorthands.margin('5px', '20px', '5px', '5px'), - }, - title: { - ...typographyStyles.subtitle2, - }, - content: { - display: 'flex', - alignItems: 'center', - justifyItems: 'space-between', - flexGrow: 1, - ...shorthands.padding('20px'), - ...typographyStyles.body1, - fontSize: tokens.fontSizeBase200, - }, - component: { - ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke2), - ...shorthands.borderRadius(tokens.borderRadiusMedium), - display: 'flex', - alignItems: 'center', - justifyItems: 'center', - }, - inputSketch: { - width: '100px', - }, - contentSlot: { - display: 'block', - ...shorthands.border('3px', 'dotted', tokens.colorNeutralStroke2), - }, - input: { - width: '210px', - ...shorthands.padding(0), - minHeight: 0, - }, - urlBefore: { - ...shorthands.padding('10px', '5px'), - backgroundColor: tokens.colorNeutralBackground3, - ...typographyStyles.caption1Strong, - }, - urlAfter: { - ...shorthands.padding('10px', '5px'), - backgroundColor: tokens.colorNeutralBackground3, - ...typographyStyles.caption1Strong, - }, -}); +import styles from './SlotSketch.module.css'; export const SlotSketch = () => { - const styles = useStyles(); - return (
-
Before
+
Before
Placeholder text
-
After
+
After
diff --git a/apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx b/apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx index ea38899e0b158..74ddf9d494d63 100644 --- a/apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx +++ b/apps/public-docsite-v9/src/Concepts/Slots/Slots.mdx @@ -42,7 +42,7 @@ Later in this topic, you will find examples covering each of these scenarios. - If you need to slightly adjust the display of a specific component instance, prefer to apply a custom style. For example, if you want to make a particular component have a thicker border, - create a class style using `makeStyles` and then apply it to the component using `className`. + write a rule in your own stylesheet and apply it to the component using `className`. See the Styling Components topic for more information. - If you want to change how a component behaves, make significant layout and style changes, @@ -138,13 +138,17 @@ This allows you pass `PresenceBadge` props. You can pass the `className` prop to customize the style of a slot. The `Avatar` badge slot supports a `className`, allowing the badge style to be customized. +```css +/* BusyBrandAvatar.module.css */ +.badge { + color: var(--color-brand-stroke-1); +} +``` + ```tsx -const useStyles = makeStyles({ - badge: { color: tokens.colorBrandStroke1 }, -}); +import styles from './BusyBrandAvatar.module.css'; const BusyBrandAvatar = () => { - const styles = useStyles(); return ; }; ``` @@ -244,8 +248,7 @@ or the `children` of a React child, array of nodes, or portal. ```ts type WithSlotShorthandValue = - | Props - | Extract; + Props | Extract; type SlotShorthandValue = React.ReactChild | React.ReactNodeArray | React.ReactPortal; ``` @@ -356,7 +359,7 @@ and define the default element types for each slot. const useButtonStyles_unstable = (state: ButtonState) ``` -Hooks like `useButtonStyles` create classes and conditionally apply them based on the input state. Classes are typically created and applied with `makeStyles` and `mergeClasses`. +Hooks like `useButtonStyles` conditionally apply the component's CSS-Module class names based on the input state, joining them with `clsx` and returning the new state. For instance, if a component is disabled then disabled styles are added. ### Declaring components slots in the state diff --git a/apps/public-docsite-v9/src/Concepts/StylingComponents.mdx b/apps/public-docsite-v9/src/Concepts/StylingComponents.mdx index 12aca92783a7f..05e591644d94d 100644 --- a/apps/public-docsite-v9/src/Concepts/StylingComponents.mdx +++ b/apps/public-docsite-v9/src/Concepts/StylingComponents.mdx @@ -10,166 +10,304 @@ Visit the **[Styling handbook](https://github.com/microsoft/fluentui/blob/master ### Getting started -To style Fluent UI React v9 components `makeStyles` is used. `makeStyles` comes from [Griffel](https://griffel.js.org) a homegrown CSS-in-JS implementation which generates atomic CSS classes. +Fluent UI React v9 components ship **plain, precompiled CSS**. Every component's rules live in one +cascade-layer family: -Get started by simply importing: - -```js -import { makeStyles } from '@fluentui/react-components'; +```css +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, + fui.components.l4, fui.components.l5, fui.utilities; ``` -To style a component, you need to call `makeStyles` in a module scope to create a React hook to be used inside a component. +That single fact is what makes styling simple: **the CSS cascade compares layers before it looks at +specificity, and unlayered CSS beats every layer.** So your own stylesheet wins over Fluent's rules by +default — no specificity war, no `!important`, no ordering tricks. -The `makeStyles` call accepts an object of items where each key is a unique identifier and each value is an object with styles. The call returns an object with classes mapped to these unique identifiers. +Write ordinary CSS (or a CSS Module, or Tailwind, or whatever your app already uses) and pass the class +to the component: -{/* TODO: format as */} +```css +/* MyForm.module.css */ +.submit { + background-color: var(--color-brand-background); + border-radius: 9999px; +} +``` ```jsx -import { makeStyles } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - root: { color: 'red' }, -}); +import { Button } from '@fluentui/react-components'; -function Component() { - const classes = useStyles(); +import styles from './MyForm.module.css'; - return
; +function MyForm() { + return ( + + ); } ``` -### Merging component styles +> ⚠ Fluent's component CSS references theme-level custom properties that something has to emit once per +> document. If you have not already, import `@fluentui/react-tailwind-theme/styles.css` at your app root — +> see [Quick start](?path=/docs/concepts-developer-quick-start--docs). Without it, spacing collapses to `0px`. -There are cases where you need to merge classes from multiple `useStyles` calls. +### Styling a component's internals — slot `className` props -#### ⚠ It is not possible to simply concatenate `useStyles` classes +`className` on the component styles its **root**. Every other part of a component is a **slot**, and every +slot takes its own `className`: -To properly merge the classes, you need to use `mergeClasses()` function, which performs merge and deduplication of atomic classes generated by `makeStyles()`. +```jsx +import { Button } from '@fluentui/react-components'; +import { CallEndRegular } from '@fluentui/react-icons'; -When `mergeClasses()` is called, it merges all classes from first to last - the latter argument overwrites the previous ones (similar to `Object.assign()`). +import styles from './MyForm.module.css'; + + Default TeachingPopoverCarouselFooter diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts index 20f194302f56b..c54437a8cc35f 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooter/useTeachingPopoverCarouselFooterStyles.styles.ts @@ -1,58 +1,60 @@ -'use client'; - -import { makeStyles, mergeClasses } from '@griffel/react'; -import type { - TeachingPopoverCarouselFooterSlots, - TeachingPopoverCarouselFooterState, -} from './TeachingPopoverCarouselFooter.types'; -import type { SlotClassNames } from '@fluentui/react-utilities'; - -export const teachingPopoverCarouselFooterClassNames: SlotClassNames = { - root: 'fui-TeachingPopoverCarouselFooter', - previous: 'fui-TeachingPopoverCarouselFooter__previous', - next: 'fui-TeachingPopoverCarouselFooter__next', +import { clsx } from 'clsx'; +import type { TeachingPopoverCarouselFooterState } from './TeachingPopoverCarouselFooter.types'; + +import styles from './TeachingPopoverCarouselFooter.module.css'; + +/** + * TeachingPopoverCarouselFooter's public identity class — the Tailwind named-group marker + * (`migration/griffel-to-tailwind/reports/DECISIONS.md`, D15.1 / D16.5). + * + * DEPRECATED FOR STYLING INTERNALS. The only supported way to style a Fluent component's + * internals is the per-slot `className` props. `root` is retained because it is still the + * component's public identity: it is a usable selector and a `group-*` variant target. The + * `fui-TeachingPopoverCarouselFooter` root static and the `__previous` / `__next` slot statics + * are gone (D16.1), and the type has narrowed from + * `SlotClassNames` to `{ root: string }`. + * + * The value is a class TOKEN, not a selector: `/` is legal inside a class name but terminates + * it in selector position, so `'.' + teachingPopoverCarouselFooterClassNames.root` is invalid + * CSS. Use `fuiSelector(...)` from `@fluentui/react-utilities` (D16.5); + * `element.classList.contains(...)` is token-taking and needs no escaping. + */ +export const teachingPopoverCarouselFooterClassNames: { root: string } = { + root: 'group/fui-teaching-popover-carousel-footer', }; -// Todo: Page change animation & styles -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'row', - }, - rootCentered: { - justifyContent: 'space-between', - gap: '8px', - }, - rootRightAligned: { - gap: '8px', - '& :first-child': { - marginInlineEnd: 'auto', - }, - }, -}); - /** Applies style classnames to slots */ export const useTeachingPopoverCarouselFooterStyles_unstable = ( state: TeachingPopoverCarouselFooterState, ): TeachingPopoverCarouselFooterState => { - const styles = useStyles(); const { layout } = state; - // eslint-disable-next-line react-hooks/immutability - state.root.className = mergeClasses( - teachingPopoverCarouselFooterClassNames.root, + // Module class FIRST, named group marker second, consumer className last (DECISIONS.md + // D16.2). `styles.root` is unconditional, so index 0 is always the hashed, selector-safe + // `fuicm-*` token — which is what keeps the marker off `classList[0]`, where nwsapi's + // `:scope` polyfill would throw on its `/` under jsdom (D15.1). + // + // `layout` selects a module class rather than a `data-*` attribute: it picks between two + // mutually exclusive looks on the very element this hook composes, and nothing reads it from + // a selector (D3 / D15.6, resolved). + // + // Cascade priority is decided by the `@layer fui.*` order in + // TeachingPopoverCarouselFooter.module.css — including the l2 half of `.right-aligned`, + // which reaches into a react-button root — not by the order of these arguments. + state.root.className = clsx( styles.root, - layout === 'centered' ? styles.rootCentered : styles.rootRightAligned, + 'group/fui-teaching-popover-carousel-footer', + layout === 'centered' ? styles.centered : styles['right-aligned'], state.root.className, ); - if (state.previous) { - // eslint-disable-next-line react-hooks/immutability - state.previous.className = mergeClasses(teachingPopoverCarouselFooterClassNames.previous, state.previous.className); - } - - // eslint-disable-next-line react-hooks/immutability - state.next.className = mergeClasses(teachingPopoverCarouselFooterClassNames.next, state.next.className); + // The `previous` / `next` assignments are GONE (D16.1 + cookbook, "A slot whose only library + // token is the static"): the Griffel hook wrote nothing to either slot but + // `fui-TeachingPopoverCarouselFooter__previous` / `__next`, so with the statics removed what + // remained was `clsx(state.previous.className)` — an identity on the consumer's own string, + // i.e. dead code implying this hook styles slots it does not. Both slots keep rendering and + // are styled by `useTeachingPopoverCarouselFooterButtonStyles_unstable`; the + // `if (state.previous)` guard went with the assignment it protected. return state; }; diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.module.css b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.module.css new file mode 100644 index 0000000000000..a1b9aec557956 --- /dev/null +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.module.css @@ -0,0 +1,160 @@ +@reference '#theme'; + +/* + * Cascade layer order (DECISIONS.md D2). Repeated on purpose — see + * TeachingPopoverBody.module.css for the full rationale. + */ +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +/* + * Layer assignment mirrors the mergeClasses ARGUMENT order of + * `useTeachingPopoverCarouselFooterButtonStyles_unstable`, not source declaration order. + * + * `root` slot + * # mergeClasses argument → target + * --------------------------------------------------------------------------------------- + * 1 teachingPopoverCarouselFooterButtonClassNames.root → REMOVED (D16.1) + * 2 useStyles.root → fui.components.l2 .root + * 3 navType === 'prev' && popoverAppearance === 'brand' + * && useStyles.brandPrevious → fui.components.l2 + * .brand-previous + * 4 navType === 'next' && popoverAppearance === 'brand' + * && useStyles.brandNext → fui.components.l2 + * .brand-next + * 5 state.root.className (react-button's output + consumer) → Button's own + * fui.components.l1 classes, + * consumer className last + * + * ── ALTITUDE: l2 THROUGHOUT ───────────────────────────────────────────────────────────── + * Every rule in this file lands on a react-button `Button` root — the styles hook calls + * `useButtonStyles_unstable(state)` on the SAME `state.root` — so all of it is "styles applied + * over ANOTHER component's hook output" (DECISIONS.md D2 amendment 2). Same shape as + * react-button's own ToggleButton / CompoundButton / MenuButton modules. + * + * ⚠ THIS SPLIT CHANGES A WINNER RELATIVE TO THE ORIGINAL ALL-GRIFFEL CODE — read before + * "simplifying" it. In the Griffel era both packages produced atomics and + * `useButtonStyles_unstable` ran FIRST, so Button's class string arrived as the LAST + * argument of this hook's `mergeClasses(...)` — and mergeClasses is last-argument-wins by + * property key, not a cascade (probe: `mergeClasses(A.a, B.b)` emits only `B`'s class). + * Button therefore OVERRODE `brandNext` / `brandPrevious` and this file's `min-width`, and + * the two brand slices rendered nothing. + * + * That inverted when react-button converted (pilot #2, ledger `react-button`): Button's + * classes became LAYERED module classes while this package's atomics were still UNLAYERED, + * and an unlayered author declaration beats every `@layer` before specificity is consulted + * (D2 amendment 5). Since that commit the repo has rendered TeachingPopover's brand footer + * buttons with the brand slices WINNING. + * + * `fui.components.l2` reproduces the CURRENT behaviour, which is also the behaviour the + * slices were obviously written for. It is the altitude the cookbook mandates for this + * shape, and the alternative — an altitude BELOW Button's l1 — is not expressible in the + * layer family. This package has no VR stories, so the flip is recorded here rather than + * adjudicated against a screenshot. + * + * ── ORDER inside l2 ───────────────────────────────────────────────────────────────────── + * The risk report lists this file as one of the 23 known argument-vs-declaration inversions + * (`brandPrevious` → `brandNext` by argument, `brandNext` first by declaration, colliding on + * `color` / `background-color` / their hover forms). The inversion is INERT: `navType` is a + * single value, so the two classes are mutually exclusive and can never both be on an element. + * The blocks below are written in ARGUMENT order anyway, per the cookbook. + * + * `.root` (arg #2) sets only `min-width`, which neither brand block touches — disjoint. + * + * ── RTL (DECISIONS.md D5) ─────────────────────────────────────────────────────────────── + * `shorthands.borderColor(...)` compiled to `[ltr, rtl]` PAIRS for the right/left longhands + * (a pure PROPERTY flip; both sides carry the same value), so those take the logical + * longhands below and no `@variant rtl` twin is needed. Nothing else here is + * direction-sensitive. + * + * ── D2 amendment 5 (UNLAYERED rules) does NOT apply here ──────────────────────────────── + * Grepped this package for `fui-Icon-filled` / `fui-Icon-regular` / `:global(`: zero hits. + * react-button is CONVERTED, so its rules are layered and `@layer fui.components.l2` can + * arbitrate against them; the unlayered escape hatch is for UNCONVERTED Griffel owners only. + * + * Verified against the compiled Griffel atomics in + * lib-commonjs/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.js. + */ + +@layer fui.components.l2 { + /* + * from useStyles.root — arg #2. + * `96px` matches no spacing step, so it takes the NUMERIC utility (D4 amendment: named + * tokens first, numeric as fallback) — which still computes through `--base-scale`. + */ + .root { + @apply min-w-96; + } +} + +@layer fui.components.l2 { + /* + * from useStyles.brandPrevious — arg #3 (`navType === 'prev' && popoverAppearance === + * 'brand'`). Source comment, carried: in brand, the foreground is always + * 'NeutralForegroundOnBrand'. + * + * `shorthands.borderColor(...)` is expanded to the four longhands the compiler emitted + * (cookbook: Griffel's shorthand/longhand priority machinery has no CSS equivalent), with + * the right/left pair written logically because Griffel flipped it under RTL. + * + * The hover / hover+active blocks reproduce the source's `':hover'` and `':hover:active'` + * selectors EXACTLY — nested `hover` + `active` catalog variants, not the catalog's + * `hover-active` compound. `hover-active` is `:hover:active, :active:focus-visible`, the + * shape Button's own Griffel source authored; this file authored the narrower one, and + * widening it would newly paint the pressed look on a keyboard-activated button. + */ + .brand-previous { + color: var(--color-neutral-foreground-on-brand); + background-color: var(--color-brand-background); + + border-block-start-color: var(--color-neutral-foreground-on-brand); + border-inline-end-color: var(--color-neutral-foreground-on-brand); + border-block-end-color: var(--color-neutral-foreground-on-brand); + border-inline-start-color: var(--color-neutral-foreground-on-brand); + + @variant hover { + color: var(--color-neutral-foreground-on-brand); + background-color: var(--color-brand-background-hover); + + border-block-start-color: var(--color-neutral-foreground-on-brand); + border-inline-end-color: var(--color-neutral-foreground-on-brand); + border-block-end-color: var(--color-neutral-foreground-on-brand); + border-inline-start-color: var(--color-neutral-foreground-on-brand); + + @variant active { + color: var(--color-neutral-foreground-on-brand); + background-color: var(--color-brand-background-pressed); + + border-block-start-color: var(--color-neutral-foreground-on-brand); + border-inline-end-color: var(--color-neutral-foreground-on-brand); + border-block-end-color: var(--color-neutral-foreground-on-brand); + border-inline-start-color: var(--color-neutral-foreground-on-brand); + } + } + } +} + +@layer fui.components.l2 { + /* + * from useStyles.brandNext — arg #4 (`navType === 'next' && popoverAppearance === 'brand'`). + * Mutually exclusive with `.brand-previous` above; see the header's inversion note. + */ + .brand-next { + color: var(--color-brand-foreground-1); + background-color: var(--color-neutral-foreground-on-brand); + + border-block-start-color: var(--color-transparent-background); + border-inline-end-color: var(--color-transparent-background); + border-block-end-color: var(--color-transparent-background); + border-inline-start-color: var(--color-transparent-background); + + @variant hover { + color: var(--color-compound-brand-foreground-1-hover); + background-color: var(--color-neutral-foreground-on-brand); + + @variant active { + color: var(--color-compound-brand-foreground-1-pressed); + background-color: var(--color-neutral-foreground-on-brand); + } + } + } +} diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx index 43c05e046df4a..60583f585f98b 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { render } from '@testing-library/react'; +import { CLASSNAME_OVERRIDES_WIN_TEST_NAME, classNameOverridesWin } from '@fluentui/react-conformance'; import { isConformant } from '../../testing/isConformant'; import { TeachingPopoverCarouselFooterButton } from './TeachingPopoverCarouselFooterButton'; import type { TeachingPopoverCarouselFooterButtonProps } from './TeachingPopoverCarouselFooterButton.types'; @@ -8,6 +9,27 @@ describe('TeachingPopoverCarouselFooterButton', () => { isConformant({ Component: TeachingPopoverCarouselFooterButton as React.FunctionComponent, displayName: 'TeachingPopoverCarouselFooterButton', + // `classname-overrides-win` (extraTests below) pins the styling override contract + // cascade-natively: the consumer `className` is composed last, and unlayered consumer CSS + // beats the component’s `@layer fui.*` rules (DECISIONS.md D2/D9). + // + // `component-has-static-classnames-object` is disabled because the BEM statics are gone + // (D16.1). + disabledTests: ['component-has-static-classnames-object'], + testOptions: { + // A TeachingPopoverCarouselFooterButton IS a react-button `Button` — + // `useButtonStyles_unstable` stamps its marker on this same element — so this root + // legitimately carries every marker below (DECISIONS.md D16.3). Declaring the whole set + // keeps `component-has-group-marker` running: it is an exact set comparison, so an + // undeclared marker still fails, and its `classList[0]` half — the D16.2 invariant that + // nwsapi's jsdom `:scope` polyfill depends on — is asserted here rather than locally. + 'has-group-marker': { + markers: ['group/fui-button', 'group/fui-teaching-popover-carousel-footer-button'], + }, + }, + extraTests: { + [CLASSNAME_OVERRIDES_WIN_TEST_NAME]: classNameOverridesWin, + }, }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx index 07121ec39dc9c..5c1b4da89a68a 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/TeachingPopoverCarouselFooterButton.tsx @@ -16,11 +16,11 @@ import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; */ export const TeachingPopoverCarouselFooterButton: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useTeachingPopoverCarouselFooterButton_unstable(props, ref); + let state = useTeachingPopoverCarouselFooterButton_unstable(props, ref); - useTeachingPopoverCarouselFooterButtonStyles_unstable(state); + state = useTeachingPopoverCarouselFooterButtonStyles_unstable(state); - useCustomStyleHook_unstable('useTeachingPopoverCarouselFooterButtonStyles_unstable')(state); + state = useCustomStyleHook_unstable('useTeachingPopoverCarouselFooterButtonStyles_unstable')(state); return renderTeachingPopoverCarouselFooterButton_unstable(state); }); diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap index bb5ba622ea546..2a69acc8cc476 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/__snapshots__/TeachingPopoverCarouselFooterButton.test.tsx.snap @@ -3,7 +3,8 @@ exports[`TeachingPopoverCarouselFooterButton renders a default state 1`] = `
diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts index e08300ed06156..37956355cd0e3 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselFooterButton/useTeachingPopoverCarouselFooterButtonStyles.styles.ts @@ -1,52 +1,44 @@ 'use client'; -import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { - TeachingPopoverCarouselFooterButtonSlots, - TeachingPopoverCarouselFooterButtonState, -} from './TeachingPopoverCarouselFooterButton.types'; -import { tokens } from '@fluentui/react-theme'; +/* + * NOTE on the directive above: + * this file keeps `'use client'` because it still calls `useButtonStyles_unstable`, so + * `enforce-use-client` sees a hook call and never reports the directive as unnecessary. Leaf + * hooks in this package call nothing and carry no directive at all; see + * useTeachingPopoverBodyStyles.styles.ts. + */ + +import { clsx } from 'clsx'; import { useButtonStyles_unstable } from '@fluentui/react-button'; +import type { TeachingPopoverCarouselFooterButtonState } from './TeachingPopoverCarouselFooterButton.types'; -export const teachingPopoverCarouselFooterButtonClassNames: SlotClassNames = { - root: 'fui-TeachingPopoverCarouselFooterButton', -}; +import styles from './TeachingPopoverCarouselFooterButton.module.css'; -const useStyles = makeStyles({ - root: { - minWidth: '96px', - }, - brandNext: { - color: tokens.colorBrandForeground1, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorTransparentBackground), - ':hover': { - color: tokens.colorCompoundBrandForeground1Hover, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - }, - ':hover:active': { - color: tokens.colorCompoundBrandForeground1Pressed, - backgroundColor: tokens.colorNeutralForegroundOnBrand, - }, - }, - brandPrevious: { - // In brand, this is always 'NeutralForegroundOnBrand' - color: tokens.colorNeutralForegroundOnBrand, - backgroundColor: tokens.colorBrandBackground, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - ':hover': { - color: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - backgroundColor: tokens.colorBrandBackgroundHover, - }, - ':hover:active': { - color: tokens.colorNeutralForegroundOnBrand, - ...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand), - backgroundColor: tokens.colorBrandBackgroundPressed, - }, - }, -}); +/** + * TeachingPopoverCarouselFooterButton's public identity class — the Tailwind named-group + * marker (`migration/griffel-to-tailwind/reports/DECISIONS.md`, D15.1 / D16.5). + * + * DEPRECATED FOR STYLING INTERNALS. The only supported way to style a Fluent component's + * internals is the per-slot `className` props. `root` is retained because it is still the + * component's public identity: it is a usable selector and a `group-*` variant target. The + * `fui-TeachingPopoverCarouselFooterButton` BEM static is gone (D16.1), and the type has + * narrowed from `SlotClassNames` to + * `{ root: string }`. + * + * This root is ALSO a react-button `Button` root, so it carries TWO markers by design — + * this one and `group/fui-button`, stamped by `useButtonStyles_unstable` on the same element + * (D16.3). A descendant, or a wrapping component, can address whichever identity it means. + * The conformance suite is told about the pair through + * `testOptions['has-group-marker'].markers`. + * + * The value is a class TOKEN, not a selector: `/` is legal inside a class name but terminates + * it in selector position, so `'.' + teachingPopoverCarouselFooterButtonClassNames.root` is + * invalid CSS. Use `fuiSelector(...)` from `@fluentui/react-utilities` (D16.5); + * `element.classList.contains(...)` is token-taking and needs no escaping. + */ +export const teachingPopoverCarouselFooterButtonClassNames: { root: string } = { + root: 'group/fui-teaching-popover-carousel-footer-button', +}; /** * Apply styling to the TeachingPopoverCarouselFooterButton slots based on the state @@ -56,22 +48,46 @@ export const useTeachingPopoverCarouselFooterButtonStyles_unstable = ( ): TeachingPopoverCarouselFooterButtonState => { 'use no memo'; // justified: compiler would optimize useTeachingPopoverCarouselFooterButtonStyles_unstable — manual opt-out to preserve runtime behavior - const styles = useStyles(); const { navType, popoverAppearance } = state; + // Module class FIRST, named group marker second, consumer className last (DECISIONS.md + // D16.2). `styles.root` is unconditional here, and `useButtonStyles_unstable` — called LAST, + // below — additionally prepends Button's own unconditional `styles.root`, so the token that + // actually renders at `classList[0]` is Button's hashed module class. Either way the marker + // is never index 0, where nwsapi's `:scope` polyfill would throw on its `/` under jsdom + // (D15.1); asserted by `component-has-group-marker`. + // + // Cascade priority is decided by the `@layer fui.*` order in + // TeachingPopoverCarouselFooterButton.module.css — every rule there is at + // `fui.components.l2`, above react-button's l1 — not by the order of these arguments. That + // altitude is the one deliberate behaviour decision in this conversion; the module header + // records why, and what it changes relative to the original all-Griffel code. + // + state = { + ...state, + root: { + ...state.root, + className: clsx( + styles.root, + 'group/fui-teaching-popover-carousel-footer-button', + navType === 'prev' && popoverAppearance === 'brand' && styles['brand-previous'], + navType === 'next' && popoverAppearance === 'brand' && styles['brand-next'], + state.root.className, + ), + }, + }; + + // Called LAST now (the Griffel version called it first and merged its output back in as the + // trailing mergeClasses argument). `useButtonStyles_unstable` composes its own classes AHEAD + // of the incoming className, so this string — consumer className already at its end — stays + // at the end of the rendered `class` attribute, which is what `classname-overrides-win` + // asserts. Same call order as react-button's own ToggleButton / CompoundButton. + // // Apply underlying fluent Button styles state = { ...state, ...useButtonStyles_unstable(state), }; - state.root.className = mergeClasses( - teachingPopoverCarouselFooterButtonClassNames.root, - styles.root, - navType === 'prev' && popoverAppearance === 'brand' && styles.brandPrevious, - navType === 'next' && popoverAppearance === 'brand' && styles.brandNext, - state.root.className, - ); - return state; }; diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.module.css b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.module.css new file mode 100644 index 0000000000000..0cc54ff5ee9ab --- /dev/null +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.module.css @@ -0,0 +1,81 @@ +@reference '#theme'; + +/* + * Cascade layer order (DECISIONS.md D2). Repeated on purpose — see + * TeachingPopoverBody.module.css for the full rationale. + */ +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +/* + * Layer assignment mirrors the mergeClasses ARGUMENT order of + * `useTeachingPopoverCarouselNavStyles_unstable`. + * + * `root` slot + * # mergeClasses argument → target + * --------------------------------------------------------------------------------------- + * 1 teachingPopoverCarouselNavClassNames.root → REMOVED (D16.1) + * 2 useStyles.root → fui.components.l1 .root + * 3 consumer className → unlayered (always wins) + * + * ── ALTITUDE: l1 ──────────────────────────────────────────────────────────────────────── + * The root is this component's own `div` (`useTeachingPopoverCarouselNav.tsx` builds it with + * `getIntrinsicElementProps('div', …)` and `role="tablist"`), so nothing sits over another + * component's hook output and `fui.components.l2` is unused. No `makeResetStyles` slice → + * `fui.base` is unused too. + * + * ── Focus indicator (DECISIONS.md D6) ─────────────────────────────────────────────────── + * The Griffel source spreads `createCustomFocusIndicatorStyle({ outline, borderRadius, + * ...shorthands.borderColor('transparent') })` — react-tabster's factory with a + * component-specific option object, NOT the Button-family ring, so there is no shared + * `fui-focus-*` utility to `@apply`. The catalog's `focus-visible-fui` variant supplies the + * `[data-fui-focus-visible]` selector (which is exactly what the factory's `createBaseSelector` + * emitted, confirmed in the compiled output) and the declarations are written out. Same call + * react-carousel's CarouselNavButton made for the same factory. + * + * ── Stroke widths (DECISIONS.md D4 addendum) ──────────────────────────────────────────── + * `outline-width` does NOT read the `--spacing-*` namespace, so `strokeWidthThick` takes a + * direct `var(--spacing-thick)` reference rather than a utility. Raw `var(--stroke-width-thick)` + * is forbidden in modules — it is the one form that does not scale with `--base-scale`. + * + * ── ORDER ─────────────────────────────────────────────────────────────────────────────── + * The focus block is written after the rest declarations. Under Griffel it won on + * SPECIFICITY (`.f89hs3r[data-fui-focus-visible]`, 0-2-0, against the flat 0-1-0 atomics); + * `:where()` flattens both to 0-1-0, so file position is what preserves the winner. There is + * nothing for it to collide with in this particular file (the base slice declares no + * `outline`, `border-radius` or `border-*-color`), but the placement is kept for consistency + * with the sibling modules where it IS load-bearing. + * + * ── RTL (DECISIONS.md D5) ─────────────────────────────────────────────────────────────── + * ONE `[ltr, rtl]` pair in the compiled output — the focus indicator's + * `border-right-color` / `border-left-color` (`Bci5o5g` / `Bdrgwmp`), value `transparent` on + * both sides, i.e. a pure PROPERTY flip → the logical longhands below, no `@variant rtl` twin. + * + * ── D2 amendment 5 (UNLAYERED rules) does NOT apply here ──────────────────────────────── + * Grepped this package for `fui-Icon-filled` / `fui-Icon-regular` / `:global(`: zero hits. + * This component renders no icons and reaches into no other component's elements. + * + * Verified against the compiled Griffel atomics in + * lib-commonjs/components/TeachingPopoverCarouselNav/useTeachingPopoverCarouselNavStyles.styles.js. + */ + +@layer fui.components.l1 { + /* from useStyles.root — arg #2 */ + .root { + @apply flex flex-row items-center justify-center; + + /* `columnGap: tokens.spacingHorizontalXS` — an inline-axis property reading a horizontal + token, so the named utility matches on both counts (D4 amendment). */ + @apply gap-x-horizontal-xs; + + /* from createCustomFocusIndicatorStyle({ outline, borderRadius, borderColor('transparent') }) */ + @variant focus-visible-fui { + outline: var(--spacing-thick) solid var(--color-stroke-focus-2); + border-radius: var(--radius-medium); + + border-block-start-color: transparent; + border-inline-end-color: transparent; + border-block-end-color: transparent; + border-inline-start-color: transparent; + } + } +} diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx index b837a587ab295..5422faa19d4a7 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { render } from '@testing-library/react'; +import { CLASSNAME_OVERRIDES_WIN_TEST_NAME, classNameOverridesWin } from '@fluentui/react-conformance'; import { isConformant } from '../../testing/isConformant'; import { TeachingPopoverCarouselNav } from './TeachingPopoverCarouselNav'; import { TeachingPopoverCarouselNavButton } from '../TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton'; @@ -8,6 +9,13 @@ describe('TeachingPopoverCarouselNav', () => { isConformant({ Component: TeachingPopoverCarouselNav, displayName: 'TeachingPopoverCarouselNav', + // `classname-overrides-win` (extraTests below) pins the styling override contract + // cascade-natively: the consumer `className` is composed last, and unlayered consumer CSS + // beats the component’s `@layer fui.*` rules (DECISIONS.md D2/D9). + disabledTests: ['component-has-static-classnames-object'], + extraTests: { + [CLASSNAME_OVERRIDES_WIN_TEST_NAME]: classNameOverridesWin, + }, }); it('renders a default state', () => { diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx index 219da20d48d79..10f905291485d 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/TeachingPopoverCarouselNav.tsx @@ -16,11 +16,11 @@ import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; */ export const TeachingPopoverCarouselNav: ForwardRefComponent = React.forwardRef( (props, ref) => { - const state = useTeachingPopoverCarouselNav_unstable(props, ref); + let state = useTeachingPopoverCarouselNav_unstable(props, ref); - useTeachingPopoverCarouselNavStyles_unstable(state); + state = useTeachingPopoverCarouselNavStyles_unstable(state); - useCustomStyleHook_unstable('useTeachingPopoverCarouselNavStyles_unstable')(state); + state = useCustomStyleHook_unstable('useTeachingPopoverCarouselNavStyles_unstable')(state); return renderTeachingPopoverCarouselNav_unstable(state); }, diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap index b79d4416e83a0..644584b18f170 100644 --- a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNav/__snapshots__/TeachingPopoverCarouselNav.test.tsx.snap @@ -3,7 +3,7 @@ exports[`TeachingPopoverCarouselNav renders a default state 1`] = `
= { - root: 'fui-TeachingPopoverCarouselNav', +/** + * TeachingPopoverCarouselNav's public identity class — the Tailwind named-group marker + * (`migration/griffel-to-tailwind/reports/DECISIONS.md`, D15.1 / D16.5). + * + * DEPRECATED FOR STYLING INTERNALS. The only supported way to style a Fluent component's + * internals is the per-slot `className` props. `root` is retained because it is still the + * component's public identity: it is a usable selector and a `group-*` variant target. The + * `fui-TeachingPopoverCarouselNav` BEM static is gone (D16.1), and the type has narrowed from + * `SlotClassNames` to `{ root: string }`. + * + * The value is a class TOKEN, not a selector: `/` is legal inside a class name but terminates + * it in selector position, so `'.' + teachingPopoverCarouselNavClassNames.root` is invalid + * CSS. Use `fuiSelector(...)` from `@fluentui/react-utilities` (D16.5); + * `element.classList.contains(...)` is token-taking and needs no escaping. + */ +export const teachingPopoverCarouselNavClassNames: { root: string } = { + root: 'group/fui-teaching-popover-carousel-nav', }; -const useStyles = makeStyles({ - root: { - display: 'flex', - flexDirection: 'row', - columnGap: tokens.spacingHorizontalXS, - alignItems: 'center', - justifyContent: 'center', - ...createCustomFocusIndicatorStyle({ - outline: `${tokens.strokeWidthThick} solid ${tokens.colorStrokeFocus2}`, - borderRadius: tokens.borderRadiusMedium, - ...shorthands.borderColor('transparent'), - }), - }, -}); - /** Applies style classnames to slots */ export const useTeachingPopoverCarouselNavStyles_unstable = ( state: TeachingPopoverCarouselNavState, ): TeachingPopoverCarouselNavState => { - const styles = useStyles(); - - // eslint-disable-next-line react-hooks/immutability - state.root.className = mergeClasses(teachingPopoverCarouselNavClassNames.root, styles.root, state.root.className); + // Module class FIRST, named group marker second, consumer className last (DECISIONS.md + // D16.2). `styles.root` is unconditional, so index 0 is always the hashed, selector-safe + // `fuicm-*` token — which is what keeps the marker off `classList[0]`, where nwsapi's + // `:scope` polyfill would throw on its `/` under jsdom (D15.1). + // + // Cascade priority is decided by the `@layer fui.*` order in + // TeachingPopoverCarouselNav.module.css, not by the order of these arguments. + state.root.className = clsx(styles.root, 'group/fui-teaching-popover-carousel-nav', state.root.className); return state; }; diff --git a/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.module.css b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.module.css new file mode 100644 index 0000000000000..97d1ebef1caf8 --- /dev/null +++ b/packages/react-components/react-teaching-popover/library/src/components/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.module.css @@ -0,0 +1,236 @@ +@reference '#theme'; + +/* + * Cascade layer order (DECISIONS.md D2). Repeated on purpose — see + * TeachingPopoverBody.module.css for the full rationale. + */ +@layer fui.theme, fui.base, fui.components, fui.components.l1, fui.components.l2, fui.components.l3, fui.components.l4, fui.components.l5, fui.utilities; + +/* + * Layer assignment mirrors the mergeClasses ARGUMENT order of + * `useTeachingPopoverCarouselNavButtonStyles_unstable`, not source declaration order. + * + * `root` slot + * # mergeClasses argument → target + * --------------------------------------------------------------------------------------- + * 1 teachingPopoverCarouselNavButtonClassNames.root → REMOVED (D16.1) + * 2 useStyles.root → fui.components.l1 .root + * (rest half only — see (b)) + * 3 isSelected ? rootSelected : rootUnselected → fui.components.l1 .root under + * `selected` / `not-selected` + * (the @supports and forced-colors + * halves are split out — (b)) + * 4 appearance === 'brand' && + * (isSelected ? rootBrand : rootBrandUnselected) → fui.components.l1 .brand under + * `selected` / `not-selected` + * 5 consumer className → unlayered (always wins) + * + * ── ALTITUDE: l1 ──────────────────────────────────────────────────────────────────────── + * The root is this component's own `