diff --git a/.agents/skills/fluentui-windmod/SKILL.md b/.agents/skills/fluentui-windmod/SKILL.md new file mode 100644 index 0000000000000..570e45aa32703 --- /dev/null +++ b/.agents/skills/fluentui-windmod/SKILL.md @@ -0,0 +1,33 @@ +--- +name: fluentui-windmod +description: Use when styling, theming or overriding Fluent UI components from @fluentui/react-windmod-preview or @fluentui/react-tailwind-theme-preview, when authoring or verifying component styles inside those packages, or when users say "style this Fluent component", "override the button styles", "theme this app", "switch to dark theme", "restyle windmod", "fui-button", "group variant", "cascade layer", or when working with .module.css files in a windmod project or migrating an app off @fluentui/react-components and Griffel. Provides the override model (cascade layers, not props), the public class and data-attribute surface, the variant catalog, theme class names, the base-scale density knob, and the authoring and verification conventions used inside the library itself. +license: MIT +--- + +# Fluent windmod + +The full skill is vendored with the package it documents, so that the copy published to consumers +and the copy used in this repository can never drift apart. + +**Read it here — start with `SKILL.md`, then follow its routing:** + +``` +packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/ + SKILL.md the agent contract — read this first + references/setup.md installing, the two stylesheets, wiring your own Tailwind + references/overriding.md restyling a component from an app + references/variant-catalog.md the two catalogs and the group-variant vocabulary + references/tokens-and-scale.md token namespaces, --base-scale, density + references/griffel-deltas.md the deliberate differences from @fluentui/react-components + references/css-var-values.md reading a resolved token value in JavaScript + references/authoring-conventions.md CONTRIBUTORS: authoring rules + the verification protocol + references/troubleshooting.md a style that will not apply + assets/ a worked override example +``` + +**Changing anything inside `packages/react-components/react-windmod-preview`** — a `*.module.css`, a +styles hook, a VR scene, a mutation table, a pixel allowance — means loading +`references/authoring-conventions.md` completely first. Those rules are strict and violations are +rejected. + +Consumer-facing install and adoption notes live in the same directory's `README.md`. diff --git a/.claude/skills/fluentui-windmod/SKILL.md b/.claude/skills/fluentui-windmod/SKILL.md new file mode 100644 index 0000000000000..f613cc35f6695 --- /dev/null +++ b/.claude/skills/fluentui-windmod/SKILL.md @@ -0,0 +1 @@ +@../../../.agents/skills/fluentui-windmod/SKILL.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca6ae6b05d575..39ce39d588f5d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -334,6 +334,8 @@ packages/react-components/react-menu-grid-preview/library @microsoft/teams-prg packages/react-components/react-menu-grid-preview/stories @microsoft/teams-prg packages/react-components/react-headless-components-preview/library @microsoft/cxe-prg packages/react-components/react-headless-components-preview/stories @microsoft/cxe-prg +packages/react-components/react-windmod-preview/library @microsoft/cxe-prg +packages/react-components/react-windmod-preview/stories @microsoft/cxe-prg # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/.gitignore b/.gitignore index 791f41cf7d65e..2a824bd51474c 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,11 @@ gulp-cache .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md .playwright-cli + +# session scratch space (never committed) +.scratch/ + +# knowledge-graph tool output (never committed) — lands at the repo root and, because it is +# written relative to the invocation directory, sometimes under packages/ where nx and beachball +# then scan it. 2,177 cache files at last count, all regenerable. +graphify-out/ diff --git a/.storybook/main.js b/.storybook/main.js index b1fd67c435302..3606681f63777 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -51,7 +51,19 @@ module.exports = /** @type {import('./types').StorybookConfig} */ ({ }), ], 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. + * + * windmod-preview `*.module.css` files open with `@reference '#theme'` and use `@apply`, + * which are not valid CSS until Tailwind's PostCSS pass has run. Without this wiring + * this storybook — and the package storybooks and docsites that compose it — render + * windmod components 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.js b/.storybook/preview.js index 04fd8067731b1..05c5702edcf6b 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,7 +1,29 @@ +// Emits the shared Tailwind theme layer once per document (see the file's header). +// Required by windmod-preview `*.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/styles.css'; import '../packages/react-components/react-storybook-addon-export-to-sandbox/src/styles.css'; +import { webLightThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; import { withLinks } from '@storybook/addon-links'; +/* + * Stands in for the theme class a real application applies at its root. + * + * The theme stylesheet bakes no default (operator ruling 2026-08-28, "Theme delivery"): + * importing a theme file makes its class AVAILABLE, and the class still has to be applied — + * the same two steps Griffel asks for with `import { webLightTheme }` + + * ``. Web light is the storybook's document theme, so + * this is where it gets set. A story that wants another theme wraps its subtree in a + * FluentProvider with that theme's class, which overrides this one locally. + * + * On `documentElement`, and `add` rather than assignment, so it composes with anything else + * that classes the root and reaches shadow-DOM-free subtrees the same way `:root` did before + * the split. The class name is imported rather than written out so it stays in lockstep with + * the generated stylesheet. + */ +document.documentElement.classList.add(webLightThemeClassName); + /** @typedef {import('../packages/react-components/react-storybook-addon-export-to-sandbox/src/index').Parameters & import('@storybook/react').Parameters} Parameters */ /** @type {import('@storybook/react').Decorator[]} */ diff --git a/change/@fluentui-babel-preset-storybook-full-source-2643295e-cd5a-4161-9b35-c42234d6c9e1.json b/change/@fluentui-babel-preset-storybook-full-source-2643295e-cd5a-4161-9b35-c42234d6c9e1.json new file mode 100644 index 0000000000000..cc520546e989c --- /dev/null +++ b/change/@fluentui-babel-preset-storybook-full-source-2643295e-cd5a-4161-9b35-c42234d6c9e1.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "fix: format through @prettier/sync and declare it as a dependency, so the preset works under prettier 3", + "packageName": "@fluentui/babel-preset-storybook-full-source", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-breadcrumb-303eadd0-08bf-49e2-96da-d47362b73490.json b/change/@fluentui-react-breadcrumb-303eadd0-08bf-49e2-96da-d47362b73490.json new file mode 100644 index 0000000000000..b1d82acd59ba4 --- /dev/null +++ b/change/@fluentui-react-breadcrumb-303eadd0-08bf-49e2-96da-d47362b73490.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "fix: use DistributiveOmit in BreadcrumbButtonBaseProps so the anchor variant of the root slot survives, restoring href", + "packageName": "@fluentui/react-breadcrumb", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-color-picker-48823fe7-aa92-48cc-a2e5-5b4de941f337.json b/change/@fluentui-react-color-picker-48823fe7-aa92-48cc-a2e5-5b4de941f337.json new file mode 100644 index 0000000000000..d599865780bf6 --- /dev/null +++ b/change/@fluentui-react-color-picker-48823fe7-aa92-48cc-a2e5-5b4de941f337.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: use nullish coalescing in adjustChannel so a channel whose value is 0 no longer falls through to the hue action", + "packageName": "@fluentui/react-color-picker", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-20d8bde3-f688-43cc-b7a2-4eeb62ae1b35.json b/change/@fluentui-react-headless-components-preview-20d8bde3-f688-43cc-b7a2-4eeb62ae1b35.json new file mode 100644 index 0000000000000..9f38aecf84d52 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-20d8bde3-f688-43cc-b7a2-4eeb62ae1b35.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: publish the Button, Link and Avatar contexts so a styling layer can read the look values the render functions already provide", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-53c1f6b8-bdc6-4e94-9a74-9b427f2b39b8.json b/change/@fluentui-react-headless-components-preview-53c1f6b8-bdc6-4e94-9a74-9b427f2b39b8.json new file mode 100644 index 0000000000000..1a881651915ff --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-53c1f6b8-bdc6-4e94-9a74-9b427f2b39b8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: apply state.arrowClassName to the Tooltip arrow element, which renderTooltip was dropping", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-9b3831cb-664b-4ed6-9ed9-3919af4d5a77.json b/change/@fluentui-react-headless-components-preview-9b3831cb-664b-4ed6-9ed9-3919af4d5a77.json new file mode 100644 index 0000000000000..c404deda4d048 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-9b3831cb-664b-4ed6-9ed9-3919af4d5a77.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: let a consumer popover prop win on PopoverSurface by moving the default into the surface slot defaultProps, so more than one Popover can be open per page", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-a905c087-3177-47a7-9b13-a6d4ab0981c9.json b/change/@fluentui-react-headless-components-preview-a905c087-3177-47a7-9b13-a6d4ab0981c9.json new file mode 100644 index 0000000000000..8d35d756ecb5c --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-a905c087-3177-47a7-9b13-a6d4ab0981c9.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: merge the popover attribute default before the base state in useMenuPopover and useListbox so a consumer value is no longer overwritten", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-aeb7e042-1610-425b-8743-4d45b486f843.json b/change/@fluentui-react-headless-components-preview-aeb7e042-1610-425b-8743-4d45b486f843.json new file mode 100644 index 0000000000000..0b436604c5781 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-aeb7e042-1610-425b-8743-4d45b486f843.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: give MenuSplitGroup a stable contexts value so useIsInMenuSplitGroup resolves true for descendant halves", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-cd6ca79d-a43a-46f5-95f7-b37575bc8d25.json b/change/@fluentui-react-headless-components-preview-cd6ca79d-a43a-46f5-95f7-b37575bc8d25.json new file mode 100644 index 0000000000000..7a74adb4a7483 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-cd6ca79d-a43a-46f5-95f7-b37575bc8d25.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: reserve the scrollbar gutter on documentElement when Dialog locks scroll, so opening a modal on a scrolling page no longer shifts fixed and centred content", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-d7a700d2-1b2d-4ce6-8079-984826fc7bb3.json b/change/@fluentui-react-headless-components-preview-d7a700d2-1b2d-4ce6-8079-984826fc7bb3.json new file mode 100644 index 0000000000000..ac966d06f0b3d --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-d7a700d2-1b2d-4ce6-8079-984826fc7bb3.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: publish the AvatarGroup context reader and provider from the ./avatar-group subpath", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-storybook-addon-export-to-sandbox-ca41b16c-8185-494b-857f-d447cf5769f6.json b/change/@fluentui-react-storybook-addon-export-to-sandbox-ca41b16c-8185-494b-857f-d447cf5769f6.json new file mode 100644 index 0000000000000..ead20342f1503 --- /dev/null +++ b/change/@fluentui-react-storybook-addon-export-to-sandbox-ca41b16c-8185-494b-857f-d447cf5769f6.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: match both path separators when detecting the addon registration so the addon options are not silently dropped on Windows", + "packageName": "@fluentui/react-storybook-addon-export-to-sandbox", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tag-picker-0744b931-aca3-460f-a24b-7ad2d12d5ccf.json b/change/@fluentui-react-tag-picker-0744b931-aca3-460f-a24b-7ad2d12d5ccf.json new file mode 100644 index 0000000000000..f4b8a4ac849cf --- /dev/null +++ b/change/@fluentui-react-tag-picker-0744b931-aca3-460f-a24b-7ad2d12d5ccf.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: cancel the pending TagPickerControl resize frame in the effect cleanup instead of the effect body, which raced away the aside-width custom property", + "packageName": "@fluentui/react-tag-picker", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tailwind-theme-preview-17f09739-cf84-42dd-93ed-6555318201d4.json b/change/@fluentui-react-tailwind-theme-preview-17f09739-cf84-42dd-93ed-6555318201d4.json new file mode 100644 index 0000000000000..4d239ac8d328c --- /dev/null +++ b/change/@fluentui-react-tailwind-theme-preview-17f09739-cf84-42dd-93ed-6555318201d4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add react-tailwind-theme-preview, the Fluent design tokens and themes as a Tailwind v4 CSS layer", + "packageName": "@fluentui/react-tailwind-theme-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-windmod-preview-3f5a9c21-8e4d-4b7a-9c0e-52d1a7f0b4e6.json b/change/@fluentui-react-windmod-preview-3f5a9c21-8e4d-4b7a-9c0e-52d1a7f0b4e6.json new file mode 100644 index 0000000000000..7ccaea9cf621b --- /dev/null +++ b/change/@fluentui-react-windmod-preview-3f5a9c21-8e4d-4b7a-9c0e-52d1a7f0b4e6.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: release preview package", + "packageName": "@fluentui/react-windmod-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/jest.preset.js b/jest.preset.js index 0c6f7edd5081c..6184b96c78a43 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -26,6 +26,27 @@ const reactDepsPaths = { '^react-test-renderer$': path.join(__dirname, 'node_modules/react-test-renderer'), }; +/** + * CSS Modules support (react-windmod-preview / react-tailwind-theme-preview). + * + * Jest resolves `@fluentui/*` to SOURCE via the tsconfig path aliases above, so any suite + * that renders a windmod component evaluates `import styles from './X.module.css'`. + * Without a mapper those suites fail to *run*, so the mapper is repo-wide rather than + * per-package (it only matches `*.module.css`, which no Griffel package imports). + * + * Resolved by path (not via `require('@fluentui/scripts-jest')`) to keep this file free + * of workspace requires: scripts/jest pulls in @fluentui/scripts-monorepo, which walks + * the repo on load. + * + * Caveat: Jest MERGES `moduleNameMapper` from a preset, but a project-level + * `snapshotSerializers` REPLACES the preset's array — packages that declare their own + * serializers AND consume windmod classes must list the css-modules serializer themselves. + */ +const cssModulesJest = { + moduleNameMapperTarget: require.resolve('./scripts/jest/src/css-modules/proxy.js'), + snapshotSerializer: require.resolve('./scripts/jest/src/css-modules/serializer.js'), +}; + /** * @type {import('@jest/types').Config.InitialOptions} */ @@ -37,7 +58,12 @@ const baseConfig = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], testPathIgnorePatterns: ['/node_modules/', '/lib/', '/lib-commonjs/', '/dist/'], testEnvironment: 'jsdom', - moduleNameMapper: { ...tsPathAliases, ...reactDepsPaths }, + moduleNameMapper: { + ...tsPathAliases, + ...reactDepsPaths, + '\\.module\\.css$': cssModulesJest.moduleNameMapperTarget, + }, + snapshotSerializers: [cssModulesJest.snapshotSerializer], cacheDirectory: '/node_modules/.cache/jest', clearMocks: true, watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], diff --git a/package.json b/package.json index 942b32c787072..5448b6fe6d25c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@oddbird/css-anchor-positioning": "0.4.0", "@phenomnomnominal/tsquery": "6.1.3", "@playwright/test": "1.56.1", + "@prettier/sync": "0.6.1", "@react-native/babel-preset": "0.73.21", "@rnx-kit/eslint-plugin": "0.8.6", "@rollup/plugin-node-resolve": "13.3.0", @@ -107,6 +108,7 @@ "@swc/core": "1.11.24", "@swc/helpers": "0.5.1", "@swc/jest": "0.2.38", + "@tailwindcss/postcss": "^4.3.2", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "16.3.0", @@ -144,7 +146,6 @@ "@types/markdown-table": "2.0.0", "@types/micromatch": "4.0.2", "@types/node": "^22.0.0", - "@types/prettier": "2.7.2", "@types/progress": "2.0.5", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", @@ -182,6 +183,7 @@ "circular-dependency-plugin": "5.2.2", "clean-css": "4.2.3", "clean-webpack-plugin": "4.0.0", + "clsx": "2.1.1", "connect-history-api-fallback": "1.6.0", "copy-webpack-plugin": "12.0.2", "cross-env": "^5.1.4", @@ -267,7 +269,8 @@ "postcss": "8.5.18", "postcss-loader": "4.1.0", "postcss-modules": "4.1.3", - "prettier": "2.8.8", + "prettier": "3.9.6", + "prettier-plugin-tailwindcss": "0.8.1", "progress": "2.0.3", "puppeteer": "24.42.0", "raw-loader": "4.0.2", @@ -304,6 +307,7 @@ "swc-loader": "0.2.6", "swc-plugin-de-indent-template-literal": "1.5.0", "syncpack": "10.6.1", + "tailwindcss": "^4.3.2", "terser": "5.39.1", "terser-webpack-plugin": "5.3.10", "through2": "4.0.2", @@ -360,6 +364,7 @@ } }, "resolutions": { + "@fluentui/react-icons": "file:../fluentui-system-icons/packages/react-icons/fluentui-react-icons-local.tgz", "@nx/devkit/minimatch": "^9.0.7", "@nx/jest/minimatch": "^9.0.7", "@phenomnomnominal/tsquery": "6.1.3", @@ -377,7 +382,6 @@ "micromatch/braces": "^3.0.3", "nx/minimatch": "^9.0.7", "playwright": "1.56.1", - "prettier": "2.8.8", "prismjs": "1.30.0", "puppeteer": "24.42.0", "shell-quote": "^1.8.2", diff --git a/packages/react-components/babel-preset-storybook-full-source/package.json b/packages/react-components/babel-preset-storybook-full-source/package.json index 41101414f808a..90698a26da8d6 100644 --- a/packages/react-components/babel-preset-storybook-full-source/package.json +++ b/packages/react-components/babel-preset-storybook-full-source/package.json @@ -11,9 +11,10 @@ "license": "MIT", "dependencies": { "@babel/core": "^7.10.4", + "@prettier/sync": "^0.6.1", "@swc/helpers": "^0.5.1", "pkg-up": "^3.1.0", - "prettier": "^2.8.8" + "prettier": "^3.0.0" }, "exports": { ".": { diff --git a/packages/react-components/babel-preset-storybook-full-source/src/fullsource.ts b/packages/react-components/babel-preset-storybook-full-source/src/fullsource.ts index e0221d52b16fb..f4d02199db596 100644 --- a/packages/react-components/babel-preset-storybook-full-source/src/fullsource.ts +++ b/packages/react-components/babel-preset-storybook-full-source/src/fullsource.ts @@ -1,5 +1,7 @@ import * as Babel from '@babel/core'; -import * as prettier from 'prettier'; +// Babel visitors are synchronous, but prettier 3's `format` is async — use the official +// synchronous wrapper (runs prettier in a worker thread under the hood). +import * as prettier from '@prettier/sync'; import * as fs from 'fs'; import * as nodePath from 'path'; diff --git a/packages/react-components/babel-preset-storybook-full-source/src/modifyImports.ts b/packages/react-components/babel-preset-storybook-full-source/src/modifyImports.ts index 03f8348eb7372..698ebe7199de4 100644 --- a/packages/react-components/babel-preset-storybook-full-source/src/modifyImports.ts +++ b/packages/react-components/babel-preset-storybook-full-source/src/modifyImports.ts @@ -29,10 +29,13 @@ export function modifyImportsPlugin(babel: typeof Babel, options: BabelPluginOpt parserOptions.plugins.push('typescript'); }, pre() { - this.imports = Object.keys(importMappings).reduce((acc, cur) => { - acc[importMappings[cur].replace] = []; - return acc; - }, {} as PluginState['imports']); + this.imports = Object.keys(importMappings).reduce( + (acc, cur) => { + acc[importMappings[cur].replace] = []; + return acc; + }, + {} as PluginState['imports'], + ); }, visitor: { Program: { diff --git a/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.test.ts b/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.test.ts index 58f54d5f4c76d..72937dd7f6b48 100644 --- a/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.test.ts +++ b/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.test.ts @@ -17,13 +17,13 @@ function slice(source: string, targetStory: string): string { * Formats sliced output the same way the downstream pipeline does. Doubles as a * syntax guard: prettier throws on invalid output (e.g. `const args = {} = {}`). */ -function format(code: string): string { +async function format(code: string): Promise { return prettier.format(code, { parser: 'babel-ts' }); } describe('sliceStorySource', () => { describe('CSF3 render function with non-plain args parameter', () => { - it('unwraps a defaulted args parameter into a valid `const args` declaration', () => { + it('unwraps a defaulted args parameter into a valid `const args` declaration', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -40,7 +40,7 @@ describe('sliceStorySource', () => { // Regression: raw AssignmentPattern id produced `const args = {…} = {…}`, // invalid syntax that prettier would reject. - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const WithDefault = () => { @@ -51,7 +51,7 @@ describe('sliceStorySource', () => { `); }); - it('unwraps a rest args parameter into a valid `const args` declaration', () => { + it('unwraps a rest args parameter into a valid `const args` declaration', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -66,7 +66,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'WithRest'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const WithRest = () => { @@ -77,7 +77,7 @@ describe('sliceStorySource', () => { `); }); - it('prefers merged meta/story args over the render param default', () => { + it('prefers merged meta/story args over the render param default', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -93,7 +93,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'Overrides'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const Overrides = () => { @@ -106,7 +106,7 @@ describe('sliceStorySource', () => { }); describe('CSF3 render method shorthand', () => { - it('normalizes a render method shorthand without args', () => { + it('normalizes a render method shorthand without args', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -123,7 +123,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'Base'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const Base = () => { @@ -133,7 +133,7 @@ describe('sliceStorySource', () => { `); }); - it('normalizes a render method shorthand with an args parameter', () => { + it('normalizes a render method shorthand with an args parameter', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -151,7 +151,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'WithArgs'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const WithArgs = () => { @@ -164,7 +164,7 @@ describe('sliceStorySource', () => { }); describe('meta-level render fallback', () => { - it('uses the meta render for a render-less story (arrow form)', () => { + it('uses the meta render for a render-less story (arrow form)', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -183,7 +183,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'Basic'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const Basic = () => { @@ -194,7 +194,7 @@ describe('sliceStorySource', () => { `); }); - it('uses the meta render for a render-less story (method shorthand)', () => { + it('uses the meta render for a render-less story (method shorthand)', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -215,7 +215,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'Basic'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const Basic = () => { @@ -226,7 +226,7 @@ describe('sliceStorySource', () => { `); }); - it('prefers a story-level render over the meta render', () => { + it('prefers a story-level render over the meta render', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -246,7 +246,7 @@ describe('sliceStorySource', () => { const sliced = slice(source, 'Custom'); - expect(format(sliced)).toMatchInlineSnapshot(` + expect(await format(sliced)).toMatchInlineSnapshot(` "import * as React from \\"react\\"; import { Button } from \\"@fluentui/react-button\\"; export const Custom = () => { @@ -259,7 +259,7 @@ describe('sliceStorySource', () => { }); describe('module-level member assignment pruning', () => { - it('keeps non-CSF member assignments on reachable helpers (e.g. `Card.displayName`)', () => { + it('keeps non-CSF member assignments on reachable helpers (e.g. `Card.displayName`)', async () => { const source = [ `import * as React from 'react';`, ``, @@ -279,7 +279,7 @@ describe('sliceStorySource', () => { expect(sliced).toContain(`Card.displayName = 'Card'`); }); - it('keeps member assignments on non-component (lowercase) identifiers', () => { + it('keeps member assignments on non-component (lowercase) identifiers', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -298,7 +298,7 @@ describe('sliceStorySource', () => { expect(sliced).toContain(`config.foo = 'bar'`); }); - it('still removes CSF2 story annotation assignments on the target story', () => { + it('still removes CSF2 story annotation assignments on the target story', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -319,7 +319,7 @@ describe('sliceStorySource', () => { }); describe('dangling reference pruning', () => { - it('drops a member assignment when its target declaration is pruned', () => { + it('drops a member assignment when its target declaration is pruned', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -342,7 +342,7 @@ describe('sliceStorySource', () => { expect(sliced).not.toContain(' { + it('keeps a member assignment when its target declaration survives', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -362,7 +362,7 @@ describe('sliceStorySource', () => { expect(sliced).toContain(`Card.displayName = 'Card'`); }); - it('drops an identifier re-assignment when its declaration is pruned', () => { + it('drops an identifier re-assignment when its declaration is pruned', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, @@ -381,7 +381,7 @@ describe('sliceStorySource', () => { expect(sliced).not.toContain('counter'); }); - it('keeps a side-effect statement referencing a global and a surviving binding', () => { + it('keeps a side-effect statement referencing a global and a surviving binding', async () => { const source = [ `import * as React from 'react';`, `import { Button } from '@fluentui/react-button';`, diff --git a/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.ts b/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.ts index 387d3982af6c1..d23bbc66c267f 100644 --- a/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.ts +++ b/packages/react-components/babel-preset-storybook-full-source/src/sliceStory.ts @@ -56,7 +56,7 @@ export function sliceStorySource(babel: typeof Babel, source: string, options: S plugins: [createSliceStoryPlugin(babel, options, context)], }); - return context.handled ? result?.code ?? null : null; + return context.handled ? (result?.code ?? null) : null; } function createSliceStoryPlugin( @@ -503,8 +503,8 @@ function buildRenderFunction( const argsId = t.isAssignmentPattern(argsParam) ? argsParam.left : t.isRestElement(argsParam) - ? argsParam.argument - : argsParam; + ? argsParam.argument + : argsParam; // Precedence for the local `args`: merged meta/story args win; otherwise fall // back to the param's default (`args = `) so the author's intended @@ -565,8 +565,8 @@ function mergeArgs( const key = t.isIdentifier(property.key) ? property.key.name : t.isStringLiteral(property.key) - ? property.key.value - : undefined; + ? property.key.value + : undefined; if (key !== undefined) { if (!byKey.has(key)) { order.push({ kind: 'key', key }); diff --git a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md index 21b597d5eb78d..e16f1f764e21f 100644 --- a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md +++ b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md @@ -9,6 +9,7 @@ import type { ButtonSlots } from '@fluentui/react-button'; import type { ButtonState } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; +import type { DistributiveOmit } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; @@ -29,7 +30,7 @@ export type BreadcrumbBaseState = Omit; export const BreadcrumbButton: ForwardRefComponent; // @public (undocumented) -export type BreadcrumbButtonBaseProps = Omit; +export type BreadcrumbButtonBaseProps = DistributiveOmit; // @public (undocumented) export type BreadcrumbButtonBaseState = Omit; diff --git a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts index a9220b965983e..95bf28bd91449 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts +++ b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, DistributiveOmit } from '@fluentui/react-utilities'; import type { ButtonProps, ButtonSlots, ButtonState } from '@fluentui/react-button'; import type { BreadcrumbProps } from '../Breadcrumb/Breadcrumb.types'; @@ -25,6 +25,6 @@ export type BreadcrumbButtonState = ComponentState & Omit & Required>; -export type BreadcrumbButtonBaseProps = Omit; +export type BreadcrumbButtonBaseProps = DistributiveOmit; export type BreadcrumbButtonBaseState = Omit; diff --git a/packages/react-components/react-color-picker/library/src/utils/adjustChannel.ts b/packages/react-components/react-color-picker/library/src/utils/adjustChannel.ts index 97b91626d17f5..39e8bdbf2c393 100644 --- a/packages/react-components/react-color-picker/library/src/utils/adjustChannel.ts +++ b/packages/react-components/react-color-picker/library/src/utils/adjustChannel.ts @@ -29,5 +29,5 @@ export type ChannelActions = { * @returns {T} - The result of the action corresponding to the specified channel, or the hue action if the channel is not found. */ export function adjustChannel(channel: ColorChannel, actions: ChannelActions): T { - return actions[channel] || actions.hue; + return actions[channel] ?? actions.hue; } diff --git a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md index 139b09b720254..08c1340b516c9 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md @@ -12,6 +12,7 @@ import { AvatarGroupItemSlots } from '@fluentui/react-avatar'; import { AvatarGroupItemBaseState as AvatarGroupItemState } from '@fluentui/react-avatar'; import { AvatarGroupBaseProps as AvatarGroupProps } from '@fluentui/react-avatar'; import type { AvatarGroupProps as AvatarGroupProps_2 } from '@fluentui/react-avatar'; +import { AvatarGroupProvider } from '@fluentui/react-avatar'; import { AvatarGroupSlots } from '@fluentui/react-avatar'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; @@ -81,6 +82,8 @@ export type AvatarGroupPopoverState = ComponentState & export { AvatarGroupProps } +export { AvatarGroupProvider } + export { AvatarGroupSlots } // @public (undocumented) diff --git a/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md b/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md index 4324a0dde26b3..d10ef90858da5 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md @@ -4,6 +4,8 @@ ```ts +import { AvatarContextProvider } from '@fluentui/react-avatar'; +import { AvatarContextValue } from '@fluentui/react-avatar'; import { AvatarBaseProps as AvatarProps } from '@fluentui/react-avatar'; import { AvatarSlots } from '@fluentui/react-avatar'; import { AvatarBaseState as AvatarState } from '@fluentui/react-avatar'; @@ -15,6 +17,10 @@ import { useAvatarContext } from '@fluentui/react-avatar'; // @public export const Avatar: ForwardRefComponent; +export { AvatarContextProvider } + +export { AvatarContextValue } + export { AvatarProps } export { AvatarSlots } diff --git a/packages/react-components/react-headless-components-preview/library/etc/button.api.md b/packages/react-components/react-headless-components-preview/library/etc/button.api.md index 26059c8cca5bb..99cafa4abe890 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/button.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/button.api.md @@ -5,15 +5,22 @@ ```ts import type { ButtonBaseState } from '@fluentui/react-button'; +import { ButtonContextProvider } from '@fluentui/react-button'; +import { ButtonContextValue } from '@fluentui/react-button'; import { ButtonBaseProps as ButtonProps } from '@fluentui/react-button'; import { ButtonSlots } from '@fluentui/react-button'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type * as React_2 from 'react'; import { renderButton_unstable as renderButton } from '@fluentui/react-button'; +import { useButtonContext } from '@fluentui/react-button'; // @public export const Button: ForwardRefComponent; +export { ButtonContextProvider } + +export { ButtonContextValue } + export { ButtonProps } export { ButtonSlots } @@ -33,6 +40,8 @@ export { renderButton } // @public export const useButton: (props: ButtonProps, ref: React_2.Ref) => ButtonState; +export { useButtonContext } + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-headless-components-preview/library/etc/link.api.md b/packages/react-components/react-headless-components-preview/library/etc/link.api.md index b9e9231e10ab1..0d702defb50e4 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/link.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/link.api.md @@ -6,14 +6,24 @@ import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { LinkBaseState } from '@fluentui/react-link'; +import { linkContextDefaultValue } from '@fluentui/react-link'; +import { LinkContextProvider } from '@fluentui/react-link'; +import { LinkContextValue } from '@fluentui/react-link'; import { LinkBaseProps as LinkProps } from '@fluentui/react-link'; import { LinkSlots } from '@fluentui/react-link'; import type * as React_2 from 'react'; import { renderLink_unstable as renderLink } from '@fluentui/react-link'; +import { useLinkContext } from '@fluentui/react-link'; // @public export const Link: ForwardRefComponent; +export { linkContextDefaultValue } + +export { LinkContextProvider } + +export { LinkContextValue } + export { LinkProps } export { LinkSlots } @@ -31,6 +41,8 @@ export { renderLink } // @public export const useLink: (props: LinkProps, ref: React_2.Ref) => LinkState; +export { useLinkContext } + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts b/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts index 8b50b843c507a..e1a0de584ce56 100644 --- a/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts +++ b/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts @@ -4,6 +4,7 @@ export { useAvatarGroup, useAvatarGroupContext, useAvatarGroupContextValues, + AvatarGroupProvider, AvatarGroupItem, renderAvatarGroupItem, useAvatarGroupItem, diff --git a/packages/react-components/react-headless-components-preview/library/src/avatar.ts b/packages/react-components/react-headless-components-preview/library/src/avatar.ts index fa9db547039f7..e8fea0948213f 100644 --- a/packages/react-components/react-headless-components-preview/library/src/avatar.ts +++ b/packages/react-components/react-headless-components-preview/library/src/avatar.ts @@ -1,2 +1,2 @@ -export { Avatar, renderAvatar, useAvatar, useAvatarContext } from './components/Avatar/index'; -export type { AvatarSlots, AvatarProps, AvatarState } from './components/Avatar/index'; +export { Avatar, AvatarContextProvider, renderAvatar, useAvatar, useAvatarContext } from './components/Avatar/index'; +export type { AvatarContextValue, AvatarSlots, AvatarProps, AvatarState } from './components/Avatar/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/button.ts b/packages/react-components/react-headless-components-preview/library/src/button.ts index 6b768102f1256..36ad8a4ef0344 100644 --- a/packages/react-components/react-headless-components-preview/library/src/button.ts +++ b/packages/react-components/react-headless-components-preview/library/src/button.ts @@ -1,2 +1,2 @@ -export { Button, renderButton, useButton } from './components/Button'; -export type { ButtonSlots, ButtonProps, ButtonState } from './components/Button'; +export { Button, ButtonContextProvider, renderButton, useButton, useButtonContext } from './components/Button'; +export type { ButtonContextValue, ButtonSlots, ButtonProps, ButtonState } from './components/Button'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/avatarContext.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/avatarContext.ts new file mode 100644 index 0000000000000..5cc0ab32df251 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/avatarContext.ts @@ -0,0 +1,15 @@ +/** + * Re-export of the Griffel Avatar context, mirroring how `Field` re-exports `useFieldContext` + * (`../Field/useFieldContextValues.ts`). + * + * The context INSTANCE has to be Griffel's: `renderTag` and `renderInteractionTagPrimary`, + * re-exported unchanged from `@fluentui/react-tags`, wrap their `media` slot in + * `AvatarContextProvider`, and `useTagContextValues` is Griffel's own + * `useTagAvatarContextValues_unstable`. A second context created here would be a different + * instance and would never receive those values. + * + * The value is look-only (`{ shape, size }`), so nothing in the headless layer consumes it — a + * styling layer does, which is why it is published rather than used here. + */ +export { AvatarContextProvider } from '@fluentui/react-avatar'; +export type { AvatarContextValue } from '@fluentui/react-avatar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts index 0e5e2dd8039fe..dc7bdc6fae55a 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts @@ -1,4 +1,6 @@ export { Avatar } from './Avatar'; export { renderAvatar } from './renderAvatar'; export { useAvatar, useAvatarContext } from './useAvatar'; +export { AvatarContextProvider } from './avatarContext'; export type { AvatarSlots, AvatarProps, AvatarState } from './Avatar.types'; +export type { AvatarContextValue } from './avatarContext'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/avatarGroupContext.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/avatarGroupContext.ts new file mode 100644 index 0000000000000..4854e8f2dc922 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/avatarGroupContext.ts @@ -0,0 +1,13 @@ +/** + * Re-export of the Griffel AvatarGroup context, mirroring `../Avatar/avatarContext.ts`. + * + * The context INSTANCE has to be Griffel's: `renderAvatarGroup` and `renderAvatarGroupPopover`, + * re-exported unchanged from `@fluentui/react-avatar`, wrap their subtrees in + * `AvatarGroupProvider`. A second context created here would be a different instance and would + * never receive those values. + * + * The value is look-only for a styling layer (`size`) plus two structural flags (`layout`, + * `isOverflow`), so nothing in the headless layer consumes it — a styling layer does, which is why + * it is published rather than used here. + */ +export { AvatarGroupProvider } from '@fluentui/react-avatar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts index 5070709391cb4..6d0fa3bf9f5ca 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts @@ -2,6 +2,7 @@ export { AvatarGroup } from './AvatarGroup'; export { renderAvatarGroup } from './renderAvatarGroup'; export { useAvatarGroup, useAvatarGroupContext } from './useAvatarGroup'; export { useAvatarGroupContextValues } from './useAvatarGroupContextValues'; +export { AvatarGroupProvider } from './avatarGroupContext'; export type { AvatarGroupSlots, AvatarGroupProps, diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/buttonContext.ts b/packages/react-components/react-headless-components-preview/library/src/components/Button/buttonContext.ts new file mode 100644 index 0000000000000..875716a27b790 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/buttonContext.ts @@ -0,0 +1,15 @@ +/** + * Re-export of the Griffel Button context, mirroring how `Field` re-exports `useFieldContext` + * (`../Field/useFieldContextValues.ts`). + * + * The context INSTANCE has to be Griffel's: the providers that publish into it are Griffel's + * (`renderMessageBarActions`, re-exported unchanged from `@fluentui/react-message-bar` by + * `../MessageBar/MessageBarActions/renderMessageBarActions.ts`, wraps its children in + * `ButtonContextProvider`). A second context created here would be a different instance and would + * never receive those values, so this is a re-export and not a re-declaration. + * + * The value is look-only (`{ size }`), so nothing in the headless layer consumes it — a styling + * layer does, which is why it is published rather than used here. + */ +export { useButtonContext, ButtonContextProvider } from '@fluentui/react-button'; +export type { ButtonContextValue } from '@fluentui/react-button'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Button/index.ts index a8b8007a07eb5..ee6f00d9cae0b 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Button/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/index.ts @@ -1,4 +1,6 @@ export { Button } from './Button'; export { renderButton } from './renderButton'; export { useButton } from './useButton'; +export { useButtonContext, ButtonContextProvider } from './buttonContext'; export type { ButtonSlots, ButtonProps, ButtonState } from './Button.types'; +export type { ButtonContextValue } from './buttonContext'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.test.tsx index bd0fcf8fe3c25..d1013e04b23db 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.test.tsx @@ -106,6 +106,96 @@ describe('Dialog', () => { expect(dialog).not.toHaveAttribute('aria-labelledby'); }); + describe('scroll lock', () => { + // jsdom reports clientWidth 0, which would read as a scrollbar on every page. + const setScrollbarWidth = (width: number) => + Object.defineProperty(document.documentElement, 'clientWidth', { + configurable: true, + value: window.innerWidth - width, + }); + + afterEach(() => { + // `setScrollbarWidth` installs a configurable OWN property shadowing the prototype getter, so + // removing that own property restores the getter. `delete` cannot express this — `clientWidth` + // is typed `readonly`, which `Partial` preserves — while `Reflect.deleteProperty` + // has identical runtime semantics and needs no cast. + Reflect.deleteProperty(document.documentElement, 'clientWidth'); + document.documentElement.style.removeProperty('scrollbar-gutter'); + document.body.style.removeProperty('overflow'); + }); + + const renderModal = () => + render( + + + + + + Dialog title + + + + + + + , + ); + + it('reserves the scrollbar gutter while a modal holds the lock', () => { + setScrollbarWidth(15); + const result = renderModal(); + + fireEvent.click(result.getByRole('button', { name: 'Open dialog' })); + + expect(document.body.style.overflow).toBe('visible clip'); + // On , not : scrollbar-gutter does not propagate to the viewport. + expect(document.documentElement.style.scrollbarGutter).toBe('stable'); + + fireEvent.click(result.getByRole('button', { name: 'Close dialog' })); + + expect(document.documentElement.style.scrollbarGutter).toBe(''); + }); + + it('reserves nothing when the scrollbar takes no layout width', () => { + setScrollbarWidth(0); + const result = renderModal(); + + fireEvent.click(result.getByRole('button', { name: 'Open dialog' })); + + expect(document.body.style.overflow).toBe('visible clip'); + expect(document.documentElement.style.scrollbarGutter).toBe(''); + }); + + it('restores a gutter the host application had already set', () => { + setScrollbarWidth(15); + document.documentElement.style.scrollbarGutter = 'both-edges'; + const result = renderModal(); + + fireEvent.click(result.getByRole('button', { name: 'Open dialog' })); + + expect(document.documentElement.style.scrollbarGutter).toBe('stable'); + + fireEvent.click(result.getByRole('button', { name: 'Close dialog' })); + + expect(document.documentElement.style.scrollbarGutter).toBe('both-edges'); + }); + + it('leaves a non-modal dialog out of the lock entirely', () => { + setScrollbarWidth(15); + const result = render( + + + Non-modal title + + , + ); + + expect(result.container.querySelector('dialog')).toHaveAttribute('data-open'); + expect(document.body.style.overflow).toBe(''); + expect(document.documentElement.style.scrollbarGutter).toBe(''); + }); + }); + it('keeps dialog mounted after close when unmountOnClose is false', () => { const result = render( diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/utils/scroll.ts b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/utils/scroll.ts index c2a583630357b..da909f82ed1a8 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/utils/scroll.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/utils/scroll.ts @@ -1,14 +1,20 @@ type ScrollLockState = { lockCount: number; previousBodyOverflow: string; + previousScrollbarGutter: string; }; const scrollLockStateByDocument = new WeakMap(); /** * Prevents background scrolling while a modal/alert dialog is open by applying - * `overflow: hidden` to ``. The `` element is intentionally left - * untouched so host-application styles on the document element are preserved. + * `overflow: hidden` to ``, and reserves the space the page scrollbar was + * occupying so nothing on the page moves sideways as it disappears. + * + * The gutter has to be reserved on ``: `scrollbar-gutter` does not propagate + * from `` to the viewport the way `overflow` does, so spelling it on `` + * reserves nothing. It is written only when the scrollbar actually takes layout + * width, because `stable` otherwise reserves a gutter the page never had. * * Nested modal dialogs share a single lock via a reference count. */ @@ -19,18 +25,27 @@ export function lockDocumentScroll(targetDocument: Document): void { return; } + const { body, documentElement } = targetDocument; + // Read the scrollbar's layout width before the lock takes it away. Overlay + // scrollbars and unscrollable pages both measure 0, and both want no gutter. + const scrollbarWidth = (targetDocument.defaultView?.innerWidth ?? 0) - documentElement.clientWidth; + scrollLockStateByDocument.set(targetDocument, { lockCount: 1, - previousBodyOverflow: targetDocument.body.style.overflow, + previousBodyOverflow: body.style.overflow, + previousScrollbarGutter: documentElement.style.scrollbarGutter, }); - targetDocument.body.style.overflow = 'visible clip'; + body.style.overflow = 'visible clip'; + if (scrollbarWidth > 0) { + documentElement.style.scrollbarGutter = 'stable'; + } } /** - * Restores the document's scroll behavior by reverting the `overflow` style - * on the `` element to its previous value. This function is typically - * called when a modal/alert dialog is closed. + * Restores the document's scroll behavior by reverting the `overflow` style on the + * `` element and the reserved scrollbar gutter on `` to their previous + * values. This function is typically called when a modal/alert dialog is closed. */ export function unlockDocumentScroll(targetDocument: Document): void { const state = scrollLockStateByDocument.get(targetDocument); @@ -44,5 +59,6 @@ export function unlockDocumentScroll(targetDocument: Document): void { } targetDocument.body.style.overflow = state.previousBodyOverflow; + targetDocument.documentElement.style.scrollbarGutter = state.previousScrollbarGutter; scrollLockStateByDocument.delete(targetDocument); } diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts index f27464593eec2..867476e11cbdc 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts @@ -1,4 +1,6 @@ export { Link } from './Link'; export { renderLink } from './renderLink'; export { useLink } from './useLink'; +export { useLinkContext, LinkContextProvider, linkContextDefaultValue } from './linkContext'; export type { LinkSlots, LinkProps, LinkState } from './Link.types'; +export type { LinkContextValue } from './linkContext'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/linkContext.ts b/packages/react-components/react-headless-components-preview/library/src/components/Link/linkContext.ts new file mode 100644 index 0000000000000..fd1b9292fb56a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/linkContext.ts @@ -0,0 +1,14 @@ +/** + * Re-export of the Griffel Link context, mirroring how `Field` re-exports `useFieldContext` + * (`../Field/useFieldContextValues.ts`). + * + * The context INSTANCE has to be Griffel's: `renderMessageBarBody`, re-exported unchanged from + * `@fluentui/react-message-bar` by `../MessageBar/MessageBarBody/renderMessageBarBody.ts`, wraps + * its children in `LinkContextProvider`. A second context created here would be a different + * instance and would never receive that value. + * + * The value is look-only (`{ inline }`), so nothing in the headless layer consumes it — a styling + * layer does, which is why it is published rather than used here. + */ +export { useLinkContext, LinkContextProvider, linkContextDefaultValue } from '@fluentui/react-link'; +export type { LinkContextValue } from '@fluentui/react-link'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx index 71c9898132543..a7d4f4ecc0316 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuSplitGroup/MenuSplitGroup.tsx @@ -8,7 +8,15 @@ import type { MenuSplitGroupProps } from '@fluentui/react-menu'; export const MenuSplitGroup: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useMenuSplitGroup(props, ref); - return renderMenuSplitGroup(state); + // useIsInMenuSplitGroup compares the provided value against the module-level default BY IDENTITY, + // so supplying a contexts argument is what lets a descendant tell it is inside a split group. The + // memo keeps that identity stable across renders; setMultiline stays the documented no-op. + const contexts = React.useMemo( + () => ({ menuSplitGroup: { setMultiline: state.setMultiline } }), + [state.setMultiline], + ); + + return renderMenuSplitGroup(state, contexts); }); MenuSplitGroup.displayName = 'MenuSplitGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx index 4e8d795e2ae94..8d4047657389c 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx @@ -16,7 +16,7 @@ export const renderTooltip = (state: TooltipState): JSXElement => { {state.children} {state.shouldRenderTooltip && ( - {state.withArrow &&
} + {state.withArrow &&
} {state.content.children} )} diff --git a/packages/react-components/react-headless-components-preview/library/src/link.ts b/packages/react-components/react-headless-components-preview/library/src/link.ts index 5a1056fe6576d..3099e39881357 100644 --- a/packages/react-components/react-headless-components-preview/library/src/link.ts +++ b/packages/react-components/react-headless-components-preview/library/src/link.ts @@ -1,2 +1,9 @@ -export { Link, renderLink, useLink } from './components/Link/index'; -export type { LinkSlots, LinkProps, LinkState } from './components/Link/index'; +export { + Link, + LinkContextProvider, + linkContextDefaultValue, + renderLink, + useLink, + useLinkContext, +} from './components/Link/index'; +export type { LinkContextValue, LinkSlots, LinkProps, LinkState } from './components/Link/index'; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts index 8ebe39c87ae43..4b51379589f00 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts @@ -16,7 +16,11 @@ export function webpack(config: WebpackFinalConfig, options: WebpackFinalOptions } const identity = (value: T) => value; -const addonFilePattern = /react-storybook-addon-export-to-sandbox\/[a-z/]+.[jt]s$/; +// Both path separators on purpose: on Windows the registered preset name is an absolute +// path with backslashes (e.g. `...\react-storybook-addon-export-to-sandbox\temp\preset.ts`); +// a forward-slash-only pattern fails to find the registration, silently drops the addon +// options, and the full-source babel plugin then crashes on undefined `importMappings`. +const addonFilePattern = /react-storybook-addon-export-to-sandbox[\\/][a-z\\/]+.[jt]s$/; const defaultOptions = { webpackRule: {}, babelLoaderOptionsUpdater: identity, diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx index 7c1b3b2a12e0c..d02d0d08ae6b8 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/TagPickerControl.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { TagPickerControl } from './TagPickerControl'; @@ -16,4 +16,82 @@ describe('TagPickerControl', () => { const result = render(Default PickerControl); expect(result.container).toMatchSnapshot(); }); + + describe('the aside width custom property', () => { + // useTagPickerControl schedules the write of --fui-TagPickerControl-aside-width from the + // ResizeObserver callback, and cancels that frame from an effect. The observer is attached by + // a ref callback, so `observe()` runs in the COMMIT phase -- before React flushes the passive + // effect. A ResizeObserver whose `observe()` invokes its callback synchronously therefore + // reproduces, deterministically, the ordering that the production race lands on 8-11 times out + // of 12: a frame is already pending by the time the effect runs. If the cancel sits in the + // effect BODY it kills that frame and the property is never written; in the effect's CLEANUP + // it only runs on unmount, which is what these two tests pin. + const realRaf = window.requestAnimationFrame; + const realCaf = window.cancelAnimationFrame; + const realResizeObserver = window.ResizeObserver; + + const ASIDE_WIDTH = 18; + let frames: { id: number; callback: FrameRequestCallback }[] = []; + let cancelledIds: number[] = []; + + beforeEach(() => { + frames = []; + cancelledIds = []; + let nextId = 1; + window.requestAnimationFrame = (callback: FrameRequestCallback) => { + const id = nextId++; + frames.push({ id, callback }); + return id; + }; + window.cancelAnimationFrame = (id: number) => { + cancelledIds.push(id); + }; + window.ResizeObserver = class implements ResizeObserver { + constructor(private callback: ResizeObserverCallback) {} + public observe(element: Element) { + this.callback([{ target: element, contentRect: { width: ASIDE_WIDTH } }] as never, this); + } + public unobserve() { + /* no-op */ + } + public disconnect() { + /* no-op */ + } + }; + }); + + afterEach(() => { + window.requestAnimationFrame = realRaf; + window.cancelAnimationFrame = realCaf; + window.ResizeObserver = realResizeObserver; + }); + + it('does not cancel the pending frame on mount, so the property is written', () => { + const result = render(Default PickerControl); + + expect(frames).toHaveLength(1); + expect(cancelledIds).not.toContain(frames[0].id); + + act(() => { + frames[0].callback(0); + }); + + const control = result.container.querySelector('.fui-TagPickerControl') as HTMLElement; + expect(control.style.getPropertyValue('--fui-TagPickerControl-aside-width')).toBe(`${ASIDE_WIDTH}px`); + }); + + it('cancels a still-pending frame on unmount', () => { + const result = render(Default PickerControl); + + expect(frames).toHaveLength(1); + + // Snapshot before unmounting: a cancel that already happened on mount would make the + // assertion below pass vacuously, which is exactly what the defective form did. + const cancelledBeforeUnmount = [...cancelledIds]; + result.unmount(); + + expect(cancelledBeforeUnmount).not.toContain(frames[0].id); + expect(cancelledIds).toContain(frames[0].id); + }); + }); }); diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx index 967d5061d4d81..5ef9ce1c52e6d 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerControl/useTagPickerControl.tsx @@ -142,9 +142,11 @@ export const useTagPickerControlBase_unstable = ( } React.useEffect(() => { - if (rafIdRef.current && targetDocument?.defaultView) { - targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); - } + return () => { + if (rafIdRef.current && targetDocument?.defaultView) { + targetDocument.defaultView.cancelAnimationFrame(rafIdRef.current); + } + }; }, [targetDocument]); return state; diff --git a/packages/react-components/react-tailwind-theme-preview/LICENSE b/packages/react-components/react-tailwind-theme-preview/LICENSE new file mode 100644 index 0000000000000..553831e4d0c92 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-tailwind-theme-preview + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license diff --git a/packages/react-components/react-tailwind-theme-preview/README.md b/packages/react-components/react-tailwind-theme-preview/README.md new file mode 100644 index 0000000000000..2fe56a6c91346 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/README.md @@ -0,0 +1,134 @@ +# @fluentui/react-tailwind-theme-preview + +**Fluent UI themes and design tokens as a Tailwind v4 CSS layer** + +Windmod component packages ship plain, precompiled CSS that references theme-level custom +properties — the design tokens, the cascade `@layer` order, `--base-scale`, `--spacing` and the +stroke widths. Something has to emit those **once per document**: this package. + +It ships in two parts. `base.css` is **theme-less**: the layer order, Tailwind's preflight (at +the head of `fui.base` — see Layering below), the token registrations, the spacing +scale, the type ramp, the stroke widths, the `prefers-reduced-motion` floor — everything that is +identical in every theme. Each theme is then its own file (`themes/web-light.css`, …) carrying +nothing but that theme's 423 custom properties inside one class. + +The base sheet also carries the **scale-region mechanism**: the invariant values are emitted at +`:root, :host, .fui-scale-region`, and a `.fui-scale-region` rule derives a unitless +`--fui-scale` factor from the element's `data-fui-scale` attribute (typed attr(), fallback 1), +which `--base-scale` multiplies in. An element carrying the class, the attribute and a theme +class scales its whole subtree; the windmod package's `ScaleRegion` component stamps all three. + +It deliberately registers **no** `@property` rules (a non-empty registry puts Blink's +transition-start on a page-global slow path). + +The only JavaScript export is the theme class-name constants; everything else is CSS. + +## Usage + +```sh +npm install @fluentui/react-windmod-preview @fluentui/react-tailwind-theme-preview +``` + +Import the base **exactly once**, at your document root, then one file per theme you ship — all +before your own styles, since theme styles must load first: + +```js +// src/main.jsx (or _app.tsx, root layout, etc.) +import '@fluentui/react-tailwind-theme-preview/base.css'; +import '@fluentui/react-tailwind-theme-preview/themes/web-dark.css'; +``` + +Then apply that theme's class. windmod's `FluentProvider` is the usual way (a block element +carrying the theme class plus the suite's base typography, text colour and background — any subtree +can be themed, and nested providers override): + +```jsx +import { Button } from '@fluentui/react-windmod-preview/button'; +import { FluentProvider, webDarkThemeClassName } from '@fluentui/react-windmod-preview/provider'; + +export default function App() { + return ( + + + + ); +} +``` + +**There is no default theme.** This mirrors Griffel exactly: there you `import { webDarkTheme }` +and pass it to `FluentProvider`, and a provider given no theme leaves every token unset. Here the +import is CSS and the value is a class name, but the contract — and the cost model, where you pay +only for the themes you ship — is the same. + +This package also works standalone for styling `@fluentui/react-headless-components-preview` +directly: import the base and a theme, apply the class to any element, reference the tokens. + +### The seven themes + +| Subpath | Class | Constant | +| -------------------------------- | ------------------------------- | --------------------------------- | +| `themes/web-light.css` | `fui-theme-web-light` | `webLightThemeClassName` | +| `themes/web-dark.css` | `fui-theme-web-dark` | `webDarkThemeClassName` | +| `themes/teams-light.css` | `fui-theme-teams-light` | `teamsLightThemeClassName` | +| `themes/teams-dark.css` | `fui-theme-teams-dark` | `teamsDarkThemeClassName` | +| `themes/teams-high-contrast.css` | `fui-theme-teams-high-contrast` | `teamsHighContrastThemeClassName` | +| `themes/teams-light-v21.css` | `fui-theme-teams-light-v21` | `teamsLightV21ThemeClassName` | +| `themes/teams-dark-v21.css` | `fui-theme-teams-dark-v21` | `teamsDarkV21ThemeClassName` | + +Each is 23.4 KB raw / ~3.8 KB gzipped (high contrast is smaller, 23.1 KB / 2.9 KB, because it +repeats far fewer distinct colours). With the 7.7 KB / 2.2 KB base, a single-theme application loads +**31 KB raw / 5.9 KB gzipped** — against 163 KB / 15.4 KB if it took all seven. + +### If you skip an import + +Without **`base.css`**, the theme's custom properties are missing entirely: numeric spacing +utilities compute to `0px` and the cascade-layer order is undefined — components render with +collapsed metrics and invert each other's overrides. + +Without a **theme file, or its class**, the structure is right but nothing is coloured: colour +tokens resolve to nothing, so backgrounds go transparent and text inherits. windmod's +`FluentProvider` warns once per document in development builds when no theme reaches it, naming +both halves of the fix — a diagnostic Griffel does not have for the equivalent mistake. + +## Layering + +All Fluent styles live in one cascade-layer family, declared by this package's stylesheet (which is +why it must load before component styles): + +```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; +``` + +- **`fui.base`** opens with Tailwind's preflight — the same placement Tailwind itself gives it — + followed by this package's global element resets (the headless icon defaults). Every component + rule, and any rule of yours, outranks the reset; within the layer the resets that follow + preflight beat it by source order. +- **Plain CSS consumers** need no setup. Unlayered CSS beats every layer, so your own selectors win + by default. +- **Tailwind consumers** who want their own utilities to beat Fluent component styles should declare + the `fui` layers before importing Tailwind (cascade layer order is first-appearance — copy the + list exactly, or the theme sheet's own statement will introduce the missing names in a different + position than the components were compiled against): + + ```css + @layer fui.theme, fui.base, fui.components, fui.utilities; + @import 'tailwindcss'; + ``` + +Levels `l3`–`l5` inside `fui.components` are deliberately empty — use them for app-global, per-page +and one-off overrides that should still lose to your unlayered CSS. + +## Subpath exports + +| Subpath | What it is | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `@fluentui/react-tailwind-theme-preview/base.css` | The emitted, plain-CSS base — theme-less. **Import this once, first.** | +| `@fluentui/react-tailwind-theme-preview/themes/.css` | One theme's class, plain CSS. Seven of them ([above](#the-seven-themes)). **Import the ones you ship.** | +| `@fluentui/react-tailwind-theme-preview/styles.css` | The base plus all seven themes in one file. Convenience only — it still bakes no default, so a class is still applied by hand. | +| `@fluentui/react-tailwind-theme-preview/theme-class-names` | The theme class-name constants (`webLightThemeClassName`, …) and the `ThemeClassName` type. | +| `@fluentui/react-tailwind-theme-preview` | `css/index.css` — Tailwind **source**, for `@reference`/`@import` from a Tailwind v4 build. Not plain CSS. | +| `@fluentui/react-tailwind-theme-preview/css/*` | The individual source layers (`index.css`, `tokens.css`, `themes/*.css`, `variants.css`, `utilities.css`) for advanced setups. | + +Only `base.css`, `themes/*.css`, `styles.css` and `theme-class-names` are consumable without a +Tailwind toolchain. diff --git a/packages/react-components/react-tailwind-theme-preview/build.js b/packages/react-components/react-tailwind-theme-preview/build.js new file mode 100644 index 0000000000000..dc9c0b1c21c43 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/build.js @@ -0,0 +1,170 @@ +/** + * Emits the package's published CSS: everything a component sheet references, nothing a + * component sheet duplicates. Every artifact must contain zero `@property` rules — a + * non-empty registry puts Blink's transition-start on a page-global slow path. + * + * css/emit.css → dist/base.css the THEME-LESS base sheet + * css/themes/.css → dist/themes/.css one file per shipped theme + * css/emit-monolith.css → dist/styles.css base + all seven, zero-config + * + * THE CONTRACT (operator ruling 2026-08-28, "Theme delivery"): import the base, import the + * theme file(s) you use, apply the class — the exact shape of Griffel's + * `import { webLightTheme }` + ``. No default theme is + * baked into any artifact, so a consumer who ships only dark never pays for light. + * + * The theme files run through the same PostCSS/Tailwind pass as the base rather than being + * copied, so a theme's bytes are identical whether they arrive via `dist/themes/*.css` or + * inlined in `dist/styles.css` — asserted below, because a divergence there would mean the + * two delivery routes could render differently. + * + * No `project.json` on purpose: the workspace plugin would infer type-check/format/build + * targets a CSS-only package cannot satisfy; nx infers this project from package.json. + * The release pipeline selects by nx tag and skips untagged projects, so the emission is + * also wired to `prepack` to guarantee the artifacts land in the tarball. + */ +const { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } = require('node:fs'); +const { basename, join } = require('node:path'); + +const postcss = require('postcss'); +// Plugin CREATOR — must be invoked (see tools/workspace-plugin/.../lib/css-modules.ts). +const tailwindcss = require('@tailwindcss/postcss'); + +const packageRoot = __dirname; +const cssDir = join(packageRoot, 'css'); +const themesSourceDir = join(cssDir, 'themes'); +const outputDir = join(packageRoot, 'dist'); +const themesOutputDir = join(outputDir, 'themes'); + +/** + * @param {string} entry absolute path of the CSS entry to compile + * @param {string} output absolute path to write + * @returns {Promise} the compiled CSS + */ +async function compile(entry, output) { + const result = await postcss([tailwindcss()]).process(readFileSync(entry, 'utf8'), { + from: entry, + to: output, + map: false, + }); + + // Checkout line endings must not leak into published artifacts: `core.autocrlf` gives + // Windows working copies CRLF sources, PostCSS preserves whatever it is fed, and the + // route-equivalence matching below is LF-exact. Normalizing here keeps the shipped bytes + // identical on every platform — the same normalization the generator's `--check` applies. + const css = result.css.replace(/\r\n/g, '\n'); + + writeFileSync(output, css); + return css; +} + +/** + * `@property` in a published artifact is a measured perf regression — fail the build. + * + * @param {string} name + * @param {string} css + */ +function assertNoRegisteredProperties(name, css) { + if (/@property\b/.test(css)) { + throw new Error(`${name} contains an @property rule; a non-empty registry slows transition-start page-wide.`); + } +} + +/** + * The declarations of the `.fui-theme-*` rule in a compiled theme sheet, as + * `--name: value;` lines with internal whitespace collapsed. + * + * Whitespace is normalized rather than compared, because it legitimately differs between + * the two routes: a long value prettier wrapped across lines in the source (`--font-base`'s + * font stack) survives verbatim when Tailwind compiles that file directly, and is + * re-serialized onto one line when the same file arrives through `@import`. What must match + * is the declaration set, which is what this returns. Custom-property values never contain a + * brace, so scanning to the rule's closing brace is exact without a CSS parser. + * + * @param {string} css + * @returns {string[] | null} + */ +function themeDeclarations(css) { + const start = css.search(/^\s*\.fui-theme-[a-z0-9-]+ \{$/m); + + if (start < 0) { + return null; + } + + const end = css.indexOf('\n }\n', start); + + if (end < 0) { + return null; + } + + const body = css.slice(css.indexOf('{', start) + 1, end); + + return Array.from( + body.matchAll(/(--[a-z0-9-]+):([^;]*);/g), + ([, name, value]) => `${name}: ${value.trim().replace(/\s+/g, ' ')};`, + ); +} + +async function main() { + mkdirSync(themesOutputDir, { recursive: true }); + + // Stale output must not survive a theme being dropped upstream — the export map would keep + // resolving a file the contract no longer contains. + for (const stale of readdirSync(themesOutputDir)) { + rmSync(join(themesOutputDir, stale), { force: true }); + } + + const base = await compile(join(cssDir, 'emit.css'), join(outputDir, 'base.css')); + assertNoRegisteredProperties('dist/base.css', base); + + /** @type {{ name: string, bytes: number }[]} */ + const themes = []; + + for (const file of readdirSync(themesSourceDir).sort()) { + if (!file.endsWith('.css')) { + continue; + } + + const css = await compile(join(themesSourceDir, file), join(themesOutputDir, file)); + assertNoRegisteredProperties(`dist/themes/${file}`, css); + themes.push({ name: basename(file, '.css'), bytes: Buffer.byteLength(css) }); + } + + const monolith = await compile(join(cssDir, 'emit-monolith.css'), join(outputDir, 'styles.css')); + assertNoRegisteredProperties('dist/styles.css', monolith); + + // Both delivery routes must ship the same declarations for a theme. The comparison is the + // CLASS RULE, not the whole file: standalone files carry a provenance header the monolith + // drops, and the monolith merges all seven into a single `@layer fui.theme` block. + const monolithDeclarations = new Set( + Array.from(monolith.matchAll(/\.fui-theme-[a-z0-9-]+ \{[\s\S]*?\n {2}\}/g), ([rule]) => + (themeDeclarations(rule + '\n') ?? []).join('\n'), + ), + ); + + for (const { name } of themes) { + const declarations = themeDeclarations(readFileSync(join(themesOutputDir, `${name}.css`), 'utf8')); + + if (!declarations?.length) { + throw new Error(`dist/themes/${name}.css contains no theme class declarations.`); + } + + if (!monolithDeclarations.has(declarations.join('\n'))) { + throw new Error( + `dist/styles.css does not carry the declarations from dist/themes/${name}.css — the monolith ` + + `and the per-theme file would render differently.`, + ); + } + } + + const themeSummary = themes.map(({ name, bytes }) => `${name} ${bytes}`).join(', '); + + console.log( + `🎨 @fluentui/react-tailwind-theme-preview: emitted dist/base.css (${Buffer.byteLength(base)} bytes), ` + + `${themes.length} themes (${themeSummary}), dist/styles.css (${Buffer.byteLength(monolith)} bytes)`, + ); +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/packages/react-components/react-tailwind-theme-preview/css/emit-monolith.css b/packages/react-components/react-tailwind-theme-preview/css/emit-monolith.css new file mode 100644 index 0000000000000..e8bc7c079cb94 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/emit-monolith.css @@ -0,0 +1,19 @@ +/* + * Build entry for dist/styles.css — the batteries-included artifact: the base sheet plus + * all seven shipped themes in one file. + * + * Kept as the all-seven monolith for zero-config parity with the windmod package, whose + * `./styles.css` is likewise the self-contained alternative to `./base.css` + per-component + * chunks. Additive precedent, nothing breaks: a consumer already importing `./styles.css` + * keeps a working import. + * + * IT STILL BAKES NO DEFAULT. Importing this file makes all seven theme CLASSES available; + * it does not theme anything on its own. A theme class still has to be applied — which is + * the whole contract, and the same thing Griffel asks for when it makes you pass a theme + * object to FluentProvider. + * + * Prefer `./base.css` + the specific `./themes/.css` files an application ships: this + * file costs every theme, and six of the seven are usually dead weight. + */ +@import './emit.css'; +@import './themes.css'; diff --git a/packages/react-components/react-tailwind-theme-preview/css/emit.css b/packages/react-components/react-tailwind-theme-preview/css/emit.css new file mode 100644 index 0000000000000..45986838fccea --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/emit.css @@ -0,0 +1,31 @@ +/* + * Build entry for dist/base.css — the THEME-LESS base sheet, loaded once per document. + * + * `css/index.css` is the `@reference` target and emits nothing from component modules; + * something must emit the theme layer's declarations once per document, and it is this + * file. `source(none)` is mandatory: without it Tailwind content-scans this package and + * dumps utilities into a layer that outranks every component layer. + * + * What lands here: the `@layer` ORDER statement (this sheet is the head-of-document owner + * of the fui.* family), the Tailwind theme/utility registrations, the custom variants, the + * theme-INVARIANT `:root, :host` values, and the unlayered `prefers-reduced-motion` floor + * with its Spinner exception. + * + * What does NOT land here: any theme's values. Per the operator ruling of 2026-08-28 there + * is no baked default — a document is themed only by importing `dist/themes/.css` and + * applying that theme's class, mirroring Griffel's `import { webLightTheme }` + `theme={…}`. + * `css/emit-monolith.css` builds the all-in-one artifact for zero-config consumers. + */ +@import './index.css' source(none); + +/* + * Headless icons stylesheet, inlined so the base import also covers icons. The icons + * package ships these rules unlayered; layers are compared before specificity, so they must + * be assigned a layer at import time. fui.base sits below every component layer, so a + * component revealing a hidden glyph overrides icon state by layer order rather than by + * stylesheet insertion order. + * + * Icon rules carry no token values, so they are theme-invariant and belong to the base + * rather than to any one theme file. + */ +@import '@fluentui/react-icons/headless/styles.css' layer(fui.base); diff --git a/packages/react-components/react-tailwind-theme-preview/css/index.css b/packages/react-components/react-tailwind-theme-preview/css/index.css new file mode 100644 index 0000000000000..1765e06efe96c --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/index.css @@ -0,0 +1,200 @@ +/** + * The `#theme` reference target for component `*.module.css` files. + * + * Library constraints (deliberately unlike an app Tailwind setup): + * - Preflight SHIPS, at the HEAD of `fui.base` — the same placement Tailwind itself gives it + * (its `base` layer), per the operator ruling of 2026-09-01 refining Decision U (2026-08-28). + * Every component rule outranks it by layer order (`fui.components.*` > `fui.base`), and + * anything else authored into `fui.base` outranks it by source order or specificity — see + * the icon-defaults note below the import. Consumer documents get a normalized base instead + * of per-browser UA defaults; components author over that clean base rather than inheriting + * the same UA quirks Griffel inherits. This is a deliberate divergence from Griffel's + * no-reset posture. Utilities still reach consumers only inlined through `@apply`. + * - Tokens register via `@theme inline` (./tokens.css), substituting `var(--token)` + * into each utility so values stay per-element CSS custom properties. A non-inline + * `@theme` alias would freeze resolution at `:root` and must not be used. + */ + +/* + * One cascade-layer family. fui.base is levelless (the global reset at its head, then element + * resets such as the headless icon defaults); l1 = base components, l2 = compositions, l3+ = + * deeper compositions (SplitButton over MenuButton over Button); the declared stack runs to l5 + * today and may grow. Winner order within a level is in-file source order (all selectors are + * :where()-flat). fui.utilities beats component styles but loses to unlayered consumer 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; + +/* + * Preflight is the FIRST content of `fui.base` — this import must stay ahead of everything + * else that lands in the layer (the icon defaults imported by ./emit.css and the storybook + * harness), so any same-layer authored rule beats it on source order at equal specificity. + */ +@import 'tailwindcss/preflight.css' layer(fui.base); + +/* + * Icon-defaults guard, immediately AFTER preflight in the same layer. + * + * The one `fui.base` occupant that cannot defend itself by source order is the headless icons + * sheet's `:where([data-fui-icon]) { display: inline; … }` — deliberately ZERO specificity so + * any class wins over it. Preflight's replaced-elements rule (`img, svg, … { display: block; + * vertical-align: middle }`) carries element specificity (0,0,1), which beats `:where()` + * regardless of order once they share a layer. This rule re-asserts the icon display default + * at the same (0,0,1) specificity, later in source order, so it wins the tie against + * preflight — restoring exactly the computed state the icons sheet had when preflight lived + * in a lower layer. `line-height: 0` needs no re-assertion (preflight does not contest it), + * and `vertical-align: middle` is left to preflight (it was unopposed before, too). + */ +@layer fui.base { + svg:where([data-fui-icon]) { + display: inline; + } +} +@import 'tailwindcss/theme.css' layer(fui.theme); +@import 'tailwindcss/utilities.css' layer(fui.utilities); + +@import './variants.css'; +@import './utilities.css'; + +@theme static { + /* + * --fui-scale is the ScaleRegion knob: 1 everywhere by default, set per subtree by the + * generated `.fui-scale-region` rule (css/tokens.css) from the element's data-fui-scale + * attribute. At the default the factor is 1, so this computes exactly calc(1rem / 16px). + * Tailwind emits `@theme` values at `:root, :host` only; the generated grouped emission + * re-declares --base-scale, --spacing and --text-icon-* at `.fui-scale-region` so a + * region's --fui-scale re-substitutes there — the generator reads the values from THIS + * block, so the two emissions cannot drift. + */ + --base-scale: calc(1rem / 16px * var(--fui-scale, 1)); + + /* + * Numeric spacing utilities (p-12, gap-8, …) read as px but compute to rem via + * --base-scale, scaling with the root font-size. ./tokens.css re-adds the named + * Fluent spacing steps as aliases of the same numeric axis, so both forms share the + * one density knob below. Stroke widths are the exception: literal base-scale values + * under --stroke-width-* (borders must not thin with layout density). + */ + --spacing-*: initial; + --spacing: calc(1px * var(--base-scale)); + + /* Drop Tailwind's default palette/typography so a stray text-red-500 fails the build + instead of diverging from Fluent tokens. Structural utilities remain. */ + --color-*: initial; + --font-*: initial; + --text-*: initial; + --font-weight-*: initial; + --tracking-*: initial; + --leading-*: initial; + --radius-*: initial; + --shadow-*: initial; + --inset-shadow-*: initial; + --drop-shadow-*: initial; + --blur-*: initial; + --animate-*: initial; + + /* + * Icon glyph sizes (text-icon-20, …). Registered here rather than in ./tokens.css + * because Fluent ships no glyph-size token family — the sizes come from the icon set + * itself. Declared AFTER `--text-*: initial` above, which would otherwise clear them. + * Same literal base-scale form as the type ramp and stroke widths. + */ + --text-icon-12: calc(12px * var(--base-scale)); + --text-icon-16: calc(16px * var(--base-scale)); + --text-icon-20: calc(20px * var(--base-scale)); + --text-icon-24: calc(24px * var(--base-scale)); + --text-icon-28: calc(28px * var(--base-scale)); + --text-icon-32: calc(32px * var(--base-scale)); + --text-icon-48: calc(48px * var(--base-scale)); +} + +/* + * Generated Fluent token registration (`bg-neutral-background-1`, `rounded-medium`, …) + * plus the theme-INVARIANT `:root, :host` values (the spacing scale and stroke widths — + * identical in all seven shipped themes, asserted by the generator). + * Must stay AFTER the `@theme static` block — the `initial` resets above clear only + * what is registered before them. + */ +@import './tokens.css'; + +/* + * NO THEME IS IMPORTED HERE, DELIBERATELY. + * + * Operator ruling 2026-08-28 ("Theme delivery"): this package mirrors Griffel's contract + * exactly. Griffel makes a consumer `import { webLightTheme }` and pass it to + * ``; there is no baked default, and without one the tokens are + * simply unset. So this file is THEME-LESS — it registers every token NAME and the + * invariant values, and nothing here gives those names a colour. + * + * A consumer imports the theme file(s) their app ships (`./themes/web-light.css`, …) and + * applies the matching class; `./themes.css` aggregates all seven for the two entries that + * genuinely want the whole catalog (the `./styles.css` monolith and the storybook harness). + * + * Do NOT re-add a default here. It would make one theme free and every other a surcharge, + * and would silently theme documents that never asked for it. + */ + +/* + * House-wide `prefers-reduced-motion` floor: one rule in place of the per-component + * suppression blocks every motion-bearing component used to carry. + * + * Deliberately UNLAYERED, and deliberately without `!important`. Layers are compared + * before specificity and unlayered author declarations outrank every layered one, so a + * bare `*` beats component rules in `fui.components.*` even though it would lose every + * specificity contest inside a layer. Never wrap this in `@layer` — the layerlessness IS + * the mechanism. + * + * It lives here rather than in `./emit.css` because two entries emit this theme — + * `./emit.css` for published consumers and `scripts/storybook/src/tailwind-theme.css` for + * storybooks — and `index.css` is the only file both share. Component modules reach it + * through `@reference '#theme'`, which emits nothing, so no component stylesheet carries + * a copy. + * + * Consequences that are intentional, not oversights: + * - Consumer inline styles, and consumer unlayered CSS ordered after this sheet, still + * win. Applications keep the last word on their own motion. + * - `animation-iteration-count: 1` stops infinite animations (the SkeletonItem shimmer, the + * indeterminate ProgressBar pulse) after a single 1ms pass; they render static. Griffel + * suppresses both too, so that is parity. The Spinner is the one motion carved back out — + * see the exception below. + * - This is broader than Griffel, which suppresses per component and leaves several + * motions running under the preference. See the windmod package's MIGRATION.md. + */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition-duration: 1ms; + transition-delay: 1ms; + animation-duration: 1ms; + animation-delay: 1ms; + animation-iteration-count: 1; + } + + /* + * Spinner exception — essential motion, and Griffel parity. + * + * A loading indicator that does not move stops being a loading indicator, so the Spinner's + * rotation is exempt from the floor. Griffel makes the same call: its own reduced-motion CSS + * slows the ring to 1.8s rather than stopping it, and that is the value restored here. + * Everything else about the Spinner stays suppressed — the tail keeps the static full-ring + * conic its component rule swaps in under the preference, exactly as Griffel's tail sits at + * `animation-iteration-count: 0`. + * + * Inside the floor's own media query, so it is equally a no-op without the preference. After + * the floor in source order and higher in specificity, so it wins on both counts. + * + * `.fui-spinner > span` is the only hook available: windmod stamps public identity classes on + * component ROOTS (`utils/groupMarker.ts`), while slot classes are hashed CSS-module idents the + * theme cannot name. The child step is safe rather than merely likely — this rule restores + * `animation-duration` and `animation-iteration-count` only, both of which are inert on an + * element whose `animation-name` is `none`. The rotating ring is the sole animated direct child + * of a Spinner root; the tail is a grandchild and is deliberately not reached. + * + * `animation-delay` is left at the floor's 1ms on purpose: it matches what Griffel computes + * under the preference, and 1ms is unobservable on a rotation that never ends. + */ + .fui-spinner > span { + animation-duration: 1.8s; + animation-iteration-count: infinite; + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes.css b/packages/react-components/react-tailwind-theme-preview/css/themes.css new file mode 100644 index 0000000000000..e07ba2e8ec72c --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes.css @@ -0,0 +1,22 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * ALL SEVEN SHIPPED THEMES. A convenience aggregate, not the recommended import: + * pulling this in costs every theme. Import only `./css/themes/.css` for the + * themes an application actually offers. + * + * Used by the `./styles.css` monolith build and by the storybook harness, both of + * which want the whole catalog by definition. + */ + +@import './themes/web-light.css'; +@import './themes/web-dark.css'; +@import './themes/teams-light.css'; +@import './themes/teams-dark.css'; +@import './themes/teams-high-contrast.css'; +@import './themes/teams-light-v21.css'; +@import './themes/teams-dark-v21.css'; diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark-v21.css b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark-v21.css new file mode 100644 index 0000000000000..6a92efd7ed4d0 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark-v21.css @@ -0,0 +1,474 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * teamsDarkV21Theme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-teams-dark-v21` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-teams-dark-v21 { + --color-neutral-foreground-1: #ffffff; + --color-neutral-foreground-1-hover: #ffffff; + --color-neutral-foreground-1-pressed: #ffffff; + --color-neutral-foreground-1-selected: #ffffff; + --color-neutral-foreground-2: #d6d6d6; + --color-neutral-foreground-2-hover: #ffffff; + --color-neutral-foreground-2-pressed: #ffffff; + --color-neutral-foreground-2-selected: #ffffff; + --color-neutral-foreground-2-brand-hover: #887dff; + --color-neutral-foreground-2-brand-pressed: #7769fa; + --color-neutral-foreground-2-brand-selected: #887dff; + --color-neutral-foreground-3: #adadad; + --color-neutral-foreground-3-hover: #d6d6d6; + --color-neutral-foreground-3-pressed: #d6d6d6; + --color-neutral-foreground-3-selected: #d6d6d6; + --color-neutral-foreground-3-brand-hover: #887dff; + --color-neutral-foreground-3-brand-pressed: #7769fa; + --color-neutral-foreground-3-brand-selected: #887dff; + --color-neutral-foreground-4: #999999; + --color-neutral-foreground-5: #adadad; + --color-neutral-foreground-5-hover: #ffffff; + --color-neutral-foreground-5-pressed: #ffffff; + --color-neutral-foreground-5-selected: #ffffff; + --color-neutral-foreground-disabled: #5c5c5c; + --color-brand-foreground-link: #887dff; + --color-brand-foreground-link-hover: #9791ff; + --color-brand-foreground-link-pressed: #7769fa; + --color-brand-foreground-link-selected: #887dff; + --color-neutral-foreground-2-link: #d6d6d6; + --color-neutral-foreground-2-link-hover: #ffffff; + --color-neutral-foreground-2-link-pressed: #ffffff; + --color-neutral-foreground-2-link-selected: #ffffff; + --color-compound-brand-foreground-1: #887dff; + --color-compound-brand-foreground-1-hover: #9791ff; + --color-compound-brand-foreground-1-pressed: #7769fa; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #242424; + --color-neutral-foreground-inverted-hover: #242424; + --color-neutral-foreground-inverted-pressed: #242424; + --color-neutral-foreground-inverted-selected: #242424; + --color-neutral-foreground-inverted-2: #242424; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #887dff; + --color-brand-foreground-2: #aba8ff; + --color-brand-foreground-2-hover: #bab8ff; + --color-brand-foreground-2-pressed: #e8e8ff; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #654cf5; + --color-brand-foreground-inverted-hover: #5a40db; + --color-brand-foreground-inverted-pressed: #4d3aba; + --color-brand-foreground-on-light: #654cf5; + --color-brand-foreground-on-light-hover: #5a40db; + --color-brand-foreground-on-light-pressed: #44359e; + --color-brand-foreground-on-light-selected: #4d3aba; + --color-neutral-background-1: #292929; + --color-neutral-background-1-hover: #3d3d3d; + --color-neutral-background-1-pressed: #1f1f1f; + --color-neutral-background-1-selected: #383838; + --color-neutral-background-2: #242424; + --color-neutral-background-2-hover: #383838; + --color-neutral-background-2-pressed: #1a1a1a; + --color-neutral-background-2-selected: #333333; + --color-neutral-background-3: #1f1f1f; + --color-neutral-background-3-hover: #333333; + --color-neutral-background-3-pressed: #141414; + --color-neutral-background-3-selected: #2e2e2e; + --color-neutral-background-4: #141414; + --color-neutral-background-4-hover: #292929; + --color-neutral-background-4-pressed: #0a0a0a; + --color-neutral-background-4-selected: #242424; + --color-neutral-background-5: #0a0a0a; + --color-neutral-background-5-hover: #1f1f1f; + --color-neutral-background-5-pressed: #000000; + --color-neutral-background-5-selected: #1a1a1a; + --color-neutral-background-6: #333333; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #1a1a1a; + --color-neutral-background-7-pressed: #0a0a0a; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #292929; + --color-neutral-background-inverted: #ffffff; + --color-neutral-background-inverted-hover: #f5f5f5; + --color-neutral-background-inverted-pressed: #e0e0e0; + --color-neutral-background-inverted-selected: #ebebeb; + --color-neutral-background-static: #3d3d3d; + --color-neutral-background-alpha: rgba(26, 26, 26, 0.5); + --color-neutral-background-alpha-2: rgba(31, 31, 31, 0.7); + --color-subtle-background: transparent; + --color-subtle-background-hover: #383838; + --color-subtle-background-pressed: #2e2e2e; + --color-subtle-background-selected: #333333; + --color-subtle-background-light-alpha-hover: rgba(36, 36, 36, 0.8); + --color-subtle-background-light-alpha-pressed: rgba(36, 36, 36, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #141414; + --color-neutral-background-disabled-2: #292929; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #575757; + --color-neutral-stencil-2: #333333; + --color-neutral-stencil-1-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-2-alpha: rgba(255, 255, 255, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.5); + --color-scrollbar-overlay: rgba(255, 255, 255, 0.6); + --color-brand-background: #5a40db; + --color-brand-background-hover: #654cf5; + --color-brand-background-pressed: #3b3185; + --color-brand-background-selected: #4d3aba; + --color-compound-brand-background: #887dff; + --color-compound-brand-background-hover: #9791ff; + --color-compound-brand-background-pressed: #7769fa; + --color-brand-background-static: #654cf5; + --color-brand-background-2: #2f2a5e; + --color-brand-background-2-hover: #3b3185; + --color-brand-background-2-pressed: #29274f; + --color-brand-background-3-static: #4d3aba; + --color-brand-background-4-static: #3b3185; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #e8e8ff; + --color-brand-background-inverted-pressed: #c8c7ff; + --color-brand-background-inverted-selected: #dcdbff; + --color-neutral-card-background: #333333; + --color-neutral-card-background-hover: #3d3d3d; + --color-neutral-card-background-pressed: #2e2e2e; + --color-neutral-card-background-selected: #383838; + --color-neutral-card-background-disabled: #141414; + --color-neutral-stroke-accessible: #adadad; + --color-neutral-stroke-accessible-hover: #bdbdbd; + --color-neutral-stroke-accessible-pressed: #b3b3b3; + --color-neutral-stroke-accessible-selected: #887dff; + --color-neutral-stroke-1: #666666; + --color-neutral-stroke-1-hover: #757575; + --color-neutral-stroke-1-pressed: #6b6b6b; + --color-neutral-stroke-1-selected: #707070; + --color-neutral-stroke-2: #525252; + --color-neutral-stroke-3: #3d3d3d; + --color-neutral-stroke-4: #3d3d3d; + --color-neutral-stroke-4-hover: #2e2e2e; + --color-neutral-stroke-4-pressed: #242424; + --color-neutral-stroke-4-selected: #3d3d3d; + --color-neutral-stroke-subtle: #0a0a0a; + --color-neutral-stroke-on-brand: #292929; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #887dff; + --color-brand-stroke-2: #44359e; + --color-brand-stroke-2-hover: #44359e; + --color-brand-stroke-2-pressed: #352e70; + --color-brand-stroke-2-contrast: #44359e; + --color-compound-brand-stroke: #7769fa; + --color-compound-brand-stroke-hover: #887dff; + --color-compound-brand-stroke-pressed: #654cf5; + --color-neutral-stroke-disabled: #424242; + --color-neutral-stroke-disabled-2: #3d3d3d; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #000000; + --color-stroke-focus-2: #ffffff; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.24); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.28); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.4); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.48); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #3f1011; + --color-palette-red-background-2: #751d1f; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #e37d80; + --color-palette-red-border-1: #d13438; + --color-palette-red-border-2: #e37d80; + --color-palette-red-foreground-1: #e37d80; + --color-palette-red-foreground-2: #f1bbbc; + --color-palette-red-foreground-3: #e37d80; + --color-palette-red-foreground-inverted: #d13438; + --color-palette-green-background-1: #052505; + --color-palette-green-background-2: #094509; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #54b054; + --color-palette-green-border-1: #107c10; + --color-palette-green-border-2: #9fd89f; + --color-palette-green-foreground-1: #54b054; + --color-palette-green-foreground-2: #9fd89f; + --color-palette-green-foreground-3: #9fd89f; + --color-palette-green-foreground-inverted: #107c10; + --color-palette-dark-orange-background-1: #411200; + --color-palette-dark-orange-background-2: #7a2101; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #e9835e; + --color-palette-dark-orange-border-1: #da3b01; + --color-palette-dark-orange-border-2: #e9835e; + --color-palette-dark-orange-foreground-1: #e9835e; + --color-palette-dark-orange-foreground-2: #f4bfab; + --color-palette-dark-orange-foreground-3: #e9835e; + --color-palette-yellow-background-1: #4c4400; + --color-palette-yellow-background-2: #817400; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #feee66; + --color-palette-yellow-border-1: #fde300; + --color-palette-yellow-border-2: #fdea3d; + --color-palette-yellow-foreground-1: #feee66; + --color-palette-yellow-foreground-2: #fef7b2; + --color-palette-yellow-foreground-3: #fdea3d; + --color-palette-yellow-foreground-inverted: #817400; + --color-palette-berry-background-1: #3a1136; + --color-palette-berry-background-2: #6d2064; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #da7ed0; + --color-palette-berry-border-1: #c239b3; + --color-palette-berry-border-2: #d161c4; + --color-palette-berry-foreground-1: #da7ed0; + --color-palette-berry-foreground-2: #edbbe7; + --color-palette-berry-foreground-3: #d161c4; + --color-palette-marigold-background-1: #463100; + --color-palette-marigold-background-2: #835b00; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #f2c661; + --color-palette-marigold-border-1: #eaa300; + --color-palette-marigold-border-2: #efb839; + --color-palette-marigold-foreground-1: #f2c661; + --color-palette-marigold-foreground-2: #f9e2ae; + --color-palette-marigold-foreground-3: #efb839; + --color-palette-light-green-background-1: #063004; + --color-palette-light-green-background-2: #0b5a08; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #5ec75a; + --color-palette-light-green-border-1: #13a10e; + --color-palette-light-green-border-2: #3db838; + --color-palette-light-green-foreground-1: #5ec75a; + --color-palette-light-green-foreground-2: #a7e3a5; + --color-palette-light-green-foreground-3: #3db838; + --color-palette-anchor-background-2: #202427; + --color-palette-anchor-border-active: #808a90; + --color-palette-anchor-foreground-2: #bcc3c7; + --color-palette-beige-background-2: #444241; + --color-palette-beige-border-active: #afabaa; + --color-palette-beige-foreground-2: #d7d4d4; + --color-palette-blue-background-2: #004377; + --color-palette-blue-border-active: #5caae5; + --color-palette-blue-foreground-2: #a9d3f2; + --color-palette-brass-background-2: #553e06; + --color-palette-brass-border-active: #c1a256; + --color-palette-brass-foreground-2: #e0cea2; + --color-palette-brown-background-2: #50301a; + --color-palette-brown-border-active: #bb8f6f; + --color-palette-brown-foreground-2: #ddc3b0; + --color-palette-cornflower-background-2: #2c3c85; + --color-palette-cornflower-border-active: #93a4f4; + --color-palette-cornflower-foreground-2: #c8d1fa; + --color-palette-cranberry-background-2: #6e0811; + --color-palette-cranberry-border-active: #dc626d; + --color-palette-cranberry-foreground-2: #eeacb2; + --color-palette-dark-green-background-2: #063b06; + --color-palette-dark-green-border-active: #4da64d; + --color-palette-dark-green-foreground-2: #9ad29a; + --color-palette-dark-red-background-2: #590815; + --color-palette-dark-red-border-active: #ac4f5e; + --color-palette-dark-red-foreground-2: #d69ca5; + --color-palette-forest-background-2: #294903; + --color-palette-forest-border-active: #85b44c; + --color-palette-forest-foreground-2: #bdd99b; + --color-palette-gold-background-2: #6c5700; + --color-palette-gold-border-active: #dac157; + --color-palette-gold-foreground-2: #ecdfa5; + --color-palette-grape-background-2: #4c0d55; + --color-palette-grape-border-active: #b55fc1; + --color-palette-grape-foreground-2: #d9a7e0; + --color-palette-lavender-background-2: #3f3682; + --color-palette-lavender-border-active: #a79cf1; + --color-palette-lavender-foreground-2: #d2ccf8; + --color-palette-light-teal-background-2: #00666d; + --color-palette-light-teal-border-active: #58d3db; + --color-palette-light-teal-foreground-2: #a6e9ed; + --color-palette-lilac-background-2: #63276d; + --color-palette-lilac-border-active: #cf87da; + --color-palette-lilac-foreground-2: #e6bfed; + --color-palette-magenta-background-2: #6b0043; + --color-palette-magenta-border-active: #d957a8; + --color-palette-magenta-foreground-2: #eca5d1; + --color-palette-mink-background-2: #343231; + --color-palette-mink-border-active: #9e9b99; + --color-palette-mink-foreground-2: #cecccb; + --color-palette-navy-background-2: #001665; + --color-palette-navy-border-active: #546fd2; + --color-palette-navy-foreground-2: #a3b2e8; + --color-palette-peach-background-2: #8f4e00; + --color-palette-peach-border-active: #ffba66; + --color-palette-peach-foreground-2: #ffddb3; + --color-palette-pink-background-2: #80215d; + --color-palette-pink-border-active: #ef85c8; + --color-palette-pink-foreground-2: #f7c0e3; + --color-palette-platinum-background-2: #3b4447; + --color-palette-platinum-border-active: #a0adb2; + --color-palette-platinum-foreground-2: #cdd6d8; + --color-palette-plum-background-2: #5a003b; + --color-palette-plum-border-active: #ad4589; + --color-palette-plum-foreground-2: #d696c0; + --color-palette-pumpkin-background-2: #712d09; + --color-palette-pumpkin-border-active: #df8e64; + --color-palette-pumpkin-foreground-2: #efc4ad; + --color-palette-purple-background-2: #341a51; + --color-palette-purple-border-active: #9470bd; + --color-palette-purple-foreground-2: #c6b1de; + --color-palette-royal-blue-background-2: #002c4e; + --color-palette-royal-blue-border-active: #4a89ba; + --color-palette-royal-blue-foreground-2: #9abfdc; + --color-palette-seafoam-background-2: #00723b; + --color-palette-seafoam-border-active: #5ae0a0; + --color-palette-seafoam-foreground-2: #a8f0cd; + --color-palette-steel-background-2: #00333f; + --color-palette-steel-border-active: #4496a9; + --color-palette-steel-foreground-2: #94c8d4; + --color-palette-teal-background-2: #02494c; + --color-palette-teal-border-active: #4cb4b7; + --color-palette-teal-foreground-2: #9bd9db; + --color-status-success-background-1: #052505; + --color-status-success-background-2: #094509; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #54b054; + --color-status-success-foreground-2: #9fd89f; + --color-status-success-foreground-3: #9fd89f; + --color-status-success-foreground-inverted: #0e700e; + --color-status-success-border-active: #54b054; + --color-status-success-border-1: #107c10; + --color-status-success-border-2: #9fd89f; + --color-status-warning-background-1: #4a1e04; + --color-status-warning-background-2: #8a3707; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #faa06b; + --color-status-warning-foreground-2: #fdcfb4; + --color-status-warning-foreground-3: #f98845; + --color-status-warning-foreground-inverted: #bc4b09; + --color-status-warning-border-active: #faa06b; + --color-status-warning-border-1: #f7630c; + --color-status-warning-border-2: #f98845; + --color-status-danger-background-1: #3b0509; + --color-status-danger-background-2: #6e0811; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #dc626d; + --color-status-danger-foreground-2: #eeacb2; + --color-status-danger-foreground-3: #eeacb2; + --color-status-danger-foreground-inverted: #b10e1c; + --color-status-danger-border-active: #dc626d; + --color-status-danger-border-1: #c50f1f; + --color-status-danger-border-2: #dc626d; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark.css b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark.css new file mode 100644 index 0000000000000..cd122d82e8dfc --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-dark.css @@ -0,0 +1,474 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * teamsDarkTheme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-teams-dark` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-teams-dark { + --color-neutral-foreground-1: #ffffff; + --color-neutral-foreground-1-hover: #ffffff; + --color-neutral-foreground-1-pressed: #ffffff; + --color-neutral-foreground-1-selected: #ffffff; + --color-neutral-foreground-2: #d6d6d6; + --color-neutral-foreground-2-hover: #ffffff; + --color-neutral-foreground-2-pressed: #ffffff; + --color-neutral-foreground-2-selected: #ffffff; + --color-neutral-foreground-2-brand-hover: #7f85f5; + --color-neutral-foreground-2-brand-pressed: #7579eb; + --color-neutral-foreground-2-brand-selected: #7f85f5; + --color-neutral-foreground-3: #adadad; + --color-neutral-foreground-3-hover: #d6d6d6; + --color-neutral-foreground-3-pressed: #d6d6d6; + --color-neutral-foreground-3-selected: #d6d6d6; + --color-neutral-foreground-3-brand-hover: #7f85f5; + --color-neutral-foreground-3-brand-pressed: #7579eb; + --color-neutral-foreground-3-brand-selected: #7f85f5; + --color-neutral-foreground-4: #999999; + --color-neutral-foreground-5: #adadad; + --color-neutral-foreground-5-hover: #ffffff; + --color-neutral-foreground-5-pressed: #ffffff; + --color-neutral-foreground-5-selected: #ffffff; + --color-neutral-foreground-disabled: #5c5c5c; + --color-brand-foreground-link: #7f85f5; + --color-brand-foreground-link-hover: #9299f7; + --color-brand-foreground-link-pressed: #7579eb; + --color-brand-foreground-link-selected: #7f85f5; + --color-neutral-foreground-2-link: #d6d6d6; + --color-neutral-foreground-2-link-hover: #ffffff; + --color-neutral-foreground-2-link-pressed: #ffffff; + --color-neutral-foreground-2-link-selected: #ffffff; + --color-compound-brand-foreground-1: #7f85f5; + --color-compound-brand-foreground-1-hover: #9299f7; + --color-compound-brand-foreground-1-pressed: #7579eb; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #242424; + --color-neutral-foreground-inverted-hover: #242424; + --color-neutral-foreground-inverted-pressed: #242424; + --color-neutral-foreground-inverted-selected: #242424; + --color-neutral-foreground-inverted-2: #242424; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #7f85f5; + --color-brand-foreground-2: #aab1fa; + --color-brand-foreground-2-hover: #b6bcfa; + --color-brand-foreground-2-pressed: #e8ebfa; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #5b5fc7; + --color-brand-foreground-inverted-hover: #4f52b2; + --color-brand-foreground-inverted-pressed: #444791; + --color-brand-foreground-on-light: #5b5fc7; + --color-brand-foreground-on-light-hover: #4f52b2; + --color-brand-foreground-on-light-pressed: #3d3e78; + --color-brand-foreground-on-light-selected: #444791; + --color-neutral-background-1: #292929; + --color-neutral-background-1-hover: #3d3d3d; + --color-neutral-background-1-pressed: #1f1f1f; + --color-neutral-background-1-selected: #383838; + --color-neutral-background-2: #242424; + --color-neutral-background-2-hover: #383838; + --color-neutral-background-2-pressed: #1a1a1a; + --color-neutral-background-2-selected: #333333; + --color-neutral-background-3: #1f1f1f; + --color-neutral-background-3-hover: #333333; + --color-neutral-background-3-pressed: #141414; + --color-neutral-background-3-selected: #2e2e2e; + --color-neutral-background-4: #141414; + --color-neutral-background-4-hover: #292929; + --color-neutral-background-4-pressed: #0a0a0a; + --color-neutral-background-4-selected: #242424; + --color-neutral-background-5: #0a0a0a; + --color-neutral-background-5-hover: #1f1f1f; + --color-neutral-background-5-pressed: #000000; + --color-neutral-background-5-selected: #1a1a1a; + --color-neutral-background-6: #333333; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #1a1a1a; + --color-neutral-background-7-pressed: #0a0a0a; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #292929; + --color-neutral-background-inverted: #ffffff; + --color-neutral-background-inverted-hover: #f5f5f5; + --color-neutral-background-inverted-pressed: #e0e0e0; + --color-neutral-background-inverted-selected: #ebebeb; + --color-neutral-background-static: #3d3d3d; + --color-neutral-background-alpha: rgba(26, 26, 26, 0.5); + --color-neutral-background-alpha-2: rgba(31, 31, 31, 0.7); + --color-subtle-background: transparent; + --color-subtle-background-hover: #383838; + --color-subtle-background-pressed: #2e2e2e; + --color-subtle-background-selected: #333333; + --color-subtle-background-light-alpha-hover: rgba(36, 36, 36, 0.8); + --color-subtle-background-light-alpha-pressed: rgba(36, 36, 36, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #141414; + --color-neutral-background-disabled-2: #292929; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #575757; + --color-neutral-stencil-2: #333333; + --color-neutral-stencil-1-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-2-alpha: rgba(255, 255, 255, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.5); + --color-scrollbar-overlay: rgba(255, 255, 255, 0.6); + --color-brand-background: #4f52b2; + --color-brand-background-hover: #5b5fc7; + --color-brand-background-pressed: #383966; + --color-brand-background-selected: #444791; + --color-compound-brand-background: #7f85f5; + --color-compound-brand-background-hover: #9299f7; + --color-compound-brand-background-pressed: #7579eb; + --color-brand-background-static: #5b5fc7; + --color-brand-background-2: #2f2f4a; + --color-brand-background-2-hover: #383966; + --color-brand-background-2-pressed: #2b2b40; + --color-brand-background-3-static: #444791; + --color-brand-background-4-static: #383966; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #e8ebfa; + --color-brand-background-inverted-pressed: #c5cbfa; + --color-brand-background-inverted-selected: #dce0fa; + --color-neutral-card-background: #333333; + --color-neutral-card-background-hover: #3d3d3d; + --color-neutral-card-background-pressed: #2e2e2e; + --color-neutral-card-background-selected: #383838; + --color-neutral-card-background-disabled: #141414; + --color-neutral-stroke-accessible: #adadad; + --color-neutral-stroke-accessible-hover: #bdbdbd; + --color-neutral-stroke-accessible-pressed: #b3b3b3; + --color-neutral-stroke-accessible-selected: #7f85f5; + --color-neutral-stroke-1: #666666; + --color-neutral-stroke-1-hover: #757575; + --color-neutral-stroke-1-pressed: #6b6b6b; + --color-neutral-stroke-1-selected: #707070; + --color-neutral-stroke-2: #525252; + --color-neutral-stroke-3: #3d3d3d; + --color-neutral-stroke-4: #3d3d3d; + --color-neutral-stroke-4-hover: #2e2e2e; + --color-neutral-stroke-4-pressed: #242424; + --color-neutral-stroke-4-selected: #3d3d3d; + --color-neutral-stroke-subtle: #0a0a0a; + --color-neutral-stroke-on-brand: #292929; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #7f85f5; + --color-brand-stroke-2: #3d3e78; + --color-brand-stroke-2-hover: #3d3e78; + --color-brand-stroke-2-pressed: #333357; + --color-brand-stroke-2-contrast: #3d3e78; + --color-compound-brand-stroke: #7579eb; + --color-compound-brand-stroke-hover: #7f85f5; + --color-compound-brand-stroke-pressed: #5b5fc7; + --color-neutral-stroke-disabled: #424242; + --color-neutral-stroke-disabled-2: #3d3d3d; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #000000; + --color-stroke-focus-2: #ffffff; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.24); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.28); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.4); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.48); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #3f1011; + --color-palette-red-background-2: #751d1f; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #e37d80; + --color-palette-red-border-1: #d13438; + --color-palette-red-border-2: #e37d80; + --color-palette-red-foreground-1: #e37d80; + --color-palette-red-foreground-2: #f1bbbc; + --color-palette-red-foreground-3: #e37d80; + --color-palette-red-foreground-inverted: #d13438; + --color-palette-green-background-1: #052505; + --color-palette-green-background-2: #094509; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #54b054; + --color-palette-green-border-1: #107c10; + --color-palette-green-border-2: #9fd89f; + --color-palette-green-foreground-1: #54b054; + --color-palette-green-foreground-2: #9fd89f; + --color-palette-green-foreground-3: #9fd89f; + --color-palette-green-foreground-inverted: #107c10; + --color-palette-dark-orange-background-1: #411200; + --color-palette-dark-orange-background-2: #7a2101; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #e9835e; + --color-palette-dark-orange-border-1: #da3b01; + --color-palette-dark-orange-border-2: #e9835e; + --color-palette-dark-orange-foreground-1: #e9835e; + --color-palette-dark-orange-foreground-2: #f4bfab; + --color-palette-dark-orange-foreground-3: #e9835e; + --color-palette-yellow-background-1: #4c4400; + --color-palette-yellow-background-2: #817400; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #feee66; + --color-palette-yellow-border-1: #fde300; + --color-palette-yellow-border-2: #fdea3d; + --color-palette-yellow-foreground-1: #feee66; + --color-palette-yellow-foreground-2: #fef7b2; + --color-palette-yellow-foreground-3: #fdea3d; + --color-palette-yellow-foreground-inverted: #817400; + --color-palette-berry-background-1: #3a1136; + --color-palette-berry-background-2: #6d2064; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #da7ed0; + --color-palette-berry-border-1: #c239b3; + --color-palette-berry-border-2: #d161c4; + --color-palette-berry-foreground-1: #da7ed0; + --color-palette-berry-foreground-2: #edbbe7; + --color-palette-berry-foreground-3: #d161c4; + --color-palette-marigold-background-1: #463100; + --color-palette-marigold-background-2: #835b00; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #f2c661; + --color-palette-marigold-border-1: #eaa300; + --color-palette-marigold-border-2: #efb839; + --color-palette-marigold-foreground-1: #f2c661; + --color-palette-marigold-foreground-2: #f9e2ae; + --color-palette-marigold-foreground-3: #efb839; + --color-palette-light-green-background-1: #063004; + --color-palette-light-green-background-2: #0b5a08; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #5ec75a; + --color-palette-light-green-border-1: #13a10e; + --color-palette-light-green-border-2: #3db838; + --color-palette-light-green-foreground-1: #5ec75a; + --color-palette-light-green-foreground-2: #a7e3a5; + --color-palette-light-green-foreground-3: #3db838; + --color-palette-anchor-background-2: #202427; + --color-palette-anchor-border-active: #808a90; + --color-palette-anchor-foreground-2: #bcc3c7; + --color-palette-beige-background-2: #444241; + --color-palette-beige-border-active: #afabaa; + --color-palette-beige-foreground-2: #d7d4d4; + --color-palette-blue-background-2: #004377; + --color-palette-blue-border-active: #5caae5; + --color-palette-blue-foreground-2: #a9d3f2; + --color-palette-brass-background-2: #553e06; + --color-palette-brass-border-active: #c1a256; + --color-palette-brass-foreground-2: #e0cea2; + --color-palette-brown-background-2: #50301a; + --color-palette-brown-border-active: #bb8f6f; + --color-palette-brown-foreground-2: #ddc3b0; + --color-palette-cornflower-background-2: #2c3c85; + --color-palette-cornflower-border-active: #93a4f4; + --color-palette-cornflower-foreground-2: #c8d1fa; + --color-palette-cranberry-background-2: #6e0811; + --color-palette-cranberry-border-active: #dc626d; + --color-palette-cranberry-foreground-2: #eeacb2; + --color-palette-dark-green-background-2: #063b06; + --color-palette-dark-green-border-active: #4da64d; + --color-palette-dark-green-foreground-2: #9ad29a; + --color-palette-dark-red-background-2: #590815; + --color-palette-dark-red-border-active: #ac4f5e; + --color-palette-dark-red-foreground-2: #d69ca5; + --color-palette-forest-background-2: #294903; + --color-palette-forest-border-active: #85b44c; + --color-palette-forest-foreground-2: #bdd99b; + --color-palette-gold-background-2: #6c5700; + --color-palette-gold-border-active: #dac157; + --color-palette-gold-foreground-2: #ecdfa5; + --color-palette-grape-background-2: #4c0d55; + --color-palette-grape-border-active: #b55fc1; + --color-palette-grape-foreground-2: #d9a7e0; + --color-palette-lavender-background-2: #3f3682; + --color-palette-lavender-border-active: #a79cf1; + --color-palette-lavender-foreground-2: #d2ccf8; + --color-palette-light-teal-background-2: #00666d; + --color-palette-light-teal-border-active: #58d3db; + --color-palette-light-teal-foreground-2: #a6e9ed; + --color-palette-lilac-background-2: #63276d; + --color-palette-lilac-border-active: #cf87da; + --color-palette-lilac-foreground-2: #e6bfed; + --color-palette-magenta-background-2: #6b0043; + --color-palette-magenta-border-active: #d957a8; + --color-palette-magenta-foreground-2: #eca5d1; + --color-palette-mink-background-2: #343231; + --color-palette-mink-border-active: #9e9b99; + --color-palette-mink-foreground-2: #cecccb; + --color-palette-navy-background-2: #001665; + --color-palette-navy-border-active: #546fd2; + --color-palette-navy-foreground-2: #a3b2e8; + --color-palette-peach-background-2: #8f4e00; + --color-palette-peach-border-active: #ffba66; + --color-palette-peach-foreground-2: #ffddb3; + --color-palette-pink-background-2: #80215d; + --color-palette-pink-border-active: #ef85c8; + --color-palette-pink-foreground-2: #f7c0e3; + --color-palette-platinum-background-2: #3b4447; + --color-palette-platinum-border-active: #a0adb2; + --color-palette-platinum-foreground-2: #cdd6d8; + --color-palette-plum-background-2: #5a003b; + --color-palette-plum-border-active: #ad4589; + --color-palette-plum-foreground-2: #d696c0; + --color-palette-pumpkin-background-2: #712d09; + --color-palette-pumpkin-border-active: #df8e64; + --color-palette-pumpkin-foreground-2: #efc4ad; + --color-palette-purple-background-2: #341a51; + --color-palette-purple-border-active: #9470bd; + --color-palette-purple-foreground-2: #c6b1de; + --color-palette-royal-blue-background-2: #002c4e; + --color-palette-royal-blue-border-active: #4a89ba; + --color-palette-royal-blue-foreground-2: #9abfdc; + --color-palette-seafoam-background-2: #00723b; + --color-palette-seafoam-border-active: #5ae0a0; + --color-palette-seafoam-foreground-2: #a8f0cd; + --color-palette-steel-background-2: #00333f; + --color-palette-steel-border-active: #4496a9; + --color-palette-steel-foreground-2: #94c8d4; + --color-palette-teal-background-2: #02494c; + --color-palette-teal-border-active: #4cb4b7; + --color-palette-teal-foreground-2: #9bd9db; + --color-status-success-background-1: #052505; + --color-status-success-background-2: #094509; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #54b054; + --color-status-success-foreground-2: #9fd89f; + --color-status-success-foreground-3: #9fd89f; + --color-status-success-foreground-inverted: #0e700e; + --color-status-success-border-active: #54b054; + --color-status-success-border-1: #107c10; + --color-status-success-border-2: #9fd89f; + --color-status-warning-background-1: #4a1e04; + --color-status-warning-background-2: #8a3707; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #faa06b; + --color-status-warning-foreground-2: #fdcfb4; + --color-status-warning-foreground-3: #f98845; + --color-status-warning-foreground-inverted: #bc4b09; + --color-status-warning-border-active: #faa06b; + --color-status-warning-border-1: #f7630c; + --color-status-warning-border-2: #f98845; + --color-status-danger-background-1: #3b0509; + --color-status-danger-background-2: #6e0811; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #dc626d; + --color-status-danger-foreground-2: #eeacb2; + --color-status-danger-foreground-3: #eeacb2; + --color-status-danger-foreground-inverted: #b10e1c; + --color-status-danger-border-active: #dc626d; + --color-status-danger-border-1: #c50f1f; + --color-status-danger-border-2: #dc626d; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/teams-high-contrast.css b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-high-contrast.css new file mode 100644 index 0000000000000..bd211087f84a6 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-high-contrast.css @@ -0,0 +1,474 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * teamsHighContrastTheme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-teams-high-contrast` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-teams-high-contrast { + --color-neutral-foreground-1: #ffffff; + --color-neutral-foreground-1-hover: #000000; + --color-neutral-foreground-1-pressed: #000000; + --color-neutral-foreground-1-selected: #000000; + --color-neutral-foreground-2: #ffffff; + --color-neutral-foreground-2-hover: #000000; + --color-neutral-foreground-2-pressed: #000000; + --color-neutral-foreground-2-selected: #000000; + --color-neutral-foreground-2-brand-hover: #000000; + --color-neutral-foreground-2-brand-pressed: #000000; + --color-neutral-foreground-2-brand-selected: #000000; + --color-neutral-foreground-3: #ffffff; + --color-neutral-foreground-3-hover: #000000; + --color-neutral-foreground-3-pressed: #000000; + --color-neutral-foreground-3-selected: #000000; + --color-neutral-foreground-3-brand-hover: #000000; + --color-neutral-foreground-3-brand-pressed: #000000; + --color-neutral-foreground-3-brand-selected: #000000; + --color-neutral-foreground-4: #ffffff; + --color-neutral-foreground-5: #ffffff; + --color-neutral-foreground-5-hover: #000000; + --color-neutral-foreground-5-pressed: #000000; + --color-neutral-foreground-5-selected: #000000; + --color-neutral-foreground-disabled: #3ff23f; + --color-brand-foreground-link: #ffff00; + --color-brand-foreground-link-hover: #ffff00; + --color-brand-foreground-link-pressed: #ffff00; + --color-brand-foreground-link-selected: #ffff00; + --color-neutral-foreground-2-link: #ffff00; + --color-neutral-foreground-2-link-hover: #ffff00; + --color-neutral-foreground-2-link-pressed: #ffff00; + --color-neutral-foreground-2-link-selected: #ffff00; + --color-compound-brand-foreground-1: #1aebff; + --color-compound-brand-foreground-1-hover: #1aebff; + --color-compound-brand-foreground-1-pressed: #1aebff; + --color-neutral-foreground-on-brand: #000000; + --color-neutral-foreground-inverted: #000000; + --color-neutral-foreground-inverted-hover: #000000; + --color-neutral-foreground-inverted-pressed: #000000; + --color-neutral-foreground-inverted-selected: #000000; + --color-neutral-foreground-inverted-2: #ffffff; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffff00; + --color-neutral-foreground-inverted-link-hover: #ffff00; + --color-neutral-foreground-inverted-link-pressed: #ffff00; + --color-neutral-foreground-inverted-link-selected: #ffff00; + --color-neutral-foreground-inverted-disabled: #3ff23f; + --color-brand-foreground-1: #ffffff; + --color-brand-foreground-2: #ffffff; + --color-brand-foreground-2-hover: #ffffff; + --color-brand-foreground-2-pressed: #ffffff; + --color-neutral-foreground-1-static: #000000; + --color-brand-foreground-inverted: #ffffff; + --color-brand-foreground-inverted-hover: #000000; + --color-brand-foreground-inverted-pressed: #000000; + --color-brand-foreground-on-light: #000000; + --color-brand-foreground-on-light-hover: #000000; + --color-brand-foreground-on-light-pressed: #000000; + --color-brand-foreground-on-light-selected: #000000; + --color-neutral-background-1: #000000; + --color-neutral-background-1-hover: #1aebff; + --color-neutral-background-1-pressed: #1aebff; + --color-neutral-background-1-selected: #1aebff; + --color-neutral-background-2: #000000; + --color-neutral-background-2-hover: #1aebff; + --color-neutral-background-2-pressed: #1aebff; + --color-neutral-background-2-selected: #1aebff; + --color-neutral-background-3: #000000; + --color-neutral-background-3-hover: #1aebff; + --color-neutral-background-3-pressed: #1aebff; + --color-neutral-background-3-selected: #1aebff; + --color-neutral-background-4: #000000; + --color-neutral-background-4-hover: #1aebff; + --color-neutral-background-4-pressed: #1aebff; + --color-neutral-background-4-selected: #1aebff; + --color-neutral-background-5: #000000; + --color-neutral-background-5-hover: #1aebff; + --color-neutral-background-5-pressed: #1aebff; + --color-neutral-background-5-selected: #1aebff; + --color-neutral-background-6: #000000; + --color-neutral-background-7: #000000; + --color-neutral-background-7-hover: #1aebff; + --color-neutral-background-7-pressed: #1aebff; + --color-neutral-background-7-selected: #1aebff; + --color-neutral-background-8: #000000; + --color-neutral-background-inverted: #000000; + --color-neutral-background-inverted-hover: #1aebff; + --color-neutral-background-inverted-pressed: #1aebff; + --color-neutral-background-inverted-selected: #1aebff; + --color-neutral-background-static: #000000; + --color-neutral-background-alpha: #000000; + --color-neutral-background-alpha-2: #000000; + --color-subtle-background: transparent; + --color-subtle-background-hover: #1aebff; + --color-subtle-background-pressed: #1aebff; + --color-subtle-background-selected: #1aebff; + --color-subtle-background-light-alpha-hover: #1aebff; + --color-subtle-background-light-alpha-pressed: #1aebff; + --color-subtle-background-light-alpha-selected: #1aebff; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: #1aebff; + --color-subtle-background-inverted-pressed: #1aebff; + --color-subtle-background-inverted-selected: #1aebff; + --color-transparent-background: transparent; + --color-transparent-background-hover: #1aebff; + --color-transparent-background-pressed: #1aebff; + --color-transparent-background-selected: #1aebff; + --color-neutral-background-disabled: #000000; + --color-neutral-background-disabled-2: #3ff23f; + --color-neutral-background-inverted-disabled: #000000; + --color-neutral-stencil-1: #ffffff; + --color-neutral-stencil-2: #ffffff; + --color-neutral-stencil-1-alpha: #ffffff; + --color-neutral-stencil-2-alpha: #ffffff; + --color-background-overlay: rgba(0, 0, 0, 0.5); + --color-scrollbar-overlay: #ffffff; + --color-brand-background: #ffffff; + --color-brand-background-hover: #1aebff; + --color-brand-background-pressed: #1aebff; + --color-brand-background-selected: #1aebff; + --color-compound-brand-background: #1aebff; + --color-compound-brand-background-hover: #1aebff; + --color-compound-brand-background-pressed: #1aebff; + --color-brand-background-static: #000000; + --color-brand-background-2: #000000; + --color-brand-background-2-hover: #000000; + --color-brand-background-2-pressed: #000000; + --color-brand-background-3-static: #000000; + --color-brand-background-4-static: #000000; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #1aebff; + --color-brand-background-inverted-pressed: #1aebff; + --color-brand-background-inverted-selected: #1aebff; + --color-neutral-card-background: #000000; + --color-neutral-card-background-hover: #1aebff; + --color-neutral-card-background-pressed: #1aebff; + --color-neutral-card-background-selected: #1aebff; + --color-neutral-card-background-disabled: #000000; + --color-neutral-stroke-accessible: #ffffff; + --color-neutral-stroke-accessible-hover: #1aebff; + --color-neutral-stroke-accessible-pressed: #1aebff; + --color-neutral-stroke-accessible-selected: #1aebff; + --color-neutral-stroke-1: #ffffff; + --color-neutral-stroke-1-hover: #1aebff; + --color-neutral-stroke-1-pressed: #1aebff; + --color-neutral-stroke-1-selected: #1aebff; + --color-neutral-stroke-2: #ffffff; + --color-neutral-stroke-3: #ffffff; + --color-neutral-stroke-4: #ffffff; + --color-neutral-stroke-4-hover: #1aebff; + --color-neutral-stroke-4-pressed: #1aebff; + --color-neutral-stroke-4-selected: #1aebff; + --color-neutral-stroke-subtle: #ffffff; + --color-neutral-stroke-on-brand: #000000; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #ffffff; + --color-brand-stroke-2: #ffffff; + --color-brand-stroke-2-hover: #1aebff; + --color-brand-stroke-2-pressed: #1aebff; + --color-brand-stroke-2-contrast: #000000; + --color-compound-brand-stroke: #1aebff; + --color-compound-brand-stroke-hover: #1aebff; + --color-compound-brand-stroke-pressed: #1aebff; + --color-neutral-stroke-disabled: #3ff23f; + --color-neutral-stroke-disabled-2: #3ff23f; + --color-neutral-stroke-inverted-disabled: #3ff23f; + --color-transparent-stroke: #ffffff; + --color-transparent-stroke-interactive: #1aebff; + --color-transparent-stroke-disabled: #3ff23f; + --color-neutral-stroke-alpha: #ffffff; + --color-neutral-stroke-alpha-2: #000000; + --color-stroke-focus-1: #000000; + --color-stroke-focus-2: #1aebff; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.24); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.28); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.4); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.48); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #000000; + --color-palette-red-background-2: #000000; + --color-palette-red-background-3: #ffffff; + --color-palette-red-border-active: #1aebff; + --color-palette-red-border-1: #ffffff; + --color-palette-red-border-2: #ffffff; + --color-palette-red-foreground-1: #ffffff; + --color-palette-red-foreground-2: #ffffff; + --color-palette-red-foreground-3: #ffffff; + --color-palette-red-foreground-inverted: #ffffff; + --color-palette-green-background-1: #000000; + --color-palette-green-background-2: #000000; + --color-palette-green-background-3: #ffffff; + --color-palette-green-border-active: #1aebff; + --color-palette-green-border-1: #ffffff; + --color-palette-green-border-2: #ffffff; + --color-palette-green-foreground-1: #ffffff; + --color-palette-green-foreground-2: #ffffff; + --color-palette-green-foreground-3: #ffffff; + --color-palette-green-foreground-inverted: #ffffff; + --color-palette-dark-orange-background-1: #000000; + --color-palette-dark-orange-background-2: #000000; + --color-palette-dark-orange-background-3: #ffffff; + --color-palette-dark-orange-border-active: #1aebff; + --color-palette-dark-orange-border-1: #ffffff; + --color-palette-dark-orange-border-2: #ffffff; + --color-palette-dark-orange-foreground-1: #ffffff; + --color-palette-dark-orange-foreground-2: #ffffff; + --color-palette-dark-orange-foreground-3: #ffffff; + --color-palette-yellow-background-1: #000000; + --color-palette-yellow-background-2: #000000; + --color-palette-yellow-background-3: #ffffff; + --color-palette-yellow-border-active: #1aebff; + --color-palette-yellow-border-1: #ffffff; + --color-palette-yellow-border-2: #ffffff; + --color-palette-yellow-foreground-1: #ffffff; + --color-palette-yellow-foreground-2: #ffffff; + --color-palette-yellow-foreground-3: #ffffff; + --color-palette-yellow-foreground-inverted: #ffffff; + --color-palette-berry-background-1: #000000; + --color-palette-berry-background-2: #000000; + --color-palette-berry-background-3: #ffffff; + --color-palette-berry-border-active: #1aebff; + --color-palette-berry-border-1: #ffffff; + --color-palette-berry-border-2: #ffffff; + --color-palette-berry-foreground-1: #ffffff; + --color-palette-berry-foreground-2: #ffffff; + --color-palette-berry-foreground-3: #ffffff; + --color-palette-marigold-background-1: #000000; + --color-palette-marigold-background-2: #000000; + --color-palette-marigold-background-3: #ffffff; + --color-palette-marigold-border-active: #1aebff; + --color-palette-marigold-border-1: #ffffff; + --color-palette-marigold-border-2: #ffffff; + --color-palette-marigold-foreground-1: #ffffff; + --color-palette-marigold-foreground-2: #ffffff; + --color-palette-marigold-foreground-3: #ffffff; + --color-palette-light-green-background-1: #000000; + --color-palette-light-green-background-2: #000000; + --color-palette-light-green-background-3: #ffffff; + --color-palette-light-green-border-active: #1aebff; + --color-palette-light-green-border-1: #ffffff; + --color-palette-light-green-border-2: #ffffff; + --color-palette-light-green-foreground-1: #ffffff; + --color-palette-light-green-foreground-2: #ffffff; + --color-palette-light-green-foreground-3: #ffffff; + --color-palette-anchor-background-2: #000000; + --color-palette-anchor-border-active: #1aebff; + --color-palette-anchor-foreground-2: #ffffff; + --color-palette-beige-background-2: #000000; + --color-palette-beige-border-active: #1aebff; + --color-palette-beige-foreground-2: #ffffff; + --color-palette-blue-background-2: #000000; + --color-palette-blue-border-active: #1aebff; + --color-palette-blue-foreground-2: #ffffff; + --color-palette-brass-background-2: #000000; + --color-palette-brass-border-active: #1aebff; + --color-palette-brass-foreground-2: #ffffff; + --color-palette-brown-background-2: #000000; + --color-palette-brown-border-active: #1aebff; + --color-palette-brown-foreground-2: #ffffff; + --color-palette-cornflower-background-2: #000000; + --color-palette-cornflower-border-active: #1aebff; + --color-palette-cornflower-foreground-2: #ffffff; + --color-palette-cranberry-background-2: #000000; + --color-palette-cranberry-border-active: #1aebff; + --color-palette-cranberry-foreground-2: #ffffff; + --color-palette-dark-green-background-2: #000000; + --color-palette-dark-green-border-active: #1aebff; + --color-palette-dark-green-foreground-2: #ffffff; + --color-palette-dark-red-background-2: #000000; + --color-palette-dark-red-border-active: #1aebff; + --color-palette-dark-red-foreground-2: #ffffff; + --color-palette-forest-background-2: #000000; + --color-palette-forest-border-active: #1aebff; + --color-palette-forest-foreground-2: #ffffff; + --color-palette-gold-background-2: #000000; + --color-palette-gold-border-active: #1aebff; + --color-palette-gold-foreground-2: #ffffff; + --color-palette-grape-background-2: #000000; + --color-palette-grape-border-active: #1aebff; + --color-palette-grape-foreground-2: #ffffff; + --color-palette-lavender-background-2: #000000; + --color-palette-lavender-border-active: #1aebff; + --color-palette-lavender-foreground-2: #ffffff; + --color-palette-light-teal-background-2: #000000; + --color-palette-light-teal-border-active: #1aebff; + --color-palette-light-teal-foreground-2: #ffffff; + --color-palette-lilac-background-2: #000000; + --color-palette-lilac-border-active: #1aebff; + --color-palette-lilac-foreground-2: #ffffff; + --color-palette-magenta-background-2: #000000; + --color-palette-magenta-border-active: #1aebff; + --color-palette-magenta-foreground-2: #ffffff; + --color-palette-mink-background-2: #000000; + --color-palette-mink-border-active: #1aebff; + --color-palette-mink-foreground-2: #ffffff; + --color-palette-navy-background-2: #000000; + --color-palette-navy-border-active: #1aebff; + --color-palette-navy-foreground-2: #ffffff; + --color-palette-peach-background-2: #000000; + --color-palette-peach-border-active: #1aebff; + --color-palette-peach-foreground-2: #ffffff; + --color-palette-pink-background-2: #000000; + --color-palette-pink-border-active: #1aebff; + --color-palette-pink-foreground-2: #ffffff; + --color-palette-platinum-background-2: #000000; + --color-palette-platinum-border-active: #1aebff; + --color-palette-platinum-foreground-2: #ffffff; + --color-palette-plum-background-2: #000000; + --color-palette-plum-border-active: #1aebff; + --color-palette-plum-foreground-2: #ffffff; + --color-palette-pumpkin-background-2: #000000; + --color-palette-pumpkin-border-active: #1aebff; + --color-palette-pumpkin-foreground-2: #ffffff; + --color-palette-purple-background-2: #000000; + --color-palette-purple-border-active: #1aebff; + --color-palette-purple-foreground-2: #ffffff; + --color-palette-royal-blue-background-2: #000000; + --color-palette-royal-blue-border-active: #1aebff; + --color-palette-royal-blue-foreground-2: #ffffff; + --color-palette-seafoam-background-2: #000000; + --color-palette-seafoam-border-active: #1aebff; + --color-palette-seafoam-foreground-2: #ffffff; + --color-palette-steel-background-2: #000000; + --color-palette-steel-border-active: #1aebff; + --color-palette-steel-foreground-2: #ffffff; + --color-palette-teal-background-2: #000000; + --color-palette-teal-border-active: #1aebff; + --color-palette-teal-foreground-2: #ffffff; + --color-status-success-background-1: #000000; + --color-status-success-background-2: #000000; + --color-status-success-background-3: #ffffff; + --color-status-success-foreground-1: #ffffff; + --color-status-success-foreground-2: #ffffff; + --color-status-success-foreground-3: #ffffff; + --color-status-success-foreground-inverted: #ffffff; + --color-status-success-border-active: #1aebff; + --color-status-success-border-1: #ffffff; + --color-status-success-border-2: #ffffff; + --color-status-warning-background-1: #000000; + --color-status-warning-background-2: #000000; + --color-status-warning-background-3: #ffffff; + --color-status-warning-foreground-1: #ffffff; + --color-status-warning-foreground-2: #ffffff; + --color-status-warning-foreground-3: #ffffff; + --color-status-warning-foreground-inverted: #ffffff; + --color-status-warning-border-active: #1aebff; + --color-status-warning-border-1: #ffffff; + --color-status-warning-border-2: #ffffff; + --color-status-danger-background-1: #000000; + --color-status-danger-background-2: #000000; + --color-status-danger-background-3: #ffffff; + --color-status-danger-background-3-hover: #1aebff; + --color-status-danger-background-3-pressed: #1aebff; + --color-status-danger-foreground-1: #ffffff; + --color-status-danger-foreground-2: #ffffff; + --color-status-danger-foreground-3: #ffffff; + --color-status-danger-foreground-inverted: #ffffff; + --color-status-danger-border-active: #1aebff; + --color-status-danger-border-1: #ffffff; + --color-status-danger-border-2: #ffffff; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light-v21.css b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light-v21.css new file mode 100644 index 0000000000000..06b26a7a8adad --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light-v21.css @@ -0,0 +1,474 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * teamsLightV21Theme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-teams-light-v21` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-teams-light-v21 { + --color-neutral-foreground-1: #242424; + --color-neutral-foreground-1-hover: #242424; + --color-neutral-foreground-1-pressed: #242424; + --color-neutral-foreground-1-selected: #242424; + --color-neutral-foreground-2: #424242; + --color-neutral-foreground-2-hover: #242424; + --color-neutral-foreground-2-pressed: #242424; + --color-neutral-foreground-2-selected: #242424; + --color-neutral-foreground-2-brand-hover: #654cf5; + --color-neutral-foreground-2-brand-pressed: #5a40db; + --color-neutral-foreground-2-brand-selected: #654cf5; + --color-neutral-foreground-3: #616161; + --color-neutral-foreground-3-hover: #424242; + --color-neutral-foreground-3-pressed: #424242; + --color-neutral-foreground-3-selected: #424242; + --color-neutral-foreground-3-brand-hover: #654cf5; + --color-neutral-foreground-3-brand-pressed: #5a40db; + --color-neutral-foreground-3-brand-selected: #654cf5; + --color-neutral-foreground-4: #707070; + --color-neutral-foreground-5: #616161; + --color-neutral-foreground-5-hover: #242424; + --color-neutral-foreground-5-pressed: #242424; + --color-neutral-foreground-5-selected: #242424; + --color-neutral-foreground-disabled: #bdbdbd; + --color-brand-foreground-link: #5a40db; + --color-brand-foreground-link-hover: #4d3aba; + --color-brand-foreground-link-pressed: #3b3185; + --color-brand-foreground-link-selected: #5a40db; + --color-neutral-foreground-2-link: #424242; + --color-neutral-foreground-2-link-hover: #242424; + --color-neutral-foreground-2-link-pressed: #242424; + --color-neutral-foreground-2-link-selected: #242424; + --color-compound-brand-foreground-1: #654cf5; + --color-compound-brand-foreground-1-hover: #5a40db; + --color-compound-brand-foreground-1-pressed: #4d3aba; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #ffffff; + --color-neutral-foreground-inverted-hover: #ffffff; + --color-neutral-foreground-inverted-pressed: #ffffff; + --color-neutral-foreground-inverted-selected: #ffffff; + --color-neutral-foreground-inverted-2: #ffffff; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #654cf5; + --color-brand-foreground-2: #5a40db; + --color-brand-foreground-2-hover: #4d3aba; + --color-brand-foreground-2-pressed: #352e70; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #887dff; + --color-brand-foreground-inverted-hover: #9791ff; + --color-brand-foreground-inverted-pressed: #887dff; + --color-brand-foreground-on-light: #654cf5; + --color-brand-foreground-on-light-hover: #5a40db; + --color-brand-foreground-on-light-pressed: #44359e; + --color-brand-foreground-on-light-selected: #4d3aba; + --color-neutral-background-1: #ffffff; + --color-neutral-background-1-hover: #f5f5f5; + --color-neutral-background-1-pressed: #e0e0e0; + --color-neutral-background-1-selected: #ebebeb; + --color-neutral-background-2: #fafafa; + --color-neutral-background-2-hover: #f0f0f0; + --color-neutral-background-2-pressed: #dbdbdb; + --color-neutral-background-2-selected: #e6e6e6; + --color-neutral-background-3: #f5f5f5; + --color-neutral-background-3-hover: #ebebeb; + --color-neutral-background-3-pressed: #d6d6d6; + --color-neutral-background-3-selected: #e0e0e0; + --color-neutral-background-4: #f0f0f0; + --color-neutral-background-4-hover: #fafafa; + --color-neutral-background-4-pressed: #f5f5f5; + --color-neutral-background-4-selected: #ffffff; + --color-neutral-background-5: #ebebeb; + --color-neutral-background-5-hover: #f5f5f5; + --color-neutral-background-5-pressed: #f0f0f0; + --color-neutral-background-5-selected: #fafafa; + --color-neutral-background-6: #e6e6e6; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #ebebeb; + --color-neutral-background-7-pressed: #d6d6d6; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #fcfcfc; + --color-neutral-background-inverted: #292929; + --color-neutral-background-inverted-hover: #3d3d3d; + --color-neutral-background-inverted-pressed: #1f1f1f; + --color-neutral-background-inverted-selected: #383838; + --color-neutral-background-static: #333333; + --color-neutral-background-alpha: rgba(255, 255, 255, 0.5); + --color-neutral-background-alpha-2: rgba(255, 255, 255, 0.8); + --color-subtle-background: transparent; + --color-subtle-background-hover: #f5f5f5; + --color-subtle-background-pressed: #e0e0e0; + --color-subtle-background-selected: #ebebeb; + --color-subtle-background-light-alpha-hover: rgba(255, 255, 255, 0.7); + --color-subtle-background-light-alpha-pressed: rgba(255, 255, 255, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #f0f0f0; + --color-neutral-background-disabled-2: #ffffff; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #e6e6e6; + --color-neutral-stencil-2: #fafafa; + --color-neutral-stencil-1-alpha: rgba(0, 0, 0, 0.1); + --color-neutral-stencil-2-alpha: rgba(0, 0, 0, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.4); + --color-scrollbar-overlay: rgba(0, 0, 0, 0.5); + --color-brand-background: #654cf5; + --color-brand-background-hover: #5a40db; + --color-brand-background-pressed: #3b3185; + --color-brand-background-selected: #4d3aba; + --color-compound-brand-background: #654cf5; + --color-compound-brand-background-hover: #5a40db; + --color-compound-brand-background-pressed: #4d3aba; + --color-brand-background-static: #654cf5; + --color-brand-background-2: #e8e8ff; + --color-brand-background-2-hover: #dcdbff; + --color-brand-background-2-pressed: #bab8ff; + --color-brand-background-3-static: #4d3aba; + --color-brand-background-4-static: #3b3185; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #e8e8ff; + --color-brand-background-inverted-pressed: #c8c7ff; + --color-brand-background-inverted-selected: #dcdbff; + --color-neutral-card-background: #fafafa; + --color-neutral-card-background-hover: #ffffff; + --color-neutral-card-background-pressed: #f5f5f5; + --color-neutral-card-background-selected: #ebebeb; + --color-neutral-card-background-disabled: #f0f0f0; + --color-neutral-stroke-accessible: #616161; + --color-neutral-stroke-accessible-hover: #575757; + --color-neutral-stroke-accessible-pressed: #4d4d4d; + --color-neutral-stroke-accessible-selected: #654cf5; + --color-neutral-stroke-1: #d1d1d1; + --color-neutral-stroke-1-hover: #c7c7c7; + --color-neutral-stroke-1-pressed: #b3b3b3; + --color-neutral-stroke-1-selected: #bdbdbd; + --color-neutral-stroke-2: #e0e0e0; + --color-neutral-stroke-3: #f0f0f0; + --color-neutral-stroke-4: #ebebeb; + --color-neutral-stroke-4-hover: #e0e0e0; + --color-neutral-stroke-4-pressed: #d6d6d6; + --color-neutral-stroke-4-selected: #ebebeb; + --color-neutral-stroke-subtle: #e0e0e0; + --color-neutral-stroke-on-brand: #ffffff; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #654cf5; + --color-brand-stroke-2: #c8c7ff; + --color-brand-stroke-2-hover: #aba8ff; + --color-brand-stroke-2-pressed: #654cf5; + --color-brand-stroke-2-contrast: #c8c7ff; + --color-compound-brand-stroke: #654cf5; + --color-compound-brand-stroke-hover: #5a40db; + --color-compound-brand-stroke-pressed: #4d3aba; + --color-neutral-stroke-disabled: #e0e0e0; + --color-neutral-stroke-disabled-2: #ebebeb; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(0, 0, 0, 0.05); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #ffffff; + --color-stroke-focus-2: #000000; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.06); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.07); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.2); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.24); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #fdf6f6; + --color-palette-red-background-2: #f1bbbc; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #d13438; + --color-palette-red-border-1: #f1bbbc; + --color-palette-red-border-2: #d13438; + --color-palette-red-foreground-1: #bc2f32; + --color-palette-red-foreground-2: #751d1f; + --color-palette-red-foreground-3: #d13438; + --color-palette-red-foreground-inverted: #dc5e62; + --color-palette-green-background-1: #f1faf1; + --color-palette-green-background-2: #9fd89f; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #107c10; + --color-palette-green-border-1: #9fd89f; + --color-palette-green-border-2: #107c10; + --color-palette-green-foreground-1: #0e700e; + --color-palette-green-foreground-2: #094509; + --color-palette-green-foreground-3: #107c10; + --color-palette-green-foreground-inverted: #359b35; + --color-palette-dark-orange-background-1: #fdf6f3; + --color-palette-dark-orange-background-2: #f4bfab; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #da3b01; + --color-palette-dark-orange-border-1: #f4bfab; + --color-palette-dark-orange-border-2: #da3b01; + --color-palette-dark-orange-foreground-1: #c43501; + --color-palette-dark-orange-foreground-2: #7a2101; + --color-palette-dark-orange-foreground-3: #da3b01; + --color-palette-yellow-background-1: #fffef5; + --color-palette-yellow-background-2: #fef7b2; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #fde300; + --color-palette-yellow-border-1: #fef7b2; + --color-palette-yellow-border-2: #fde300; + --color-palette-yellow-foreground-1: #817400; + --color-palette-yellow-foreground-2: #817400; + --color-palette-yellow-foreground-3: #fde300; + --color-palette-yellow-foreground-inverted: #fef7b2; + --color-palette-berry-background-1: #fdf5fc; + --color-palette-berry-background-2: #edbbe7; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #c239b3; + --color-palette-berry-border-1: #edbbe7; + --color-palette-berry-border-2: #c239b3; + --color-palette-berry-foreground-1: #af33a1; + --color-palette-berry-foreground-2: #6d2064; + --color-palette-berry-foreground-3: #c239b3; + --color-palette-marigold-background-1: #fefbf4; + --color-palette-marigold-background-2: #f9e2ae; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #eaa300; + --color-palette-marigold-border-1: #f9e2ae; + --color-palette-marigold-border-2: #eaa300; + --color-palette-marigold-foreground-1: #d39300; + --color-palette-marigold-foreground-2: #835b00; + --color-palette-marigold-foreground-3: #eaa300; + --color-palette-light-green-background-1: #f2fbf2; + --color-palette-light-green-background-2: #a7e3a5; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #13a10e; + --color-palette-light-green-border-1: #a7e3a5; + --color-palette-light-green-border-2: #13a10e; + --color-palette-light-green-foreground-1: #11910d; + --color-palette-light-green-foreground-2: #0b5a08; + --color-palette-light-green-foreground-3: #13a10e; + --color-palette-anchor-background-2: #bcc3c7; + --color-palette-anchor-border-active: #394146; + --color-palette-anchor-foreground-2: #202427; + --color-palette-beige-background-2: #d7d4d4; + --color-palette-beige-border-active: #7a7574; + --color-palette-beige-foreground-2: #444241; + --color-palette-blue-background-2: #a9d3f2; + --color-palette-blue-border-active: #0078d4; + --color-palette-blue-foreground-2: #004377; + --color-palette-brass-background-2: #e0cea2; + --color-palette-brass-border-active: #986f0b; + --color-palette-brass-foreground-2: #553e06; + --color-palette-brown-background-2: #ddc3b0; + --color-palette-brown-border-active: #8e562e; + --color-palette-brown-foreground-2: #50301a; + --color-palette-cornflower-background-2: #c8d1fa; + --color-palette-cornflower-border-active: #4f6bed; + --color-palette-cornflower-foreground-2: #2c3c85; + --color-palette-cranberry-background-2: #eeacb2; + --color-palette-cranberry-border-active: #c50f1f; + --color-palette-cranberry-foreground-2: #6e0811; + --color-palette-dark-green-background-2: #9ad29a; + --color-palette-dark-green-border-active: #0b6a0b; + --color-palette-dark-green-foreground-2: #063b06; + --color-palette-dark-red-background-2: #d69ca5; + --color-palette-dark-red-border-active: #750b1c; + --color-palette-dark-red-foreground-2: #420610; + --color-palette-forest-background-2: #bdd99b; + --color-palette-forest-border-active: #498205; + --color-palette-forest-foreground-2: #294903; + --color-palette-gold-background-2: #ecdfa5; + --color-palette-gold-border-active: #c19c00; + --color-palette-gold-foreground-2: #6c5700; + --color-palette-grape-background-2: #d9a7e0; + --color-palette-grape-border-active: #881798; + --color-palette-grape-foreground-2: #4c0d55; + --color-palette-lavender-background-2: #d2ccf8; + --color-palette-lavender-border-active: #7160e8; + --color-palette-lavender-foreground-2: #3f3682; + --color-palette-light-teal-background-2: #a6e9ed; + --color-palette-light-teal-border-active: #00b7c3; + --color-palette-light-teal-foreground-2: #00666d; + --color-palette-lilac-background-2: #e6bfed; + --color-palette-lilac-border-active: #b146c2; + --color-palette-lilac-foreground-2: #63276d; + --color-palette-magenta-background-2: #eca5d1; + --color-palette-magenta-border-active: #bf0077; + --color-palette-magenta-foreground-2: #6b0043; + --color-palette-mink-background-2: #cecccb; + --color-palette-mink-border-active: #5d5a58; + --color-palette-mink-foreground-2: #343231; + --color-palette-navy-background-2: #a3b2e8; + --color-palette-navy-border-active: #0027b4; + --color-palette-navy-foreground-2: #001665; + --color-palette-peach-background-2: #ffddb3; + --color-palette-peach-border-active: #ff8c00; + --color-palette-peach-foreground-2: #8f4e00; + --color-palette-pink-background-2: #f7c0e3; + --color-palette-pink-border-active: #e43ba6; + --color-palette-pink-foreground-2: #80215d; + --color-palette-platinum-background-2: #cdd6d8; + --color-palette-platinum-border-active: #69797e; + --color-palette-platinum-foreground-2: #3b4447; + --color-palette-plum-background-2: #d696c0; + --color-palette-plum-border-active: #77004d; + --color-palette-plum-foreground-2: #43002b; + --color-palette-pumpkin-background-2: #efc4ad; + --color-palette-pumpkin-border-active: #ca5010; + --color-palette-pumpkin-foreground-2: #712d09; + --color-palette-purple-background-2: #c6b1de; + --color-palette-purple-border-active: #5c2e91; + --color-palette-purple-foreground-2: #341a51; + --color-palette-royal-blue-background-2: #9abfdc; + --color-palette-royal-blue-border-active: #004e8c; + --color-palette-royal-blue-foreground-2: #002c4e; + --color-palette-seafoam-background-2: #a8f0cd; + --color-palette-seafoam-border-active: #00cc6a; + --color-palette-seafoam-foreground-2: #00723b; + --color-palette-steel-background-2: #94c8d4; + --color-palette-steel-border-active: #005b70; + --color-palette-steel-foreground-2: #00333f; + --color-palette-teal-background-2: #9bd9db; + --color-palette-teal-border-active: #038387; + --color-palette-teal-foreground-2: #02494c; + --color-status-success-background-1: #f1faf1; + --color-status-success-background-2: #9fd89f; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #0e700e; + --color-status-success-foreground-2: #094509; + --color-status-success-foreground-3: #107c10; + --color-status-success-foreground-inverted: #54b054; + --color-status-success-border-active: #107c10; + --color-status-success-border-1: #9fd89f; + --color-status-success-border-2: #107c10; + --color-status-warning-background-1: #fff9f5; + --color-status-warning-background-2: #fdcfb4; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #bc4b09; + --color-status-warning-foreground-2: #8a3707; + --color-status-warning-foreground-3: #bc4b09; + --color-status-warning-foreground-inverted: #faa06b; + --color-status-warning-border-active: #f7630c; + --color-status-warning-border-1: #fdcfb4; + --color-status-warning-border-2: #bc4b09; + --color-status-danger-background-1: #fdf3f4; + --color-status-danger-background-2: #eeacb2; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #b10e1c; + --color-status-danger-foreground-2: #6e0811; + --color-status-danger-foreground-3: #c50f1f; + --color-status-danger-foreground-inverted: #dc626d; + --color-status-danger-border-active: #c50f1f; + --color-status-danger-border-1: #eeacb2; + --color-status-danger-border-2: #c50f1f; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light.css b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light.css new file mode 100644 index 0000000000000..c24e23cc610dc --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/teams-light.css @@ -0,0 +1,474 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * teamsLightTheme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-teams-light` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-teams-light { + --color-neutral-foreground-1: #242424; + --color-neutral-foreground-1-hover: #242424; + --color-neutral-foreground-1-pressed: #242424; + --color-neutral-foreground-1-selected: #242424; + --color-neutral-foreground-2: #424242; + --color-neutral-foreground-2-hover: #242424; + --color-neutral-foreground-2-pressed: #242424; + --color-neutral-foreground-2-selected: #242424; + --color-neutral-foreground-2-brand-hover: #5b5fc7; + --color-neutral-foreground-2-brand-pressed: #4f52b2; + --color-neutral-foreground-2-brand-selected: #5b5fc7; + --color-neutral-foreground-3: #616161; + --color-neutral-foreground-3-hover: #424242; + --color-neutral-foreground-3-pressed: #424242; + --color-neutral-foreground-3-selected: #424242; + --color-neutral-foreground-3-brand-hover: #5b5fc7; + --color-neutral-foreground-3-brand-pressed: #4f52b2; + --color-neutral-foreground-3-brand-selected: #5b5fc7; + --color-neutral-foreground-4: #707070; + --color-neutral-foreground-5: #616161; + --color-neutral-foreground-5-hover: #242424; + --color-neutral-foreground-5-pressed: #242424; + --color-neutral-foreground-5-selected: #242424; + --color-neutral-foreground-disabled: #bdbdbd; + --color-brand-foreground-link: #4f52b2; + --color-brand-foreground-link-hover: #444791; + --color-brand-foreground-link-pressed: #383966; + --color-brand-foreground-link-selected: #4f52b2; + --color-neutral-foreground-2-link: #424242; + --color-neutral-foreground-2-link-hover: #242424; + --color-neutral-foreground-2-link-pressed: #242424; + --color-neutral-foreground-2-link-selected: #242424; + --color-compound-brand-foreground-1: #5b5fc7; + --color-compound-brand-foreground-1-hover: #4f52b2; + --color-compound-brand-foreground-1-pressed: #444791; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #ffffff; + --color-neutral-foreground-inverted-hover: #ffffff; + --color-neutral-foreground-inverted-pressed: #ffffff; + --color-neutral-foreground-inverted-selected: #ffffff; + --color-neutral-foreground-inverted-2: #ffffff; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #5b5fc7; + --color-brand-foreground-2: #4f52b2; + --color-brand-foreground-2-hover: #444791; + --color-brand-foreground-2-pressed: #333357; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #7f85f5; + --color-brand-foreground-inverted-hover: #9299f7; + --color-brand-foreground-inverted-pressed: #7f85f5; + --color-brand-foreground-on-light: #5b5fc7; + --color-brand-foreground-on-light-hover: #4f52b2; + --color-brand-foreground-on-light-pressed: #3d3e78; + --color-brand-foreground-on-light-selected: #444791; + --color-neutral-background-1: #ffffff; + --color-neutral-background-1-hover: #f5f5f5; + --color-neutral-background-1-pressed: #e0e0e0; + --color-neutral-background-1-selected: #ebebeb; + --color-neutral-background-2: #fafafa; + --color-neutral-background-2-hover: #f0f0f0; + --color-neutral-background-2-pressed: #dbdbdb; + --color-neutral-background-2-selected: #e6e6e6; + --color-neutral-background-3: #f5f5f5; + --color-neutral-background-3-hover: #ebebeb; + --color-neutral-background-3-pressed: #d6d6d6; + --color-neutral-background-3-selected: #e0e0e0; + --color-neutral-background-4: #f0f0f0; + --color-neutral-background-4-hover: #fafafa; + --color-neutral-background-4-pressed: #f5f5f5; + --color-neutral-background-4-selected: #ffffff; + --color-neutral-background-5: #ebebeb; + --color-neutral-background-5-hover: #f5f5f5; + --color-neutral-background-5-pressed: #f0f0f0; + --color-neutral-background-5-selected: #fafafa; + --color-neutral-background-6: #e6e6e6; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #ebebeb; + --color-neutral-background-7-pressed: #d6d6d6; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #fcfcfc; + --color-neutral-background-inverted: #292929; + --color-neutral-background-inverted-hover: #3d3d3d; + --color-neutral-background-inverted-pressed: #1f1f1f; + --color-neutral-background-inverted-selected: #383838; + --color-neutral-background-static: #333333; + --color-neutral-background-alpha: rgba(255, 255, 255, 0.5); + --color-neutral-background-alpha-2: rgba(255, 255, 255, 0.8); + --color-subtle-background: transparent; + --color-subtle-background-hover: #f5f5f5; + --color-subtle-background-pressed: #e0e0e0; + --color-subtle-background-selected: #ebebeb; + --color-subtle-background-light-alpha-hover: rgba(255, 255, 255, 0.7); + --color-subtle-background-light-alpha-pressed: rgba(255, 255, 255, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #f0f0f0; + --color-neutral-background-disabled-2: #ffffff; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #e6e6e6; + --color-neutral-stencil-2: #fafafa; + --color-neutral-stencil-1-alpha: rgba(0, 0, 0, 0.1); + --color-neutral-stencil-2-alpha: rgba(0, 0, 0, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.4); + --color-scrollbar-overlay: rgba(0, 0, 0, 0.5); + --color-brand-background: #5b5fc7; + --color-brand-background-hover: #4f52b2; + --color-brand-background-pressed: #383966; + --color-brand-background-selected: #444791; + --color-compound-brand-background: #5b5fc7; + --color-compound-brand-background-hover: #4f52b2; + --color-compound-brand-background-pressed: #444791; + --color-brand-background-static: #5b5fc7; + --color-brand-background-2: #e8ebfa; + --color-brand-background-2-hover: #dce0fa; + --color-brand-background-2-pressed: #b6bcfa; + --color-brand-background-3-static: #444791; + --color-brand-background-4-static: #383966; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #e8ebfa; + --color-brand-background-inverted-pressed: #c5cbfa; + --color-brand-background-inverted-selected: #dce0fa; + --color-neutral-card-background: #fafafa; + --color-neutral-card-background-hover: #ffffff; + --color-neutral-card-background-pressed: #f5f5f5; + --color-neutral-card-background-selected: #ebebeb; + --color-neutral-card-background-disabled: #f0f0f0; + --color-neutral-stroke-accessible: #616161; + --color-neutral-stroke-accessible-hover: #575757; + --color-neutral-stroke-accessible-pressed: #4d4d4d; + --color-neutral-stroke-accessible-selected: #5b5fc7; + --color-neutral-stroke-1: #d1d1d1; + --color-neutral-stroke-1-hover: #c7c7c7; + --color-neutral-stroke-1-pressed: #b3b3b3; + --color-neutral-stroke-1-selected: #bdbdbd; + --color-neutral-stroke-2: #e0e0e0; + --color-neutral-stroke-3: #f0f0f0; + --color-neutral-stroke-4: #ebebeb; + --color-neutral-stroke-4-hover: #e0e0e0; + --color-neutral-stroke-4-pressed: #d6d6d6; + --color-neutral-stroke-4-selected: #ebebeb; + --color-neutral-stroke-subtle: #e0e0e0; + --color-neutral-stroke-on-brand: #ffffff; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #5b5fc7; + --color-brand-stroke-2: #c5cbfa; + --color-brand-stroke-2-hover: #aab1fa; + --color-brand-stroke-2-pressed: #5b5fc7; + --color-brand-stroke-2-contrast: #c5cbfa; + --color-compound-brand-stroke: #5b5fc7; + --color-compound-brand-stroke-hover: #4f52b2; + --color-compound-brand-stroke-pressed: #444791; + --color-neutral-stroke-disabled: #e0e0e0; + --color-neutral-stroke-disabled-2: #ebebeb; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(0, 0, 0, 0.05); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #ffffff; + --color-stroke-focus-2: #000000; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.06); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.07); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.2); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.24); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #fdf6f6; + --color-palette-red-background-2: #f1bbbc; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #d13438; + --color-palette-red-border-1: #f1bbbc; + --color-palette-red-border-2: #d13438; + --color-palette-red-foreground-1: #bc2f32; + --color-palette-red-foreground-2: #751d1f; + --color-palette-red-foreground-3: #d13438; + --color-palette-red-foreground-inverted: #dc5e62; + --color-palette-green-background-1: #f1faf1; + --color-palette-green-background-2: #9fd89f; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #107c10; + --color-palette-green-border-1: #9fd89f; + --color-palette-green-border-2: #107c10; + --color-palette-green-foreground-1: #0e700e; + --color-palette-green-foreground-2: #094509; + --color-palette-green-foreground-3: #107c10; + --color-palette-green-foreground-inverted: #359b35; + --color-palette-dark-orange-background-1: #fdf6f3; + --color-palette-dark-orange-background-2: #f4bfab; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #da3b01; + --color-palette-dark-orange-border-1: #f4bfab; + --color-palette-dark-orange-border-2: #da3b01; + --color-palette-dark-orange-foreground-1: #c43501; + --color-palette-dark-orange-foreground-2: #7a2101; + --color-palette-dark-orange-foreground-3: #da3b01; + --color-palette-yellow-background-1: #fffef5; + --color-palette-yellow-background-2: #fef7b2; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #fde300; + --color-palette-yellow-border-1: #fef7b2; + --color-palette-yellow-border-2: #fde300; + --color-palette-yellow-foreground-1: #817400; + --color-palette-yellow-foreground-2: #817400; + --color-palette-yellow-foreground-3: #fde300; + --color-palette-yellow-foreground-inverted: #fef7b2; + --color-palette-berry-background-1: #fdf5fc; + --color-palette-berry-background-2: #edbbe7; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #c239b3; + --color-palette-berry-border-1: #edbbe7; + --color-palette-berry-border-2: #c239b3; + --color-palette-berry-foreground-1: #af33a1; + --color-palette-berry-foreground-2: #6d2064; + --color-palette-berry-foreground-3: #c239b3; + --color-palette-marigold-background-1: #fefbf4; + --color-palette-marigold-background-2: #f9e2ae; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #eaa300; + --color-palette-marigold-border-1: #f9e2ae; + --color-palette-marigold-border-2: #eaa300; + --color-palette-marigold-foreground-1: #d39300; + --color-palette-marigold-foreground-2: #835b00; + --color-palette-marigold-foreground-3: #eaa300; + --color-palette-light-green-background-1: #f2fbf2; + --color-palette-light-green-background-2: #a7e3a5; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #13a10e; + --color-palette-light-green-border-1: #a7e3a5; + --color-palette-light-green-border-2: #13a10e; + --color-palette-light-green-foreground-1: #11910d; + --color-palette-light-green-foreground-2: #0b5a08; + --color-palette-light-green-foreground-3: #13a10e; + --color-palette-anchor-background-2: #bcc3c7; + --color-palette-anchor-border-active: #394146; + --color-palette-anchor-foreground-2: #202427; + --color-palette-beige-background-2: #d7d4d4; + --color-palette-beige-border-active: #7a7574; + --color-palette-beige-foreground-2: #444241; + --color-palette-blue-background-2: #a9d3f2; + --color-palette-blue-border-active: #0078d4; + --color-palette-blue-foreground-2: #004377; + --color-palette-brass-background-2: #e0cea2; + --color-palette-brass-border-active: #986f0b; + --color-palette-brass-foreground-2: #553e06; + --color-palette-brown-background-2: #ddc3b0; + --color-palette-brown-border-active: #8e562e; + --color-palette-brown-foreground-2: #50301a; + --color-palette-cornflower-background-2: #c8d1fa; + --color-palette-cornflower-border-active: #4f6bed; + --color-palette-cornflower-foreground-2: #2c3c85; + --color-palette-cranberry-background-2: #eeacb2; + --color-palette-cranberry-border-active: #c50f1f; + --color-palette-cranberry-foreground-2: #6e0811; + --color-palette-dark-green-background-2: #9ad29a; + --color-palette-dark-green-border-active: #0b6a0b; + --color-palette-dark-green-foreground-2: #063b06; + --color-palette-dark-red-background-2: #d69ca5; + --color-palette-dark-red-border-active: #750b1c; + --color-palette-dark-red-foreground-2: #420610; + --color-palette-forest-background-2: #bdd99b; + --color-palette-forest-border-active: #498205; + --color-palette-forest-foreground-2: #294903; + --color-palette-gold-background-2: #ecdfa5; + --color-palette-gold-border-active: #c19c00; + --color-palette-gold-foreground-2: #6c5700; + --color-palette-grape-background-2: #d9a7e0; + --color-palette-grape-border-active: #881798; + --color-palette-grape-foreground-2: #4c0d55; + --color-palette-lavender-background-2: #d2ccf8; + --color-palette-lavender-border-active: #7160e8; + --color-palette-lavender-foreground-2: #3f3682; + --color-palette-light-teal-background-2: #a6e9ed; + --color-palette-light-teal-border-active: #00b7c3; + --color-palette-light-teal-foreground-2: #00666d; + --color-palette-lilac-background-2: #e6bfed; + --color-palette-lilac-border-active: #b146c2; + --color-palette-lilac-foreground-2: #63276d; + --color-palette-magenta-background-2: #eca5d1; + --color-palette-magenta-border-active: #bf0077; + --color-palette-magenta-foreground-2: #6b0043; + --color-palette-mink-background-2: #cecccb; + --color-palette-mink-border-active: #5d5a58; + --color-palette-mink-foreground-2: #343231; + --color-palette-navy-background-2: #a3b2e8; + --color-palette-navy-border-active: #0027b4; + --color-palette-navy-foreground-2: #001665; + --color-palette-peach-background-2: #ffddb3; + --color-palette-peach-border-active: #ff8c00; + --color-palette-peach-foreground-2: #8f4e00; + --color-palette-pink-background-2: #f7c0e3; + --color-palette-pink-border-active: #e43ba6; + --color-palette-pink-foreground-2: #80215d; + --color-palette-platinum-background-2: #cdd6d8; + --color-palette-platinum-border-active: #69797e; + --color-palette-platinum-foreground-2: #3b4447; + --color-palette-plum-background-2: #d696c0; + --color-palette-plum-border-active: #77004d; + --color-palette-plum-foreground-2: #43002b; + --color-palette-pumpkin-background-2: #efc4ad; + --color-palette-pumpkin-border-active: #ca5010; + --color-palette-pumpkin-foreground-2: #712d09; + --color-palette-purple-background-2: #c6b1de; + --color-palette-purple-border-active: #5c2e91; + --color-palette-purple-foreground-2: #341a51; + --color-palette-royal-blue-background-2: #9abfdc; + --color-palette-royal-blue-border-active: #004e8c; + --color-palette-royal-blue-foreground-2: #002c4e; + --color-palette-seafoam-background-2: #a8f0cd; + --color-palette-seafoam-border-active: #00cc6a; + --color-palette-seafoam-foreground-2: #00723b; + --color-palette-steel-background-2: #94c8d4; + --color-palette-steel-border-active: #005b70; + --color-palette-steel-foreground-2: #00333f; + --color-palette-teal-background-2: #9bd9db; + --color-palette-teal-border-active: #038387; + --color-palette-teal-foreground-2: #02494c; + --color-status-success-background-1: #f1faf1; + --color-status-success-background-2: #9fd89f; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #0e700e; + --color-status-success-foreground-2: #094509; + --color-status-success-foreground-3: #107c10; + --color-status-success-foreground-inverted: #54b054; + --color-status-success-border-active: #107c10; + --color-status-success-border-1: #9fd89f; + --color-status-success-border-2: #107c10; + --color-status-warning-background-1: #fff9f5; + --color-status-warning-background-2: #fdcfb4; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #bc4b09; + --color-status-warning-foreground-2: #8a3707; + --color-status-warning-foreground-3: #bc4b09; + --color-status-warning-foreground-inverted: #faa06b; + --color-status-warning-border-active: #f7630c; + --color-status-warning-border-1: #fdcfb4; + --color-status-warning-border-2: #bc4b09; + --color-status-danger-background-1: #fdf3f4; + --color-status-danger-background-2: #eeacb2; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #b10e1c; + --color-status-danger-foreground-2: #6e0811; + --color-status-danger-foreground-3: #c50f1f; + --color-status-danger-foreground-inverted: #dc626d; + --color-status-danger-border-active: #c50f1f; + --color-status-danger-border-1: #eeacb2; + --color-status-danger-border-2: #c50f1f; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/web-dark.css b/packages/react-components/react-tailwind-theme-preview/css/themes/web-dark.css new file mode 100644 index 0000000000000..0a0b1d752cead --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/web-dark.css @@ -0,0 +1,475 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * webDarkTheme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-web-dark` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-web-dark { + --color-neutral-foreground-1: #ffffff; + --color-neutral-foreground-1-hover: #ffffff; + --color-neutral-foreground-1-pressed: #ffffff; + --color-neutral-foreground-1-selected: #ffffff; + --color-neutral-foreground-2: #d6d6d6; + --color-neutral-foreground-2-hover: #ffffff; + --color-neutral-foreground-2-pressed: #ffffff; + --color-neutral-foreground-2-selected: #ffffff; + --color-neutral-foreground-2-brand-hover: #479ef5; + --color-neutral-foreground-2-brand-pressed: #2886de; + --color-neutral-foreground-2-brand-selected: #479ef5; + --color-neutral-foreground-3: #adadad; + --color-neutral-foreground-3-hover: #d6d6d6; + --color-neutral-foreground-3-pressed: #d6d6d6; + --color-neutral-foreground-3-selected: #d6d6d6; + --color-neutral-foreground-3-brand-hover: #479ef5; + --color-neutral-foreground-3-brand-pressed: #2886de; + --color-neutral-foreground-3-brand-selected: #479ef5; + --color-neutral-foreground-4: #999999; + --color-neutral-foreground-5: #adadad; + --color-neutral-foreground-5-hover: #ffffff; + --color-neutral-foreground-5-pressed: #ffffff; + --color-neutral-foreground-5-selected: #ffffff; + --color-neutral-foreground-disabled: #5c5c5c; + --color-brand-foreground-link: #479ef5; + --color-brand-foreground-link-hover: #62abf5; + --color-brand-foreground-link-pressed: #2886de; + --color-brand-foreground-link-selected: #479ef5; + --color-neutral-foreground-2-link: #d6d6d6; + --color-neutral-foreground-2-link-hover: #ffffff; + --color-neutral-foreground-2-link-pressed: #ffffff; + --color-neutral-foreground-2-link-selected: #ffffff; + --color-compound-brand-foreground-1: #479ef5; + --color-compound-brand-foreground-1-hover: #62abf5; + --color-compound-brand-foreground-1-pressed: #2886de; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #242424; + --color-neutral-foreground-inverted-hover: #242424; + --color-neutral-foreground-inverted-pressed: #242424; + --color-neutral-foreground-inverted-selected: #242424; + --color-neutral-foreground-inverted-2: #242424; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #479ef5; + --color-brand-foreground-2: #62abf5; + --color-brand-foreground-2-hover: #96c6fa; + --color-brand-foreground-2-pressed: #ebf3fc; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #0f6cbd; + --color-brand-foreground-inverted-hover: #115ea3; + --color-brand-foreground-inverted-pressed: #0f548c; + --color-brand-foreground-on-light: #0f6cbd; + --color-brand-foreground-on-light-hover: #115ea3; + --color-brand-foreground-on-light-pressed: #0e4775; + --color-brand-foreground-on-light-selected: #0f548c; + --color-neutral-background-1: #292929; + --color-neutral-background-1-hover: #3d3d3d; + --color-neutral-background-1-pressed: #1f1f1f; + --color-neutral-background-1-selected: #383838; + --color-neutral-background-2: #1f1f1f; + --color-neutral-background-2-hover: #333333; + --color-neutral-background-2-pressed: #141414; + --color-neutral-background-2-selected: #2e2e2e; + --color-neutral-background-3: #141414; + --color-neutral-background-3-hover: #292929; + --color-neutral-background-3-pressed: #0a0a0a; + --color-neutral-background-3-selected: #242424; + --color-neutral-background-4: #0a0a0a; + --color-neutral-background-4-hover: #1f1f1f; + --color-neutral-background-4-pressed: #000000; + --color-neutral-background-4-selected: #1a1a1a; + --color-neutral-background-5: #000000; + --color-neutral-background-5-hover: #141414; + --color-neutral-background-5-pressed: #050505; + --color-neutral-background-5-selected: #0f0f0f; + --color-neutral-background-6: #333333; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #1a1a1a; + --color-neutral-background-7-pressed: #0a0a0a; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #292929; + --color-neutral-background-inverted: #ffffff; + --color-neutral-background-inverted-hover: #f5f5f5; + --color-neutral-background-inverted-pressed: #e0e0e0; + --color-neutral-background-inverted-selected: #ebebeb; + --color-neutral-background-static: #3d3d3d; + --color-neutral-background-alpha: rgba(26, 26, 26, 0.5); + --color-neutral-background-alpha-2: rgba(31, 31, 31, 0.7); + --color-subtle-background: transparent; + --color-subtle-background-hover: #383838; + --color-subtle-background-pressed: #2e2e2e; + --color-subtle-background-selected: #333333; + --color-subtle-background-light-alpha-hover: rgba(36, 36, 36, 0.8); + --color-subtle-background-light-alpha-pressed: rgba(36, 36, 36, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #141414; + --color-neutral-background-disabled-2: #292929; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #575757; + --color-neutral-stencil-2: #333333; + --color-neutral-stencil-1-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-2-alpha: rgba(255, 255, 255, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.5); + --color-scrollbar-overlay: rgba(255, 255, 255, 0.6); + --color-brand-background: #115ea3; + --color-brand-background-hover: #0f6cbd; + --color-brand-background-pressed: #0c3b5e; + --color-brand-background-selected: #0f548c; + --color-compound-brand-background: #479ef5; + --color-compound-brand-background-hover: #62abf5; + --color-compound-brand-background-pressed: #2886de; + --color-brand-background-static: #0f6cbd; + --color-brand-background-2: #082338; + --color-brand-background-2-hover: #0c3b5e; + --color-brand-background-2-pressed: #061724; + --color-brand-background-3-static: #0f548c; + --color-brand-background-4-static: #0c3b5e; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #ebf3fc; + --color-brand-background-inverted-pressed: #b4d6fa; + --color-brand-background-inverted-selected: #cfe4fa; + --color-neutral-card-background: #333333; + --color-neutral-card-background-hover: #3d3d3d; + --color-neutral-card-background-pressed: #2e2e2e; + --color-neutral-card-background-selected: #383838; + --color-neutral-card-background-disabled: #141414; + --color-neutral-stroke-accessible: #adadad; + --color-neutral-stroke-accessible-hover: #bdbdbd; + --color-neutral-stroke-accessible-pressed: #b3b3b3; + --color-neutral-stroke-accessible-selected: #479ef5; + --color-neutral-stroke-1: #666666; + --color-neutral-stroke-1-hover: #757575; + --color-neutral-stroke-1-pressed: #6b6b6b; + --color-neutral-stroke-1-selected: #707070; + --color-neutral-stroke-2: #525252; + --color-neutral-stroke-3: #3d3d3d; + --color-neutral-stroke-4: #3d3d3d; + --color-neutral-stroke-4-hover: #2e2e2e; + --color-neutral-stroke-4-pressed: #242424; + --color-neutral-stroke-4-selected: #3d3d3d; + --color-neutral-stroke-subtle: #0a0a0a; + --color-neutral-stroke-on-brand: #292929; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #479ef5; + --color-brand-stroke-2: #0e4775; + --color-brand-stroke-2-hover: #0e4775; + --color-brand-stroke-2-pressed: #0a2e4a; + --color-brand-stroke-2-contrast: #0e4775; + --color-compound-brand-stroke: #479ef5; + --color-compound-brand-stroke-hover: #62abf5; + --color-compound-brand-stroke-pressed: #2886de; + --color-neutral-stroke-disabled: #424242; + --color-neutral-stroke-disabled-2: #3d3d3d; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(255, 255, 255, 0.1); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #000000; + --color-stroke-focus-2: #ffffff; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.24); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.28); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.4); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.48); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #3f1011; + --color-palette-red-background-2: #751d1f; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #e37d80; + --color-palette-red-border-1: #d13438; + --color-palette-red-border-2: #e37d80; + --color-palette-red-foreground-1: #e37d80; + --color-palette-red-foreground-2: #f1bbbc; + --color-palette-red-foreground-3: #e37d80; + --color-palette-red-foreground-inverted: #d13438; + --color-palette-green-background-1: #052505; + --color-palette-green-background-2: #094509; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #54b054; + --color-palette-green-border-1: #107c10; + --color-palette-green-border-2: #9fd89f; + --color-palette-green-foreground-1: #54b054; + --color-palette-green-foreground-2: #9fd89f; + --color-palette-green-foreground-3: #9fd89f; + --color-palette-green-foreground-inverted: #107c10; + --color-palette-dark-orange-background-1: #411200; + --color-palette-dark-orange-background-2: #7a2101; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #e9835e; + --color-palette-dark-orange-border-1: #da3b01; + --color-palette-dark-orange-border-2: #e9835e; + --color-palette-dark-orange-foreground-1: #e9835e; + --color-palette-dark-orange-foreground-2: #f4bfab; + --color-palette-dark-orange-foreground-3: #e9835e; + --color-palette-yellow-background-1: #4c4400; + --color-palette-yellow-background-2: #817400; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #feee66; + --color-palette-yellow-border-1: #fde300; + --color-palette-yellow-border-2: #fdea3d; + --color-palette-yellow-foreground-1: #feee66; + --color-palette-yellow-foreground-2: #fef7b2; + --color-palette-yellow-foreground-3: #fdea3d; + --color-palette-yellow-foreground-inverted: #817400; + --color-palette-berry-background-1: #3a1136; + --color-palette-berry-background-2: #6d2064; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #da7ed0; + --color-palette-berry-border-1: #c239b3; + --color-palette-berry-border-2: #d161c4; + --color-palette-berry-foreground-1: #da7ed0; + --color-palette-berry-foreground-2: #edbbe7; + --color-palette-berry-foreground-3: #d161c4; + --color-palette-marigold-background-1: #463100; + --color-palette-marigold-background-2: #835b00; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #f2c661; + --color-palette-marigold-border-1: #eaa300; + --color-palette-marigold-border-2: #efb839; + --color-palette-marigold-foreground-1: #f2c661; + --color-palette-marigold-foreground-2: #f9e2ae; + --color-palette-marigold-foreground-3: #efb839; + --color-palette-light-green-background-1: #063004; + --color-palette-light-green-background-2: #0b5a08; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #5ec75a; + --color-palette-light-green-border-1: #13a10e; + --color-palette-light-green-border-2: #3db838; + --color-palette-light-green-foreground-1: #5ec75a; + --color-palette-light-green-foreground-2: #a7e3a5; + --color-palette-light-green-foreground-3: #3db838; + --color-palette-anchor-background-2: #202427; + --color-palette-anchor-border-active: #808a90; + --color-palette-anchor-foreground-2: #bcc3c7; + --color-palette-beige-background-2: #444241; + --color-palette-beige-border-active: #afabaa; + --color-palette-beige-foreground-2: #d7d4d4; + --color-palette-blue-background-2: #004377; + --color-palette-blue-border-active: #5caae5; + --color-palette-blue-foreground-2: #a9d3f2; + --color-palette-brass-background-2: #553e06; + --color-palette-brass-border-active: #c1a256; + --color-palette-brass-foreground-2: #e0cea2; + --color-palette-brown-background-2: #50301a; + --color-palette-brown-border-active: #bb8f6f; + --color-palette-brown-foreground-2: #ddc3b0; + --color-palette-cornflower-background-2: #2c3c85; + --color-palette-cornflower-border-active: #93a4f4; + --color-palette-cornflower-foreground-2: #c8d1fa; + --color-palette-cranberry-background-2: #6e0811; + --color-palette-cranberry-border-active: #dc626d; + --color-palette-cranberry-foreground-2: #eeacb2; + --color-palette-dark-green-background-2: #063b06; + --color-palette-dark-green-border-active: #4da64d; + --color-palette-dark-green-foreground-2: #9ad29a; + --color-palette-dark-red-background-2: #590815; + --color-palette-dark-red-border-active: #ac4f5e; + --color-palette-dark-red-foreground-2: #d69ca5; + --color-palette-forest-background-2: #294903; + --color-palette-forest-border-active: #85b44c; + --color-palette-forest-foreground-2: #bdd99b; + --color-palette-gold-background-2: #6c5700; + --color-palette-gold-border-active: #dac157; + --color-palette-gold-foreground-2: #ecdfa5; + --color-palette-grape-background-2: #4c0d55; + --color-palette-grape-border-active: #b55fc1; + --color-palette-grape-foreground-2: #d9a7e0; + --color-palette-lavender-background-2: #3f3682; + --color-palette-lavender-border-active: #a79cf1; + --color-palette-lavender-foreground-2: #d2ccf8; + --color-palette-light-teal-background-2: #00666d; + --color-palette-light-teal-border-active: #58d3db; + --color-palette-light-teal-foreground-2: #a6e9ed; + --color-palette-lilac-background-2: #63276d; + --color-palette-lilac-border-active: #cf87da; + --color-palette-lilac-foreground-2: #e6bfed; + --color-palette-magenta-background-2: #6b0043; + --color-palette-magenta-border-active: #d957a8; + --color-palette-magenta-foreground-2: #eca5d1; + --color-palette-mink-background-2: #343231; + --color-palette-mink-border-active: #9e9b99; + --color-palette-mink-foreground-2: #cecccb; + --color-palette-navy-background-2: #001665; + --color-palette-navy-border-active: #546fd2; + --color-palette-navy-foreground-2: #a3b2e8; + --color-palette-peach-background-2: #8f4e00; + --color-palette-peach-border-active: #ffba66; + --color-palette-peach-foreground-2: #ffddb3; + --color-palette-pink-background-2: #80215d; + --color-palette-pink-border-active: #ef85c8; + --color-palette-pink-foreground-2: #f7c0e3; + --color-palette-platinum-background-2: #3b4447; + --color-palette-platinum-border-active: #a0adb2; + --color-palette-platinum-foreground-2: #cdd6d8; + --color-palette-plum-background-2: #5a003b; + --color-palette-plum-border-active: #ad4589; + --color-palette-plum-foreground-2: #d696c0; + --color-palette-pumpkin-background-2: #712d09; + --color-palette-pumpkin-border-active: #df8e64; + --color-palette-pumpkin-foreground-2: #efc4ad; + --color-palette-purple-background-2: #341a51; + --color-palette-purple-border-active: #9470bd; + --color-palette-purple-foreground-2: #c6b1de; + --color-palette-royal-blue-background-2: #002c4e; + --color-palette-royal-blue-border-active: #4a89ba; + --color-palette-royal-blue-foreground-2: #9abfdc; + --color-palette-seafoam-background-2: #00723b; + --color-palette-seafoam-border-active: #5ae0a0; + --color-palette-seafoam-foreground-2: #a8f0cd; + --color-palette-steel-background-2: #00333f; + --color-palette-steel-border-active: #4496a9; + --color-palette-steel-foreground-2: #94c8d4; + --color-palette-teal-background-2: #02494c; + --color-palette-teal-border-active: #4cb4b7; + --color-palette-teal-foreground-2: #9bd9db; + --color-status-success-background-1: #052505; + --color-status-success-background-2: #094509; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #54b054; + --color-status-success-foreground-2: #9fd89f; + --color-status-success-foreground-3: #9fd89f; + --color-status-success-foreground-inverted: #0e700e; + --color-status-success-border-active: #54b054; + --color-status-success-border-1: #107c10; + --color-status-success-border-2: #9fd89f; + --color-status-warning-background-1: #4a1e04; + --color-status-warning-background-2: #8a3707; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #faa06b; + --color-status-warning-foreground-2: #fdcfb4; + --color-status-warning-foreground-3: #f98845; + --color-status-warning-foreground-inverted: #bc4b09; + --color-status-warning-border-active: #faa06b; + --color-status-warning-border-1: #f7630c; + --color-status-warning-border-2: #f98845; + --color-status-danger-background-1: #3b0509; + --color-status-danger-background-2: #6e0811; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #dc626d; + --color-status-danger-foreground-2: #eeacb2; + --color-status-danger-foreground-3: #eeacb2; + --color-status-danger-foreground-inverted: #b10e1c; + --color-status-danger-border-active: #dc626d; + --color-status-danger-border-1: #c50f1f; + --color-status-danger-border-2: #dc626d; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', + sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.24), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.28); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/themes/web-light.css b/packages/react-components/react-tailwind-theme-preview/css/themes/web-light.css new file mode 100644 index 0000000000000..5f152b2520e9d --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/themes/web-light.css @@ -0,0 +1,475 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/theme-values.json) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * webLightTheme — 423 custom properties, nothing else. + * + * Import this file and apply `.fui-theme-web-light` to theme a subtree. There is no + * default theme: without a theme file + class, these variables are unset (Griffel + * parity — its tokens are equally unset without a `theme` object). The class-name + * constant ships from ./theme-class-names; the generator asserts the lockstep. + * + * Pairs with the base sheet (./base.css), which owns the @layer order statement, the + * utility registrations and the theme-INVARIANT spacing/stroke values. Load the base + * sheet first; this file only fills an already-ordered fui.theme layer. + */ + +@layer fui.theme { + .fui-theme-web-light { + --color-neutral-foreground-1: #242424; + --color-neutral-foreground-1-hover: #242424; + --color-neutral-foreground-1-pressed: #242424; + --color-neutral-foreground-1-selected: #242424; + --color-neutral-foreground-2: #424242; + --color-neutral-foreground-2-hover: #242424; + --color-neutral-foreground-2-pressed: #242424; + --color-neutral-foreground-2-selected: #242424; + --color-neutral-foreground-2-brand-hover: #0f6cbd; + --color-neutral-foreground-2-brand-pressed: #115ea3; + --color-neutral-foreground-2-brand-selected: #0f6cbd; + --color-neutral-foreground-3: #616161; + --color-neutral-foreground-3-hover: #424242; + --color-neutral-foreground-3-pressed: #424242; + --color-neutral-foreground-3-selected: #424242; + --color-neutral-foreground-3-brand-hover: #0f6cbd; + --color-neutral-foreground-3-brand-pressed: #115ea3; + --color-neutral-foreground-3-brand-selected: #0f6cbd; + --color-neutral-foreground-4: #707070; + --color-neutral-foreground-5: #616161; + --color-neutral-foreground-5-hover: #242424; + --color-neutral-foreground-5-pressed: #242424; + --color-neutral-foreground-5-selected: #242424; + --color-neutral-foreground-disabled: #bdbdbd; + --color-brand-foreground-link: #115ea3; + --color-brand-foreground-link-hover: #0f548c; + --color-brand-foreground-link-pressed: #0c3b5e; + --color-brand-foreground-link-selected: #115ea3; + --color-neutral-foreground-2-link: #424242; + --color-neutral-foreground-2-link-hover: #242424; + --color-neutral-foreground-2-link-pressed: #242424; + --color-neutral-foreground-2-link-selected: #242424; + --color-compound-brand-foreground-1: #0f6cbd; + --color-compound-brand-foreground-1-hover: #115ea3; + --color-compound-brand-foreground-1-pressed: #0f548c; + --color-neutral-foreground-on-brand: #ffffff; + --color-neutral-foreground-inverted: #ffffff; + --color-neutral-foreground-inverted-hover: #ffffff; + --color-neutral-foreground-inverted-pressed: #ffffff; + --color-neutral-foreground-inverted-selected: #ffffff; + --color-neutral-foreground-inverted-2: #ffffff; + --color-neutral-foreground-static-inverted: #ffffff; + --color-neutral-foreground-inverted-link: #ffffff; + --color-neutral-foreground-inverted-link-hover: #ffffff; + --color-neutral-foreground-inverted-link-pressed: #ffffff; + --color-neutral-foreground-inverted-link-selected: #ffffff; + --color-neutral-foreground-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-brand-foreground-1: #0f6cbd; + --color-brand-foreground-2: #115ea3; + --color-brand-foreground-2-hover: #0f548c; + --color-brand-foreground-2-pressed: #0a2e4a; + --color-neutral-foreground-1-static: #242424; + --color-brand-foreground-inverted: #479ef5; + --color-brand-foreground-inverted-hover: #62abf5; + --color-brand-foreground-inverted-pressed: #479ef5; + --color-brand-foreground-on-light: #0f6cbd; + --color-brand-foreground-on-light-hover: #115ea3; + --color-brand-foreground-on-light-pressed: #0e4775; + --color-brand-foreground-on-light-selected: #0f548c; + --color-neutral-background-1: #ffffff; + --color-neutral-background-1-hover: #f5f5f5; + --color-neutral-background-1-pressed: #e0e0e0; + --color-neutral-background-1-selected: #ebebeb; + --color-neutral-background-2: #fafafa; + --color-neutral-background-2-hover: #f0f0f0; + --color-neutral-background-2-pressed: #dbdbdb; + --color-neutral-background-2-selected: #e6e6e6; + --color-neutral-background-3: #f5f5f5; + --color-neutral-background-3-hover: #ebebeb; + --color-neutral-background-3-pressed: #d6d6d6; + --color-neutral-background-3-selected: #e0e0e0; + --color-neutral-background-4: #f0f0f0; + --color-neutral-background-4-hover: #fafafa; + --color-neutral-background-4-pressed: #f5f5f5; + --color-neutral-background-4-selected: #ffffff; + --color-neutral-background-5: #ebebeb; + --color-neutral-background-5-hover: #f5f5f5; + --color-neutral-background-5-pressed: #f0f0f0; + --color-neutral-background-5-selected: #fafafa; + --color-neutral-background-6: #e6e6e6; + --color-neutral-background-7: #00000000; + --color-neutral-background-7-hover: #ebebeb; + --color-neutral-background-7-pressed: #d6d6d6; + --color-neutral-background-7-selected: #00000000; + --color-neutral-background-8: #fcfcfc; + --color-neutral-background-inverted: #292929; + --color-neutral-background-inverted-hover: #3d3d3d; + --color-neutral-background-inverted-pressed: #1f1f1f; + --color-neutral-background-inverted-selected: #383838; + --color-neutral-background-static: #333333; + --color-neutral-background-alpha: rgba(255, 255, 255, 0.5); + --color-neutral-background-alpha-2: rgba(255, 255, 255, 0.8); + --color-subtle-background: transparent; + --color-subtle-background-hover: #f5f5f5; + --color-subtle-background-pressed: #e0e0e0; + --color-subtle-background-selected: #ebebeb; + --color-subtle-background-light-alpha-hover: rgba(255, 255, 255, 0.7); + --color-subtle-background-light-alpha-pressed: rgba(255, 255, 255, 0.5); + --color-subtle-background-light-alpha-selected: transparent; + --color-subtle-background-inverted: transparent; + --color-subtle-background-inverted-hover: rgba(0, 0, 0, 0.1); + --color-subtle-background-inverted-pressed: rgba(0, 0, 0, 0.3); + --color-subtle-background-inverted-selected: rgba(0, 0, 0, 0.2); + --color-transparent-background: transparent; + --color-transparent-background-hover: transparent; + --color-transparent-background-pressed: transparent; + --color-transparent-background-selected: transparent; + --color-neutral-background-disabled: #f0f0f0; + --color-neutral-background-disabled-2: #ffffff; + --color-neutral-background-inverted-disabled: rgba(255, 255, 255, 0.1); + --color-neutral-stencil-1: #e6e6e6; + --color-neutral-stencil-2: #fafafa; + --color-neutral-stencil-1-alpha: rgba(0, 0, 0, 0.1); + --color-neutral-stencil-2-alpha: rgba(0, 0, 0, 0.05); + --color-background-overlay: rgba(0, 0, 0, 0.4); + --color-scrollbar-overlay: rgba(0, 0, 0, 0.5); + --color-brand-background: #0f6cbd; + --color-brand-background-hover: #115ea3; + --color-brand-background-pressed: #0c3b5e; + --color-brand-background-selected: #0f548c; + --color-compound-brand-background: #0f6cbd; + --color-compound-brand-background-hover: #115ea3; + --color-compound-brand-background-pressed: #0f548c; + --color-brand-background-static: #0f6cbd; + --color-brand-background-2: #ebf3fc; + --color-brand-background-2-hover: #cfe4fa; + --color-brand-background-2-pressed: #96c6fa; + --color-brand-background-3-static: #0f548c; + --color-brand-background-4-static: #0c3b5e; + --color-brand-background-inverted: #ffffff; + --color-brand-background-inverted-hover: #ebf3fc; + --color-brand-background-inverted-pressed: #b4d6fa; + --color-brand-background-inverted-selected: #cfe4fa; + --color-neutral-card-background: #fafafa; + --color-neutral-card-background-hover: #ffffff; + --color-neutral-card-background-pressed: #f5f5f5; + --color-neutral-card-background-selected: #ebebeb; + --color-neutral-card-background-disabled: #f0f0f0; + --color-neutral-stroke-accessible: #616161; + --color-neutral-stroke-accessible-hover: #575757; + --color-neutral-stroke-accessible-pressed: #4d4d4d; + --color-neutral-stroke-accessible-selected: #0f6cbd; + --color-neutral-stroke-1: #d1d1d1; + --color-neutral-stroke-1-hover: #c7c7c7; + --color-neutral-stroke-1-pressed: #b3b3b3; + --color-neutral-stroke-1-selected: #bdbdbd; + --color-neutral-stroke-2: #e0e0e0; + --color-neutral-stroke-3: #f0f0f0; + --color-neutral-stroke-4: #ebebeb; + --color-neutral-stroke-4-hover: #e0e0e0; + --color-neutral-stroke-4-pressed: #d6d6d6; + --color-neutral-stroke-4-selected: #ebebeb; + --color-neutral-stroke-subtle: #e0e0e0; + --color-neutral-stroke-on-brand: #ffffff; + --color-neutral-stroke-on-brand-2: #ffffff; + --color-neutral-stroke-on-brand-2-hover: #ffffff; + --color-neutral-stroke-on-brand-2-pressed: #ffffff; + --color-neutral-stroke-on-brand-2-selected: #ffffff; + --color-brand-stroke-1: #0f6cbd; + --color-brand-stroke-2: #b4d6fa; + --color-brand-stroke-2-hover: #77b7f7; + --color-brand-stroke-2-pressed: #0f6cbd; + --color-brand-stroke-2-contrast: #b4d6fa; + --color-compound-brand-stroke: #0f6cbd; + --color-compound-brand-stroke-hover: #115ea3; + --color-compound-brand-stroke-pressed: #0f548c; + --color-neutral-stroke-disabled: #e0e0e0; + --color-neutral-stroke-disabled-2: #ebebeb; + --color-neutral-stroke-inverted-disabled: rgba(255, 255, 255, 0.4); + --color-transparent-stroke: transparent; + --color-transparent-stroke-interactive: transparent; + --color-transparent-stroke-disabled: transparent; + --color-neutral-stroke-alpha: rgba(0, 0, 0, 0.05); + --color-neutral-stroke-alpha-2: rgba(255, 255, 255, 0.2); + --color-stroke-focus-1: #ffffff; + --color-stroke-focus-2: #000000; + --color-neutral-shadow-ambient: rgba(0, 0, 0, 0.12); + --color-neutral-shadow-key: rgba(0, 0, 0, 0.14); + --color-neutral-shadow-ambient-lighter: rgba(0, 0, 0, 0.06); + --color-neutral-shadow-key-lighter: rgba(0, 0, 0, 0.07); + --color-neutral-shadow-ambient-darker: rgba(0, 0, 0, 0.2); + --color-neutral-shadow-key-darker: rgba(0, 0, 0, 0.24); + --color-brand-shadow-ambient: rgba(0, 0, 0, 0.3); + --color-brand-shadow-key: rgba(0, 0, 0, 0.25); + --color-palette-red-background-1: #fdf6f6; + --color-palette-red-background-2: #f1bbbc; + --color-palette-red-background-3: #d13438; + --color-palette-red-border-active: #d13438; + --color-palette-red-border-1: #f1bbbc; + --color-palette-red-border-2: #d13438; + --color-palette-red-foreground-1: #bc2f32; + --color-palette-red-foreground-2: #751d1f; + --color-palette-red-foreground-3: #d13438; + --color-palette-red-foreground-inverted: #dc5e62; + --color-palette-green-background-1: #f1faf1; + --color-palette-green-background-2: #9fd89f; + --color-palette-green-background-3: #107c10; + --color-palette-green-border-active: #107c10; + --color-palette-green-border-1: #9fd89f; + --color-palette-green-border-2: #107c10; + --color-palette-green-foreground-1: #0e700e; + --color-palette-green-foreground-2: #094509; + --color-palette-green-foreground-3: #107c10; + --color-palette-green-foreground-inverted: #359b35; + --color-palette-dark-orange-background-1: #fdf6f3; + --color-palette-dark-orange-background-2: #f4bfab; + --color-palette-dark-orange-background-3: #da3b01; + --color-palette-dark-orange-border-active: #da3b01; + --color-palette-dark-orange-border-1: #f4bfab; + --color-palette-dark-orange-border-2: #da3b01; + --color-palette-dark-orange-foreground-1: #c43501; + --color-palette-dark-orange-foreground-2: #7a2101; + --color-palette-dark-orange-foreground-3: #da3b01; + --color-palette-yellow-background-1: #fffef5; + --color-palette-yellow-background-2: #fef7b2; + --color-palette-yellow-background-3: #fde300; + --color-palette-yellow-border-active: #fde300; + --color-palette-yellow-border-1: #fef7b2; + --color-palette-yellow-border-2: #fde300; + --color-palette-yellow-foreground-1: #817400; + --color-palette-yellow-foreground-2: #817400; + --color-palette-yellow-foreground-3: #fde300; + --color-palette-yellow-foreground-inverted: #fef7b2; + --color-palette-berry-background-1: #fdf5fc; + --color-palette-berry-background-2: #edbbe7; + --color-palette-berry-background-3: #c239b3; + --color-palette-berry-border-active: #c239b3; + --color-palette-berry-border-1: #edbbe7; + --color-palette-berry-border-2: #c239b3; + --color-palette-berry-foreground-1: #af33a1; + --color-palette-berry-foreground-2: #6d2064; + --color-palette-berry-foreground-3: #c239b3; + --color-palette-marigold-background-1: #fefbf4; + --color-palette-marigold-background-2: #f9e2ae; + --color-palette-marigold-background-3: #eaa300; + --color-palette-marigold-border-active: #eaa300; + --color-palette-marigold-border-1: #f9e2ae; + --color-palette-marigold-border-2: #eaa300; + --color-palette-marigold-foreground-1: #d39300; + --color-palette-marigold-foreground-2: #835b00; + --color-palette-marigold-foreground-3: #eaa300; + --color-palette-light-green-background-1: #f2fbf2; + --color-palette-light-green-background-2: #a7e3a5; + --color-palette-light-green-background-3: #13a10e; + --color-palette-light-green-border-active: #13a10e; + --color-palette-light-green-border-1: #a7e3a5; + --color-palette-light-green-border-2: #13a10e; + --color-palette-light-green-foreground-1: #11910d; + --color-palette-light-green-foreground-2: #0b5a08; + --color-palette-light-green-foreground-3: #13a10e; + --color-palette-anchor-background-2: #bcc3c7; + --color-palette-anchor-border-active: #394146; + --color-palette-anchor-foreground-2: #202427; + --color-palette-beige-background-2: #d7d4d4; + --color-palette-beige-border-active: #7a7574; + --color-palette-beige-foreground-2: #444241; + --color-palette-blue-background-2: #a9d3f2; + --color-palette-blue-border-active: #0078d4; + --color-palette-blue-foreground-2: #004377; + --color-palette-brass-background-2: #e0cea2; + --color-palette-brass-border-active: #986f0b; + --color-palette-brass-foreground-2: #553e06; + --color-palette-brown-background-2: #ddc3b0; + --color-palette-brown-border-active: #8e562e; + --color-palette-brown-foreground-2: #50301a; + --color-palette-cornflower-background-2: #c8d1fa; + --color-palette-cornflower-border-active: #4f6bed; + --color-palette-cornflower-foreground-2: #2c3c85; + --color-palette-cranberry-background-2: #eeacb2; + --color-palette-cranberry-border-active: #c50f1f; + --color-palette-cranberry-foreground-2: #6e0811; + --color-palette-dark-green-background-2: #9ad29a; + --color-palette-dark-green-border-active: #0b6a0b; + --color-palette-dark-green-foreground-2: #063b06; + --color-palette-dark-red-background-2: #d69ca5; + --color-palette-dark-red-border-active: #750b1c; + --color-palette-dark-red-foreground-2: #420610; + --color-palette-forest-background-2: #bdd99b; + --color-palette-forest-border-active: #498205; + --color-palette-forest-foreground-2: #294903; + --color-palette-gold-background-2: #ecdfa5; + --color-palette-gold-border-active: #c19c00; + --color-palette-gold-foreground-2: #6c5700; + --color-palette-grape-background-2: #d9a7e0; + --color-palette-grape-border-active: #881798; + --color-palette-grape-foreground-2: #4c0d55; + --color-palette-lavender-background-2: #d2ccf8; + --color-palette-lavender-border-active: #7160e8; + --color-palette-lavender-foreground-2: #3f3682; + --color-palette-light-teal-background-2: #a6e9ed; + --color-palette-light-teal-border-active: #00b7c3; + --color-palette-light-teal-foreground-2: #00666d; + --color-palette-lilac-background-2: #e6bfed; + --color-palette-lilac-border-active: #b146c2; + --color-palette-lilac-foreground-2: #63276d; + --color-palette-magenta-background-2: #eca5d1; + --color-palette-magenta-border-active: #bf0077; + --color-palette-magenta-foreground-2: #6b0043; + --color-palette-mink-background-2: #cecccb; + --color-palette-mink-border-active: #5d5a58; + --color-palette-mink-foreground-2: #343231; + --color-palette-navy-background-2: #a3b2e8; + --color-palette-navy-border-active: #0027b4; + --color-palette-navy-foreground-2: #001665; + --color-palette-peach-background-2: #ffddb3; + --color-palette-peach-border-active: #ff8c00; + --color-palette-peach-foreground-2: #8f4e00; + --color-palette-pink-background-2: #f7c0e3; + --color-palette-pink-border-active: #e43ba6; + --color-palette-pink-foreground-2: #80215d; + --color-palette-platinum-background-2: #cdd6d8; + --color-palette-platinum-border-active: #69797e; + --color-palette-platinum-foreground-2: #3b4447; + --color-palette-plum-background-2: #d696c0; + --color-palette-plum-border-active: #77004d; + --color-palette-plum-foreground-2: #43002b; + --color-palette-pumpkin-background-2: #efc4ad; + --color-palette-pumpkin-border-active: #ca5010; + --color-palette-pumpkin-foreground-2: #712d09; + --color-palette-purple-background-2: #c6b1de; + --color-palette-purple-border-active: #5c2e91; + --color-palette-purple-foreground-2: #341a51; + --color-palette-royal-blue-background-2: #9abfdc; + --color-palette-royal-blue-border-active: #004e8c; + --color-palette-royal-blue-foreground-2: #002c4e; + --color-palette-seafoam-background-2: #a8f0cd; + --color-palette-seafoam-border-active: #00cc6a; + --color-palette-seafoam-foreground-2: #00723b; + --color-palette-steel-background-2: #94c8d4; + --color-palette-steel-border-active: #005b70; + --color-palette-steel-foreground-2: #00333f; + --color-palette-teal-background-2: #9bd9db; + --color-palette-teal-border-active: #038387; + --color-palette-teal-foreground-2: #02494c; + --color-status-success-background-1: #f1faf1; + --color-status-success-background-2: #9fd89f; + --color-status-success-background-3: #107c10; + --color-status-success-foreground-1: #0e700e; + --color-status-success-foreground-2: #094509; + --color-status-success-foreground-3: #107c10; + --color-status-success-foreground-inverted: #54b054; + --color-status-success-border-active: #107c10; + --color-status-success-border-1: #9fd89f; + --color-status-success-border-2: #107c10; + --color-status-warning-background-1: #fff9f5; + --color-status-warning-background-2: #fdcfb4; + --color-status-warning-background-3: #f7630c; + --color-status-warning-foreground-1: #bc4b09; + --color-status-warning-foreground-2: #8a3707; + --color-status-warning-foreground-3: #bc4b09; + --color-status-warning-foreground-inverted: #faa06b; + --color-status-warning-border-active: #f7630c; + --color-status-warning-border-1: #fdcfb4; + --color-status-warning-border-2: #bc4b09; + --color-status-danger-background-1: #fdf3f4; + --color-status-danger-background-2: #eeacb2; + --color-status-danger-background-3: #c50f1f; + --color-status-danger-background-3-hover: #b10e1c; + --color-status-danger-background-3-pressed: #960b18; + --color-status-danger-foreground-1: #b10e1c; + --color-status-danger-foreground-2: #6e0811; + --color-status-danger-foreground-3: #c50f1f; + --color-status-danger-foreground-inverted: #dc626d; + --color-status-danger-border-active: #c50f1f; + --color-status-danger-border-1: #eeacb2; + --color-status-danger-border-2: #c50f1f; + --radius-none: 0; + --radius-small: calc(2px * var(--base-scale)); + --radius-medium: calc(4px * var(--base-scale)); + --radius-large: calc(6px * var(--base-scale)); + --radius-x-large: calc(8px * var(--base-scale)); + --radius-2-x-large: calc(12px * var(--base-scale)); + --radius-3-x-large: calc(16px * var(--base-scale)); + --radius-4-x-large: calc(24px * var(--base-scale)); + --radius-5-x-large: calc(32px * var(--base-scale)); + --radius-6-x-large: calc(40px * var(--base-scale)); + --radius-circular: 10000px; + --font-base: + 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', + sans-serif; + --font-monospace: Consolas, 'Courier New', Courier, monospace; + --font-numeric: + Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, + 'Helvetica Neue', sans-serif; + --text-base-100: calc(10px * var(--base-scale)); + --text-base-200: calc(12px * var(--base-scale)); + --text-base-300: calc(14px * var(--base-scale)); + --text-base-400: calc(16px * var(--base-scale)); + --text-base-500: calc(20px * var(--base-scale)); + --text-base-600: calc(24px * var(--base-scale)); + --text-hero-700: calc(28px * var(--base-scale)); + --text-hero-800: calc(32px * var(--base-scale)); + --text-hero-900: calc(40px * var(--base-scale)); + --text-hero-1000: calc(68px * var(--base-scale)); + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --shadow-2: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-4: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-8: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-16: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-28: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-64: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.12), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.14); + --shadow-2-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-4-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(2px * var(--base-scale)) calc(4px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-8-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(4px * var(--base-scale)) calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-16-brand: + 0 0 calc(2px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(8px * var(--base-scale)) calc(16px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-28-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(14px * var(--base-scale)) calc(28px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --shadow-64-brand: + 0 0 calc(8px * var(--base-scale)) rgba(0, 0, 0, 0.3), + 0 calc(32px * var(--base-scale)) calc(64px * var(--base-scale)) rgba(0, 0, 0, 0.25); + --duration-ultra-fast: 50ms; + --duration-faster: 100ms; + --duration-fast: 150ms; + --duration-normal: 200ms; + --duration-gentle: 250ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-ultra-slow: 500ms; + --ease-accelerate-max: cubic-bezier(0.9, 0.1, 1, 0.2); + --ease-accelerate-mid: cubic-bezier(1, 0, 1, 1); + --ease-accelerate-min: cubic-bezier(0.8, 0, 0.78, 1); + --ease-decelerate-max: cubic-bezier(0.1, 0.9, 0.2, 1); + --ease-decelerate-mid: cubic-bezier(0, 0, 0, 1); + --ease-decelerate-min: cubic-bezier(0.33, 0, 0.1, 1); + --ease-easy-ease-max: cubic-bezier(0.8, 0, 0.2, 1); + --ease-easy-ease: cubic-bezier(0.33, 0, 0.67, 1); + --ease-linear: cubic-bezier(0, 0, 1, 1); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/tokens.css b/packages/react-components/react-tailwind-theme-preview/css/tokens.css new file mode 100644 index 0000000000000..5bb2017c39bfe --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/tokens.css @@ -0,0 +1,701 @@ +/* + * DO NOT EDIT — generated file. + * + * Generator: packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Source: @fluentui/tokens@1.0.0-alpha.24 (packages/tokens/src/tokens.ts) + * Regenerate: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + * + * 467 Fluent tokens: 467 registered, none excluded. + * + * `inline` is MANDATORY: it substitutes the canonical var(--token-name) into each + * utility, so values resolve per-element — the `:root, :host` defaults below, + * overridden per subtree by the theme classes (css/themes.css). + * A plain `@theme` alias would freeze resolution at `:root`, breaking scoped + * theming. `reference` suppresses the self-referential aliases that plain + * `inline` would emit now that each runtime variable IS its theme key (theming + * the kebab-name model) — the values are emitted in the fui.theme block below, not by JS. + * + * SPACING IS THE ONE EXCEPTION: --spacing-horizontal-* / --spacing-vertical-* are + * ALIASES OF THE NUMERIC AXIS — calc(var(--spacing) * N), the same shape p-12 + * compiles to — so named and numeric spacing are ONE system with ONE density knob + * (--spacing). Stroke widths are the deliberate exception: their PUBLIC variables + * are --stroke-width-thin/thick/thicker/thickest with literal + * calc( * var(--base-scale)) values (borders must not thin when layout density + * changes); --spacing-thin/… are PRIVATE hooks aliasing them for utility generation. + * Provider spacing/strokeWidth overrides do not reach these; all 7 shipped themes + * carry identical values. + * + * Font sizes (--text-*) carry the same literal calc( * var(--base-scale)) form as + * stroke widths: type follows the root font size, not the --spacing density knob. Their + * paired line heights (--leading-*) are UNITLESS RATIOS of the ramp pairing (Decision U: + * --leading-base-300 = 20px/14px = calc(20 / 14)) — the ratio multiplies the element’s + * own font-size, which already rides base-scale, so line boxes scale for free. They are + * theme-invariant (ramp asserted identical across themes) and emitted below, not per theme. + * Shadow offsets and blur radii ride the base-scale axis, so an elevation + * keeps its proportion to the box it lifts. Border radii ride the same axis, so a + * corner keeps its proportion to the control it rounds (0 and the 10000px circular + * sentinel excepted — neither meaningfully scales). + * + * ORDER MATTERS: index.css imports this AFTER its `@theme static` block, whose + * `--color-*: initial` / `--spacing-*: initial` clear only what precedes them. + * + * Rationale, namespace mapping, emission reasons: see the generator source. + */ + +@theme inline reference { + /* Colors — 366 tokens → --color-* (bg-* text-* border-* fill-* stroke-* outline-* ring-* decoration-*) */ + --color-neutral-foreground-1: var(--color-neutral-foreground-1); + --color-neutral-foreground-1-hover: var(--color-neutral-foreground-1-hover); + --color-neutral-foreground-1-pressed: var(--color-neutral-foreground-1-pressed); + --color-neutral-foreground-1-selected: var(--color-neutral-foreground-1-selected); + --color-neutral-foreground-2: var(--color-neutral-foreground-2); + --color-neutral-foreground-2-hover: var(--color-neutral-foreground-2-hover); + --color-neutral-foreground-2-pressed: var(--color-neutral-foreground-2-pressed); + --color-neutral-foreground-2-selected: var(--color-neutral-foreground-2-selected); + --color-neutral-foreground-2-brand-hover: var(--color-neutral-foreground-2-brand-hover); + --color-neutral-foreground-2-brand-pressed: var(--color-neutral-foreground-2-brand-pressed); + --color-neutral-foreground-2-brand-selected: var(--color-neutral-foreground-2-brand-selected); + --color-neutral-foreground-3: var(--color-neutral-foreground-3); + --color-neutral-foreground-3-hover: var(--color-neutral-foreground-3-hover); + --color-neutral-foreground-3-pressed: var(--color-neutral-foreground-3-pressed); + --color-neutral-foreground-3-selected: var(--color-neutral-foreground-3-selected); + --color-neutral-foreground-3-brand-hover: var(--color-neutral-foreground-3-brand-hover); + --color-neutral-foreground-3-brand-pressed: var(--color-neutral-foreground-3-brand-pressed); + --color-neutral-foreground-3-brand-selected: var(--color-neutral-foreground-3-brand-selected); + --color-neutral-foreground-4: var(--color-neutral-foreground-4); + --color-neutral-foreground-5: var(--color-neutral-foreground-5); + --color-neutral-foreground-5-hover: var(--color-neutral-foreground-5-hover); + --color-neutral-foreground-5-pressed: var(--color-neutral-foreground-5-pressed); + --color-neutral-foreground-5-selected: var(--color-neutral-foreground-5-selected); + --color-neutral-foreground-disabled: var(--color-neutral-foreground-disabled); + --color-brand-foreground-link: var(--color-brand-foreground-link); + --color-brand-foreground-link-hover: var(--color-brand-foreground-link-hover); + --color-brand-foreground-link-pressed: var(--color-brand-foreground-link-pressed); + --color-brand-foreground-link-selected: var(--color-brand-foreground-link-selected); + --color-neutral-foreground-2-link: var(--color-neutral-foreground-2-link); + --color-neutral-foreground-2-link-hover: var(--color-neutral-foreground-2-link-hover); + --color-neutral-foreground-2-link-pressed: var(--color-neutral-foreground-2-link-pressed); + --color-neutral-foreground-2-link-selected: var(--color-neutral-foreground-2-link-selected); + --color-compound-brand-foreground-1: var(--color-compound-brand-foreground-1); + --color-compound-brand-foreground-1-hover: var(--color-compound-brand-foreground-1-hover); + --color-compound-brand-foreground-1-pressed: var(--color-compound-brand-foreground-1-pressed); + --color-neutral-foreground-on-brand: var(--color-neutral-foreground-on-brand); + --color-neutral-foreground-inverted: var(--color-neutral-foreground-inverted); + --color-neutral-foreground-inverted-hover: var(--color-neutral-foreground-inverted-hover); + --color-neutral-foreground-inverted-pressed: var(--color-neutral-foreground-inverted-pressed); + --color-neutral-foreground-inverted-selected: var(--color-neutral-foreground-inverted-selected); + --color-neutral-foreground-inverted-2: var(--color-neutral-foreground-inverted-2); + --color-neutral-foreground-static-inverted: var(--color-neutral-foreground-static-inverted); + --color-neutral-foreground-inverted-link: var(--color-neutral-foreground-inverted-link); + --color-neutral-foreground-inverted-link-hover: var(--color-neutral-foreground-inverted-link-hover); + --color-neutral-foreground-inverted-link-pressed: var(--color-neutral-foreground-inverted-link-pressed); + --color-neutral-foreground-inverted-link-selected: var(--color-neutral-foreground-inverted-link-selected); + --color-neutral-foreground-inverted-disabled: var(--color-neutral-foreground-inverted-disabled); + --color-brand-foreground-1: var(--color-brand-foreground-1); + --color-brand-foreground-2: var(--color-brand-foreground-2); + --color-brand-foreground-2-hover: var(--color-brand-foreground-2-hover); + --color-brand-foreground-2-pressed: var(--color-brand-foreground-2-pressed); + --color-neutral-foreground-1-static: var(--color-neutral-foreground-1-static); + --color-brand-foreground-inverted: var(--color-brand-foreground-inverted); + --color-brand-foreground-inverted-hover: var(--color-brand-foreground-inverted-hover); + --color-brand-foreground-inverted-pressed: var(--color-brand-foreground-inverted-pressed); + --color-brand-foreground-on-light: var(--color-brand-foreground-on-light); + --color-brand-foreground-on-light-hover: var(--color-brand-foreground-on-light-hover); + --color-brand-foreground-on-light-pressed: var(--color-brand-foreground-on-light-pressed); + --color-brand-foreground-on-light-selected: var(--color-brand-foreground-on-light-selected); + --color-neutral-background-1: var(--color-neutral-background-1); + --color-neutral-background-1-hover: var(--color-neutral-background-1-hover); + --color-neutral-background-1-pressed: var(--color-neutral-background-1-pressed); + --color-neutral-background-1-selected: var(--color-neutral-background-1-selected); + --color-neutral-background-2: var(--color-neutral-background-2); + --color-neutral-background-2-hover: var(--color-neutral-background-2-hover); + --color-neutral-background-2-pressed: var(--color-neutral-background-2-pressed); + --color-neutral-background-2-selected: var(--color-neutral-background-2-selected); + --color-neutral-background-3: var(--color-neutral-background-3); + --color-neutral-background-3-hover: var(--color-neutral-background-3-hover); + --color-neutral-background-3-pressed: var(--color-neutral-background-3-pressed); + --color-neutral-background-3-selected: var(--color-neutral-background-3-selected); + --color-neutral-background-4: var(--color-neutral-background-4); + --color-neutral-background-4-hover: var(--color-neutral-background-4-hover); + --color-neutral-background-4-pressed: var(--color-neutral-background-4-pressed); + --color-neutral-background-4-selected: var(--color-neutral-background-4-selected); + --color-neutral-background-5: var(--color-neutral-background-5); + --color-neutral-background-5-hover: var(--color-neutral-background-5-hover); + --color-neutral-background-5-pressed: var(--color-neutral-background-5-pressed); + --color-neutral-background-5-selected: var(--color-neutral-background-5-selected); + --color-neutral-background-6: var(--color-neutral-background-6); + --color-neutral-background-7: var(--color-neutral-background-7); + --color-neutral-background-7-hover: var(--color-neutral-background-7-hover); + --color-neutral-background-7-pressed: var(--color-neutral-background-7-pressed); + --color-neutral-background-7-selected: var(--color-neutral-background-7-selected); + --color-neutral-background-8: var(--color-neutral-background-8); + --color-neutral-background-inverted: var(--color-neutral-background-inverted); + --color-neutral-background-inverted-hover: var(--color-neutral-background-inverted-hover); + --color-neutral-background-inverted-pressed: var(--color-neutral-background-inverted-pressed); + --color-neutral-background-inverted-selected: var(--color-neutral-background-inverted-selected); + --color-neutral-background-static: var(--color-neutral-background-static); + --color-neutral-background-alpha: var(--color-neutral-background-alpha); + --color-neutral-background-alpha-2: var(--color-neutral-background-alpha-2); + --color-subtle-background: var(--color-subtle-background); + --color-subtle-background-hover: var(--color-subtle-background-hover); + --color-subtle-background-pressed: var(--color-subtle-background-pressed); + --color-subtle-background-selected: var(--color-subtle-background-selected); + --color-subtle-background-light-alpha-hover: var(--color-subtle-background-light-alpha-hover); + --color-subtle-background-light-alpha-pressed: var(--color-subtle-background-light-alpha-pressed); + --color-subtle-background-light-alpha-selected: var(--color-subtle-background-light-alpha-selected); + --color-subtle-background-inverted: var(--color-subtle-background-inverted); + --color-subtle-background-inverted-hover: var(--color-subtle-background-inverted-hover); + --color-subtle-background-inverted-pressed: var(--color-subtle-background-inverted-pressed); + --color-subtle-background-inverted-selected: var(--color-subtle-background-inverted-selected); + --color-transparent-background: var(--color-transparent-background); + --color-transparent-background-hover: var(--color-transparent-background-hover); + --color-transparent-background-pressed: var(--color-transparent-background-pressed); + --color-transparent-background-selected: var(--color-transparent-background-selected); + --color-neutral-background-disabled: var(--color-neutral-background-disabled); + --color-neutral-background-disabled-2: var(--color-neutral-background-disabled-2); + --color-neutral-background-inverted-disabled: var(--color-neutral-background-inverted-disabled); + --color-neutral-stencil-1: var(--color-neutral-stencil-1); + --color-neutral-stencil-2: var(--color-neutral-stencil-2); + --color-neutral-stencil-1-alpha: var(--color-neutral-stencil-1-alpha); + --color-neutral-stencil-2-alpha: var(--color-neutral-stencil-2-alpha); + --color-background-overlay: var(--color-background-overlay); + --color-scrollbar-overlay: var(--color-scrollbar-overlay); + --color-brand-background: var(--color-brand-background); + --color-brand-background-hover: var(--color-brand-background-hover); + --color-brand-background-pressed: var(--color-brand-background-pressed); + --color-brand-background-selected: var(--color-brand-background-selected); + --color-compound-brand-background: var(--color-compound-brand-background); + --color-compound-brand-background-hover: var(--color-compound-brand-background-hover); + --color-compound-brand-background-pressed: var(--color-compound-brand-background-pressed); + --color-brand-background-static: var(--color-brand-background-static); + --color-brand-background-2: var(--color-brand-background-2); + --color-brand-background-2-hover: var(--color-brand-background-2-hover); + --color-brand-background-2-pressed: var(--color-brand-background-2-pressed); + --color-brand-background-3-static: var(--color-brand-background-3-static); + --color-brand-background-4-static: var(--color-brand-background-4-static); + --color-brand-background-inverted: var(--color-brand-background-inverted); + --color-brand-background-inverted-hover: var(--color-brand-background-inverted-hover); + --color-brand-background-inverted-pressed: var(--color-brand-background-inverted-pressed); + --color-brand-background-inverted-selected: var(--color-brand-background-inverted-selected); + --color-neutral-card-background: var(--color-neutral-card-background); + --color-neutral-card-background-hover: var(--color-neutral-card-background-hover); + --color-neutral-card-background-pressed: var(--color-neutral-card-background-pressed); + --color-neutral-card-background-selected: var(--color-neutral-card-background-selected); + --color-neutral-card-background-disabled: var(--color-neutral-card-background-disabled); + --color-neutral-stroke-accessible: var(--color-neutral-stroke-accessible); + --color-neutral-stroke-accessible-hover: var(--color-neutral-stroke-accessible-hover); + --color-neutral-stroke-accessible-pressed: var(--color-neutral-stroke-accessible-pressed); + --color-neutral-stroke-accessible-selected: var(--color-neutral-stroke-accessible-selected); + --color-neutral-stroke-1: var(--color-neutral-stroke-1); + --color-neutral-stroke-1-hover: var(--color-neutral-stroke-1-hover); + --color-neutral-stroke-1-pressed: var(--color-neutral-stroke-1-pressed); + --color-neutral-stroke-1-selected: var(--color-neutral-stroke-1-selected); + --color-neutral-stroke-2: var(--color-neutral-stroke-2); + --color-neutral-stroke-3: var(--color-neutral-stroke-3); + --color-neutral-stroke-4: var(--color-neutral-stroke-4); + --color-neutral-stroke-4-hover: var(--color-neutral-stroke-4-hover); + --color-neutral-stroke-4-pressed: var(--color-neutral-stroke-4-pressed); + --color-neutral-stroke-4-selected: var(--color-neutral-stroke-4-selected); + --color-neutral-stroke-subtle: var(--color-neutral-stroke-subtle); + --color-neutral-stroke-on-brand: var(--color-neutral-stroke-on-brand); + --color-neutral-stroke-on-brand-2: var(--color-neutral-stroke-on-brand-2); + --color-neutral-stroke-on-brand-2-hover: var(--color-neutral-stroke-on-brand-2-hover); + --color-neutral-stroke-on-brand-2-pressed: var(--color-neutral-stroke-on-brand-2-pressed); + --color-neutral-stroke-on-brand-2-selected: var(--color-neutral-stroke-on-brand-2-selected); + --color-brand-stroke-1: var(--color-brand-stroke-1); + --color-brand-stroke-2: var(--color-brand-stroke-2); + --color-brand-stroke-2-hover: var(--color-brand-stroke-2-hover); + --color-brand-stroke-2-pressed: var(--color-brand-stroke-2-pressed); + --color-brand-stroke-2-contrast: var(--color-brand-stroke-2-contrast); + --color-compound-brand-stroke: var(--color-compound-brand-stroke); + --color-compound-brand-stroke-hover: var(--color-compound-brand-stroke-hover); + --color-compound-brand-stroke-pressed: var(--color-compound-brand-stroke-pressed); + --color-neutral-stroke-disabled: var(--color-neutral-stroke-disabled); + --color-neutral-stroke-disabled-2: var(--color-neutral-stroke-disabled-2); + --color-neutral-stroke-inverted-disabled: var(--color-neutral-stroke-inverted-disabled); + --color-transparent-stroke: var(--color-transparent-stroke); + --color-transparent-stroke-interactive: var(--color-transparent-stroke-interactive); + --color-transparent-stroke-disabled: var(--color-transparent-stroke-disabled); + --color-neutral-stroke-alpha: var(--color-neutral-stroke-alpha); + --color-neutral-stroke-alpha-2: var(--color-neutral-stroke-alpha-2); + --color-stroke-focus-1: var(--color-stroke-focus-1); + --color-stroke-focus-2: var(--color-stroke-focus-2); + --color-neutral-shadow-ambient: var(--color-neutral-shadow-ambient); + --color-neutral-shadow-key: var(--color-neutral-shadow-key); + --color-neutral-shadow-ambient-lighter: var(--color-neutral-shadow-ambient-lighter); + --color-neutral-shadow-key-lighter: var(--color-neutral-shadow-key-lighter); + --color-neutral-shadow-ambient-darker: var(--color-neutral-shadow-ambient-darker); + --color-neutral-shadow-key-darker: var(--color-neutral-shadow-key-darker); + --color-brand-shadow-ambient: var(--color-brand-shadow-ambient); + --color-brand-shadow-key: var(--color-brand-shadow-key); + --color-palette-red-background-1: var(--color-palette-red-background-1); + --color-palette-red-background-2: var(--color-palette-red-background-2); + --color-palette-red-background-3: var(--color-palette-red-background-3); + --color-palette-red-border-active: var(--color-palette-red-border-active); + --color-palette-red-border-1: var(--color-palette-red-border-1); + --color-palette-red-border-2: var(--color-palette-red-border-2); + --color-palette-red-foreground-1: var(--color-palette-red-foreground-1); + --color-palette-red-foreground-2: var(--color-palette-red-foreground-2); + --color-palette-red-foreground-3: var(--color-palette-red-foreground-3); + --color-palette-red-foreground-inverted: var(--color-palette-red-foreground-inverted); + --color-palette-green-background-1: var(--color-palette-green-background-1); + --color-palette-green-background-2: var(--color-palette-green-background-2); + --color-palette-green-background-3: var(--color-palette-green-background-3); + --color-palette-green-border-active: var(--color-palette-green-border-active); + --color-palette-green-border-1: var(--color-palette-green-border-1); + --color-palette-green-border-2: var(--color-palette-green-border-2); + --color-palette-green-foreground-1: var(--color-palette-green-foreground-1); + --color-palette-green-foreground-2: var(--color-palette-green-foreground-2); + --color-palette-green-foreground-3: var(--color-palette-green-foreground-3); + --color-palette-green-foreground-inverted: var(--color-palette-green-foreground-inverted); + --color-palette-dark-orange-background-1: var(--color-palette-dark-orange-background-1); + --color-palette-dark-orange-background-2: var(--color-palette-dark-orange-background-2); + --color-palette-dark-orange-background-3: var(--color-palette-dark-orange-background-3); + --color-palette-dark-orange-border-active: var(--color-palette-dark-orange-border-active); + --color-palette-dark-orange-border-1: var(--color-palette-dark-orange-border-1); + --color-palette-dark-orange-border-2: var(--color-palette-dark-orange-border-2); + --color-palette-dark-orange-foreground-1: var(--color-palette-dark-orange-foreground-1); + --color-palette-dark-orange-foreground-2: var(--color-palette-dark-orange-foreground-2); + --color-palette-dark-orange-foreground-3: var(--color-palette-dark-orange-foreground-3); + --color-palette-yellow-background-1: var(--color-palette-yellow-background-1); + --color-palette-yellow-background-2: var(--color-palette-yellow-background-2); + --color-palette-yellow-background-3: var(--color-palette-yellow-background-3); + --color-palette-yellow-border-active: var(--color-palette-yellow-border-active); + --color-palette-yellow-border-1: var(--color-palette-yellow-border-1); + --color-palette-yellow-border-2: var(--color-palette-yellow-border-2); + --color-palette-yellow-foreground-1: var(--color-palette-yellow-foreground-1); + --color-palette-yellow-foreground-2: var(--color-palette-yellow-foreground-2); + --color-palette-yellow-foreground-3: var(--color-palette-yellow-foreground-3); + --color-palette-yellow-foreground-inverted: var(--color-palette-yellow-foreground-inverted); + --color-palette-berry-background-1: var(--color-palette-berry-background-1); + --color-palette-berry-background-2: var(--color-palette-berry-background-2); + --color-palette-berry-background-3: var(--color-palette-berry-background-3); + --color-palette-berry-border-active: var(--color-palette-berry-border-active); + --color-palette-berry-border-1: var(--color-palette-berry-border-1); + --color-palette-berry-border-2: var(--color-palette-berry-border-2); + --color-palette-berry-foreground-1: var(--color-palette-berry-foreground-1); + --color-palette-berry-foreground-2: var(--color-palette-berry-foreground-2); + --color-palette-berry-foreground-3: var(--color-palette-berry-foreground-3); + --color-palette-marigold-background-1: var(--color-palette-marigold-background-1); + --color-palette-marigold-background-2: var(--color-palette-marigold-background-2); + --color-palette-marigold-background-3: var(--color-palette-marigold-background-3); + --color-palette-marigold-border-active: var(--color-palette-marigold-border-active); + --color-palette-marigold-border-1: var(--color-palette-marigold-border-1); + --color-palette-marigold-border-2: var(--color-palette-marigold-border-2); + --color-palette-marigold-foreground-1: var(--color-palette-marigold-foreground-1); + --color-palette-marigold-foreground-2: var(--color-palette-marigold-foreground-2); + --color-palette-marigold-foreground-3: var(--color-palette-marigold-foreground-3); + --color-palette-light-green-background-1: var(--color-palette-light-green-background-1); + --color-palette-light-green-background-2: var(--color-palette-light-green-background-2); + --color-palette-light-green-background-3: var(--color-palette-light-green-background-3); + --color-palette-light-green-border-active: var(--color-palette-light-green-border-active); + --color-palette-light-green-border-1: var(--color-palette-light-green-border-1); + --color-palette-light-green-border-2: var(--color-palette-light-green-border-2); + --color-palette-light-green-foreground-1: var(--color-palette-light-green-foreground-1); + --color-palette-light-green-foreground-2: var(--color-palette-light-green-foreground-2); + --color-palette-light-green-foreground-3: var(--color-palette-light-green-foreground-3); + --color-palette-anchor-background-2: var(--color-palette-anchor-background-2); + --color-palette-anchor-border-active: var(--color-palette-anchor-border-active); + --color-palette-anchor-foreground-2: var(--color-palette-anchor-foreground-2); + --color-palette-beige-background-2: var(--color-palette-beige-background-2); + --color-palette-beige-border-active: var(--color-palette-beige-border-active); + --color-palette-beige-foreground-2: var(--color-palette-beige-foreground-2); + --color-palette-blue-background-2: var(--color-palette-blue-background-2); + --color-palette-blue-border-active: var(--color-palette-blue-border-active); + --color-palette-blue-foreground-2: var(--color-palette-blue-foreground-2); + --color-palette-brass-background-2: var(--color-palette-brass-background-2); + --color-palette-brass-border-active: var(--color-palette-brass-border-active); + --color-palette-brass-foreground-2: var(--color-palette-brass-foreground-2); + --color-palette-brown-background-2: var(--color-palette-brown-background-2); + --color-palette-brown-border-active: var(--color-palette-brown-border-active); + --color-palette-brown-foreground-2: var(--color-palette-brown-foreground-2); + --color-palette-cornflower-background-2: var(--color-palette-cornflower-background-2); + --color-palette-cornflower-border-active: var(--color-palette-cornflower-border-active); + --color-palette-cornflower-foreground-2: var(--color-palette-cornflower-foreground-2); + --color-palette-cranberry-background-2: var(--color-palette-cranberry-background-2); + --color-palette-cranberry-border-active: var(--color-palette-cranberry-border-active); + --color-palette-cranberry-foreground-2: var(--color-palette-cranberry-foreground-2); + --color-palette-dark-green-background-2: var(--color-palette-dark-green-background-2); + --color-palette-dark-green-border-active: var(--color-palette-dark-green-border-active); + --color-palette-dark-green-foreground-2: var(--color-palette-dark-green-foreground-2); + --color-palette-dark-red-background-2: var(--color-palette-dark-red-background-2); + --color-palette-dark-red-border-active: var(--color-palette-dark-red-border-active); + --color-palette-dark-red-foreground-2: var(--color-palette-dark-red-foreground-2); + --color-palette-forest-background-2: var(--color-palette-forest-background-2); + --color-palette-forest-border-active: var(--color-palette-forest-border-active); + --color-palette-forest-foreground-2: var(--color-palette-forest-foreground-2); + --color-palette-gold-background-2: var(--color-palette-gold-background-2); + --color-palette-gold-border-active: var(--color-palette-gold-border-active); + --color-palette-gold-foreground-2: var(--color-palette-gold-foreground-2); + --color-palette-grape-background-2: var(--color-palette-grape-background-2); + --color-palette-grape-border-active: var(--color-palette-grape-border-active); + --color-palette-grape-foreground-2: var(--color-palette-grape-foreground-2); + --color-palette-lavender-background-2: var(--color-palette-lavender-background-2); + --color-palette-lavender-border-active: var(--color-palette-lavender-border-active); + --color-palette-lavender-foreground-2: var(--color-palette-lavender-foreground-2); + --color-palette-light-teal-background-2: var(--color-palette-light-teal-background-2); + --color-palette-light-teal-border-active: var(--color-palette-light-teal-border-active); + --color-palette-light-teal-foreground-2: var(--color-palette-light-teal-foreground-2); + --color-palette-lilac-background-2: var(--color-palette-lilac-background-2); + --color-palette-lilac-border-active: var(--color-palette-lilac-border-active); + --color-palette-lilac-foreground-2: var(--color-palette-lilac-foreground-2); + --color-palette-magenta-background-2: var(--color-palette-magenta-background-2); + --color-palette-magenta-border-active: var(--color-palette-magenta-border-active); + --color-palette-magenta-foreground-2: var(--color-palette-magenta-foreground-2); + --color-palette-mink-background-2: var(--color-palette-mink-background-2); + --color-palette-mink-border-active: var(--color-palette-mink-border-active); + --color-palette-mink-foreground-2: var(--color-palette-mink-foreground-2); + --color-palette-navy-background-2: var(--color-palette-navy-background-2); + --color-palette-navy-border-active: var(--color-palette-navy-border-active); + --color-palette-navy-foreground-2: var(--color-palette-navy-foreground-2); + --color-palette-peach-background-2: var(--color-palette-peach-background-2); + --color-palette-peach-border-active: var(--color-palette-peach-border-active); + --color-palette-peach-foreground-2: var(--color-palette-peach-foreground-2); + --color-palette-pink-background-2: var(--color-palette-pink-background-2); + --color-palette-pink-border-active: var(--color-palette-pink-border-active); + --color-palette-pink-foreground-2: var(--color-palette-pink-foreground-2); + --color-palette-platinum-background-2: var(--color-palette-platinum-background-2); + --color-palette-platinum-border-active: var(--color-palette-platinum-border-active); + --color-palette-platinum-foreground-2: var(--color-palette-platinum-foreground-2); + --color-palette-plum-background-2: var(--color-palette-plum-background-2); + --color-palette-plum-border-active: var(--color-palette-plum-border-active); + --color-palette-plum-foreground-2: var(--color-palette-plum-foreground-2); + --color-palette-pumpkin-background-2: var(--color-palette-pumpkin-background-2); + --color-palette-pumpkin-border-active: var(--color-palette-pumpkin-border-active); + --color-palette-pumpkin-foreground-2: var(--color-palette-pumpkin-foreground-2); + --color-palette-purple-background-2: var(--color-palette-purple-background-2); + --color-palette-purple-border-active: var(--color-palette-purple-border-active); + --color-palette-purple-foreground-2: var(--color-palette-purple-foreground-2); + --color-palette-royal-blue-background-2: var(--color-palette-royal-blue-background-2); + --color-palette-royal-blue-border-active: var(--color-palette-royal-blue-border-active); + --color-palette-royal-blue-foreground-2: var(--color-palette-royal-blue-foreground-2); + --color-palette-seafoam-background-2: var(--color-palette-seafoam-background-2); + --color-palette-seafoam-border-active: var(--color-palette-seafoam-border-active); + --color-palette-seafoam-foreground-2: var(--color-palette-seafoam-foreground-2); + --color-palette-steel-background-2: var(--color-palette-steel-background-2); + --color-palette-steel-border-active: var(--color-palette-steel-border-active); + --color-palette-steel-foreground-2: var(--color-palette-steel-foreground-2); + --color-palette-teal-background-2: var(--color-palette-teal-background-2); + --color-palette-teal-border-active: var(--color-palette-teal-border-active); + --color-palette-teal-foreground-2: var(--color-palette-teal-foreground-2); + --color-status-success-background-1: var(--color-status-success-background-1); + --color-status-success-background-2: var(--color-status-success-background-2); + --color-status-success-background-3: var(--color-status-success-background-3); + --color-status-success-foreground-1: var(--color-status-success-foreground-1); + --color-status-success-foreground-2: var(--color-status-success-foreground-2); + --color-status-success-foreground-3: var(--color-status-success-foreground-3); + --color-status-success-foreground-inverted: var(--color-status-success-foreground-inverted); + --color-status-success-border-active: var(--color-status-success-border-active); + --color-status-success-border-1: var(--color-status-success-border-1); + --color-status-success-border-2: var(--color-status-success-border-2); + --color-status-warning-background-1: var(--color-status-warning-background-1); + --color-status-warning-background-2: var(--color-status-warning-background-2); + --color-status-warning-background-3: var(--color-status-warning-background-3); + --color-status-warning-foreground-1: var(--color-status-warning-foreground-1); + --color-status-warning-foreground-2: var(--color-status-warning-foreground-2); + --color-status-warning-foreground-3: var(--color-status-warning-foreground-3); + --color-status-warning-foreground-inverted: var(--color-status-warning-foreground-inverted); + --color-status-warning-border-active: var(--color-status-warning-border-active); + --color-status-warning-border-1: var(--color-status-warning-border-1); + --color-status-warning-border-2: var(--color-status-warning-border-2); + --color-status-danger-background-1: var(--color-status-danger-background-1); + --color-status-danger-background-2: var(--color-status-danger-background-2); + --color-status-danger-background-3: var(--color-status-danger-background-3); + --color-status-danger-background-3-hover: var(--color-status-danger-background-3-hover); + --color-status-danger-background-3-pressed: var(--color-status-danger-background-3-pressed); + --color-status-danger-foreground-1: var(--color-status-danger-foreground-1); + --color-status-danger-foreground-2: var(--color-status-danger-foreground-2); + --color-status-danger-foreground-3: var(--color-status-danger-foreground-3); + --color-status-danger-foreground-inverted: var(--color-status-danger-foreground-inverted); + --color-status-danger-border-active: var(--color-status-danger-border-active); + --color-status-danger-border-1: var(--color-status-danger-border-1); + --color-status-danger-border-2: var(--color-status-danger-border-2); + + /* Border radii — 11 tokens → --radius-* (rounded-* rounded-s-* rounded-e-* …) */ + --radius-none: var(--radius-none); + --radius-small: var(--radius-small); + --radius-medium: var(--radius-medium); + --radius-large: var(--radius-large); + --radius-x-large: var(--radius-x-large); + --radius-2-x-large: var(--radius-2-x-large); + --radius-3-x-large: var(--radius-3-x-large); + --radius-4-x-large: var(--radius-4-x-large); + --radius-5-x-large: var(--radius-5-x-large); + --radius-6-x-large: var(--radius-6-x-large); + --radius-circular: var(--radius-circular); + + /* Font families — 3 tokens → --font-* (font-*) */ + --font-base: var(--font-base); + --font-monospace: var(--font-monospace); + --font-numeric: var(--font-numeric); + + /* Font sizes — 10 tokens → --text-* (text-*) */ + --text-base-100: var(--text-base-100); + --text-base-200: var(--text-base-200); + --text-base-300: var(--text-base-300); + --text-base-400: var(--text-base-400); + --text-base-500: var(--text-base-500); + --text-base-600: var(--text-base-600); + --text-hero-700: var(--text-hero-700); + --text-hero-800: var(--text-hero-800); + --text-hero-900: var(--text-hero-900); + --text-hero-1000: var(--text-hero-1000); + + /* Font weights — 4 tokens → --font-weight-* (font-*) */ + --font-weight-regular: var(--font-weight-regular); + --font-weight-medium: var(--font-weight-medium); + --font-weight-semibold: var(--font-weight-semibold); + --font-weight-bold: var(--font-weight-bold); + + /* Line heights — UNITLESS ratios of the paired font sizes — 10 tokens → --leading-* (leading-*) */ + --leading-base-100: var(--leading-base-100); + --leading-base-200: var(--leading-base-200); + --leading-base-300: var(--leading-base-300); + --leading-base-400: var(--leading-base-400); + --leading-base-500: var(--leading-base-500); + --leading-base-600: var(--leading-base-600); + --leading-hero-700: var(--leading-hero-700); + --leading-hero-800: var(--leading-hero-800); + --leading-hero-900: var(--leading-hero-900); + --leading-hero-1000: var(--leading-hero-1000); + + /* Shadows — 12 tokens → --shadow-* (shadow-*) */ + --shadow-2: var(--shadow-2); + --shadow-4: var(--shadow-4); + --shadow-8: var(--shadow-8); + --shadow-16: var(--shadow-16); + --shadow-28: var(--shadow-28); + --shadow-64: var(--shadow-64); + --shadow-2-brand: var(--shadow-2-brand); + --shadow-4-brand: var(--shadow-4-brand); + --shadow-8-brand: var(--shadow-8-brand); + --shadow-16-brand: var(--shadow-16-brand); + --shadow-28-brand: var(--shadow-28-brand); + --shadow-64-brand: var(--shadow-64-brand); + + /* Stroke widths — PRIVATE hooks aliasing the canonical --stroke-width-* variables — 4 tokens → --spacing-* (w-thin h-thick p-thin gap-thin … AND var(--spacing-thin) for ring widths) */ + --spacing-thin: var(--stroke-width-thin); + --spacing-thick: var(--stroke-width-thick); + --spacing-thicker: var(--stroke-width-thicker); + --spacing-thickest: var(--stroke-width-thickest); + + /* Horizontal spacing, inline axis — numeric-axis ALIASES (calc(var(--spacing) * N)) — 11 tokens → --spacing-horizontal-* (px-* ps-* pe-* mx-* gap-x-* …) */ + --spacing-horizontal-none: 0; + --spacing-horizontal-xxs: calc(var(--spacing) * 2); + --spacing-horizontal-xs: calc(var(--spacing) * 4); + --spacing-horizontal-s-nudge: calc(var(--spacing) * 6); + --spacing-horizontal-s: calc(var(--spacing) * 8); + --spacing-horizontal-m-nudge: calc(var(--spacing) * 10); + --spacing-horizontal-m: calc(var(--spacing) * 12); + --spacing-horizontal-l: calc(var(--spacing) * 16); + --spacing-horizontal-xl: calc(var(--spacing) * 20); + --spacing-horizontal-xxl: calc(var(--spacing) * 24); + --spacing-horizontal-xxxl: calc(var(--spacing) * 32); + + /* Vertical spacing, block axis — numeric-axis ALIASES (calc(var(--spacing) * N)) — 11 tokens → --spacing-vertical-* (py-* pt-* pb-* my-* gap-y-* …) */ + --spacing-vertical-none: 0; + --spacing-vertical-xxs: calc(var(--spacing) * 2); + --spacing-vertical-xs: calc(var(--spacing) * 4); + --spacing-vertical-s-nudge: calc(var(--spacing) * 6); + --spacing-vertical-s: calc(var(--spacing) * 8); + --spacing-vertical-m-nudge: calc(var(--spacing) * 10); + --spacing-vertical-m: calc(var(--spacing) * 12); + --spacing-vertical-l: calc(var(--spacing) * 16); + --spacing-vertical-xl: calc(var(--spacing) * 20); + --spacing-vertical-xxl: calc(var(--spacing) * 24); + --spacing-vertical-xxxl: calc(var(--spacing) * 32); + + /* Transition durations — 8 tokens → --transition-duration-* (duration-*) */ + --transition-duration-ultra-fast: var(--duration-ultra-fast); + --transition-duration-faster: var(--duration-faster); + --transition-duration-fast: var(--duration-fast); + --transition-duration-normal: var(--duration-normal); + --transition-duration-gentle: var(--duration-gentle); + --transition-duration-slow: var(--duration-slow); + --transition-duration-slower: var(--duration-slower); + --transition-duration-ultra-slow: var(--duration-ultra-slow); + + /* Easing curves — 9 tokens → --ease-* (ease-*) */ + --ease-accelerate-max: var(--ease-accelerate-max); + --ease-accelerate-mid: var(--ease-accelerate-mid); + --ease-accelerate-min: var(--ease-accelerate-min); + --ease-decelerate-max: var(--ease-decelerate-max); + --ease-decelerate-mid: var(--ease-decelerate-mid); + --ease-decelerate-min: var(--ease-decelerate-min); + --ease-easy-ease-max: var(--ease-easy-ease-max); + --ease-easy-ease: var(--ease-easy-ease); + --ease-linear: var(--ease-linear); + + /* z-index (theme-absent tokens — fallbacks carried verbatim) — 8 tokens → --z-index-* (z-*) */ + --z-index-background: var(--z-index-background, 0); + --z-index-content: var(--z-index-content, 1); + --z-index-overlay: var(--z-index-overlay, 1000); + --z-index-popup: var(--z-index-popup, 2000); + --z-index-messages: var(--z-index-messages, 3000); + --z-index-floating: var(--z-index-floating, 4000); + --z-index-priority: var(--z-index-priority, 5000); + --z-index-debug: var(--z-index-debug, 6000); + + /* Stroke widths — width-namespace MIRROR of the --spacing-* hooks — 4 tokens → --border-width-* (border-* border-t/b/s/e/x/y-* divide-*) */ + --border-width-thin: var(--spacing-thin); + --border-width-thick: var(--spacing-thick); + --border-width-thicker: var(--spacing-thicker); + --border-width-thickest: var(--spacing-thickest); + + /* Stroke widths — width-namespace MIRROR of the --spacing-* hooks — 4 tokens → --outline-width-* (outline-*) */ + --outline-width-thin: var(--spacing-thin); + --outline-width-thick: var(--spacing-thick); + --outline-width-thicker: var(--spacing-thicker); + --outline-width-thickest: var(--spacing-thickest); + + /* Stroke widths — width-namespace MIRROR of the --spacing-* hooks — 4 tokens → --outline-offset-* (outline-offset-*) */ + --outline-offset-thin: var(--spacing-thin); + --outline-offset-thick: var(--spacing-thick); + --outline-offset-thicker: var(--spacing-thicker); + --outline-offset-thickest: var(--spacing-thickest); + + /* Stroke widths — width-namespace MIRROR of the --spacing-* hooks — 4 tokens → --text-decoration-thickness-* (decoration-*) */ + --text-decoration-thickness-thin: var(--spacing-thin); + --text-decoration-thickness-thick: var(--spacing-thick); + --text-decoration-thickness-thicker: var(--spacing-thicker); + --text-decoration-thickness-thickest: var(--spacing-thickest); +} + +/* + * REAL CUSTOM PROPERTIES — the one block in this file that emits declarations. + * + * `@theme inline` above registers utility NAMES and emits no variables, which is + * correct for every other namespace. The spacing namespace needs real variables: + * the border/outline/decoration widths have named utilities via the width-namespace + * mirrors above, but ring-width, divide-* values and underline-offset have no + * themable namespace and are authored as a direct var(--spacing-thin); and the + * `tokens.*` JS constants in @fluentui/tokens are var() reference strings against + * the canonical names emitted here (charts inline styles etc.). + * + * THE OLD camelCase NAMES (--colorNeutralBackground1, --spacingHorizontalM, …) + * ARE GONE for the ENTIRE token set: single + * vocabulary, documented major break for hand-written consumer CSS. Theming + * the static-theme model removed FluentProvider's runtime theme style tag, so this block + * (plus the per-theme classes in css/themes/) is the ONLY writer of token values. + * + * THEME-INVARIANT ONLY. Every value below is identical in all seven shipped themes + * (asserted by `analyzeThemeEmission`), so it belongs to the base sheet rather than + * to any one theme. The theme-VARIANT variables are NOT emitted here — there is no + * default theme; a consumer imports the theme file(s) they use and applies the class, + * exactly as Griffel makes them import `webLightTheme` and pass it to the provider. + * + * Emitted ONCE PER DOCUMENT (D13): `@reference` drops this block, so component + * `*.module.css` output stays free of theme declarations; css/emit.css compiles it + * into dist/base.css alongside --base-scale and --spacing, which utility-sourced + * spacing already depends on identically. + * + * `:root, :host` — not bare `:root` — matches the selector Tailwind emits its own + * `@theme` block on, so a shadow-DOM consumer that sees --base-scale/--spacing sees + * these too (verified in the compiled dist/styles.css). `.fui-scale-region` is + * GROUPED into the same rule: a scale region re-declares every invariant formula at + * the region element, so each one re-substitutes the region’s `--fui-scale` there + * (an inherited custom property arrives pre-substituted; only a re-declaration at or + * below the region can pick the local factor up). The Tailwind-owned scale axis is + * NOT repeated here — `@theme` already covers `:root`/`:host`; its mirror lives in + * the region-only rule below, the one place it is needed. + */ +@layer fui.theme { + :root, + :host, + .fui-scale-region { + /* + * Stroke widths — PUBLIC set-contract. Literal base-scale values, deliberately + * NOT coupled to the --spacing density knob: borders must not thin when layout + * density changes. + */ + --stroke-width-thin: calc(1px * var(--base-scale)); + --stroke-width-thick: calc(2px * var(--base-scale)); + --stroke-width-thicker: calc(3px * var(--base-scale)); + --stroke-width-thickest: calc(4px * var(--base-scale)); + + /* + * PRIVATE internal hooks — Tailwind utility generation only (w-thin, p-thick, …) + * and module-authored var(--spacing-thin) border/outline widths. Do NOT set or + * read these from consumer code; the public contract is --stroke-width-*. + */ + --spacing-thin: var(--stroke-width-thin); + --spacing-thick: var(--stroke-width-thick); + --spacing-thicker: var(--stroke-width-thicker); + --spacing-thickest: var(--stroke-width-thickest); + + /* Spacing — numeric-axis aliases; --spacing (see css/index.css) is the density knob. */ + --spacing-horizontal-none: 0; + --spacing-horizontal-xxs: calc(var(--spacing) * 2); + --spacing-horizontal-xs: calc(var(--spacing) * 4); + --spacing-horizontal-s-nudge: calc(var(--spacing) * 6); + --spacing-horizontal-s: calc(var(--spacing) * 8); + --spacing-horizontal-m-nudge: calc(var(--spacing) * 10); + --spacing-horizontal-m: calc(var(--spacing) * 12); + --spacing-horizontal-l: calc(var(--spacing) * 16); + --spacing-horizontal-xl: calc(var(--spacing) * 20); + --spacing-horizontal-xxl: calc(var(--spacing) * 24); + --spacing-horizontal-xxxl: calc(var(--spacing) * 32); + --spacing-vertical-none: 0; + --spacing-vertical-xxs: calc(var(--spacing) * 2); + --spacing-vertical-xs: calc(var(--spacing) * 4); + --spacing-vertical-s-nudge: calc(var(--spacing) * 6); + --spacing-vertical-s: calc(var(--spacing) * 8); + --spacing-vertical-m-nudge: calc(var(--spacing) * 10); + --spacing-vertical-m: calc(var(--spacing) * 12); + --spacing-vertical-l: calc(var(--spacing) * 16); + --spacing-vertical-xl: calc(var(--spacing) * 20); + --spacing-vertical-xxl: calc(var(--spacing) * 24); + --spacing-vertical-xxxl: calc(var(--spacing) * 32); + + /* + * Line heights — UNITLESS ratios of the 1:1 ramp pairing (Decision U), derived at + * generation time (--leading-base-300 = 20px/14px). Theme-INVARIANT: the type ramp + * is asserted byte-identical across all shipped themes, so these live in the base + * sheet, not the per-theme files. No --base-scale factor — the paired --text-* + * carries it, and a ratio multiplies the element’s own computed font-size. + */ + --leading-base-100: 1.4; + --leading-base-200: calc(16 / 12); + --leading-base-300: calc(20 / 14); + --leading-base-400: 1.375; + --leading-base-500: 1.4; + --leading-base-600: calc(32 / 24); + --leading-hero-700: calc(36 / 28); + --leading-hero-800: 1.25; + --leading-hero-900: 1.3; + --leading-hero-1000: calc(92 / 68); + } + + /* + * The scale-region knob. `--fui-scale` is a unitless factor on `--base-scale` + * (css/index.css: `calc(1rem / 16px * var(--fui-scale, 1))`), read off the + * element’s own data-fui-scale attribute via typed attr() — the same coercion + * the component packages use for data-size. No attribute coerces to the + * fallback 1, so a bare `.fui-scale-region` is inert. Because the factor is an + * attribute read on THIS element, nested regions replace the scale absolutely — + * they never compound. The class must be co-located with a theme class: theme + * tokens (type ramp, shadows) re-substitute only where their theme-class + * declarations re-apply, which is what FluentProvider’s ScaleRegion component + * arranges. + */ + .fui-scale-region { + --fui-scale: attr(data-fui-scale type(), 1); + + /* + * Scale axis — mirrored from css/index.css’s `@theme static` block (the + * generator reads the values out of that file; see readScaleAxisDeclarations). + * Tailwind emits these at `:root, :host` only — covered there — so the mirror + * exists ONLY here, where the region’s `--fui-scale` must reach it. + */ + --base-scale: calc(1rem / 16px * var(--fui-scale, 1)); + --spacing: calc(1px * var(--base-scale)); + --text-icon-12: calc(12px * var(--base-scale)); + --text-icon-16: calc(16px * var(--base-scale)); + --text-icon-20: calc(20px * var(--base-scale)); + --text-icon-24: calc(24px * var(--base-scale)); + --text-icon-28: calc(28px * var(--base-scale)); + --text-icon-32: calc(32px * var(--base-scale)); + --text-icon-48: calc(48px * var(--base-scale)); + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/utilities.css b/packages/react-components/react-tailwind-theme-preview/css/utilities.css new file mode 100644 index 0000000000000..6bc69da9fc756 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/utilities.css @@ -0,0 +1,85 @@ +/* + * Focus-ring utilities (CSS replacement for react-tabster's Griffel focus factories). + * + * `@utility` on purpose: `@apply` inlines the declarations at the call site inside the + * caller's `fui.*` layer — nothing lands in Tailwind's `utilities` layer, which would + * outrank every component layer. Declarations only; the caller supplies the selector via + * the `focus-visible` / `focus-within` variants. + * + * Parameterisation: the `--fui-focus-*` custom properties are knobs with `var()` + * fallbacks reproducing the compiled Griffel defaults. Each utility resets its knobs to + * `initial` (guaranteed-invalid for unregistered properties) so an ancestor's knob never + * leaks into a nested component (SplitButton nests Buttons); override a knob AFTER the + * `@apply`, from a higher layer, or (for fui-focus-outline) on the ::after itself. + * + * Knobs are deliberately NOT `@property`-registered: any non-empty registry puts Blink's + * transition-start on a page-global slow path (~9.4 µs per started transition). Do not + * reintroduce `@property` without re-running that perf gate. + */ + +/* + * Numeric font sizes on the base-scale axis: `text-18` → `calc(18px * var(--base-scale))`. + * + * The counterpart of the numeric SPACING axis. `p-12` compiles to `calc(var(--spacing) * 12)` + * and so reads as px while computing through `--base-scale`; font sizes had no such axis, so a + * size with no named Fluent token had to be written as a bracket escape + * (`text-[length:calc(18px*var(--base-scale))]`). Those spellings restate the scale mechanism at + * every call site, which is exactly where it drifts — a hand-written bracket form that forgets + * `var(--base-scale)` is invisible at the default root and silently freezes at every other. + * + * NAMESPACE, and why it does not collide. This is a FUNCTIONAL utility matching a bare integer + * only: `--value(integer)` rejects any candidate that is not one, so `text-base-300`, + * `text-icon-16` and `text-neutral-foreground-1` do not match it and fall through to the core + * `text-*` utility, which resolves them against `--text-*` / `--color-*` as before. Fluent + * registers no bare-numeric `--text-` key, so the integer space is unclaimed — verified by + * building the windmod package and diffing the compiled declarations, not by reading the docs. + * + * Scope: sizes with no named token. A size that HAS one (`text-base-300`, `text-icon-16`) must + * still use it — the token carries the design intent, `text-14` merely carries a number. + */ +@utility text-* { + font-size: calc(--value(integer) * 1px * var(--base-scale)); +} + +/* Default ring (`createFocusOutlineStyle()`): a ::after inset by the outline width. */ +@utility fui-focus-outline { + --fui-focus-outline-width: initial; + --fui-focus-outline-color: initial; + --fui-focus-outline-radius: initial; + --fui-focus-outline-offset: initial; + + @apply border-transparent; + + @variant after { + @apply pointer-events-none absolute z-1; + + border: var(--fui-focus-outline-width, var(--stroke-width-thick)) solid + var(--fui-focus-outline-color, var(--color-stroke-focus-2)); + border-radius: var(--fui-focus-outline-radius, var(--radius-medium)); + inset: calc(0px - var(--fui-focus-outline-width, var(--stroke-width-thick)) - var(--fui-focus-outline-offset, 0px)); + } + + @variant forced-colors { + @variant after { + @apply border-[Highlight]; + } + } +} + +/* Button-family ring (`createCustomFocusIndicatorStyle()`): own border + inset shadow. + Declaration order matters against a lower-layer `border:` shorthand. */ +@utility fui-focus-ring { + --fui-focus-ring-color: initial; + --fui-focus-ring-radius: initial; + --fui-focus-ring-width: initial; + --fui-focus-ring-inset-width: initial; + + border-color: var(--fui-focus-ring-color, var(--color-stroke-focus-2)); + border-radius: var(--fui-focus-ring-radius, var(--radius-medium)); + border-width: var(--fui-focus-ring-width, var(--stroke-width-thin)); + + @apply z-1 outline-thick outline-transparent-stroke outline-solid; + + box-shadow: 0 0 0 var(--fui-focus-ring-inset-width, var(--spacing-thin)) + var(--fui-focus-ring-color, var(--color-stroke-focus-2)) inset; +} diff --git a/packages/react-components/react-tailwind-theme-preview/css/variants.css b/packages/react-components/react-tailwind-theme-preview/css/variants.css new file mode 100644 index 0000000000000..f7dabe8f99d4f --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/css/variants.css @@ -0,0 +1,169 @@ +/** + * Custom variant catalog — the single place state selectors are defined; never define + * variants in component modules. Names follow the headless data-attribute vocabulary. + * + * Every entry is the canonical `&:where(…)` form: zero added specificity, and Tailwind + * rewrites it into `group-/` automatically, so every entry composes with + * named groups. Ancestor-form entries silently stop group-composing — do not add one. + * At-rule variants (forced-colors) cannot take a group prefix; nest the group variant + * inside them. No union entries: comma-separate variants at the call site + * (@variant hover, focus { … }). + * + * A catalog entry is vocabulary, not a usage index, so having no in-repo consumer is not by + * itself a reason to delete one. Three kinds of entry stay unconsumed on purpose: names that + * shadow a Tailwind built-in whose selector is narrower and not `:where()`-flat (`invalid`, + * `required`, `read-only`, `ltr` — dropping the entry silently substitutes the built-in); the + * unused half of a polarity pair (`focused`, `not-selected`, `not-indeterminate`, + * `has-actions`); and the unstyled member of a live enumeration (`layout-horizontal`, + * `layout-horizontal-stacked`, `layout-singleline`). A positive entry consumed only through + * Tailwind's automatic `not-` compound (`scroll-state-none` via `@variant not-scroll-state-none`) + * is load-bearing, not unconsumed — the derivation needs it and emits the same zero specificity. + * Delete an entry only when its selector can match nothing the tree renders, or when it can + * never be false. + */ + +/* direction (computed, correct under nesting) */ +@custom-variant rtl (&:where(:dir(rtl))); +@custom-variant ltr (&:where(:dir(ltr))); + +/* orientation. The headless library spells vertical orientation two ways: an enumerated + * data-orientation (Divider, TabList, ColorSlider, AlphaSlider) and a boolean data-vertical + * (Slider, Toolbar and its parts). Separate entries, one spelling each. */ +@custom-variant vertical (&:where([data-orientation='vertical'])); +@custom-variant horizontal (&:where([data-orientation='horizontal'])); +@custom-variant vertical-flag (&:where([data-vertical])); + +/* content alignment along the main axis */ +@custom-variant align-content-start (&:where([data-align-content='start'])); +@custom-variant align-content-center (&:where([data-align-content='center'])); +@custom-variant align-content-end (&:where([data-align-content='end'])); + +/* anchored-surface placement — data-placement on the positioned surface: above|below|before|after main axis, -start/-end/-top/-bottom alignment, suffix-less = centered */ +@custom-variant placement-above (&:where([data-placement^='above'])); +@custom-variant placement-below (&:where([data-placement^='below'])); +@custom-variant placement-before (&:where([data-placement^='before'])); +@custom-variant placement-after (&:where([data-placement^='after'])); +@custom-variant placement-start (&:where([data-placement$='-start'])); +@custom-variant placement-end (&:where([data-placement$='-end'])); +@custom-variant placement-top (&:where([data-placement$='-top'])); +@custom-variant placement-bottom (&:where([data-placement$='-bottom'])); +@custom-variant placement-above-centered (&:where([data-placement='above'])); +@custom-variant placement-below-centered (&:where([data-placement='below'])); +@custom-variant placement-before-centered (&:where([data-placement='before'])); +@custom-variant placement-after-centered (&:where([data-placement='after'])); + +/* generic states (native/ARIA + data- spellings of one state per entry) */ +@custom-variant disabled (&:where([disabled], [data-disabled], :disabled)); +@custom-variant enabled (&:where(:not([disabled], [data-disabled], :disabled))); +@custom-variant checked (&:where([data-checked], :checked)); +@custom-variant not-checked (&:where(:not([data-checked], :checked))); +@custom-variant checked-mixed (&:where([data-checked='mixed'], [aria-checked='mixed'])); +@custom-variant not-checked-mixed (&:where(:not([data-checked='mixed'], [aria-checked='mixed']))); +@custom-variant selected (&:where([data-selected], [aria-selected='true'])); +@custom-variant not-selected (&:where(:not([data-selected], [aria-selected='true']))); +@custom-variant multiselect (&:where([data-multiselect])); +@custom-variant current (&:where([data-current], [aria-current]:not([aria-current='false']))); +@custom-variant open (&:where([open], [data-open], :open)); +@custom-variant expanded (&:where([data-expanded], [aria-expanded='true'])); +@custom-variant invalid (&:where([data-invalid], :invalid, [aria-invalid='true'])); +@custom-variant required (&:where([required], [data-required], :required)); +@custom-variant read-only (&:where([readonly], [data-readonly], :read-only)); +@custom-variant placeholder-shown (&:where([data-placeholder], :placeholder-shown)); +@custom-variant indeterminate (&:where([data-indeterminate], :indeterminate)); +@custom-variant not-indeterminate (&:where(:not([data-indeterminate], :indeterminate))); + +/* size scale. Numeric data-size values get NO variants (size-16 would read as a + * Tailwind utility) — coerce with typed attr(): calc(attr(data-size type()) * 1px) */ +@custom-variant size-extra-tiny (&:where([data-size='extra-tiny'])); +@custom-variant size-tiny (&:where([data-size='tiny'])); +@custom-variant size-extra-small (&:where([data-size='extra-small'])); +@custom-variant size-small (&:where([data-size='small'])); +@custom-variant size-medium (&:where([data-size='medium'])); +@custom-variant size-large (&:where([data-size='large'])); +@custom-variant size-extra-large (&:where([data-size='extra-large'])); +@custom-variant size-huge (&:where([data-size='huge'])); +@custom-variant size-full (&:where([data-size='full'])); + +/* icon slot states */ +@custom-variant icon-only (&:where([data-icon-only])); +@custom-variant not-icon-only (&:where(:not([data-icon-only]))); +@custom-variant with-icon (&:where([data-icon-position])); +@custom-variant icon-before (&:where([data-icon-position='before'])); +@custom-variant icon-after (&:where([data-icon-position='after'])); +@custom-variant icon-present (&:where([data-icon])); +@custom-variant not-icon-present (&:where(:not([data-icon]))); + +/* label position. Every producer stamps data-label-position unconditionally, so a + * presence-only entry over it can never be false — select on the value. */ +@custom-variant label-above (&:where([data-label-position='above'])); +@custom-variant label-after (&:where([data-label-position='after'])); +@custom-variant label-before (&:where([data-label-position='before'])); +@custom-variant label-below (&:where([data-label-position='below'])); + +/* description presence */ +@custom-variant with-description (&:where([data-description])); +@custom-variant not-with-description (&:where(:not([data-description]))); + +/* layout */ +@custom-variant layout-vertical (&:where([data-layout='vertical'])); +@custom-variant layout-horizontal (&:where([data-layout='horizontal'])); +@custom-variant layout-horizontal-stacked (&:where([data-layout='horizontal-stacked'])); +@custom-variant layout-singleline (&:where([data-layout='singleline'])); +@custom-variant layout-multiline (&:where([data-layout='multiline'])); +@custom-variant layout-grid (&:where([data-layout='grid'])); + +/* per-component booleans */ +@custom-variant inset (&:where([data-inset])); +@custom-variant interactive (&:where([data-interactive])); +@custom-variant inline (&:where([data-inline])); +@custom-variant multiline (&:where([data-multiline])); +@custom-variant content-before (&:where([data-content-before])); +@custom-variant content-after (&:where([data-content-after])); +@custom-variant has-actions (&:where([data-has-actions])); +@custom-variant not-has-actions (&:where(:not([data-has-actions]))); +@custom-variant submenu-open (&:where([data-submenu-open])); + +/* interaction */ +@custom-variant hover (&:where(:hover)); +@custom-variant focus (&:where(:focus)); +@custom-variant focused (&:where([data-focused])); +@custom-variant not-focused (&:where(:not([data-focused]))); +@custom-variant not-focus-within (&:where(:not(:focus-within))); +@custom-variant active (&:where(:active)); +@custom-variant hover-active (&:where(:hover:active, :active:focus-visible)); +@custom-variant at-rest (&:where(:not(:hover, :active:focus-visible))); + +/* disabled family. disabled-native is the narrowest: only native form controls match it. A + * component that renders as either a button or an anchor needs it to reach the button form + * alone, since disabled and disabled-control also match the data-disabled an anchor carries. */ +@custom-variant disabled-focusable (&:where([data-disabled-focusable])); +@custom-variant disabled-native (&:where(:disabled)); +@custom-variant disabled-control (&:where([disabled], [data-disabled], :disabled, [aria-disabled='true'])); +@custom-variant enabled-control (&:where(:not([disabled], [data-disabled], :disabled, [aria-disabled='true']))); + +/* focus visibility. focus-within is the permissive native pseudo-class (the old keyborg + * attribute branch was subsumed by it — the union collapsed to :focus-within alone). + * focus-within-visible is keyboard-only via the browser's native modality heuristic: + * :has(:focus-visible) matches createFocusOutlineStyle for non-text controls (range, + * radio, checkbox — measured: no match on mouse click, match on keyboard and on a + * mouse-then-keyboard modality switch). Text-editable descendants always match, which + * is the native behavior text controls want anyway. */ +@custom-variant focus-visible (&:where([data-fui-focus-visible], :focus-visible)); +@custom-variant focus-within (&:where(:focus-within)); +@custom-variant focus-within-visible (&:where(:has(:focus-visible))); + +/* The active-descendant ring, written by @fluentui/react-aria under keyboard navigation only. + The attribute is identical on both implementations, so it needs a name rather than a mapping. */ +@custom-variant activedescendant-focusvisible (&:where([data-activedescendant-focusvisible])); + +/* content presence — data-empty mirrors 'no JSX children'; :empty is the unstamped fallback */ +@custom-variant empty (&:where([data-empty], :empty)); +@custom-variant not-empty (&:where(:not([data-empty], :empty))); + +/* content presence, nullish reading — data-content-empty mirrors 'children is null or undefined', + so the falsy-but-rendered `0` and `''` still count as present. Griffel needs both readings on + one element (CompoundButton gates its icon margin nullishly, Button's gate is falsiness), so + the two cannot share an attribute. No :empty fallback: the roots that carry it always have + slot children, and the unstamped case is exactly the one that must not match. */ +@custom-variant content-empty (&:where([data-content-empty])); +@custom-variant not-content-empty (&:where(:not([data-content-empty]))); diff --git a/packages/react-components/react-tailwind-theme-preview/package.json b/packages/react-components/react-tailwind-theme-preview/package.json new file mode 100644 index 0000000000000..845ef11ef518b --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/package.json @@ -0,0 +1,74 @@ +{ + "name": "@fluentui/react-tailwind-theme-preview", + "version": "0.1.0", + "description": "Fluent UI themes and design tokens as a Tailwind v4 CSS layer: cascade layer order, custom variants, the base-scale spacing bridge, the generated Fluent token registration (css/tokens.css, `@theme inline` utility names over kebab-case custom properties), and the 7 shipped themes as static CSS classes, one file each (css/themes/web-light.css …). Theming follows Griffel's contract exactly: import the theme-less base plus the theme file(s) you use, then apply that theme's class (`fui-theme-web-dark`) to any element to theme its subtree — there is NO baked default, so you pay only for the themes you ship. Spacing registers the 22 named tokens as aliases of the numeric axis (one density knob, --spacing); stroke widths keep literal base-scale values under --stroke-width-* on purpose (borders must not thin with layout density). The kebab-case variable namespace is self-contained by design — it does not interoperate with classic FluentProvider runtime theming (camelCase variables). Pairs with @fluentui/react-windmod-preview, and is equally usable for styling @fluentui/react-headless-components-preview directly.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + }, + "scripts": { + "build": "node build.js", + "generate-tokens-css": "node scripts/generate-tokens-css.js", + "verify-tokens-css": "node scripts/generate-tokens-css.js --check", + "generate-theme-values": "node scripts/generate-theme-values.js", + "verify-theme-values": "node scripts/generate-theme-values.js --check", + "test-spacing-behavior": "node scripts/probe-spacing-behavior.mjs", + "prepack": "node build.js" + }, + "style": "css/index.css", + "//exports": "HAND-AUTHORED ASSET SUBPATHS — every entry below is CSS or a hand written theme-class-names module; this package ships no TypeScript entry points at all, so none of them can be derived from source. The `export-maps-sync` Nx generator (tools/workspace-plugin, wired into CI as `yarn nx sync:check`) rebuilds a project's whole map from project.json's metadata.exportMap and keeps only what that block declares. Two things keep this map safe. First, the generator skips any project that resolves zero source entry points — with no src/index.ts and no subpath globs there is no `main`/`module`/`typings` to own and no map to derive, so it leaves the package entirely alone. Second, this package is not tagged `vNext` + `platform:web`, so it is out of the generator's scope regardless. If it is ever tagged for the vNext publish pipeline, the first reason still holds; and should it ever grow a TypeScript entry point, declare these keys in metadata.exportMap.staticSubpaths — `./base.css`, `./styles.css`, all seven `./themes/*.css`, `./theme-class-names` and `./css/*` — so the generator reads them back verbatim instead of dropping them. Verified after the per-theme split: `yarn nx sync` still leaves this package untouched (no project.json, zero resolved source entry points, untagged).", + "//exports:themes": "PER-THEME SUBPATHS ARE DECLARED EXPLICITLY, not as a `./themes/*` wildcard. Seven keys are cheap, and the point of the split is that a consumer can SEE what they are choosing between — a wildcard hides the catalog and would resolve names that are not shipped themes. The stems are derived from the theme class names in theme-class-names.mjs (`fui-theme-web-light` -> `web-light`), which the generator asserts on every run, so these keys, the emitted files and the class constants cannot drift apart.", + "exports": { + ".": "./css/index.css", + "./base.css": "./dist/base.css", + "./styles.css": "./dist/styles.css", + "./themes/web-light.css": "./dist/themes/web-light.css", + "./themes/web-dark.css": "./dist/themes/web-dark.css", + "./themes/teams-light.css": "./dist/themes/teams-light.css", + "./themes/teams-dark.css": "./dist/themes/teams-dark.css", + "./themes/teams-high-contrast.css": "./dist/themes/teams-high-contrast.css", + "./themes/teams-light-v21.css": "./dist/themes/teams-light-v21.css", + "./themes/teams-dark-v21.css": "./dist/themes/teams-dark-v21.css", + "./theme-class-names": { + "types": "./theme-class-names.d.mts", + "require": "./theme-class-names.cjs", + "default": "./theme-class-names.mjs" + }, + "./css/*": "./css/*", + "./package.json": "./package.json" + }, + "files": [ + "*.md", + "css", + "dist/base.css", + "dist/themes", + "dist/styles.css", + "theme-class-names.mjs", + "theme-class-names.cjs", + "theme-class-names.d.mts" + ], + "sideEffects": [ + "**/*.css" + ], + "nx": { + "name": "react-tailwind-theme-preview", + "targets": { + "build": { + "inputs": [ + "{projectRoot}/css/**", + "{projectRoot}/build.js" + ], + "outputs": [ + "{projectRoot}/dist" + ] + } + } + } +} diff --git a/packages/react-components/react-tailwind-theme-preview/scripts/generate-theme-values.js b/packages/react-components/react-tailwind-theme-preview/scripts/generate-theme-values.js new file mode 100644 index 0000000000000..fe61af01d7fca --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/scripts/generate-theme-values.js @@ -0,0 +1,116 @@ +// @ts-check +/** + * Emits `theme-values.json` — a committed snapshot of the 7 shipped theme objects' + * token values. + * + * WHY A SNAPSHOT: this package's generator + * (`scripts/generate-tokens-css.js`) deliberately has no dependency edge on this + * package's BUILD output — a fresh clone must be able to regenerate/verify its CSS + * without building anything. The theme VALUES, however, are computed + * (`createLightTheme(brandWeb)`, …), so they cannot be text-scraped from source the way + * `tokens.ts` is. This committed JSON is the bridge: the CSS generator reads it, and + * `--check` verifies it against the built tokens package. + * + * Regenerating (when a theme value intentionally changes): `yarn nx run tokens:build`, + * then `node scripts/generate-theme-values.js` from this package. + * + * `--check` exits non-zero when the committed file is missing or stale. + */ + +const fs = require('fs'); +const path = require('path'); + +const PACKAGE_ROOT = path.resolve(__dirname, '..'); +const REPO_ROOT = path.resolve(PACKAGE_ROOT, '..', '..', '..'); +const OUTPUT = path.join(PACKAGE_ROOT, 'theme-values.json'); + +/** The shipped themes. Keep in sync with `src/themes/index.ts` and `src/themes/themeClassNames.ts`. */ +const THEME_NAMES = [ + 'webLightTheme', + 'webDarkTheme', + 'teamsLightTheme', + 'teamsDarkTheme', + 'teamsHighContrastTheme', + 'teamsLightV21Theme', + 'teamsDarkV21Theme', +]; + +function loadThemes() { + const libEntry = path.join(REPO_ROOT, 'packages', 'tokens', 'lib-commonjs', 'index.cjs'); + + if (!fs.existsSync(libEntry)) { + throw new Error( + `Cannot find built output at ${libEntry}. Run \`yarn nx run tokens:build\` first — ` + + 'theme values are computed (createLightTheme etc.), so the snapshot is generated from the built package.', + ); + } + + // eslint-disable-next-line @typescript-eslint/no-require-imports + const tokensPackage = require(libEntry); + /** @type {Record>} */ + const themes = {}; + + for (const name of THEME_NAMES) { + const theme = tokensPackage[name]; + + if (!theme || typeof theme !== 'object') { + throw new Error(`Theme \`${name}\` is missing from the built package — shipped-theme set drifted.`); + } + + themes[name] = theme; + } + + return themes; +} + +function render() { + const themes = loadThemes(); + const packageJson = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')); + + const document = { + $schema: 'fluent-theme-values', + description: + 'Committed snapshot of the shipped Fluent theme objects (theme key -> value). ' + + 'Consumed by scripts/generate-tokens-css.js to emit the static theme CSS classes. ' + + 'Regenerate after yarn nx run tokens:build.', + generatedBy: 'packages/react-components/react-tailwind-theme-preview/scripts/generate-theme-values.js', + source: `${packageJson.name}@${packageJson.version}`, + themes, + }; + + return `${JSON.stringify(document, null, 2)}\n`; +} + +function main() { + const args = process.argv.slice(2); + const check = args.includes('--check'); + const contents = render(); + + if (check) { + if (!fs.existsSync(OUTPUT)) { + console.error(`MISSING: ${OUTPUT} — run \`node scripts/generate-theme-values.js\`.`); + process.exitCode = 1; + return; + } + + const existing = fs.readFileSync(OUTPUT, 'utf8').replace(/\r\n/g, '\n'); + + if (existing !== contents) { + console.error(`STALE: ${OUTPUT} — run \`node scripts/generate-theme-values.js\`.`); + process.exitCode = 1; + return; + } + + console.log(`OK: ${OUTPUT} is current.`); + return; + } + + fs.writeFileSync(OUTPUT, contents); + console.log(`Wrote ${OUTPUT}.`); +} + +if (require.main === module) { + main(); +} + +module.exports = { THEME_NAMES, loadThemes, render, OUTPUT }; diff --git a/packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js b/packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js new file mode 100644 index 0000000000000..9f8c971388a07 --- /dev/null +++ b/packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js @@ -0,0 +1,1652 @@ +// @ts-check +/** + * Generates `css/tokens.css` — the `@theme inline reference` registration giving every + * Fluent token a Tailwind utility name (`bg-neutral-background-1`, `rounded-medium`, …) + * plus the theme-INVARIANT `:root, :host` values — and `css/themes/.css`, ONE FILE + * PER SHIPPED THEME, each carrying exactly its own class. `css/themes.css` is a generated + * all-seven aggregate of `@import`s for the two entries that want the whole catalog. + * + * There is NO DEFAULT THEME anywhere in the output — see the note below the constants. + * + * `inline` substitutes the token variable into each utility so it resolves per-element; + * a plain `@theme` alias would freeze resolution at `:root`. `reference` suppresses the + * self-referential alias emission that self-named registrations (`--color-x: var(--color-x)`) + * would otherwise produce (~19KB of cyclic declarations). + * + * Spacing: the 22 spacing tokens register as aliases of the numeric axis + * (`calc(var(--spacing) * N)`), so named and numeric spacing share one density knob. + * The 4 strokeWidth tokens keep literal base-scale values under `--stroke-width-*` + * (borders must not thin with layout density); `--spacing-thin/…` are private hooks + * feeding utility generation. All of these are also emitted as real custom properties, + * because border/outline/ring/decoration widths do not consume the spacing namespace. + * + * tokens.ts is consumed as the token-name inventory; theme VALUES come from the + * committed theme-values.json snapshot; the emitted class names are asserted against + * theme-class-names.mjs. + * + * Run: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js + * Verify: node packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js --check + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const PACKAGE_ROOT = path.resolve(__dirname, '..'); +const REPO_ROOT = path.resolve(PACKAGE_ROOT, '..', '..', '..'); +const TOKENS_PACKAGE = path.join(REPO_ROOT, 'packages', 'tokens'); +const TOKENS_SOURCE = path.join(TOKENS_PACKAGE, 'src', 'tokens.ts'); +const SPACINGS_SOURCE = path.join(TOKENS_PACKAGE, 'src', 'global', 'spacings.ts'); +const STROKE_WIDTHS_SOURCE = path.join(TOKENS_PACKAGE, 'src', 'global', 'strokeWidths.ts'); +const THEME_VALUES_SOURCE = path.join(PACKAGE_ROOT, 'theme-values.json'); +const THEME_CLASSNAMES_SOURCE = path.join(PACKAGE_ROOT, 'theme-class-names.mjs'); +const DEFAULT_OUTPUT = path.join(PACKAGE_ROOT, 'css', 'tokens.css'); +const THEMES_DIR = path.join(PACKAGE_ROOT, 'css', 'themes'); +/** Aggregate that pulls in all seven — the monolith build entry's one import. */ +const THEMES_OUTPUT = path.join(PACKAGE_ROOT, 'css', 'themes.css'); + +/** + * NO DEFAULT THEME — operator ruling 2026-08-28 ("Theme delivery"). + * + * Windmod mirrors Griffel's contract exactly: Griffel makes the consumer + * `import { webLightTheme }` and pass it to ``, with no baked + * default (tokens are simply unset without one). So `:root, :host` carries ONLY the + * theme-INVARIANT values here, and every theme's 433 variables live in their own file + * under `css/themes/`, reaching a document only when the consumer imports that file and + * applies its class. + * + * The previous design emitted `webLightTheme` at `:root, :host`, which made light free and + * every other theme a surcharge. Removing it makes the cost symmetric, the contract + * explicit, and the pay-per-theme story real. + */ + +const GENERATOR_ID = 'packages/react-components/react-tailwind-theme-preview/scripts/generate-tokens-css.js'; + +/** The `@theme` modifiers the emitted block carries. See the module header for why. */ +const THEME_MODIFIERS = 'inline reference'; + +/** + * camelCase → kebab-case, with digit runs and acronym runs as their own segments. + * + * colorNeutralBackground1 → color-neutral-background-1 + * fontSizeHero1000 → font-size-hero-1000 + * borderRadius2XLarge → border-radius-2-x-large + * shadow2Brand → shadow-2-brand + * + * NOT used for the spacing namespace — those names come from SPACING_SCALE's pinned table. + * + * @param {string} name + * @returns {string} + */ +function kebabCase(name) { + return name + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // lower|digit → Upper + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // ACRONYM → Word + .replace(/([A-Za-z])([0-9])/g, '$1-$2') // letter → digit + .replace(/([0-9])([A-Za-z])/g, '$1-$2') // digit → letter + .toLowerCase(); +} + +/** + * Fluent's spacing scale — the ONE namespace whose registered values are NOT + * `var(--fluentToken)` references (numeric-axis aliases). + * + * WHY NUMERIC-AXIS ALIASES AND NOT `var(--spacingHorizontalM)` OR PX LITERALS + * --------------------------------------------------------------------------- + * The numeric spacing utilities (`p-12`, `gap-8`) compile to + * `calc(var(--spacing) * N)` — they compute px→rem through the `--spacing` base + * (`calc(1px * var(--base-scale))`, css/index.css). Registering the semantic tokens as + * runtime token `var()` references would create two spacing systems that scale + * differently: `p-12` would follow the user's root font-size while `p-horizontal-m` + * stayed frozen at the provider's literal px. Registering each token as + * `calc(var(--spacing) * )` — multiplier = canonical px / SPACING_BASE_PX — + * makes BOTH scales aliases of ONE axis: `p-horizontal-m` and `p-12` emit the same + * calc shape, compute the same length at default, and respond identically to a + * `--spacing` override on any subtree (`--spacing` is the single density knob). + * The earlier literal form (`calc(12px * var(--base-scale))`) was arithmetically + * identical at default but bypassed `--spacing`, so a subtree density override reached + * numeric utilities and missed named ones. Probe-verified that `@theme inline` carries + * the `var(--spacing)` reference verbatim into utilities. + * + * COST, ACCEPTED: a FluentProvider `theme` override of `spacingHorizontalM` no longer + * reaches utility-sourced spacing (a literal `var(--spacingHorizontalM)` still honours it, + * but those are now FORBIDDEN in component modules — see CONVERSION_GUIDE). All 7 shipped + * themes carry byte-identical spacing values (asserted: every theme spreads the same + * `horizontalSpacings`/`verticalSpacings` objects), so no shipped theme changes behavior. + * + * ALSO ACCEPTED: `--spacing-*` is axis-agnostic, so `py-horizontal-m` compiles. The axis in + * the name is a convention, not a constraint — the same is already true of a literal + * `var(--spacingHorizontalM)` in a `padding-block` declaration. + * + * `utility` is the EXPLICIT suffix table. It deliberately does NOT go + * through `kebabCase` — that would turn `SNudge` into `s-nudge` correctly but `XXS` into + * `xxs` only by accident of the acronym rule; pinning the table keeps these names stable + * and independent of the generic algorithm. + * + * `global` is the key in `packages/tokens/src/global/spacings.ts`; `px` is asserted against + * it at generation time so a scale change upstream trips the build instead of silently + * desyncing the utilities from the tokens. + * + * @type {{ suffix: string, utility: string, global: string, px: number }[]} + */ +const SPACING_SCALE = [ + { suffix: 'None', utility: 'none', global: 'none', px: 0 }, + { suffix: 'XXS', utility: 'xxs', global: 'xxs', px: 2 }, + { suffix: 'XS', utility: 'xs', global: 'xs', px: 4 }, + { suffix: 'SNudge', utility: 's-nudge', global: 'sNudge', px: 6 }, + { suffix: 'S', utility: 's', global: 's', px: 8 }, + { suffix: 'MNudge', utility: 'm-nudge', global: 'mNudge', px: 10 }, + { suffix: 'M', utility: 'm', global: 'm', px: 12 }, + { suffix: 'L', utility: 'l', global: 'l', px: 16 }, + { suffix: 'XL', utility: 'xl', global: 'xl', px: 20 }, + { suffix: 'XXL', utility: 'xxl', global: 'xxl', px: 24 }, + { suffix: 'XXXL', utility: 'xxxl', global: 'xxxl', px: 32 }, +]; + +/** + * Fluent's stroke widths — border/outline/divider thickness — joining the SAME `--spacing-*` + * namespace as SPACING_SCALE, on the same literal-value terms (). + * + * THE SPACING NAMESPACE PLUS FOUR WIDTH-NAMESPACE MIRRORS + * ------------------------------------------------------- + * PROBE-MEASURED against the installed registry (v4.3.3), not reasoned: + * CONSUMES `--spacing-*` p m gap space-x w h min-w max-w size inset top start basis + * translate scroll-m scroll-p indent leading (+ every axis/side form) + * OWN NAMESPACES border-* (+ every side form; divide-* reads the same + * `--border-width` key) → `--border-width-*`; outline-* → + * `--outline-width-*`; outline-offset-* → `--outline-offset-*`; + * decoration-* → `--text-decoration-thickness-*` + * NO NAMESPACE ring ring-offset inset-ring underline-offset stroke + * + * Spacing registration buys the dimensional utilities (`w-thin`, `h-thick`, `p-thin`, + * `gap-thin`); the width-namespace MIRRORS (`mirrorNamespaces` on the NAMESPACES entry) buy + * the border-ish named forms (`border-thin`, `outline-thick`, `-outline-offset-thickest`, + * `decoration-thin`). Each mirror registers `var(--spacing-)`, so a named utility + * compiles byte-identically to the `border-(length:--spacing-thin)` var-reference spelling it + * replaces. The four steps are ALSO emitted as real custom properties (`emit: true` on the + * NAMESPACES entry) for the ring/underline families that have no themable namespace and for + * raw `var(--spacing-thin)` declarations. `@theme inline` emits no variables. + * + * CANONICAL `--stroke-width-*` VALUES STAY LITERAL `calc( * var(--base-scale))` — + * DELIBERATELY NOT `--spacing`-COUPLED + * ----------------------------------------------------------------------------------- + * Theming the alias model () rebased the 22 spacing tokens onto the + * `--spacing` numeric axis; these four are the intentional exception. `--spacing` is the + * layout DENSITY knob — a subtree that halves it should compress padding and gaps, but + * borders must NOT thin with it: a 1px hairline is a 1px hairline at any density. Stroke + * widths therefore keep the raw `--base-scale` form and ignore `--spacing` overrides. + * + * NAME SPLIT: the PUBLIC set-contract variable is + * `--stroke-width-`, emitted with the literal value; the spacing-namespace + * `--spacing-` names are PRIVATE internal hooks registered/emitted as + * `var(--stroke-width-)` so Tailwind's spacing-consuming utility families + * (`w-thin`, `p-thick`, `gap-thicker`, …) and module-authored `var(--spacing-thin)` + * border/outline widths keep working. `--stroke-width-*` is deliberately NOT registered + * in `@theme` — Tailwind's `--stroke-width-*` namespace drives SVG `stroke-width` + * utilities, which would be the wrong property. + * + * `global` is the key in `packages/tokens/src/global/strokeWidths.ts`; `px` is asserted against + * it at generation time, exactly like SPACING_SCALE. + * + * @type {{ suffix: string, utility: string, global: string, px: number }[]} + */ +const STROKE_WIDTH_SCALE = [ + { suffix: 'Thin', utility: 'thin', global: 'strokeWidthThin', px: 1 }, + { suffix: 'Thick', utility: 'thick', global: 'strokeWidthThick', px: 2 }, + { suffix: 'Thicker', utility: 'thicker', global: 'strokeWidthThicker', px: 3 }, + { suffix: 'Thickest', utility: 'thickest', global: 'strokeWidthThickest', px: 4 }, +]; + +/** + * The px value of the `--spacing` numeric-axis base, as registered in css/index.css + * (`--spacing: calc(1px * var(--base-scale))`). Spacing-token multipliers are derived + * against it, so if the base ever changes, every multiplier recomputes — and the exactness + * guard in `spacingTokenValue` throws on any px value the new base cannot express exactly. + */ +const SPACING_BASE_PX = 1; + +/** + * The numeric-axis alias for a spacing step: `calc(var(--spacing) * )`, the + * same compiled shape as Tailwind's numeric utilities (`p-12` → `calc(var(--spacing) * 12)`), + * so named and numeric spacing respond identically to a `--spacing` override anywhere in + * the tree. Zero stays a plain `0` — a `calc(… * 0)` would be valid but noisy, and `0` is + * unitless-safe everywhere. + * + * The multiplier MUST reproduce the canonical px exactly at the default base; anything + * else is a silent visual change, so it throws. + * + * @param {{ px: number }} step + * @returns {string} + */ +function spacingTokenValue(step) { + const { px } = step; + if (px === 0) { + return '0'; + } + const multiplier = px / SPACING_BASE_PX; + if (multiplier * SPACING_BASE_PX !== px) { + throw new Error( + `Spacing step ${px}px is not exactly expressible as a multiple of the ${SPACING_BASE_PX}px --spacing base.`, + ); + } + return `calc(var(--spacing) * ${multiplier})`; +} + +/** + * The canonical (public set-contract) variable name for a stroke width step. + * + * @param {{ utility: string }} step + * @returns {string} + */ +function strokeWidthCanonicalName(step) { + return `--stroke-width-${step.utility}`; +} + +/** + * The canonical `--stroke-width-*` value: literal base-scale, deliberately NOT + * `--spacing`-coupled — borders must not thin when layout density changes (see the + * STROKE_WIDTH_SCALE doc block). + * + * @param {{ px: number }} step + * @returns {string} + */ +function strokeWidthCanonicalValue(step) { + return step.px === 0 ? '0' : `calc(${step.px}px * var(--base-scale))`; +} + +/** + * The scale-axis declarations css/index.css owns (`@theme static`): `--base-scale`, the + * numeric `--spacing` axis, and the `--text-icon-*` glyph sizes. + * + * Tailwind emits `@theme` values at `:root, :host` — a fixed selector — while the scale-region + * mechanism needs every scale-riding formula re-declared AT the region element so it + * re-substitutes the region's `--fui-scale` (an inherited custom property is already + * substituted where its declaration wins; descendants inherit the resolved stream). The + * region-only `.fui-scale-region` rule therefore repeats these three families — ONLY there: + * at `:root`/`:host` Tailwind's own emission already covers them, so repeating them in the + * grouped rule would be a pure no-op (operator-caught 2026-09-02 and removed). They are READ + * out of css/index.css rather than duplicated as text, so a formula edit there flows into the + * next generator run and a drift fails `--check`. + * + * @returns {string[]} declaration lines, indented for the fui.theme block + */ +function readScaleAxisDeclarations() { + const source = fs.readFileSync(path.join(PACKAGE_ROOT, 'css', 'index.css'), 'utf8'); + const names = [ + '--base-scale', + '--spacing', + '--text-icon-12', + '--text-icon-16', + '--text-icon-20', + '--text-icon-24', + '--text-icon-28', + '--text-icon-32', + '--text-icon-48', + ]; + + return names.map(name => { + const match = source.match(new RegExp(`^ ${name}: ([^;\\n]+);`, 'm')); + if (!match) { + throw new Error( + `css/index.css no longer declares \`${name}\` on a single line in its @theme block — ` + + `the scale-region emission mirrors that declaration and cannot proceed without it.`, + ); + } + return ` ${name}: ${match[1]};`; + }); +} + +/** + * A theme value rewritten onto the `--base-scale` axis: `14px` → `calc(14px * var(--base-scale))`, + * the same literal form stroke widths use. Applies to every namespace carrying + * `baseScaled: true` with no `scaleValue` override (the type ramp — font sizes and their paired + * line heights; shadows are base-scaled too but composite, so they use shadowScaledValue) in EVERY theme, + * so the whole UI scales coherently with the root font size. Rendering is unchanged at the + * default 16px root, where `--base-scale` is 1. + * + * A non-px value would silently produce `calc( * var(--base-scale))`, so it throws instead. + * + * @param {string} tokenName + * @param {string} value the raw theme value, e.g. `14px` + * @returns {string} + */ +function baseScaledValue(tokenName, value) { + if (!/^\d+(?:\.\d+)?px$/.test(value)) { + throw new Error( + `Token \`${tokenName}\` is registered as base-scaled but its value \`${value}\` is not a plain px length.`, + ); + } + + return `calc(${value} * var(--base-scale))`; +} + +/** + * The unitless line-height ratio for a `lineHeight*` token: canonical ramp px divided by the + * 1:1-paired `fontSize*` token's px (Decision U — see the lineHeight NAMESPACES entry). + * + * Emits an exact decimal when the division is finite (`22px / 16px` → `1.375`) and an exact + * `calc(A / B)` fraction when it repeats (`20px / 14px` → `calc(20 / 14)`) — a rounded decimal + * would be a silent visual change, and calc division is exact by construction. Finiteness is + * decided arithmetically: the reduced fraction's denominator must factor into 2s and 5s only. + * + * @param {string} tokenName the lineHeight token, for errors + * @param {string} lineHeightValue raw theme value, e.g. `20px` + * @param {string} fontSizeValue the paired fontSize token's raw theme value, e.g. `14px` + * @returns {string} + */ +function unitlessLeadingValue(tokenName, lineHeightValue, fontSizeValue) { + const parsePx = (value, role) => { + const match = /^(\d+)px$/.exec(value); + if (!match) { + throw new Error( + `Token \`${tokenName}\`: ${role} value \`${value}\` is not an integer px length — ` + + 'the unitless ratio derivation only understands the integer-px type ramp.', + ); + } + return Number(match[1]); + }; + + const lineHeightPx = parsePx(lineHeightValue, 'line-height'); + const fontSizePx = parsePx(fontSizeValue, 'paired font-size'); + if (fontSizePx === 0) { + throw new Error(`Token \`${tokenName}\`: paired font-size is 0px — cannot form a ratio.`); + } + + const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b)); + let denominator = fontSizePx / gcd(lineHeightPx, fontSizePx); + while (denominator % 2 === 0) denominator /= 2; + while (denominator % 5 === 0) denominator /= 5; + + return denominator === 1 ? String(lineHeightPx / fontSizePx) : `calc(${lineHeightPx} / ${fontSizePx})`; +} + +/** + * A shadow theme value with every LENGTH rewritten onto the `--base-scale` axis, leaving the + * colour components untouched: `0 1px 2px rgba(0, 0, 0, .14)` → + * `0 calc(1px * var(--base-scale)) calc(2px * var(--base-scale)) rgba(0, 0, 0, .14)`. + * + * WHY SHADOWS SCALE, AND WHY THERE IS NO UNITLESS ALTERNATIVE + * ---------------------------------------------------------- + * An elevation is a proportional cue: `--shadow-16`'s 8px drop reads as "this surface floats" + * only relative to the box casting it. Once the type ramp and the spacing axis both follow the + * root font size, a frozen-px shadow shrinks visually as everything around it grows — at a 32px + * root the whole UI doubles while the shadow stays put, and the elevation ladder (2/4/8/16/28/64) + * compresses into visual noise. Unlike line-height, `box-shadow` has no unitless form, so the + * calc ramp is the only spelling available. + * + * `0` is left bare rather than rewritten to `calc(0px * …)`: it is already scale-invariant, and + * the shorter form keeps the emitted value readable. + * + * Anything that is neither a bare `0`, a px length, nor a colour function throws — a silent + * `calc( * var(--base-scale))` would be far worse than a failed build. + * + * @param {string} tokenName + * @param {string} value the raw theme value, e.g. `0 0 2px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.14)` + * @returns {string} + */ +function shadowScaledValue(tokenName, value) { + // Split on the commas SEPARATING layers, not the commas inside rgba(...). + const layers = []; + let depth = 0; + let current = ''; + for (const char of value) { + if (char === '(') depth++; + if (char === ')') depth--; + if (char === ',' && depth === 0) { + layers.push(current); + current = ''; + continue; + } + current += char; + } + layers.push(current); + + return layers + .map(layer => { + const parts = layer.trim().split(/\s+(?![^(]*\))/); + return parts + .map(part => { + if (part === '0') return part; + if (/^-?\d+(?:\.\d+)?px$/.test(part)) return `calc(${part} * var(--base-scale))`; + if (/^(?:rgba?|hsla?|color)\(/.test(part) || /^#[0-9a-f]{3,8}$/i.test(part)) return part; + throw new Error( + `Token \`${tokenName}\` is registered as base-scaled but its component \`${part}\` ` + + `(in \`${value}\`) is neither a bare 0, a px length, nor a colour.`, + ); + }) + .join(' '); + }) + .join(', '); +} + +/** + * A radius theme value on the `--base-scale` axis, with the two non-scaling members of the + * family carried through untouched: + * + * - `borderRadiusNone` is `0` — already scale-invariant, and `calc(0 * …)` would be noise; + * - `borderRadiusCircular` is the `10000px` fully-round SENTINEL: the browser clamps a + * border-radius to half the box at used-value time, so its rendered value never equals + * 10000px and multiplying it by `--base-scale` changes nothing while making the emitted + * value unrecognizable. It stays literal, and its value is asserted so an upstream retune + * to a real length trips the build instead of silently shipping unscaled. + * + * Every other step is a plain px length and rides the ramp via `baseScaledValue` + * (operator ruling 2026-09-02, superseding the scale batch's B-17 radii-off-the-ramp + * reading: radii scale "consistent with the rest of the system"). + * + * @param {string} tokenName + * @param {string} value the raw theme value, e.g. `4px` + * @returns {string} + */ +function radiusScaledValue(tokenName, value) { + if (value === '0') { + return value; + } + if (tokenName === 'borderRadiusCircular') { + if (value !== '10000px') { + throw new Error( + `Token \`borderRadiusCircular\` is \`${value}\`, not the 10000px fully-round sentinel this ` + + 'generator leaves unscaled. A real length here needs a decision: scale it or re-justify the carve-out.', + ); + } + return value; + } + return baseScaledValue(tokenName, value); +} + +/** + * The spacing-namespace PRIVATE hook value for a stroke width step: an alias of the + * canonical variable, so utility families and module-authored `var(--spacing-thin)` + * resolve through the one public definition. + * + * @param {{ utility: string, px: number }} step + * @returns {string} + */ +function strokeWidthValue(step) { + return `var(${strokeWidthCanonicalName(step)})`; +} + +/** + * Reads an object literal out of a `packages/tokens/src/global/*.ts` module and asserts the + * pinned table above still describes it — so an upstream scale change trips the build instead + * of silently desyncing the utilities from the tokens. Text extraction for the same reason as + * `readTokens`: no build step, no dependency edge on @fluentui/tokens. + * + * @param {object} options + * @param {string} options.source absolute path of the module to read + * @param {RegExp} options.declaration must capture the object body in group 1 + * @param {string} options.declarationText human-readable form of `declaration`, for errors + * @param {{ global: string, px: number }[]} options.table the pinned table to assert + * @param {string} options.tableName the table's identifier, for errors + * @returns {Record} + */ +function readGlobalScale({ source, declaration, declarationText, table, tableName }) { + const text = fs.readFileSync(source, 'utf8'); + + const match = declaration.exec(text); + if (!match) { + throw new Error(`${source}: expected a \`${declarationText}\` declaration.`); + } + + /** @type {Record} */ + const scale = {}; + const entry = /^[ \t]*([A-Za-z][A-Za-z0-9_]*):[ \t]*'([^']*)',[ \t]*$/gm; + let entryMatch; + while ((entryMatch = entry.exec(match[1])) !== null) { + scale[entryMatch[1]] = entryMatch[2]; + } + + const keys = Object.keys(scale); + if (keys.length !== table.length) { + throw new Error( + `${source}: parsed ${keys.length} steps but ${tableName} has ${table.length}. ` + + `Update ${tableName} in ${GENERATOR_ID}.`, + ); + } + + for (const { global: key, px } of table) { + const expected = px === 0 ? '0' : `${px}px`; + if (scale[key] !== expected) { + throw new Error( + `${source}: step \`${key}\` is \`${scale[key]}\` but ${tableName} says \`${expected}\`. ` + + `Update ${tableName} in ${GENERATOR_ID}.`, + ); + } + } + + return scale; +} + +/** + * Asserts SPACING_SCALE still matches the private `spacings` literal in packages/tokens. + * + * @returns {Record} + */ +function readSpacingScale() { + return readGlobalScale({ + source: SPACINGS_SOURCE, + declaration: /const spacings: SpacingTokens = \{([^}]*)\}/, + declarationText: 'const spacings: SpacingTokens = {', + table: SPACING_SCALE, + tableName: 'SPACING_SCALE', + }); +} + +/** + * Asserts STROKE_WIDTH_SCALE still matches the `strokeWidths` literal in packages/tokens. + * Same guarantee as `readSpacingScale`: these four values are hardcoded into the generated CSS, + * so an upstream change to 1/2/3/4px MUST throw rather than ship a silent divergence. + * + * @returns {Record} + */ +function readStrokeWidthScale() { + return readGlobalScale({ + source: STROKE_WIDTHS_SOURCE, + declaration: /export const strokeWidths: StrokeWidthTokens = \{([^}]*)\}/, + declarationText: 'export const strokeWidths: StrokeWidthTokens = {', + table: STROKE_WIDTH_SCALE, + tableName: 'STROKE_WIDTH_SCALE', + }); +} + +/** + * Reads `packages/tokens/src/themes/themeClassNames.ts` as text and extracts the shipped + * theme → class-name constants, asserting each class name equals its derivation from the + * theme export name (`fui-theme-` + kebab of the name minus `Theme`; digits attach to the + * preceding segment, so `teamsDarkV21Theme` → `fui-theme-teams-dark-v21` — deliberately + * NOT this file's `kebabCase`, whose letter→digit rule would produce `v-21`). + * + * Text extraction for the same reason as `readTokens`: no build step, no dependency edge + * on @fluentui/tokens. `themeClassNames.test.ts` in packages/tokens asserts the same + * derivation from the jest side. + * + * @returns {Record} theme export name → class name + */ +function readThemeClassNames() { + const text = fs.readFileSync(THEME_CLASSNAMES_SOURCE, 'utf8'); + + /** @type {Record} */ + const classNames = {}; + const entry = /^export const ([A-Za-z][A-Za-z0-9]*Theme)ClassName = '([^']+)';$/gm; + let match; + while ((match = entry.exec(text)) !== null) { + classNames[match[1]] = match[2]; + } + + if (Object.keys(classNames).length === 0) { + throw new Error(`${THEME_CLASSNAMES_SOURCE}: parsed zero theme class-name constants — the file shape changed.`); + } + + for (const [themeName, className] of Object.entries(classNames)) { + const base = themeName.replace(/Theme$/, ''); + const derived = `fui-theme-${base.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()}`; + if (className !== derived) { + throw new Error( + `${THEME_CLASSNAMES_SOURCE}: \`${themeName}ClassName\` is \`${className}\` but the derivation from the ` + + `export name is \`${derived}\`. The class names are derived, not free-form — fix one side.`, + ); + } + } + + return classNames; +} + +/** + * Reads the committed `packages/tokens/theme-values.json` snapshot — the VALUE source for + * the theme emission. The snapshot exists because theme values are + * computed (`createLightTheme(brandWeb)`, …) and so cannot be text-scraped the way + * `tokens.ts` is; `packages/tokens/src/themes/themeValues.test.ts` asserts on every jest + * run that it deep-equals the computed themes. + * + * @returns {Record>} theme export name → (token → value) + */ +function readThemeValues() { + if (!fs.existsSync(THEME_VALUES_SOURCE)) { + throw new Error( + `${THEME_VALUES_SOURCE} is missing. Run \`yarn workspace @fluentui/tokens generate-theme-values\` ` + + '(after building packages/tokens) and commit the result.', + ); + } + + const document = JSON.parse(fs.readFileSync(THEME_VALUES_SOURCE, 'utf8')); + + if (!document.themes || typeof document.themes !== 'object') { + throw new Error(`${THEME_VALUES_SOURCE}: expected a top-level \`themes\` object.`); + } + + return document.themes; +} + +/** + * Cross-asserts the three inputs of the theme emission and splits the token set: + * + * - the snapshot's theme set must equal the class-name constants' theme set; + * - every theme must carry exactly the same keys: every non-zIndex token, no extras + * (zIndex tokens are theme-absent by design — their defaults ride the `tokens.*` + * var() fallback); + * - every theme's spacing/stroke values must equal the pinned scales — those 26 tokens + * are THEME-INVARIANT and already emitted at `:root, :host` in their density-knob + * calc form, which a literal per-theme re-emission would silently break, so they are + * asserted identical and EXCLUDED from the per-theme classes; + * - every theme's lineHeight values AND their 1:1-paired fontSize values must be + * byte-identical across themes — the unitless `--leading-*` ratios (Decision U) are + * derived from the pairing and emitted ONCE at `:root, :host` in the base sheet, so a + * theme with a divergent ramp needs a design decision, not a silently wrong shared ratio. + * They are likewise EXCLUDED from the per-theme classes. + * + * @param {{ name: string, value: string }[]} tokens parsed tokens.ts entries + * @returns {{ + * variantTokens: { name: string, canonical: string, scaleValue: ((tokenName: string, value: string) => string) | null }[], + * leadingTokens: { name: string, canonical: string, value: string }[], + * themes: Record>, + * classNames: Record, + * }} + */ +function analyzeThemeEmission(tokens) { + const classNames = readThemeClassNames(); + const themes = readThemeValues(); + + const snapshotThemes = Object.keys(themes).sort(); + const constantThemes = Object.keys(classNames).sort(); + if (JSON.stringify(snapshotThemes) !== JSON.stringify(constantThemes)) { + throw new Error( + `Shipped-theme sets disagree: theme-values.json has [${snapshotThemes}] but themeClassNames.ts has ` + + `[${constantThemes}]. Regenerate the snapshot and/or update the constants.`, + ); + } + // No default-theme guard: there is no default theme (see the NO DEFAULT THEME note at the + // top). The set equality above already proves every named theme has a snapshot. + + /** @type {{ name: string, canonical: string, scaleValue: ((tokenName: string, value: string) => string) | null }[]} */ + const variantTokens = []; + /** @type {{ name: string, expected: string }[]} */ + const invariantTokens = []; + /** @type {string[]} */ + const themeAbsentTokens = []; + /** @type {{ name: string, canonical: string, fontSizeName: string, value: string }[]} */ + const leadingTokens = []; + + for (const { name } of tokens) { + const classification = classify(name); + if (classification.kind !== 'register') { + continue; + } + if (classification.group.scale) { + const px = classification.step.px; + invariantTokens.push({ name, expected: px === 0 ? '0' : `${px}px` }); + } else if (classification.group.prefix === 'zIndex') { + themeAbsentTokens.push(name); + } else if (classification.group.unitlessRatio === true) { + leadingTokens.push({ + name, + canonical: /** @type {string} */ (classification.canonical), + fontSizeName: `fontSize${name.slice(classification.group.prefix.length)}`, + value: '', // derived below, once the themes are proven to agree + }); + } else { + variantTokens.push({ + name, + canonical: /** @type {string} */ (classification.canonical), + // Namespaces on the base-scale axis carry a rewrite; `scaleValue` lets a namespace whose + // values are composite (shadows) override the plain-px default. + scaleValue: + classification.group.baseScaled === true ? (classification.group.scaleValue ?? baseScaledValue) : null, + }); + } + } + + const expectedKeys = [ + ...variantTokens.map(token => token.name), + ...invariantTokens.map(token => token.name), + ...leadingTokens.map(token => token.name), + ].sort(); + + for (const [themeName, theme] of Object.entries(themes)) { + const themeKeys = Object.keys(theme).sort(); + if (JSON.stringify(themeKeys) !== JSON.stringify(expectedKeys)) { + const missing = expectedKeys.filter(key => !(key in theme)); + const extra = themeKeys.filter(key => !expectedKeys.includes(key)); + throw new Error( + `Theme \`${themeName}\` keys drifted from tokens.ts (zIndex excluded): ` + + `missing [${missing}], extra [${extra}]. Regenerate theme-values.json.`, + ); + } + + for (const { name, expected } of invariantTokens) { + if (theme[name] !== expected) { + throw new Error( + `Theme \`${themeName}\` has \`${name}: ${theme[name]}\` but spacing/stroke tokens are theme-invariant ` + + `(pinned ${expected}). The density-knob emission at :root depends on this — a genuinely divergent ` + + 'theme needs a design decision, not a silent literal emission.', + ); + } + } + + for (const name of themeAbsentTokens) { + if (name in theme) { + throw new Error(`Theme \`${themeName}\` unexpectedly carries the theme-absent token \`${name}\`.`); + } + } + } + + // Unitless leading ratios (Decision U): prove the whole pairing is theme-invariant — + // BOTH halves, since a ratio derived from theme A silently misdescribes a theme whose + // line-height OR font-size diverges — then derive each ratio once. + const themeEntries = Object.entries(themes); + const [referenceThemeName, referenceTheme] = themeEntries[0]; + for (const token of leadingTokens) { + if (!(token.fontSizeName in referenceTheme)) { + throw new Error( + `Token \`${token.name}\` has no 1:1-paired \`${token.fontSizeName}\` in the theme snapshot — ` + + 'the unitless ratio derivation depends on the ramp pairing.', + ); + } + for (const [themeName, theme] of themeEntries) { + for (const key of [token.name, token.fontSizeName]) { + if (theme[key] !== referenceTheme[key]) { + throw new Error( + `Theme \`${themeName}\` has \`${key}: ${theme[key]}\` but \`${referenceThemeName}\` has ` + + `\`${referenceTheme[key]}\`. The type ramp must be theme-invariant for the shared unitless ` + + '`--leading-*` ratios in the base sheet to be correct — a genuinely divergent ramp needs a ' + + 'design decision, not a silently wrong shared ratio.', + ); + } + } + } + token.value = unitlessLeadingValue(token.name, referenceTheme[token.name], referenceTheme[token.fontSizeName]); + } + + return { variantTokens, leadingTokens, themes, classNames }; +} + +/** + * Token-name prefix → Tailwind theme namespace. Order matters: the FIRST matching entry + * wins, so longer prefixes that share a stem (fontFamily/fontSize/fontWeight) come first. + * + * `namespace` is the Tailwind theme key prefix; `utility` documents the utilities it feeds + * and is only used for the generated section comments. + * + * Every namespace below was read out of the installed Tailwind v4.3.3 utility registry + * (`node_modules/tailwindcss/dist/lib.mjs`), not guessed: + * duration → themeKeys ["--transition-duration"] (NOT "--duration") + * ease → themeKeys ["--ease"] + * z → themeKeys ["--z-index"] + * font → ["--font"] then ["--font-weight"] + * rounded → ["--radius"], text → ["--text"], leading → ["--leading"], shadow → ["--shadow"] + */ +const NAMESPACES = [ + { + prefix: 'color', + namespace: 'color', + utility: 'bg-* text-* border-* fill-* stroke-* outline-* ring-* decoration-*', + heading: 'Colors', + }, + { + prefix: 'fontFamily', + namespace: 'font', + utility: 'font-*', + heading: 'Font families', + }, + { + prefix: 'fontSize', + namespace: 'text', + utility: 'text-*', + heading: 'Font sizes', + baseScaled: true, + }, + { + prefix: 'fontWeight', + namespace: 'font-weight', + utility: 'font-*', + heading: 'Font weights', + }, + { + prefix: 'lineHeight', + namespace: 'leading', + utility: 'leading-*', + heading: 'Line heights — UNITLESS ratios of the paired font sizes', + // UNITLESS RATIOS — Decision U (2026-08-28), superseding the earlier px-length rejection. + // -------------------------------------------------------------------------------------- + // Each `--leading-*` is the idiomatic unitless line-height: the ramp's canonical px + // divided by its 1:1-paired font size (`--leading-base-300` = 20px/14px = calc(20 / 14)). + // Ratios are DERIVED from theme-values.json at generation time, never hardcoded — an + // exact decimal where the division is finite, a `calc(A / B)` fraction where it repeats. + // No `--base-scale` factor: the paired `--text-*` carries base-scale, and a ratio + // multiplies the element's own computed font-size, so line boxes scale with it for free. + // + // The lane-D measurement that previously rejected this (1323 elements changing used + // value, because `line-height` inherits a ratio as a NUMBER where a length inherited a + // fixed px line box) still holds mechanically — it is COMPENSATED in the windmod + // modules: every rule whose descendants relied on inheriting a px line box authors its + // own explicit ratio paired against its authored font-size (lane-D's rule-surface map, + // .scratch/leading-shadow/rule-surface.txt). The one cohort that BLOCKED per-site + // ratios — Tab's root dividing by Chrome's UA + + +); +``` + +A family import is one line no matter how many of its parts you use, so a `Dialog` or `Menu` migration is +usually a _shorter_ import block than the Griffel original: + +```diff +-import { Dialog, DialogSurface, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-components'; ++import { Dialog, DialogSurface, DialogTitle, DialogBody, DialogActions } from '@fluentui/react-windmod-preview/dialog'; +``` + +### The family map + +**There is no root barrel.** `@fluentui/react-windmod-preview` exports nothing — every component is +imported from its **family** subpath. The families are the ones +[`@fluentui/react-headless-components-preview`](https://www.npmjs.com/package/@fluentui/react-headless-components-preview) +already uses, so the two layers have the same shape: whatever `.../react-headless-components-preview/menu` +gives you the hooks for, `.../react-windmod-preview/menu` gives you the styled components for. + +A family is the unit you actually build with. Implementing a menu means `Menu`, `MenuTrigger`, +`MenuPopover`, `MenuList` and `MenuItem` — five imports from one subpath, one line: + +```tsx +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-windmod-preview/menu'; +import { Button } from '@fluentui/react-windmod-preview/button'; +import { CardHeader, CardPreview } from '@fluentui/react-windmod-preview/card'; +``` + +**What this costs and what it saves.** A family barrel's **JavaScript still tree-shakes**: import one +component and its unused siblings' code is dropped (measured — importing only `MenuItem` from `./menu` +retains 6 windmod modules against 36 for the whole family). Its **CSS comes along**, because each class +map side-effect-imports its own chunk and the family barrel keeps the family's chunks reachable. That is +intentional: families are used together, and the cost is bounded by the family rather than by the suite. +If you need one component from a large family and nothing else, the CSS you pay for is that family's, +not the library's. + +**52 family subpaths** carry components. All are kebab-case, and all but `./use-css-var-value` match a +headless subpath one-for-one. + +| Subpath | Components | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `./accordion` | Accordion, AccordionHeader, AccordionItem, AccordionPanel | +| `./avatar` | Avatar | +| `./avatar-group` | AvatarGroup, AvatarGroupItem, AvatarGroupPopover | +| `./badge` | Badge | +| `./breadcrumb` | Breadcrumb, BreadcrumbButton, BreadcrumbDivider, BreadcrumbItem | +| `./button` | Button | +| `./card` | Card, CardFooter, CardHeader, CardPreview | +| `./checkbox` | Checkbox | +| `./color-picker` | ColorPicker, AlphaSlider, ColorArea, ColorSlider | +| `./combobox` | Combobox, Listbox, Option, OptionGroup | +| `./compound-button` | CompoundButton | +| `./dialog` | Dialog, DialogSurface, DialogTrigger, DialogHeader, DialogTitle, DialogBody, DialogActions | +| `./divider` | Divider | +| `./drawer` | Drawer, DrawerBody, DrawerFooter, DrawerHeader, DrawerHeaderNavigation, DrawerHeaderTitle, InlineDrawer, OverlayDrawer | +| `./dropdown` | Dropdown | +| `./field` | Field | +| `./image` | Image | +| `./info-label` | InfoLabel, InfoButton | +| `./input` | Input | +| `./interaction-tag` | InteractionTag, InteractionTagPrimary, InteractionTagSecondary | +| `./label` | Label | +| `./link` | Link | +| `./menu` | Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuItemContextProvider, MenuItemCheckbox, MenuItemLink, MenuItemRadio, MenuItemSwitch, MenuList, MenuPopover, MenuSplitGroup, MenuTrigger | +| `./menu-button` | MenuButton | +| `./message-bar` | MessageBar, MessageBarActions, MessageBarBody, MessageBarTitle | +| `./nav` | Nav, NavCategory, NavCategoryItem, NavDivider, NavDrawer, NavDrawerBody, NavDrawerFooter, NavDrawerHeader, NavItem, NavSectionHeader, NavSubItem, NavSubItemGroup | +| `./persona` | Persona | +| `./popover` | Popover, PopoverSurface, PopoverTrigger | +| `./progress-bar` | ProgressBar | +| `./provider` | FluentProvider | +| `./radio-group` | RadioGroup, Radio | +| `./rating` | Rating, RatingItem | +| `./rating-display` | RatingDisplay | +| `./search-box` | SearchBox | +| `./select` | Select | +| `./skeleton` | Skeleton, SkeletonItem | +| `./slider` | Slider | +| `./spin-button` | SpinButton | +| `./spinner` | Spinner | +| `./split-button` | SplitButton | +| `./swatch-picker` | SwatchPicker, ColorSwatch, EmptySwatch, ImageSwatch, SwatchPickerRow | +| `./switch` | Switch | +| `./tab-list` | TabList, Tab | +| `./tag` | Tag | +| `./tag-group` | TagGroup | +| `./tag-picker` | TagPicker, TagPickerButton, TagPickerControl, TagPickerGroup, TagPickerInput, TagPickerList, TagPickerOption, TagPickerOptionGroup | +| `./teaching-popover` | TeachingPopover, TeachingPopoverBody, TeachingPopoverCarousel, TeachingPopoverCarouselCard, TeachingPopoverCarouselFooter, TeachingPopoverCarouselFooterButton, TeachingPopoverCarouselNav, TeachingPopoverCarouselNavButton, TeachingPopoverCarouselPageCount, TeachingPopoverFooter, TeachingPopoverHeader, TeachingPopoverSurface, TeachingPopoverTitle, TeachingPopoverTrigger | +| `./textarea` | Textarea | +| `./toast` | Toast, ToastBody, ToastFooter, ToastTitle, Toaster | +| `./toggle-button` | ToggleButton | +| `./toolbar` | Toolbar, ToolbarButton, ToolbarDivider, ToolbarGroup, ToolbarRadioButton, ToolbarRadioGroup, ToolbarToggleButton | +| `./tooltip` | Tooltip | + +`FluentProvider` lives at `./provider` rather than `./fluent-provider` because that is where headless +puts its `Provider` — the family is "the provider", and windmod's is the themed one. The subpath also +carries the theme class names, so the provider and its themes come from one import. + +Two subpaths export no component: + +| Subpath | What it is | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `./positioning` | the headless positioning primitives (`usePositioning`, `resolvePositioningShorthand`, …), re-exported — windmod adds no styling layer here | +| `./use-css-var-value` | `useCssVarValue` / `invalidateCssVars` — read a token's resolved value off an element, with caching. windmod-only; headless has no counterpart | + +Five more subpaths are not JavaScript at all: + +| Subpath | What it is | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `./base.css` | the root stylesheet — cascade-layer order + global custom-property registrations; load it head-of-document ([above](#installation-and-imports)) | +| `./styles.css` | the batteries-included monolith — the root sheet plus all 131 component chunks in one file | +| `./css/*` | the individual component chunks (`./css/components/Button/Button.css`, …). Your bundler reaches these through the class maps; the key exists for SSR/CommonJS pipelines that want to collect them by hand | +| `./variants.css` | the component-specific variant catalog, so your own Tailwind CSS can compose against windmod's states | +| `./package.json` | — | + +If you were importing from an internal Fluent package directly (`@fluentui/react-button`, +`@fluentui/react-overflow`, …), note that `@fluentui/no-restricted-imports` blocks that route; the family +subpath is the sanctioned entry point here. + +## What is identical + +- **The DOM.** Element structure, slot names, `role`s and ARIA wiring match Griffel's, because both + libraries render through the same headless renderers. +- **The props.** Every component's props are the headless props plus the same look props Griffel takes + (`appearance`, `shape`, `size`, `orientation`, …) with the same defaults. `ButtonProps`, `FieldProps`, + `TooltipProps` and the rest are assignable from your existing call sites unless listed under + [What differs deliberately](#what-differs-deliberately). +- **The pixels**, at the default 16px root font size. Rest-state rendering is byte-identical across the + shipped matrix, including high-contrast (forced-colors) rules, RTL, and the icon glyph swaps. +- **Context behaviour.** Components read their own contexts the way Griffel's do: `ButtonContext`, + `LinkContext`, `AvatarContext`, `TagGroupContext`, `InteractionTagContext`, `FieldContext`. A `Button` + inside `MessageBarActions`, a `Tag` inside a `TagGroup`, an `Avatar` inside a `Persona` all pick up the + container's `size`/`appearance` without being told. (One merge rule differs — see + [delta 23](#23-a-local-prop-beats-a-context-value).) +- **TabList's animated indicator**, ported mechanism-for-mechanism: measured rects → custom properties → + CSS transition. +- **Slot `className` merging.** Your class lands last on every slot, as it does today. + +## What differs deliberately + +Fifty-nine differences, each one a decision rather than a defect. + +### Setup and API surface + +#### 1. `theme` is a CSS class name, not a theme object + +Griffel's provider takes a JS theme object and writes CSS custom properties at runtime. windmod's applies a +class; the variables are already in the stylesheet. + +The two steps are the same as Griffel's — import the theme, hand it to the provider — with a CSS +import standing in for the JS one. Passing a theme is equally **required** in both: neither has a +default, and neither renders correct colours without one. + +```diff +-import { FluentProvider, webDarkTheme } from '@fluentui/react-components'; +- ++import '@fluentui/react-tailwind-theme-preview/themes/web-dark.css'; ++import { FluentProvider, webDarkThemeClassName } from '@fluentui/react-windmod-preview/provider'; ++ +``` + +All seven themes ship as class names —`webLightThemeClassName`, `webDarkThemeClassName`, +`teamsLightThemeClassName`, `teamsDarkThemeClassName`, `teamsHighContrastThemeClassName`, +`teamsLightV21ThemeClassName`, `teamsDarkV21ThemeClassName` — plus a `themeClassNames` record and a +`ThemeClassName` type: + +```tsx +import { themeClassNames, type ThemeClassName } from '@fluentui/react-windmod-preview/provider'; + +export const pickTheme = (dark: boolean): ThemeClassName => + dark ? themeClassNames.webDarkTheme : themeClassNames.webLightTheme; +``` + +Each name has a matching stylesheet subpath — `themeClassNames.webDarkTheme` is the class in +`…/themes/web-dark.css` — so importing the right file is mechanical: kebab-case the theme name +without its `Theme` suffix. + +**If you built a custom theme** by passing a modified theme object, port it to a CSS class that redeclares +the token custom properties. `theme` accepts any string, so your own class name works: +``. With no baked default, a class that declares the whole +token set needs no shipped theme file at all; one that declares a few overrides is applied +alongside a shipped class (`theme="fui-theme-web-light my-brand-theme"`) and wins by source order +within the shared `fui.theme` layer. + +#### 2. The theme is a separate, required import — one file per theme + +`@fluentui/react-tailwind-theme-preview/base.css` carries the preflight +([delta 58](#58-tailwinds-preflight-ships--at-the-head-of-the-base-layer)), the type ramp, the spacing scale +and the token registrations; each `…/themes/.css` carries one theme's palette. Nothing renders +correctly without the base, and nothing is _coloured_ without a theme file plus its class. + +This is the same shape as Griffel, where the theme is a separate JS import you pass to the +provider — only the cost model changes with it. Griffel's themes are JS objects, so an app that +imports one ships one; the CSS equivalent has to be per-file for that to stay true, which is what +these subpaths are. Importing web light alone is 31 KB raw / 5.9 KB gzipped against the 163 KB / +15.4 KB of all seven. + +Load them before your own CSS so your rules stay unlayered +(see [delta 10](#10-cascade-layers-replace-specificity-juggling)). + +#### 3. The provider is a real element, and it paints + +`FluentProvider` renders a `div` carrying the base typography, text colour and +`background-color: var(--color-neutral-background-1)`. Two consequences: + +- **Layout.** If you wrap children in a provider _inside your own flex or grid container_, the provider is + now the single flex/grid item, not the children. Griffel's provider has always been a real div, so an app + migrating from `@fluentui/react-components` already accounts for this — but an app migrating from an + earlier windmod preview does not. +- **Paint.** A provider dropped onto a coloured surface repaints that surface. Set `background: transparent` + via `className` if that is not what you want. + +`targetDocument` and `dir` are supported. `customStyleHooks_unstable`, `overrides_unstable` and +`applyStylesToPortals` are not. + +#### 4. There is no `useCustomStyleHook_unstable` + +Griffel's per-component style-hook override escape hatch has no windmod counterpart, on any component. +Restyle through CSS instead — [delta 10](#10-cascade-layers-replace-specificity-juggling) explains why that +is now the easier path. + +#### 5. Public class names are lower-case, and `classNames.root` is a pair + +Each component root carries **two** public classes: `fui-button` (the identity class — safe for your CSS +and for `querySelector`, no escaping needed) and `group/fui-button` (Tailwind's named-group class). +Everything else is a hashed ident. + +```diff +-.fui-Button { /* Griffel */ } ++.fui-button { /* windmod */ } +``` + +The exported `buttonClassNames.root` is the whole **space-separated pair**, not a single class. Use it in +`className`, not in a selector: + +```tsx +import { buttonClassNames } from '@fluentui/react-windmod-preview/button'; + +buttonClassNames.root; // "fui-button group/fui-button" +document.querySelectorAll('.fui-button'); // ✅ +document.querySelectorAll('.' + buttonClassNames.root); // ❌ invalid selector +``` + +The class-name records expose `root` only. Griffel's per-slot entries (`fui-Button__icon` and friends) have +no equivalent: internal slots use hashed idents and `data-*` state attributes (`data-open`, `data-size`, +`data-placement`, `data-appearance`, …). Target slots through the group variants instead: + +```tsx + +``` + +No `group` name declaration is required — the marker is already on the root. You may still add your own +`group/name` via `className` to disambiguate nested instances. + +#### 6. There is no root barrel — every import names a family + +`import { Button } from '@fluentui/react-windmod-preview'` does not resolve. Components come from their +family subpath, matching the headless package's own grouping: see [the family map](#the-family-map). The +exported names are unchanged, so a migration is a path rewrite, not an API rewrite — and a codemod-able +one, since the family of every name is fixed. + +#### 7. `Input` and `Textarea` drop the deprecated shadow appearances + +`InputAppearance` is `'outline' | 'underline' | 'filled-darker' | 'filled-lighter'` and +`TextareaAppearance` is `'outline' | 'filled-darker' | 'filled-lighter'`. Griffel's deprecated +`filled-darker-shadow` and `filled-lighter-shadow` are not shipped. Map them to their non-shadow twins. + +#### 8. `required` renders no asterisk on `Checkbox` and `Switch` + +The headless controls ship no asterisk element, and windmod does not invent one. The `required` attribute +still reaches the native input, so validation and assistive-technology behaviour are unchanged; only the +visual `*` is missing. If you need it, wrap the control in a `Field` with `required`, whose `Label` does +render one. + +### The styling model + +#### 9. Everything scales with the root font size + +Griffel writes literal pixels. windmod puts spacing, control heights, radii and the whole type ramp on a +single density knob: + +```css +--base-scale: calc(1rem / 16px * var(--fui-scale, 1)); +--spacing: calc(1px * var(--base-scale)); +--text-base-300: calc(14px * var(--base-scale)); +--radius-medium: calc(4px * var(--base-scale)); +``` + +At the default 16px root font size the two libraries are identical — that is the parity condition. Change +`html { font-size: … }` and the whole windmod UI scales coherently where Griffel's would only move its +text. This is an improvement, but it _is_ a difference: if your app sets a non-16px root font size, expect +windmod controls to be larger or smaller than the Griffel ones they replace. Keep the root font size at +16px to keep the two libraries pixel-aligned; `--base-scale` and the scales derived from it are declared at +the document root, so a raw override has to go there too, not on a provider or a subtree. + +A handful of literals stay fixed by design where Griffel is also fixed (stroke widths, a few 1px nudges). + +Border radii are part of the scaled system too: under a non-default root font size a windmod corner grows +with the control it rounds, where Griffel's stays at its literal px. At the default 16px root the two are +identical, like every other family on the knob. (`--radius-none` is 0 and `--radius-circular` is the +10000px fully-round sentinel the browser clamps to half the box — neither meaningfully scales, so both +stay literal.) + +**New capability — subtree scaling.** Griffel has no equivalent of scaling one part of a page; windmod +ships `ScaleRegion` (exported from `/provider`) for exactly that. `` multiplies +the ambient base scale for its subtree — spacing, control geometry, strokes, the type ramp, icon glyphs, +shadow geometry and border radii all follow, exactly as under the root font size. The factor is +absolute (nested regions replace, never compound), the rendered div is `display: contents`, and the region +must sit inside a themed `FluentProvider`. See the README's ScaleRegion section. + +#### 10. Cascade layers replace specificity juggling + +Every package style lives inside a `fui.*` cascade layer. Your CSS is unlayered, and **unlayered CSS beats +layered CSS regardless of selector weight**. Overriding a component is now a one-class rule with no +`!important` and no specificity arms race: + +```css +/* wins over the component's own layered rules */ +.my-brand-button { + background-color: rebeccapurple; +} +``` + +```tsx + +``` + +The corollary: **do not wrap your overrides in a `@layer`**, or you will lose the contest you used to win. + +#### 11. Spacing tokens are inlined at build time + +Tailwind's `--spacing-*` namespace resolves at compile time, so a rule that reads +`var(--spacingHorizontalM)` in Griffel reads `calc(var(--spacing) * 12)` in windmod. Redeclaring a Fluent +_spacing_ token at runtime therefore does not move windmod's layout. Colour, typography and stroke tokens +are still live `var()` references and can be overridden at runtime as before; use `--base-scale` for +density. + +#### 12. Some computed style strings differ without any visual difference + +Tailwind's shadow and ring utilities prepend fully transparent layers, so +`getComputedStyle(el).boxShadow` returns a longer string than Griffel's `var(--shadow4)` — same painted +result. `shadow-none` likewise computes to five transparent layers rather than the literal `none`. Snapshot +tests that assert on computed `box-shadow` strings need updating; nothing about the rendering does. + +Translucent colours are the other case. Where Griffel spells an alpha mix as `color-mix(in srgb, …)`, +windmod writes Tailwind's slash modifier — `bg-(--token)/30` — which compiles to `color-mix(in oklab, …)`, +oklab being Tailwind's default interpolation space and the one the modern web has settled on. These are +**alpha-only** mixes: the same colour on both sides, only its opacity changing, so premultiplied +interpolation lands on the same painted result whichever space names it. Only the serialization differs — +`oklab(… / 0.3)` against `color(srgb … / 0.3)` — which a computed-style diff of the two libraries will +show. [Delta 55](#55-unselected-carousel-dots-are-mixed-in-oklab-not-srgb) prints the two side by side on +the one component where they are worth reading. + +Griffel pairs each such mix with an `@supports not (color-mix(…))` fallback. **windmod deliberately carries +none.** `color-mix()` shipped in every engine in early 2023, comfortably inside the CSS anchor positioning +floor this package already requires +([delta 27](#27-anchored-components-require-css-anchor-positioning--with-no-fallback)) — a browser that +cannot mix two colours cannot position an anchored surface either, so the fallback would only ever run on +an engine that has already failed elsewhere. + +#### 13. Text alignment is logical, not physical + +The provider sets `text-align: start` where Griffel compiles a `left`/`right` pair. Identical whenever an +element's direction matches the provider's. The one divergence is a `dir`-flipped subtree inside a provider +of the opposite direction: windmod follows the subtree (correct i18n behaviour), Griffel stays physically +left. + +### Focus and keyboard + +#### 14. Focus rings follow the browser, not keyborg + +Griffel gates focus rings on keyborg, a script that stamps a `data-fui-focus-visible` attribute during +**Tab** navigation. windmod uses the browser's own `:focus-visible` / `:has(:focus-visible)`. Measured +equivalent for non-text controls, with two edges where the native heuristic is more generous: + +- **Keyboard, then click.** Focus a control with Tab, then click it: the ring persists under windmod and + disappears under Griffel. +- **Click, then arrow key.** Click a radio or slider, then press an arrow: the ring appears under windmod + and does not under Griffel. + +Text-editable controls (`Input`, `Textarea`, `SearchBox`) ring on click on both libraries. Neither edge is a +defect; both are native `:focus-visible` semantics. No action needed unless you have visual tests that +capture focus. + +#### 15. Arrow-key navigation comes from `focusgroup`, not tabster + +`Toolbar`, `TabList`, `TagGroup` and `SwatchPicker` publish the `focusgroup` attribute where Griffel +emits a `data-tabster` mover configuration. Behaviour is broadly equivalent but not identical, and +`focusgroup` needs browser support or a polyfill. This is the headless layer's choice; windmod passes it +through. + +`Nav` is the exception: it publishes no `focusgroup` at all, so its rows are reached with **Tab**, one stop +per row, where Griffel's tabster mover makes the whole nav a single tab stop with arrow keys moving between +rows. A keyboard user tabbing past a long navigation therefore passes through every row instead of skipping +the block in one press. This too is the headless layer's choice; windmod passes it through. + +#### 16. `Accordion`'s `navigation` and `BreadcrumbButton`'s `focusMode` do nothing + +Both props are absent from the headless surface, so TypeScript rejects them. Arrow-key navigation inside an +Accordion or a Breadcrumb is simply not present. If a JS caller passes `navigation="circular"` anyway, it +lands on the DOM as an unknown attribute rather than being interpreted. + +#### 17. `TagGroup` does not restore focus after a dismiss + +Griffel moves focus to the neighbouring tag when the focused tag is dismissed. The headless base hook has +no such wrapper, so focus drops to the document. Handle it in your `onDismiss` if the interaction matters: +move focus yourself before the tag unmounts. + +### Motion and mounting + +#### 18. `Accordion` has no collapse animation + +The headless panel drops the presence wrapper that carried Griffel's 200ms height + opacity collapse. +Panels open and close instantly. + +#### 19. `Accordion` keeps closed panels mounted — state persists + +Griffel unmounts a closed panel. windmod hides it (`display: none`, plus `inert` and `tabIndex=-1` from the +headless layer). **A closed panel's React state, uncontrolled input values, scroll position and running +timers all survive a close/open cycle**, where Griffel would have thrown them away. + +This is usually what people want, but it is a real behavioural change: if you relied on unmount to reset a +form inside a panel, reset it explicitly, or key the panel's children on the open state. + +#### 20. `Nav` has no category collapse motion, and an open sub-item group clips + +Opening a category is instantaneous. Griffel's collapse motion leaves an open `NavSubItemGroup` at +`overflow-y: auto`; windmod's stays `overflow: hidden`. In every unconstrained case the group is +content-sized and the two are indistinguishable. **If you height-constrain a `NavSubItemGroup`, Griffel +gives you a scrolling box and windmod gives you a clipping one.** Add `overflow-y: auto` yourself if you +constrain the height. + +#### 21. `MessageBar` has no group animation and does not announce + +- `MessageBarGroup` is not shipped: the headless package has none, and Griffel's has no visual contract to + reskin. A windmod `MessageBar` cannot be animated into or out of a group, and placing one inside a + Griffel `MessageBarGroup` will not wire up the motion ref either. +- A windmod `MessageBar` does not call `announce()` — windmod's provider supplies no announce context, so + the call is a no-op. Nest windmod inside a Griffel `FluentProvider` if you need the live region, or + announce yourself. + +#### 22. `prefers-reduced-motion` is suppressed globally, not per component + +Griffel suppresses motion piecemeal: `react-motion` swaps every presence atom for a 1ms one when the +preference is set, and about a dozen components additionally author their own +`@media (prefers-reduced-motion: reduce)` CSS. Motion that neither path covers keeps running. + +windmod suppresses once, in the theme. `@fluentui/react-tailwind-theme-preview` ships one rule and one +deliberate exception to it; no windmod component authors a duration override of its own: + +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition-duration: 1ms; + transition-delay: 1ms; + animation-duration: 1ms; + animation-delay: 1ms; + animation-iteration-count: 1; + } + + /* Essential motion, exempt on purpose — see below. */ + .fui-spinner > span { + animation-duration: 1.8s; + animation-iteration-count: infinite; + } +} +``` + +The rule is **unlayered**, not `!important`. Layers are compared before specificity, so an unlayered +declaration outranks everything in `fui.components.*` (see +[delta 10](#10-cascade-layers-replace-specificity-juggling)) while your own unlayered CSS and your inline +styles still beat it. If one motion has to survive the preference in your app, re-declare its duration +from unlayered CSS or inline — there is no `!important` to fight. + +**Where the two libraries visibly differ.** Measured under emulated `prefers-reduced-motion: reduce`: +the Spinner's rotation is the only thing in windmod that computes a duration above 1ms, while Griffel +still moves in these places. + +| Motion | Griffel under `reduce` | windmod under `reduce` | +| ------------------------------------------- | -------------------------------------- | ---------------------- | +| `ProgressBar` determinate `width` | tweens over 0.3s on every value change | jumps | +| `AccordionHeader` chevron | rotates over 0.2s | jumps | +| `Nav` row background (hover, selection) | 0.1s | instant | +| `NavItem` selection indicator and icon swap | 0.1s keyframes | instant | + +The `Spinner` is not in that table, and the exception in the rule above is why. + +It closes one gap in the other direction too: the `NavCategoryItem` chevron used to keep its 0.15s +rotation under the preference, where Griffel's equivalent was already suppressed. It no longer does. + +**The `Spinner` keeps spinning — deliberately.** A loading indicator that does not move stops being a +loading indicator, so the Spinner's rotation is the one motion carved out of the floor, at Griffel's own +under-reduce value: **1.8s, linear, infinite** — slower than the 1.5s it runs at without the preference, +never stopped. Measured under `reduce`, both libraries report the identical animation on the ring +(`duration 1800ms, iterations Infinity, running`). Only the rotation is exempt; the Spinner's tail still +takes the floor and rests as the static full-ring conic its own rule swaps in, which is what Griffel's +tail does too (Griffel pins it at `animation-iteration-count: 0`). + +Infinite animations are otherwise the class to watch. `animation-iteration-count: 1` means the +`SkeletonItem` shimmer and the indeterminate `ProgressBar` pulse each run exactly one 1ms pass and then +render their un-animated state — a flat block and a static full-width bar. Both match Griffel, which +suppresses both as well. + +If you have your own essential motion, the same escape hatch is open to you: the floor is unlayered but +selector-less, so any rule of your own with a class in it already outranks it. + +One consequence of the bare `*`: `transition-property` defaults to `all`, so under the preference every +element in the document — yours included, styled by Fluent or not — carries a 1ms transition on every +animatable property. It is imperceptible, but `transitionend` now fires where it previously would not. +Do not key logic on a `transitionend` arriving only for elements you styled. + +### Composition and context + +#### 23. A local prop beats a context value + +Where Griffel spells `size = contextSize ?? 32` (context wins even over an explicit prop), windmod's merge +helper gives the **local prop** priority and falls back to context. Callbacks compose rather than replace +(context first, then local), `className` concatenates with yours last, and `style` spreads the same way. + +Concretely: an `Avatar` with an explicit `size` inside a container publishing a different size keeps its own +size under windmod and takes the container's under Griffel. If you were relying on a container to override +explicit sizes, remove the explicit prop. + +#### 24. `Field` does not push `size` into its control + +`Field` sizes its own `Label` and layout but does not propagate `size` to the control it wraps. +`aria-invalid` and `required` do propagate. Measured cost of the gap: 8px of control height and 2px of font +size at both `small` and `large`. + +Pass `size` to both: + +```tsx + + + +``` + +#### 25. A nested `Field` renders its label with the outer Field's orientation and size + +Griffel does not. Avoid nesting `Field`s, or set the inner label's props explicitly. + +#### 26. `Card` selection is not focus-aware + +Griffel builds a focus-aware predicate so that clicking a focusable element _inside_ a selectable card does +not toggle the card. The headless surface exposes `shouldRestrictTriggerAction` but supplies no default, so +a windmod selectable `Card` toggles on any inner click. Supply the predicate yourself if the card contains +interactive content. + +Relatedly, `focusMode` and the tabster groupper are absent by construction: a windmod `Card` is never +focusable by itself and never traps Tab. + +### Browser and network behaviour + +#### 27. Anchored components require CSS anchor positioning — with no fallback + +The headless positioning layer uses native CSS anchor positioning (`anchor-name`, `position-area`, +`position-try-fallbacks`). There is **no** `@supports` guard, no feature detection, no polyfill and no +floating-ui fallback path anywhere in it. + +**On a browser without CSS anchor positioning the surface renders unpositioned at the viewport origin.** As +of today that means Firefox and Safari. `Tooltip` and `Popover` are the anchored components this package +ships, so the exposure is bounded to them, but it is total on those engines. The measured displacement for +an unpositioned Popover surface is dx −1016.953, dy −419 from its trigger. + +If you must support them, either polyfill CSS anchor positioning, or keep `Tooltip` and `Popover` on +`@fluentui/react-components` — both compose over windmod children without trouble. + +#### 28. `Tooltip`'s arrow sits differently on corner placements + +Griffel's floating-ui centres the arrow on the target; CSS anchor positioning pins it near the edge. On the +aligned corner placements (`above-start`, `below-end`, and their siblings) the arrow is displaced by +roughly **−36.6px / +35.0px** relative to Griffel. Surfaces also land on fractional device pixels where +floating-ui writes integer transforms, a 0.047–0.375px offset that shifts glyph rasterization. + +Both CSS routes to close the arrow gap (`anchor-center`, an `anchor-size()` clamp) were measured and +neither works. Edge-centred placements (`above`, `below`, `before`, `after`) are unaffected. This is the +mechanism behind four of the eighteen ratified pixel allowances — see +[Where the pixels are allowed to differ](#where-the-pixels-are-allowed-to-differ). + +#### 29. The `AlphaSlider` checkerboard is inlined — it works offline + +Griffel fetches the transparency checkerboard from a CDN. windmod embeds the same 94-byte PNG as a `data:` +URI in its stylesheet, byte-for-byte identical to the asset Griffel downloads. + +Consequence: on an **offline, air-gapped or CSP-restricted origin the windmod `AlphaSlider` paints its +checkerboard and the Griffel one does not**, and windmod issues no network request for it at all. A +deliberate improvement; listed here because it is a behavioural difference you may be measuring. + +### AvatarGroup + +#### 30. The overflow button's ARIA is different + +It carries `aria-haspopup="true"`; Griffel's carries a `data-tabster` focus-restorer instead. The accessible +name (`View more people.`) and `aria-expanded` are identical on both. A snapshot test pinning the button's +attributes needs updating. + +#### 31. A consumer's own trigger-button children are honoured + +Griffel overwrites `triggerButton.children` with its glyph whenever `indicator="icon"`, discarding whatever +the consumer passed. windmod follows the library-wide default-glyph rule: the glyph is a fallback, and +consumer children always win. `triggerButton={{ children: null }}` falls back to the glyph on both +libraries, so only a non-nullish value diverges. + +#### 32. The overflow popover no longer traps focus by default + +Griffel's `AvatarGroupPopover` set `trapFocus: true`. The headless surface forwards `trapFocus` but leaves +it unset, and windmod does not add a default, because turning it on switches the native `` from +`popover="auto"` to `showModal()` and makes the rest of the page inert. Pass `trapFocus` explicitly to +restore the old behaviour. + +#### 33. `AvatarGroupItem` no longer reads the provider direction + +Griffel's item calls `useFluent()` and merges a second class set under RTL; windmod's pie geometry is +direction-aware in CSS. Behaviour is identical inside a provider; outside one, windmod follows the +document's actual `dir` while Griffel falls back to `ltr`. + +#### 34. The overflow popover's arrow is off-centre on aligned placements + +`withArrow` is forwarded and not defaulted on either library, so no arrow renders out of the box. If you +opt in on an aligned placement (`above-start`, `below-end`, `before-top`, `after-bottom`), the arrow's +cross-axis position sits a fixed 8px from the aligned edge rather than centred on the trigger, because CSS +anchor positioning has no equivalent of floating-ui's arrow middleware — the same mechanism as +[delta 28](#28-tooltips-arrow-sits-differently-on-corner-placements). Centred placements are pixel-exact. + +### Popover surfaces + +These four apply to every component built on `PopoverSurface` — `Popover`, `Tooltip` and the +`AvatarGroup` overflow popover. + +#### 35. A focus-trapping surface carries no `aria-modal` + +Griffel sets `aria-modal` when `trapFocus` is on. The headless surface is a native `` opened with +`showModal()`, and `dialog:modal` already conveys modality to assistive technology, so no attribute is +written. An assertion pinning `aria-modal` needs updating; the announced modality is unchanged. + +#### 36. There is no enter motion + +Griffel fades and slides its surface in (`appear: true`). The headless surface ships no motion slot and +windmod adds none, so the surface appears at its final position immediately. Nothing about the resting +render differs. + +#### 37. A surface outside every provider reads the document root's theme + +The surface is rendered inline and promoted to the top layer, so it inherits theme variables from its +position in the DOM. A trigger that sits outside every `FluentProvider` therefore renders its surface with +the document root's theme. Griffel's portalled surface has the same fallback for a different reason — it +derives its theme from React context. Wrap the trigger in a `FluentProvider` to control the surface's +theme. + +#### 38. The surface inherits arbitrary CSS from its DOM ancestors + +Because the surface stays where it is written, any inherited property set on an element between the +provider and the trigger — `letter-spacing`, `text-transform`, `font-variant`, a `color` on a wrapper — +reaches it. Griffel's portalled surface sees none of it; only the theme class travels with the portal. If +you relied on a portal isolating the surface from an ancestor's inherited styles, set those properties +explicitly on the surface. + +### Component defaults + +#### 39. `TagPicker` resolves an unrecognised `size` to a `medium` tag, where Griffel resolves `extra-small` + +A `TagPicker`'s `size` selects the size of the `Tag`s in its group: `medium` → `extra-small`, +`large` → `small`, `extra-large` → `medium`. The three listed values map identically in both +libraries. They differ only in the fallback: windmod's returns `medium` — windmod `Tag`'s own +unresolved size — where Griffel's returns `extra-small`, so the windmod tag family has one +unresolved-size answer rather than two. + +`TagPickerProps['size']` is a closed union, so TypeScript cannot reach this branch. It decides only +what a JavaScript caller passing an unlisted value gets: a 32px-tall tag under windmod against a +20px one under Griffel. Pass one of the three documented sizes and the two libraries agree exactly. + +### `InfoLabel` and `InfoButton` + +#### 40. `InfoButton` has no `inline` prop, and its popover is always in the top layer + +Griffel's `InfoButton` took an `inline` prop (default `true`) choosing between rendering the popover inline +and portaling it. The headless surface is always promoted into the browser's native top layer, so there is +no inline/portal switch to make and the prop is not re-added. Everything [delta 27](#27-anchored-components-require-css-anchor-positioning--with-no-fallback) +says about anchored components applies here unchanged, and so does [delta 28](#28-tooltips-arrow-sits-differently-on-corner-placements): +`InfoButton`'s **default** placement is `above-start`, an aligned one, so the arrow displacement is the +out-of-the-box appearance rather than an edge case. Because the trigger is a 24px button — narrower than +twice the arrow's inset from the surface edge — floating-ui compensates by translating the whole surface +(measured 1px at `size="medium"`, 3px at `size="large"`) where CSS anchor positioning pins the surface edge +to the trigger edge. Centred placements (`above`, `below`, `before`, `after`) are pixel-exact. + +#### 41. `InfoButton` moves focus to the popover, and closes when focus leaves to nothing + +Two behaviours come from the headless hook, not from the styled layer: + +- **Opening the popover focuses the surface**, so a screen reader reads the info before anything else. + Griffel leaves focus on the button. +- **The popover closes when focus leaves to nothing** — a window blur, or a click on the page background. + Griffel keeps it open in that case. + +The trigger's ARIA is also richer: it carries `aria-haspopup="true"` always and `aria-details` pointing at +the surface while open, where Griffel carries neither and relies on tabster's focus restorer. Nothing is +lost — the `aria-labelledby` pairing of the label and the button is identical on both — but a snapshot test +pinning the button's attributes needs updating. + +#### 42. `` renders the default glyph + +Across windmod, a slot's default content is restored with a `??` fallback, which fires on `null` as well as +`undefined`. `{null}` therefore renders the info glyph where Griffel renders +nothing. Any other value — a string, an element, a fragment — wins over the default on both libraries. To +render a button with no glyph, pass an empty element rather than `null`. + +### Teaching and guidance + +#### 43. `TeachingPopover` no longer traps focus by default + +Griffel's `TeachingPopover` sets `trapFocus: true` by default, "because the default TeachingPopover view +has buttons/carousel". The headless component does not, so the surface is a light-dismissable +`popover="auto"` rather than a modal dialog, and its role is `group` rather than `dialog`. Nothing moves +visually — the trap adds `aria-modal` and a tabster configuration and paints nothing. + +Pass `trapFocus` explicitly to restore the old default. + +#### 44. `TeachingPopoverFooter`'s buttons are slots again, and only one of them closes + +The headless base hook drops the `primary` and `secondary` Button slots and takes buttons as children of +the root instead. windmod restores both slots on the windmod `Button`, exactly as Griffel has them — +including the brand role inversion (on a brand surface the emphasis moves to `secondary`) and the +auto-close handler, which is wired to the `secondary` where there is one and to the `primary` otherwise, +never to both. + +Consequence for code written against the headless footer: **children still win.** A footer given its own +children renders them and ignores the two slots, so a headless-shaped call site keeps working unchanged. + +One state-shape difference goes with the slots: Griffel's footer state lists the slot components on +`state.components` (`{ root, primary: Button, secondary: Button }`), while windmod's keeps the headless +state's `{ root: 'div' }` and carries the resolved `primary`/`secondary` slots on the state itself. Nothing +renders differently — `components` is `@deprecated` in `@fluentui/react-utilities` and no windmod render +function reads it — but code that introspects `state.components` to discover the footer's slot elements +will not find them there. + +#### 45. `TeachingPopoverCarouselFooter` has no `initialStepText` or `finalStepText` + +The carousel now ships — `TeachingPopoverCarousel` and its six family members each have a windmod component +and a subpath. Its one breaking API change is here: Griffel's footer takes `initialStepText` and +`finalStepText` as **required** footer props and feeds them into each nav button's `altText` default. The +headless footer this component composes drops both, and windmod does not restore them. Trailing-step text is +supplied per button instead, as the `altText` the headless slot type already declares: + +```tsx +// Griffel + + +// windmod + +``` + +Neither half of the port fails silently. `altText` and `navType` are both **required** on the slot — exactly +as Griffel's own footer-button props declare them — so a port that drops the text does not compile; and +`initialStepText` / `finalStepText` are absent from windmod's footer props type, so passing them is a type +error rather than an accepted no-op. + +The rest of the family's deltas are grouped under [the teaching carousel](#the-teaching-carousel) below. + +#### 46. A `Combobox` or `Dropdown` listbox is not clamped to the space around the trigger + +Griffel passes `autoSize: true` to floating-ui, which shrinks the listbox to whatever room is left between +the trigger and the viewport edge and scrolls the remainder. The headless positioning layer has no +`autoSize` — nothing reads it, and since the `positioning` prop was narrowed to the props the layer actually +honours, the key is no longer even accepted. A listbox with more options than fit below the trigger +therefore flips to `above` (the fallback positions still apply) and, failing that, extends past the viewport +edge rather than shrinking. Cap it yourself when the option count is unbounded: + +```tsx + +``` + +### Two places windmod renders more correctly than Griffel + +Both were found by the forced-colors survey and are invisible to every VR scene. In each case matching +Griffel exactly would have meant importing a defect, so windmod keeps the correct rendering. Listed here +so a pixel-diffing migration audit is not surprised by them. + +#### 47. A current `BreadcrumbButton` keeps its hover and press styling + +Griffel's cascade lets a later rule beat the current-item styling it evidently intends: under forced +colors the current breadcrumb's hover and press states lose their system colours, and in normal mode its +press state loses too (16 cells measured). windmod's layer order applies the current-item styling as +written, so a pressed current breadcrumb looks the way the design intends. Matching Griffel would have +required deliberately making a windmod rule lose in normal mode as well. No action needed. + +#### 48. Pressing a `Card` styles the card, not its descendants + +Griffel's compiled interaction selector is `.card:hover, .card :active` — the space before `:active` is a +stray descendant combinator, so the pressed styling matches active _descendants_ instead of the pressed +card (8 forced-colors cells measured). windmod authors `:active` on the card itself. Copying Griffel +would have imported the typo; it is filed upstream instead. A windmod `Card` therefore shows a pressed +state where a Griffel `Card` shows none. + +### NavDrawer + +#### 49. The navigation landmark sits on the drawer body, not the drawer root + +Griffel's `NavDrawer` stamps `role="navigation"` on the drawer root; the headless render puts it on +the `NavDrawerBody`, and windmod inherits the headless placement. The landmark is announced either +way and its contents are identical — but assistive technology reports it on a different element, and +a selector or test targeting `[role="navigation"]` finds the body rather than the root. The package's +own test suite pins the headless placement so a future upstream move is caught. + +#### 50. `NavDrawer` has no `tabbable` prop + +Griffel adds `tabbable` on `NavDrawerProps` and feeds it to `useArrowNavigationGroup` in the body. +The headless state omits the prop and its context helper pins the value `false`, so windmod cannot +forward what never arrives. With Nav's move to Tab-per-row navigation (see +[delta 15](#15-arrow-key-navigation-comes-from-focusgroup-not-tabster)) the prop's original role has +narrowed; consumers who set `tabbable` today should verify their keyboard order against the headless +model. + +### The teaching carousel + +The carousel family ships as of this release. Its one breaking prop change is +[delta 45](#45-teachingpopovercarouselfooter-has-no-initialsteptext-or-finalsteptext); the rest of the +family's differences are below. Two of them are places windmod renders or behaves more correctly than +Griffel, kept rather than matched, and marked as such. + +#### 51. Every carousel dot is in the tab order + +Griffel's `TeachingPopoverCarouselNav` wires tabster arrow navigation over the dots and marks the selected +one as the group's default focusable, so the whole nav is a single tab stop and the arrow keys move between +dots. The headless nav ships neither, and windmod inherits that: each dot is its own tab stop and the arrow +keys do nothing. Nothing paints differently — this is keyboard order only. It is the same headless move +that [delta 15](#15-arrow-key-navigation-comes-from-focusgroup-not-tabster) records for `Nav`. + +#### 52. `layout="offset"` right-alignment reaches only the footer's own children + +Griffel's compiled selector for the offset footer's right-aligned region is a **descendant** combinator, so +it also catches nested first children — the first dot of a `TeachingPopoverCarouselNav` placed inside the +footer picks up the alignment along with the footer's own first child. windmod authors a direct-child +selector, which is what the rule evidently intends. Reproducing Griffel here would have meant importing the +combinator; it is filed upstream instead. A footer whose layout depends on the wider match will need the +alignment stated on the nested element. + +#### 53. `appearance={undefined}` on a carousel footer button no longer clobbers the derived appearance + +`TeachingPopoverCarouselFooterButton` derives its `appearance` from the surrounding surface's appearance and +its own `navType`. Griffel spreads the consumer's props over that derived value (`{appearance: derived, +...props}`), so an **explicit** `appearance={undefined}` overwrites it and the button falls back to +`secondary`. windmod resolves `appearance ?? derived`, which fires on `undefined` and keeps the derived +value. Only an explicit-undefined call site is affected; passing a real appearance overrides on both +libraries, and passing nothing derives on both. **windmod is the more correct of the two here**, and the +behaviour matches how windmod treats every other look prop. + +#### 54. `disabledFocusable` on a carousel footer button actually blocks the click + +Griffel's carousel footer button renders on the base root and discards the ARIA button layer, so a +`disabledFocusable` button stays focusable — as intended — but still fires its `onClick`. windmod composes +through the windmod `Button`, which keeps `useARIAButtonProps`' guarded handler, so the click is blocked +while focus is retained. **windmod is the more correct of the two here.** A call site that relied on the +handler firing on a `disabledFocusable` nav button will stop receiving it. + +#### 55. Unselected carousel dots are mixed in oklab, not sRGB + +The unselected dot is the component's one translucent colour. Griffel authors it as +`color-mix(in srgb, …)`; windmod authors it with Tailwind's opacity modifier, which compiles to +`color-mix(in oklab, …)` — oklab is Tailwind's default interpolation space, and windmod does not fight it. +[Delta 12](#12-some-computed-style-strings-differ-without-any-visual-difference) states the rule; this is +the one component where the numbers are worth printing. The two computed values differ in serialization on +the two unselected dots and are identical on the two selected ones: + +| dot | windmod (oklab) | Griffel (`in srgb`) | +| ------------------- | ------------------------------------------------- | ----------------------------------------------- | +| neutral, unselected | `oklab(0.526287 -0.0470482 -0.141687 / 0.3)` | `color(srgb 0.0588235 0.423529 0.741176 / 0.3)` | +| brand, unselected | `oklab(0.999994 0.0000455678 0.0000200868 / 0.3)` | `color(srgb 1 1 1 / 0.3)` | +| neutral, selected | `rgb(15, 108, 189)` | `rgb(15, 108, 189)` — identical | +| brand, selected | `rgb(255, 255, 255)` | `rgb(255, 255, 255)` — identical | + +**This costs nothing in pixels.** The two LTR carousel scenes reach strict zero under `--disable-gpu`, so +at 30% over the surfaces this family paints on the two mixes rasterize the same; no pixel allowance was +needed or granted for the colour. (The RTL scene's residual 6 px under the flag is a harness settle-pass +artifact, not a colour term — see the table below.) It is recorded here because a computed-style diff of +the two libraries will show the two rows above, and because it is a divergence windmod authored +deliberately rather than one it inherited. + +Griffel also carries an `@supports not (color-mix(…))` fallback; windmod deliberately carries none. +Tailwind emits an opaque `background-color` outside its own `@supports` guard, which is unreachable inside +windmod's documented browser floor. Measured with the guard's contents removed from the live stylesheet, +that emission paints the neutral dot `rgb(15, 108, 189)` and the brand dot `rgb(255, 255, 255)`, both fully +opaque — identical on the neutral dot to what windmod paints with no fallback at all, and closer to the +intended look on the brand dot. Griffel's own fallback paints **both** dots `rgb(15, 108, 189)` at 30%, so +its brand dot comes out brand blue rather than white; that is a bug in it. So on a browser without +`color-mix` windmod loses the 30% dimming that Griffel's fallback keeps, and Griffel keeps the dimming on +the wrong hue. + +#### 56. The footer button's brand press styling is authored to Griffel's measured rendering + +Where a Griffel declaration on this family is defeated by Griffel's own `mergeClasses` ordering and never +paints, windmod omits it rather than reviving it — otherwise windmod would paint a button Griffel never +paints. That is a parity decision, not a divergence, with one exception worth stating. + +The pressed background of a `previous` button on a **brand** surface has no stable value in Griffel: two +atomic rules paint it at equal specificity with different selector keys, so both survive `mergeClasses` and +document insertion order decides. In the composed component — the only arrangement a consumer can render — +Griffel's later Button-outline rule wins and the surface shows through (`rgba(0, 0, 0, 0)`). windmod authors +that value. Take a Griffel measurement of this cell from an isolated harness rather than from a real +`TeachingPopover` and you will read `rgb(12, 59, 94)` instead, from the same Griffel build. + +### Icon slots + +#### 57. Fluent icons are the styled contract — a bring-your-own `` is not restyled + +Where Griffel shapes a slot's glyph with an `& svg` selector, windmod selects `[data-fui-icon]` — the +attribute every `@fluentui/react-icons` icon stamps (SVG icons an empty value, font icons `"font"`). For +any Fluent icon, bundled or not, the two are identical; a font icon additionally picks up the slot's glyph +sizing that an svg-type selector never reached. The divergence is an arbitrary `` a consumer passes +into an icon slot (an `Input` `contentBefore`, an `Option` `checkIcon`, …): Griffel would restyle it, +windmod leaves it alone. Stamp `data-fui-icon` on it to opt in. + +### The base sheet and the type ramp + +#### 58. Tailwind's preflight ships — at the head of the base layer + +The theme package includes Tailwind's preflight. This is a deliberate divergence from Griffel's +posture: `@fluentui/react-components` ships no global reset and renders over whatever element +defaults the document brings; windmod authors every component over a normalized base instead. + +The guarantee that makes it safe is the layering. Preflight ships at the **head of `fui.base`** — +the same placement Tailwind itself gives it — below every component layer, so every component rule +in both packages outranks it by construction, and your own unlayered CSS beats it the same way it +beats everything else in the library. The rest of `fui.base` (the theme's global element resets) +follows it in source order and wins any same-layer contest. + +What to check when migrating: the reset is document-global, exactly like the preflight of a Tailwind +app — element defaults (heading sizes, list markers and margins, button font, `fieldset`/`legend`, +table borders) are normalized everywhere the theme sheet loads, not only under the provider. Re-check +any markup of your own that relied on UA default styling, inside the provider especially; markup you +style yourself is unaffected, because any rule you author outranks the reset. + +#### 59. Leading tokens are unitless ratios, not lengths + +Griffel's line-height tokens are px strings — `tokens.lineHeightBase300` is `'20px'`. windmod's +`--leading-*` tokens are the same ramp expressed as unitless ratios of their paired font-size: +`--leading-base-300` is `calc(20 / 14)`, `--leading-base-400` is `1.375`, and so on down the ramp — +always the ramp's line-height px over its paired font-size px. Because the ratios are theme-invariant, +they are declared once in `./base.css` rather than per theme. + +Rendered boxes do not move: every windmod rule that sets a `leading-*` sets an authored font-size on +the same element, so the ratio multiplies exactly the font-size the old length encoded (verified with +an element-keyed probe across all 91 scenes — zero rect changes). + +Two things do change: + +- **Inheritance.** A length line-height inherits as that computed length; a number inherits as a + ratio and recomputes against every descendant's own font-size. A descendant of a windmod element + that sets only `font-size` and counted on inheriting Griffel's fixed px line box now gets + `font-size × ratio` instead. +- **Reading the token.** `getComputedStyle(el).getPropertyValue('--leading-base-300')` — or + `useCssVarValue` — no longer returns a length. Multiply by the paired font-size token to get one: + `calc(var(--text-base-300) * var(--leading-base-300))` is the windmod spelling of Griffel's + `lineHeightBase300`. + +## Where the pixels are allowed to differ + +The parity gate is strict zero: pixelmatch at threshold 0, with the antialiasing classifier absorbing +sub-perceptual rasterization noise. One differing pixel that is not classified as antialiasing fails the +scene. Seventy-three of the ninety-one scenes hold that unconditionally. The remaining eighteen carry a +numeric ceiling — counted under the same rule — recorded on the scene itself, each granted individually +and each recorded with the control that bounds it. **No allowance is a tolerance band** — every one names a specific mechanism, and a diff that does +not decompose the documented way fails the scene even when it sits under the ceiling. All eighteen are +below. + +Four of the eighteen are known to be pure GPU rasterization: re-run with `--disable-gpu` and they are +strict zero, which is the strongest statement in the table — the CSS is exactly correct. The rest split +into genuine geometry (survives software rasterization) and rows where the no-GPU mode is itself the +noisier one and the GPU gate stays authoritative. + +The **Decision** column is the identifier the allowance was ratified under; rows sharing one were granted +together, as a single class on a single body of evidence. + +| Scene(s) | Ceiling | Decision | Mechanism | Under `--disable-gpu` | +| ----------------------------------------------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teaching-popover` | 21093 | P | GPU rasterization of the UA-mandated `position: fixed` top-layer surface. No CSS component at all. | **0 — completely compliant** | +| `teaching-popover-carousel` | 18386 | Q6 | The same top-layer compositing texture as `teaching-popover`, over the larger carousel surface. No CSS component: the promoted Griffel-vs-Griffel identity control reads 0 in both raster modes. | **0 — completely compliant** (proven ×5) | +| `teaching-popover-carousel-brand` | 17816 | Q6 | Same class and same procedure as the row above, on the brand surface. | **0 — completely compliant** (proven ×5) | +| `teaching-popover-carousel-rtl` | 18386 | Q6 | Same class as the two rows above. Geometry is byte-identical — a full-precision rect walk reads 0 — after the `PopoverSurface` 1/64 px RTL arrow fix that shipped with this cycle. | 6 — a settle-pass harness artifact, not windmod: `getAnimations().cancel()` de-promotes only the Griffel side, and a Griffel-vs-Griffel control bounds it at exactly 6 | +| `toast-inverted` | 8 | J | A 1-ULP blend split in _Griffel's own_ bimodal rasterization of the inverted error glyph; windmod is byte-stable on Griffel's majority face. | **0 — completely compliant** | +| `teaching-popover-placements` | 8883 | P | 5824 px of the arrow displacement of [delta 28](#28-tooltips-arrow-sits-differently-on-corner-placements) plus 3059 px of the same top-layer rasterization as `teaching-popover`. | 5824 — the predicted arrow share survives | +| `info-label-open` | 8545 | O | Whole-assembly translation, dx=1 at `medium` and dx=3 at `large`; the surface bands are byte-identical once shifted. | 8107 — persists (positioning, not compositing) | +| `tooltip` | 8164 | G | Sub-pixel glyph displacement (six cells) plus the `above-start`/`below-end` arrow displacement of [delta 28](#28-tooltips-arrow-sits-differently-on-corner-placements). | 4219 — both mechanisms survive | +| `popover` | 6551 | G | 1274 px arrow paint ([delta 28](#28-tooltips-arrow-sits-differently-on-corner-placements)'s mechanism) + 3398 px of its shadow derivative + 1949 px of fixed-vs-absolute drop-shadow rasterization. | 4366 — arrow and fringe survive | +| `dialog-scroll` | 1335 | H | A 2px scroller-height delta: windmod's grid tracks resolve inside the content box where Griffel's separate scroller overflows its parent by the border. Structurally unclosable. | 1335 — persists in full, 0% GPU component | +| `tag-picker-open-ltr` / `-rtl`, `-width-ltr` / `-rtl` | 19 / 2 / 57 / 44 | N | Coverage-gamma anti-aliasing: Chrome blends windmod's native top-layer listbox linearly while Griffel's inline surface takes the gamma path. A Griffel-vs-Griffel control reproduces windmod pixel-identically on 3 of the 4 scenes. | Regresses to 9348/9348/4673/4673 — **GPU gate authoritative** | +| `menu`, `menu-rtl` | 413 / 412 | I | The same top-layer compositing class as the TagPicker rows. Both ceilings cover measured bimodal GPU modes (`menu` 403–413, `menu-rtl` 402–412). | Both rows are bimodal under the flag and carry no no-GPU expectation. **GPU gate authoritative** | +| `menu-sequel`, `menu-sequel-rtl` | 462 / 460 | Q5 | The same top-layer GPU-compositing class as the `menu` rows, over the larger menu the scene opens: shadow quantisation at the surface edges plus switch-thumb anti-aliasing, on byte-identical rects (138 px from the plain menu items, 322 px from the split-group cells). Promoting Griffel's own menu popover onto that compositing path reproduces the gate exactly — overlap 460/460, nothing on either side alone, in both directions. | No no-GPU expectation, as on the `menu` rows: software rasterization is bimodal for this family. **GPU gate authoritative** | + +**Why `--disable-gpu` is not simply a cleaner baseline.** Software rasterization does not put the two +surfaces back on the same path — it puts them on a _different pair_ of paths. Shipped-green strict-zero +control scenes (`combobox-open-ltr`, `dropdown-open-ltr`, …) measure 8964 under the flag with a +byte-identical diff signature to the TagPicker and Menu rows, which is exactly how those rows are known to +be harness noise rather than a windmod defect. A no-GPU number is also never the GPU number minus a +component: both captures move, by roughly 43 000 px each. + +**None of this is consumer-visible.** Every row is a sub-pixel or few-pixel rendering artifact on one +scene; the two libraries agree on layout, colour and type everywhere in the table. It is recorded here +because a team taking the package on deserves to know precisely which pixels were signed off and on what +evidence, and because a VR harness pointed at this package will reproduce these numbers. + +## What is not shipped + +windmod reskins what the headless package ships and invents nothing. These have no windmod component: + +| Not shipped | Why | +| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Text` and the typography components, `Table`/`DataGrid`, `Tree`, the standalone `Carousel`, `Virtualizer`, `List` | no headless counterpart (the teaching-popover carousel is a different component and does ship — see [the teaching carousel](#the-teaching-carousel)) | +| `MessageBarGroup` | no headless counterpart, and no visual contract to reskin (see [delta 21](#21-messagebar-has-no-group-animation-and-does-not-announce)) | +| `Overflow` and family | **scoped out permanently** — see below | +| `Persona`'s `presence` slot and `presenceOnly` prop | the headless surface omits both; a windmod `Persona` cannot render a presence badge | + +All of these compose over windmod components without a shim: they are Griffel-styled containers around +windmod-styled children, and nothing in either library's CSS fights the other. + +### `Overflow` will never be ported, and needs no migration step + +Keep importing it from `@fluentui/react-components`: + +```tsx +import { Overflow, OverflowItem, useOverflowMenu } from '@fluentui/react-components'; +import { Button } from '@fluentui/react-windmod-preview/button'; +``` + +`Overflow` is renderless. It emits no element of its own — it clones its single child, drives the +priority-overflow engine over that child's subtree, and stamps `data-overflowing`, `data-overflow-item`, +`data-overflow-menu`, `data-overflow-divider` and `data-overflow-group`. A windmod port reskins a headless +component, and a component with no skin has nothing to reskin. + +The whole Griffel `Overflow` family ships **two** CSS declarations, both token-free and theme-free: + +```css +[data-overflowing] { + display: none; +} +[data-overflow-menu] { + flex-shrink: 0; +} +``` + +Neither says anything about how the items _look_, which is exactly why they are as correct over +windmod-styled items as over Griffel-styled ones. This was measured, not assumed: eight windmod `Button`s +driven across eight container widths, the count of items the engine marked `data-overflowing` equalling the +count the browser actually gave `display: none` at every width, the `+N` trigger appearing with the right +count, no horizontal spill, and full reversal on widening. The cascade win is structural — the component's +own `display: inline-flex` is layered and `[data-overflowing] { display: none }` is unlayered. + +The README's **Overflow** section carries the full worked example, including the Griffel-free +`@fluentui/react-headless-components-preview/overflow` route and the two declarations you must supply +yourself if you take it. + +## Known issues that affect both libraries + +These live in shared upstream code, so windmod is at exact parity with `@fluentui/react-components` on each +one. They are listed because they are consumer-actionable, not because migrating causes them. + +- **`InteractionTagPrimary` silently loses selection when you pass `onClick`.** The base hook spreads + `...props` _after_ its own merged `onClick`, so your handler **replaces** the selection call instead of + composing with it. Worse, `onClick={undefined}` — what forwarding an optional prop produces — loses both + the consumer call and the selection. Work around it by calling the selection yourself, or by never + forwarding an `onClick` that may be `undefined`. +- **`BreadcrumbButton`'s `as` prop is mis-parsed.** The element type resolves as + `as ?? href ? 'a' : 'button'`, which parses as `(as ?? href) ? 'a' : 'button'` — so any truthy `as` + (including `as="button"`) yields an ``, and the `as` value itself is never used. Set `href` to get an + anchor and omit `as`. +- **`SwatchPicker` and `SwatchPickerRow` discard a consumer `style`.** Both base hooks destructure `style` + out of props and never re-apply it. Use `className` instead. +- **A vertical `AlphaSlider` reports a horizontal orientation to assistive technology.** Its base hook omits + `role="group"` on the root and `min` / `max` / `tabIndex` / `aria-orientation` on the input, all of which + the sibling `ColorSlider` sets. Supply them through the slots if you ship a vertical alpha slider. +- **A consumer cannot override `ColorArea`'s progress custom properties.** The two slider hooks spread + `{...hookVariables, ...style}` (consumer wins) while `ColorArea` spreads them the other way round (hook + wins), so `--fui-Slider--progress` is overridable and `--fui-AreaX--progress` is not. +- **`SpinButton`'s held-mouse spin does not report its spin state.** The mouse path writes to an internal + field that is never exposed, so a pressed look driven by that state never applies while the pointer is + held. Keyboard-held spins are unaffected. +- **A disabled `ColorSwatch` loses its 1px border on hover.** The hover reset is not undone by the disabled + rule, so the swatch outline disappears under the cursor while `cursor: not-allowed` still shows. + Reproduced faithfully from Griffel. +- **A `MenuSplitGroup` whose two halves are different elements loses its seam styling.** The group selects + its halves positionally with `:nth-of-type`, which counts by ELEMENT TYPE — so a `MenuItemLink` action + half (an ``) beside a `MenuItem` trigger half (a `
`) makes each half the _first_ of its own type. + The zeroed inner radii, the leading padding and the 1px divider all stop applying, and both halves take + `flex: 1` instead of only the first. Griffel's selector has the same shape and misfires identically, so + both libraries render this the same wrong way. Keep both halves on the same element type. + +### Mixing windmod with `@fluentui/react-components` + +Griffel containers around windmod children work. The reverse — a windmod child inside a _headless_ (unstyled) +container — has gaps, because a windmod component reads the **windmod** context and only a windmod container +publishes it: + +- A windmod `Tag` inside a headless `TagGroup` or `TagPickerGroup` falls back to `filled` / `medium`. +- A windmod `InteractionTagPrimary` or `InteractionTagSecondary` inside a headless `InteractionTag` falls + back to `filled` / `rounded` / `medium`. +- A windmod `NavItem` inside a headless `Nav` gets `density: 'medium'` regardless of the Nav's props. + +Use windmod containers for windmod children, and the values propagate normally. diff --git a/packages/react-components/react-windmod-preview/library/README.md b/packages/react-components/react-windmod-preview/library/README.md new file mode 100644 index 0000000000000..ecfc91848bb9d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/README.md @@ -0,0 +1,206 @@ +# @fluentui/react-windmod-preview + +**Fluent UI React components styled with Tailwind v4 + CSS Modules ("windmod")** + +Each component composes the corresponding +[`@fluentui/react-headless-components-preview`](../../react-headless-components-preview/library) +hooks (`useX` → `useXStyles` → `renderX`) with build-time-compiled CSS Modules that replicate the +Griffel suite's visuals. Theming is pure CSS via +[`@fluentui/react-tailwind-theme-preview`](../../react-tailwind-theme-preview) theme classes, +applied by `FluentProvider`, which renders a block element carrying the suite's base typography, +text colour and background onto its subtree. No Griffel, no runtime style injection. + +> **Preview** — this package tracks `react-headless-components-preview`, which is itself in +> preview: APIs may change without notice, and coverage is limited to the components the headless +> package ships. Not production-ready. + +Using a coding agent? This package ships an [Agent Skill](./skills/fluentui-windmod/SKILL.md) covering +the override model, the class and `data-*` surface, the variant catalog and the theme classes. It +travels in the tarball, so it stays in step with the version you installed — point your agent at +`node_modules/@fluentui/react-windmod-preview/skills/fluentui-windmod/SKILL.md` +([how](./skills/fluentui-windmod/README.md)). + +Coming from `@fluentui/react-components`? [MIGRATION.md](./MIGRATION.md) is the port guide: the family +map, the parity contract, every deliberate difference, and what is not shipped. + +## Usage + +```sh +npm install @fluentui/react-windmod-preview @fluentui/react-tailwind-theme-preview +``` + +```js +// Once per document, before your own styles: the theme-less base, then one file per theme you ship. +import '@fluentui/react-tailwind-theme-preview/base.css'; +import '@fluentui/react-tailwind-theme-preview/themes/web-dark.css'; +``` + +```jsx +import { Button } from '@fluentui/react-windmod-preview/button'; +import { FluentProvider, webDarkThemeClassName } from '@fluentui/react-windmod-preview/provider'; +import { Tooltip } from '@fluentui/react-windmod-preview/tooltip'; + +export default function App() { + return ( + + + + + + ); +} +``` + +Component styles are delivered **per component**: each component's class map side-effect-imports +its own compiled stylesheet, so a bundler ships only what the app uses. Alongside them, load this +package's root sheet — `@fluentui/react-windmod-preview/base.css` (3.8 KB: the cascade-layer order +and the global `@property` registrations) — once per document, ahead of everything else, either +directly or by `@import`ing it at the top of your own root stylesheet. + +Themes follow Griffel's contract: there is **no default**, so you import the theme's stylesheet and +pass its class — the same two steps as `import { webDarkTheme }` + `theme={webDarkTheme}`, and you +pay for only the themes you ship (5.9 KB gzip for base + one, against 15.4 KB for all seven). + +CommonJS/SSR consumers instead import `@fluentui/react-windmod-preview/styles.css`, the +batteries-included aggregate carrying the root sheet and every component in one file; the theme +package's `styles.css` is the matching all-in-one for base + all seven themes. + +See [MIGRATION.md](./MIGRATION.md#installation-and-imports) for both setups. + +## Scaling a subtree — ScaleRegion + +```jsx +import { FluentProvider, ScaleRegion, webDarkThemeClassName } from '@fluentui/react-windmod-preview/provider'; + + + {/* everything in here renders half again as large */} +; +``` + +`scale` is a **unitless multiplier on the ambient base scale**: spacing, control geometry, stroke +widths, the type ramp, icon glyphs, shadow geometry and border radii all follow it coherently, +exactly as the whole UI already follows the browser's root font size (a region at `scale={1.5}` in +a document whose root font size the user bumped 2× renders 3× the design size). It works in both +directions — `> 1` for a zoomed tutorial highlight, `< 1` for condensed density. + +Steps are **absolute**: a nested `ScaleRegion` replaces the ambient factor with its own — factors +never compound, and a nested region without `scale` resets its subtree to 1. The region renders a +`display: contents` div, so it adds no box to your layout. It must sit inside a themed +`FluentProvider` (it re-stamps the provider's theme class; a development-only warning fires +otherwise). Components that render into the top layer (Menu, Dialog, Tooltip, …) keep the scale — +top-layer elements stay in the DOM tree for inheritance. + +## Styling contract + +- Slot `className` props merge last — your classes win via cascade layers (consumer CSS is + unlayered; all package styles live in `fui.*` layers). +- Each component's root carries a pair of public identity classes: `fui-button` (the documented + identity class — safe for consumer CSS and `querySelector`, no escaping needed) and + `group/fui-button` (Tailwind's real named-group class). Internals use hashed idents and + `data-*` state attributes (`data-open`, `data-placement`, `data-size`, …). +- Children inside a component can target it directly with `group-/fui-` + (e.g. `group-disabled/fui-button:text-red-500`) — no group name declaration required. + Consumers may additionally add their own `group/name` via `className` to disambiguate nested + instances of the same component, but are never required to. + +## Overflow + +**There is no windmod `Overflow`, and none is coming — nothing about it needs porting.** Keep +importing `Overflow` / `OverflowItem` / `useOverflowMenu` from `@fluentui/react-components` and +compose them with windmod components directly. + +`Overflow` is renderless. It emits no element of its own: it clones its single child, drives +[`@fluentui/priority-overflow`](../../priority-overflow) over that child's subtree, and stamps +`data-overflowing`, `data-overflow-item`, `data-overflow-menu`, `data-overflow-divider` and +`data-overflow-group` on the elements the engine measures. A windmod port re-skins a headless +component; a component with no skin has nothing to re-skin. + +The entire Griffel `Overflow` family ships **two** CSS declarations, both token-free and +theme-free (for scale, windmod `Button` reproduces 105): + +```css +[data-overflowing] { + display: none; +} +[data-overflow-menu] { + flex-shrink: 0; +} +``` + +Neither says anything about how the items look, which is exactly why they are as correct over +windmod-styled items as over Griffel-styled ones. + +```jsx +import { Overflow, OverflowItem, useOverflowMenu } from '@fluentui/react-components'; +import { Button } from '@fluentui/react-windmod-preview/button'; + +// `Menu` is not part of this package; a count of what the engine hid needs no extra component. +const OverflowCount = () => { + const { ref, overflowCount, isOverflowing } = useOverflowMenu(); + return isOverflowing ? ( + + ) : null; +}; + +export const Commands = ({ labels }) => ( + +
+ {labels.map(label => ( + + + + ))} + +
+
+); +``` + +### Compatibility statement (measured, not assumed) + +The live story `Windmod/Overflow → WithWindmodComponents` +(`stories/src/Overflow/OverflowWithWindmod.stories.tsx`) was driven in headless Chrome over eight +container widths — 1000, 760, 600, 480, 360, 260, 180 and back to 1000 px — with eight windmod +`Button`s as the overflow items and a windmod `Button` as the `+N` trigger. All 18 assertions +passed: + +- At every width the number of items the engine marked `data-overflowing` equalled the number of + items the browser actually gave `display: none` (0, 2, 4, 5, 6, 7, 8, 0) — the items genuinely + disappear, they are not merely marked. +- Every hidden element carried the `fui-button` identity class, so what disappeared really was a + windmod component. +- The `+N` trigger appeared exactly when items were hidden, reported the correct count, and + computed `flex-shrink: 0`. +- `scrollWidth` equalled `clientWidth` at every width — the strip never spilled its box. +- Widening back to 1000 px restored all eight items and removed the trigger: the engine is + reversible over windmod components. + +Why the hiding wins the cascade, also measured: on a hidden item, exactly two rules in the +document declare `display` — windmod's `.fuicm-button-root-… { display: inline-flex }` inside the +`fui.components.l1` layer, and Griffel's `[data-overflowing] { display: none }`, which is +**unlayered**. Unlayered CSS outranks layered CSS regardless of selector weight, so the win does +not depend on specificity (Griffel's selector happens to be heavier as well). + +### If you want the Griffel-free `Overflow` + +`@fluentui/react-headless-components-preview/overflow` re-exports Griffel's `OverflowItem`, +`OverflowDivider` and every hook unchanged, and swaps in an `Overflow` that is Griffel's minus the +styles hook — that missing hook is the entire delta. It is smaller and pulls in no Griffel, but it +ships no CSS, so the engine stamps the attributes and nothing acts on them. **Supply the two +declarations yourself, from an unlayered stylesheet:** + +```css +/* Consumer CSS is unlayered, which is what lets these out-rank the component's own layered + `display` — do not wrap them in a `@layer`. */ +.my-overflow-container [data-overflowing] { + display: none; +} +.my-overflow-container [data-overflow-menu] { + flex-shrink: 0; +} +``` + +The same probe run above measured this path side by side with the Griffel one and produced +identical visibility numbers at all eight widths. diff --git a/packages/react-components/react-windmod-preview/library/config/api-extractor.json b/packages/react-components/react-windmod-preview/library/config/api-extractor.json new file mode 100644 index 0000000000000..8d482156d10d5 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json", + "mainEntryPointFilePath": "/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts" +} diff --git a/packages/react-components/react-windmod-preview/library/config/tests.cjs b/packages/react-components/react-windmod-preview/library/config/tests.cjs new file mode 100644 index 0000000000000..439dc140f2e5a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/config/tests.cjs @@ -0,0 +1,40 @@ +/** Jest test setup file. */ + +require('@testing-library/jest-dom'); + +// jsdom implements neither ResizeObserver nor the native methods, and the headless +// components this package decorates call both unguarded. Same shims, same reasons, as the headless +// package's own config/tests.cjs. +global.ResizeObserver = class ResizeObserver { + observe() { + // no-op for jsdom + } + unobserve() { + // no-op for jsdom + } + disconnect() { + // no-op for jsdom + } +}; + +if (typeof HTMLDialogElement !== 'undefined') { + const proto = HTMLDialogElement.prototype; + + if (!proto.showModal) { + proto.showModal = function showModal() { + this.setAttribute('open', ''); + }; + } + + if (!proto.show) { + proto.show = function show() { + this.setAttribute('open', ''); + }; + } + + if (!proto.close) { + proto.close = function close() { + this.removeAttribute('open'); + }; + } +} diff --git a/packages/react-components/react-windmod-preview/library/docs/Spec.md b/packages/react-components/react-windmod-preview/library/docs/Spec.md new file mode 100644 index 0000000000000..d2663916218dc --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/docs/Spec.md @@ -0,0 +1,63 @@ +# @fluentui/react-windmod-preview Spec + +## Background + +_Description and use cases of this component_ + +## Prior Art + +_Include background research done for this component_ + +- _Link to Open UI research_ +- _Link to comparison of v7 and v0_ +- _Link to GitHub epic issue for the converged component_ + +## Sample Code + +_Provide some representative example code that uses the proposed API for the component_ + +## Variants + +_Describe visual or functional variants of this control, if applicable. For example, a slider could have a 2D variant._ + +## API + +_List the **Props** and **Slots** proposed for the component. Ideally this would just be a link to the component's `.types.ts` file_ + +## Structure + +- _**Public**_ +- _**Internal**_ +- _**DOM** - how the component will be rendered as HTML elements_ + +## Migration + +_Describe what will need to be done to upgrade from the existing implementations:_ + +- _Migration from v8_ +- _Migration from v0_ + +## Behaviors + +_Explain how the component will behave in use, including:_ + +- _Component States_ +- _Interaction_ + - _Keyboard_ + - _Cursor_ + - _Touch_ + - _Screen readers_ + +## Accessibility + +Base accessibility information is included in the design document. After the spec is filled and review, outcomes from it need to be communicated to design and incorporated in the design document. + +- Decide whether to use **native element** or follow **ARIA** and provide reasons +- Identify the **[ARIA](https://www.w3.org/TR/wai-aria-practices-1.2/) pattern** and, if the component is listed there, follow its specification as possible. +- Identify accessibility **variants**, the `role` ([ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#role_definitions)) of the component, its `slots` and `aria-*` props. +- Describe the **keyboard navigation**: Tab Order and Arrow Key Navigation. Describe any other keyboard **shortcuts** used +- Specify texts for **state change announcements** - [ARIA live regions + ](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) (number of available items in dropdown, error messages, confirmations, ...) +- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them +- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation) +- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases) diff --git a/packages/react-components/react-windmod-preview/library/eslint.config.cjs b/packages/react-components/react-windmod-preview/library/eslint.config.cjs new file mode 100644 index 0000000000000..ec2e7cb1fc479 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/eslint.config.cjs @@ -0,0 +1,5 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [...fluentPlugin.configs['flat/react']]; diff --git a/packages/react-components/react-windmod-preview/library/etc/accordion.api.md b/packages/react-components/react-windmod-preview/library/etc/accordion.api.md new file mode 100644 index 0000000000000..d4a12d68cb5d1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/accordion.api.md @@ -0,0 +1,144 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AccordionContextValues } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionHeaderProps as AccordionHeaderProps_2 } from '@fluentui/react-headless-components-preview/accordion'; +import { AccordionHeaderSlots } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionHeaderState as AccordionHeaderState_2 } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionItemProps as AccordionItemProps_2 } from '@fluentui/react-headless-components-preview/accordion'; +import { AccordionItemSlots } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionItemState as AccordionItemState_2 } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionPanelProps as AccordionPanelProps_2 } from '@fluentui/react-headless-components-preview/accordion'; +import { AccordionPanelSlots } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionPanelState as AccordionPanelState_2 } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionProps as AccordionProps_2 } from '@fluentui/react-headless-components-preview/accordion'; +import { AccordionSlots } from '@fluentui/react-headless-components-preview/accordion'; +import type { AccordionState as AccordionState_2 } from '@fluentui/react-headless-components-preview/accordion'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAccordion } from '@fluentui/react-headless-components-preview/accordion'; +import { renderAccordionHeader } from '@fluentui/react-headless-components-preview/accordion'; +import { renderAccordionItem } from '@fluentui/react-headless-components-preview/accordion'; +import { renderAccordionPanel } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordion } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionContext } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionContextValues } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionHeader } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionHeaderContextValues } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionItem } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionItemContextValues } from '@fluentui/react-headless-components-preview/accordion'; +import { useAccordionPanel } from '@fluentui/react-headless-components-preview/accordion'; + +// @public +export const Accordion: ForwardRefComponent; + +// @public +export const accordionClassNames: { + root: string; +}; + +export { AccordionContextValues } + +// @public +export const AccordionHeader: ForwardRefComponent; + +// @public +export const accordionHeaderClassNames: { + root: string; +}; + +// @public +export type AccordionHeaderProps = AccordionHeaderProps_2 & { + inline?: boolean; + size?: AccordionHeaderSize; +}; + +// @public +export type AccordionHeaderSize = 'small' | 'medium' | 'large' | 'extra-large'; + +export { AccordionHeaderSlots } + +// @public +export type AccordionHeaderState = AccordionHeaderState_2 & Required>; + +// @public +export const AccordionItem: ForwardRefComponent; + +// @public +export const accordionItemClassNames: { + root: string; +}; + +// @public +export type AccordionItemProps = AccordionItemProps_2; + +export { AccordionItemSlots } + +// @public +export type AccordionItemState = AccordionItemState_2; + +// @public +export const AccordionPanel: ForwardRefComponent; + +// @public +export const accordionPanelClassNames: { + root: string; +}; + +// @public +export type AccordionPanelProps = AccordionPanelProps_2; + +export { AccordionPanelSlots } + +// @public +export type AccordionPanelState = AccordionPanelState_2; + +// @public +export type AccordionProps = AccordionProps_2; + +export { AccordionSlots } + +// @public +export type AccordionState = AccordionState_2; + +export { renderAccordion } + +export { renderAccordionHeader } + +export { renderAccordionItem } + +export { renderAccordionPanel } + +export { useAccordion } + +export { useAccordionContext } + +export { useAccordionContextValues } + +export { useAccordionHeader } + +export { useAccordionHeaderContextValues } + +// @public +export const useAccordionHeaderStyles: (state: AccordionHeaderState) => AccordionHeaderState; + +export { useAccordionItem } + +export { useAccordionItemContextValues } + +// @public +export const useAccordionItemStyles: (state: AccordionItemState) => AccordionItemState; + +export { useAccordionPanel } + +// @public +export const useAccordionPanelStyles: (state: AccordionPanelState) => AccordionPanelState; + +// @public +export const useAccordionStyles: (state: AccordionState) => AccordionState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/avatar-group.api.md b/packages/react-components/react-windmod-preview/library/etc/avatar-group.api.md new file mode 100644 index 0000000000000..ab7bf62ea17bd --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/avatar-group.api.md @@ -0,0 +1,145 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AvatarGroupContextValue } from '@fluentui/react-headless-components-preview/avatar-group'; +import { AvatarGroupContextValues } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupItemProps as AvatarGroupItemProps_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupItemSlots as AvatarGroupItemSlots_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupItemState as AvatarGroupItemState_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupPopoverPopoverProps } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupPopoverProps as AvatarGroupPopoverProps_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupPopoverSlots as AvatarGroupPopoverSlots_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupPopoverState as AvatarGroupPopoverState_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupProps as AvatarGroupProps_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import { AvatarGroupProvider } from '@fluentui/react-headless-components-preview/avatar-group'; +import { AvatarGroupSlots } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarGroupState as AvatarGroupState_2 } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { AvatarProps as AvatarProps_2 } from '@fluentui/react-headless-components-preview/avatar'; +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { PartitionAvatarGroupItems } from '@fluentui/react-headless-components-preview/avatar-group'; +import { partitionAvatarGroupItems } from '@fluentui/react-headless-components-preview/avatar-group'; +import { PartitionAvatarGroupItemsOptions } from '@fluentui/react-headless-components-preview/avatar-group'; +import { PopoverSurfaceProps } from '@fluentui/react-headless-components-preview/popover'; +import { renderAvatarGroup } from '@fluentui/react-headless-components-preview/avatar-group'; +import { renderAvatarGroupItem } from '@fluentui/react-headless-components-preview/avatar-group'; +import { renderAvatarGroupPopover } from '@fluentui/react-headless-components-preview/avatar-group'; +import type { Slot } from '@fluentui/react-utilities'; +import type { TooltipProps as TooltipProps_2 } from '@fluentui/react-headless-components-preview/tooltip'; +import { useAvatarGroup } from '@fluentui/react-headless-components-preview/avatar-group'; +import { useAvatarGroupContext } from '@fluentui/react-headless-components-preview/avatar-group'; +import { useAvatarGroupItem } from '@fluentui/react-headless-components-preview/avatar-group'; +import { useAvatarGroupPopover } from '@fluentui/react-headless-components-preview/avatar-group'; + +// @public +export const AvatarGroup: ForwardRefComponent; + +// @public +export const avatarGroupClassNames: { + root: string; +}; + +export { AvatarGroupContextValue } + +export { AvatarGroupContextValues } + +// @public +export const AvatarGroupItem: ForwardRefComponent; + +// @public +export const avatarGroupItemClassNames: { + root: string; +}; + +// @public +export type AvatarGroupItemProps = AvatarGroupItemProps_2 & { + color?: AvatarColor; + idForColor?: string | undefined; +}; + +// @public +export type AvatarGroupItemSlots = Omit & { + avatar: NonNullable>; +}; + +// @public +export type AvatarGroupItemState = ComponentState & Pick & { + size: AvatarSize; +}; + +// @public +export const AvatarGroupPopover: { + (props: AvatarGroupPopoverProps): JSXElement; + displayName: string; +}; + +// @public +export const avatarGroupPopoverClassNames: { + triggerButton: string; +}; + +export { AvatarGroupPopoverPopoverProps } + +// @public +export type AvatarGroupPopoverProps = Omit>, 'children'> & AvatarGroupPopoverPopoverProps & Pick; + +// @public +export type AvatarGroupPopoverSlots = Omit & { + popoverSurface: NonNullable>; + tooltip: NonNullable>; +}; + +// @public +export type AvatarGroupPopoverState = ComponentState & Pick & { + size: AvatarSize; +}; + +// @public +export type AvatarGroupProps = AvatarGroupProps_2 & { + size?: AvatarSize; +}; + +export { AvatarGroupProvider } + +export { AvatarGroupSlots } + +// @public +export type AvatarGroupState = AvatarGroupState_2 & Required>; + +export { PartitionAvatarGroupItems } + +export { partitionAvatarGroupItems } + +export { PartitionAvatarGroupItemsOptions } + +export { renderAvatarGroup } + +export { renderAvatarGroupItem } + +export { renderAvatarGroupPopover } + +export { useAvatarGroup } + +export { useAvatarGroupContext } + +export { useAvatarGroupItem } + +// @public +export const useAvatarGroupItemStyles: (state: AvatarGroupItemState) => AvatarGroupItemState; + +export { useAvatarGroupPopover } + +// @public +export const useAvatarGroupPopoverStyles: (state: AvatarGroupPopoverState) => AvatarGroupPopoverState; + +// @public +export const useAvatarGroupStyles: (state: AvatarGroupState) => AvatarGroupState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/avatar.api.md b/packages/react-components/react-windmod-preview/library/etc/avatar.api.md new file mode 100644 index 0000000000000..5d801efba9bf2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/avatar.api.md @@ -0,0 +1,66 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AvatarProps as AvatarProps_2 } from '@fluentui/react-headless-components-preview/avatar'; +import { AvatarSlots } from '@fluentui/react-headless-components-preview/avatar'; +import type { AvatarState as AvatarState_2 } from '@fluentui/react-headless-components-preview/avatar'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAvatar } from '@fluentui/react-headless-components-preview/avatar'; +import { useAvatar } from '@fluentui/react-headless-components-preview/avatar'; + +// @public +export const Avatar: ForwardRefComponent; + +// @public +export type AvatarActive = 'active' | 'inactive' | 'unset'; + +// @public +export type AvatarActiveAppearance = 'ring' | 'shadow' | 'ring-shadow'; + +// @public +export const avatarClassNames: { + root: string; +}; + +// @public +export type AvatarColor = 'neutral' | 'brand' | 'colorful' | AvatarNamedColor; + +// @public +export type AvatarNamedColor = 'dark-red' | 'cranberry' | 'red' | 'pumpkin' | 'peach' | 'marigold' | 'gold' | 'brass' | 'brown' | 'forest' | 'seafoam' | 'dark-green' | 'light-teal' | 'teal' | 'steel' | 'blue' | 'royal-blue' | 'cornflower' | 'navy' | 'lavender' | 'purple' | 'grape' | 'lilac' | 'pink' | 'magenta' | 'plum' | 'beige' | 'mink' | 'platinum' | 'anchor'; + +// @public +export type AvatarProps = AvatarProps_2 & { + active?: AvatarActive; + activeAppearance?: AvatarActiveAppearance; + color?: AvatarColor; + idForColor?: string | undefined; + shape?: AvatarShape; + size?: AvatarSize; +}; + +// @public +export type AvatarShape = 'circular' | 'square'; + +// @public +export type AvatarSize = 16 | 20 | 24 | 28 | 32 | 36 | 40 | 48 | 56 | 64 | 72 | 96 | 120 | 128; + +export { AvatarSlots } + +// @public +export type AvatarState = AvatarState_2 & Required> & { + color: Exclude; +}; + +export { renderAvatar } + +export { useAvatar } + +// @public +export const useAvatarStyles: (state: AvatarState) => AvatarState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/badge.api.md b/packages/react-components/react-windmod-preview/library/etc/badge.api.md new file mode 100644 index 0000000000000..c1bef81900dac --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/badge.api.md @@ -0,0 +1,56 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { BadgeProps as BadgeProps_2 } from '@fluentui/react-headless-components-preview/badge'; +import { BadgeSlots } from '@fluentui/react-headless-components-preview/badge'; +import type { BadgeState as BadgeState_2 } from '@fluentui/react-headless-components-preview/badge'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderBadge } from '@fluentui/react-headless-components-preview/badge'; +import { useBadge } from '@fluentui/react-headless-components-preview/badge'; + +// @public +export const Badge: ForwardRefComponent; + +// @public +export type BadgeAppearance = 'filled' | 'ghost' | 'outline' | 'tint'; + +// @public +export const badgeClassNames: { + root: string; +}; + +// @public +export type BadgeColor = 'brand' | 'danger' | 'important' | 'informative' | 'severe' | 'subtle' | 'success' | 'warning'; + +// @public +export type BadgeProps = BadgeProps_2 & { + appearance?: BadgeAppearance; + color?: BadgeColor; + shape?: BadgeShape; + size?: BadgeSize; +}; + +// @public +export type BadgeShape = 'circular' | 'rounded' | 'square'; + +// @public +export type BadgeSize = 'tiny' | 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'; + +export { BadgeSlots } + +// @public +export type BadgeState = BadgeState_2 & Required>; + +export { renderBadge } + +export { useBadge } + +// @public +export const useBadgeStyles: (state: BadgeState) => BadgeState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/breadcrumb.api.md b/packages/react-components/react-windmod-preview/library/etc/breadcrumb.api.md new file mode 100644 index 0000000000000..2677090e986fe --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/breadcrumb.api.md @@ -0,0 +1,138 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { BreadcrumbButtonProps as BreadcrumbButtonProps_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { BreadcrumbButtonSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbButtonState as BreadcrumbButtonState_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { BreadcrumbContextValues } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbDividerProps as BreadcrumbDividerProps_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { BreadcrumbDividerSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbDividerState as BreadcrumbDividerState_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbItemProps as BreadcrumbItemProps_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { BreadcrumbItemSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbItemState as BreadcrumbItemState_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbProps as BreadcrumbProps_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { BreadcrumbSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { BreadcrumbState as BreadcrumbState_2 } from '@fluentui/react-headless-components-preview/breadcrumb'; +import type { ButtonProps as ButtonProps_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderBreadcrumb } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { renderBreadcrumbButton } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { renderBreadcrumbDivider } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { renderBreadcrumbItem } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumb } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumbButton } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumbContext } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumbContextValues } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumbDivider } from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useBreadcrumbItem } from '@fluentui/react-headless-components-preview/breadcrumb'; + +// @public +export const Breadcrumb: ForwardRefComponent; + +// @public +export const BreadcrumbButton: ForwardRefComponent; + +// @public +export const breadcrumbButtonClassNames: { + root: string; +}; + +// @public +export type BreadcrumbButtonProps = BreadcrumbButtonProps_2; + +export { BreadcrumbButtonSlots } + +// @public +export type BreadcrumbButtonState = BreadcrumbButtonState_2 & Required> & Required>; + +// @public +export const breadcrumbClassNames: { + root: string; +}; + +export { BreadcrumbContextValues } + +// @public +export const BreadcrumbDivider: ForwardRefComponent; + +// @public +export const breadcrumbDividerClassNames: { + root: string; +}; + +// @public +export type BreadcrumbDividerProps = BreadcrumbDividerProps_2; + +export { BreadcrumbDividerSlots } + +// @public +export type BreadcrumbDividerState = BreadcrumbDividerState_2 & Pick; + +// @public +export const BreadcrumbItem: ForwardRefComponent; + +// @public +export const breadcrumbItemClassNames: { + root: string; +}; + +// @public +export type BreadcrumbItemProps = BreadcrumbItemProps_2; + +export { BreadcrumbItemSlots } + +// @public +export type BreadcrumbItemState = BreadcrumbItemState_2; + +// @public +export type BreadcrumbProps = BreadcrumbProps_2 & { + size?: BreadcrumbSize; +}; + +// @public +export type BreadcrumbSize = 'small' | 'medium' | 'large'; + +export { BreadcrumbSlots } + +// @public +export type BreadcrumbState = BreadcrumbState_2 & Required>; + +export { renderBreadcrumb } + +export { renderBreadcrumbButton } + +export { renderBreadcrumbDivider } + +export { renderBreadcrumbItem } + +export { useBreadcrumb } + +export { useBreadcrumbButton } + +// @public +export const useBreadcrumbButtonStyles: (state: BreadcrumbButtonState) => BreadcrumbButtonState; + +export { useBreadcrumbContext } + +export { useBreadcrumbContextValues } + +export { useBreadcrumbDivider } + +// @public +export const useBreadcrumbDividerStyles: (state: BreadcrumbDividerState) => BreadcrumbDividerState; + +export { useBreadcrumbItem } + +// @public +export const useBreadcrumbItemStyles: (state: BreadcrumbItemState) => BreadcrumbItemState; + +// @public +export const useBreadcrumbStyles: (state: BreadcrumbState) => BreadcrumbState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/button.api.md b/packages/react-components/react-windmod-preview/library/etc/button.api.md new file mode 100644 index 0000000000000..58625a8d578e5 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/button.api.md @@ -0,0 +1,52 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ButtonProps as ButtonProps_2 } from '@fluentui/react-headless-components-preview/button'; +import { ButtonSlots } from '@fluentui/react-headless-components-preview/button'; +import type { ButtonState as ButtonState_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderButton } from '@fluentui/react-headless-components-preview/button'; +import { useButton } from '@fluentui/react-headless-components-preview/button'; + +// @public +export const Button: ForwardRefComponent; + +// @public +export type ButtonAppearance = 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent'; + +// @public +export const buttonClassNames: { + root: string; +}; + +// @public +export type ButtonProps = ButtonProps_2 & { + appearance?: ButtonAppearance; + shape?: ButtonShape; + size?: ButtonSize; +}; + +// @public +export type ButtonShape = 'rounded' | 'circular' | 'square'; + +// @public +export type ButtonSize = 'small' | 'medium' | 'large'; + +export { ButtonSlots } + +// @public +export type ButtonState = ButtonState_2 & Required>; + +export { renderButton } + +export { useButton } + +// @public +export const useButtonStyles: (state: ButtonState) => ButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/card.api.md b/packages/react-components/react-windmod-preview/library/etc/card.api.md new file mode 100644 index 0000000000000..8804fc678bae6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/card.api.md @@ -0,0 +1,148 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CardContextValue } from '@fluentui/react-headless-components-preview/card'; +import type { CardFooterProps as CardFooterProps_2 } from '@fluentui/react-headless-components-preview/card'; +import { CardFooterSlots } from '@fluentui/react-headless-components-preview/card'; +import type { CardFooterState as CardFooterState_2 } from '@fluentui/react-headless-components-preview/card'; +import type { CardHeaderProps as CardHeaderProps_2 } from '@fluentui/react-headless-components-preview/card'; +import { CardHeaderSlots } from '@fluentui/react-headless-components-preview/card'; +import type { CardHeaderState as CardHeaderState_2 } from '@fluentui/react-headless-components-preview/card'; +import { CardOnSelectionChangeEvent } from '@fluentui/react-headless-components-preview/card'; +import type { CardPreviewProps as CardPreviewProps_2 } from '@fluentui/react-headless-components-preview/card'; +import { CardPreviewSlots } from '@fluentui/react-headless-components-preview/card'; +import type { CardPreviewState as CardPreviewState_2 } from '@fluentui/react-headless-components-preview/card'; +import type { CardProps as CardProps_2 } from '@fluentui/react-headless-components-preview/card'; +import { CardSlots } from '@fluentui/react-headless-components-preview/card'; +import type { CardState as CardState_2 } from '@fluentui/react-headless-components-preview/card'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCard } from '@fluentui/react-headless-components-preview/card'; +import { renderCardFooter } from '@fluentui/react-headless-components-preview/card'; +import { renderCardHeader } from '@fluentui/react-headless-components-preview/card'; +import { renderCardPreview } from '@fluentui/react-headless-components-preview/card'; +import { useCard } from '@fluentui/react-headless-components-preview/card'; +import { useCardContext } from '@fluentui/react-headless-components-preview/card'; +import { useCardContextValue } from '@fluentui/react-headless-components-preview/card'; +import { useCardFooter } from '@fluentui/react-headless-components-preview/card'; +import { useCardHeader } from '@fluentui/react-headless-components-preview/card'; +import { useCardPreview } from '@fluentui/react-headless-components-preview/card'; + +// @public +export const Card: ForwardRefComponent; + +// @public +export type CardAppearance = 'filled' | 'filled-alternative' | 'outline' | 'subtle'; + +// @public +export const cardClassNames: { + root: string; +}; + +export { CardContextValue } + +// @public +export const CardFooter: ForwardRefComponent; + +// @public +export const cardFooterClassNames: { + root: string; +}; + +// @public +export type CardFooterProps = CardFooterProps_2; + +export { CardFooterSlots } + +// @public (undocumented) +export type CardFooterState = CardFooterState_2; + +// @public +export const CardHeader: ForwardRefComponent; + +// @public +export const cardHeaderClassNames: { + root: string; +}; + +// @public +export type CardHeaderProps = CardHeaderProps_2; + +export { CardHeaderSlots } + +// @public (undocumented) +export type CardHeaderState = CardHeaderState_2; + +export { CardOnSelectionChangeEvent } + +// @public +export type CardOrientation = 'horizontal' | 'vertical'; + +// @public +export const CardPreview: ForwardRefComponent; + +// @public +export const cardPreviewClassNames: { + root: string; +}; + +// @public +export type CardPreviewProps = CardPreviewProps_2; + +export { CardPreviewSlots } + +// @public (undocumented) +export type CardPreviewState = CardPreviewState_2; + +// @public (undocumented) +export type CardProps = CardProps_2 & { + appearance?: CardAppearance; + orientation?: CardOrientation; + size?: CardSize; +}; + +// @public +export type CardSize = 'small' | 'medium' | 'large'; + +export { CardSlots } + +// @public (undocumented) +export type CardState = CardState_2 & Required>; + +export { renderCard } + +export { renderCardFooter } + +export { renderCardHeader } + +export { renderCardPreview } + +export { useCard } + +export { useCardContext } + +export { useCardContextValue } + +export { useCardFooter } + +// @public +export const useCardFooterStyles: (state: CardFooterState) => CardFooterState; + +export { useCardHeader } + +// @public +export const useCardHeaderStyles: (state: CardHeaderState) => CardHeaderState; + +export { useCardPreview } + +// @public +export const useCardPreviewStyles: (state: CardPreviewState) => CardPreviewState; + +// @public +export const useCardStyles: (state: CardState) => CardState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/checkbox.api.md b/packages/react-components/react-windmod-preview/library/etc/checkbox.api.md new file mode 100644 index 0000000000000..97355bd13124f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/checkbox.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { CheckboxProps as CheckboxProps_2 } from '@fluentui/react-headless-components-preview/checkbox'; +import { CheckboxSlots } from '@fluentui/react-headless-components-preview/checkbox'; +import type { CheckboxState as CheckboxState_2 } from '@fluentui/react-headless-components-preview/checkbox'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCheckbox } from '@fluentui/react-headless-components-preview/checkbox'; +import { useCheckbox } from '@fluentui/react-headless-components-preview/checkbox'; + +// @public +export const Checkbox: ForwardRefComponent; + +// @public +export const checkboxClassNames: { + root: string; +}; + +// @public +export type CheckboxProps = CheckboxProps_2 & { + shape?: CheckboxShape; + size?: CheckboxSize; +}; + +// @public +export type CheckboxShape = 'square' | 'circular'; + +// @public +export type CheckboxSize = 'medium' | 'large'; + +export { CheckboxSlots } + +// @public +export type CheckboxState = CheckboxState_2 & Required>; + +export { renderCheckbox } + +export { useCheckbox } + +// @public +export const useCheckboxStyles: (state: CheckboxState) => CheckboxState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/color-picker.api.md b/packages/react-components/react-windmod-preview/library/etc/color-picker.api.md new file mode 100644 index 0000000000000..6a05fcef3d32a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/color-picker.api.md @@ -0,0 +1,143 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AlphaSliderProps as AlphaSliderProps_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import { AlphaSliderSlots } from '@fluentui/react-headless-components-preview/color-picker'; +import type { AlphaSliderState as AlphaSliderState_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorAreaProps as ColorAreaProps_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import { ColorAreaSlots } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorAreaState as ColorAreaState_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import { ColorPickerContextValue } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorPickerProps as ColorPickerProps_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import { ColorPickerSlots } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorPickerState as ColorPickerState_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorSliderProps as ColorSliderProps_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import { ColorSliderSlots } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ColorSliderState as ColorSliderState_2 } from '@fluentui/react-headless-components-preview/color-picker'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAlphaSlider } from '@fluentui/react-headless-components-preview/color-picker'; +import { renderColorArea } from '@fluentui/react-headless-components-preview/color-picker'; +import { renderColorPicker } from '@fluentui/react-headless-components-preview/color-picker'; +import { renderColorSlider } from '@fluentui/react-headless-components-preview/color-picker'; +import { useAlphaSlider } from '@fluentui/react-headless-components-preview/color-picker'; +import { useColorArea } from '@fluentui/react-headless-components-preview/color-picker'; +import { useColorPicker } from '@fluentui/react-headless-components-preview/color-picker'; +import { useColorPickerContextValue } from '@fluentui/react-headless-components-preview/color-picker'; +import { useColorPickerContextValues } from '@fluentui/react-headless-components-preview/color-picker'; +import { useColorSlider } from '@fluentui/react-headless-components-preview/color-picker'; + +// @public +export const AlphaSlider: ForwardRefComponent; + +// @public +export const alphaSliderClassNames: { + root: string; +}; + +// @public +export type AlphaSliderProps = AlphaSliderProps_2 & { + shape?: ColorPickerShape; +}; + +export { AlphaSliderSlots } + +// @public +export type AlphaSliderState = AlphaSliderState_2 & Required>; + +// @public +export const ColorArea: ForwardRefComponent; + +// @public +export const colorAreaClassNames: { + root: string; +}; + +// @public +export type ColorAreaProps = ColorAreaProps_2 & { + shape?: ColorPickerShape; +}; + +export { ColorAreaSlots } + +// @public +export type ColorAreaState = ColorAreaState_2 & Required>; + +// @public +export const ColorPicker: ForwardRefComponent; + +// @public +export const colorPickerClassNames: { + root: string; +}; + +export { ColorPickerContextValue } + +// @public +export type ColorPickerProps = ColorPickerProps_2 & { + shape?: ColorPickerShape; +}; + +// @public +export type ColorPickerShape = 'rounded' | 'square'; + +export { ColorPickerSlots } + +// @public +export type ColorPickerState = ColorPickerState_2 & Pick; + +// @public +export const ColorSlider: ForwardRefComponent; + +// @public +export const colorSliderClassNames: { + root: string; +}; + +// @public +export type ColorSliderProps = ColorSliderProps_2 & { + shape?: ColorPickerShape; +}; + +export { ColorSliderSlots } + +// @public +export type ColorSliderState = ColorSliderState_2 & Required>; + +export { renderAlphaSlider } + +export { renderColorArea } + +export { renderColorPicker } + +export { renderColorSlider } + +export { useAlphaSlider } + +// @public +export const useAlphaSliderStyles: (state: AlphaSliderState) => AlphaSliderState; + +export { useColorArea } + +// @public +export const useColorAreaStyles: (state: ColorAreaState) => ColorAreaState; + +export { useColorPicker } + +export { useColorPickerContextValue } + +export { useColorPickerContextValues } + +// @public +export const useColorPickerStyles: (state: ColorPickerState) => ColorPickerState; + +export { useColorSlider } + +// @public +export const useColorSliderStyles: (state: ColorSliderState) => ColorSliderState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/combobox.api.md b/packages/react-components/react-windmod-preview/library/etc/combobox.api.md new file mode 100644 index 0000000000000..d19d2b10a3251 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/combobox.api.md @@ -0,0 +1,139 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComboboxProps as ComboboxProps_2 } from '@fluentui/react-headless-components-preview/combobox'; +import { ComboboxSlots } from '@fluentui/react-headless-components-preview/combobox'; +import type { ComboboxState as ComboboxState_2 } from '@fluentui/react-headless-components-preview/combobox'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { ListboxContextValues } from '@fluentui/react-headless-components-preview/combobox'; +import { ListboxProps } from '@fluentui/react-headless-components-preview/combobox'; +import { ListboxSlots } from '@fluentui/react-headless-components-preview/combobox'; +import { ListboxState } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionGroupProps } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionGroupSlots } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionGroupState } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionProps } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionSlots } from '@fluentui/react-headless-components-preview/combobox'; +import { OptionState } from '@fluentui/react-headless-components-preview/combobox'; +import { renderCombobox } from '@fluentui/react-headless-components-preview/combobox'; +import { renderListbox } from '@fluentui/react-headless-components-preview/combobox'; +import { renderOption } from '@fluentui/react-headless-components-preview/combobox'; +import { renderOptionGroup } from '@fluentui/react-headless-components-preview/combobox'; +import { useCombobox } from '@fluentui/react-headless-components-preview/combobox'; +import { useComboboxContextValues } from '@fluentui/react-headless-components-preview/combobox'; +import { useComboboxFilter } from '@fluentui/react-headless-components-preview/combobox'; +import { useListbox } from '@fluentui/react-headless-components-preview/combobox'; +import { useListboxContextValues } from '@fluentui/react-headless-components-preview/combobox'; +import { useOption } from '@fluentui/react-headless-components-preview/combobox'; +import { useOptionGroup } from '@fluentui/react-headless-components-preview/combobox'; + +// @public +export const Combobox: ForwardRefComponent; + +// @public +export type ComboboxAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const comboboxClassNames: { + root: string; +}; + +// @public +export type ComboboxProps = ComboboxProps_2 & { + appearance?: ComboboxAppearance; + size?: ComboboxSize; +}; + +// @public +export type ComboboxSize = 'small' | 'medium' | 'large'; + +export { ComboboxSlots } + +// @public +export type ComboboxState = ComboboxState_2 & Required>; + +// @public +export const Listbox: ForwardRefComponent; + +// @public +export const listboxClassNames: { + root: string; +}; + +export { ListboxContextValues } + +export { ListboxProps } + +export { ListboxSlots } + +export { ListboxState } + +// @public +const Option_2: ForwardRefComponent; +export { Option_2 as Option } + +// @public +export const optionClassNames: { + root: string; +}; + +// @public +export const OptionGroup: ForwardRefComponent; + +// @public +export const optionGroupClassNames: { + root: string; +}; + +export { OptionGroupProps } + +export { OptionGroupSlots } + +export { OptionGroupState } + +export { OptionProps } + +export { OptionSlots } + +export { OptionState } + +export { renderCombobox } + +export { renderListbox } + +export { renderOption } + +export { renderOptionGroup } + +export { useCombobox } + +export { useComboboxContextValues } + +export { useComboboxFilter } + +// @public +export const useComboboxStyles: (state: ComboboxState) => ComboboxState; + +export { useListbox } + +export { useListboxContextValues } + +// @public +export const useListboxStyles: (state: ListboxState) => ListboxState; + +export { useOption } + +export { useOptionGroup } + +// @public +export const useOptionGroupStyles: (state: OptionGroupState) => OptionGroupState; + +// @public +export const useOptionStyles: (state: OptionState) => OptionState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/compound-button.api.md b/packages/react-components/react-windmod-preview/library/etc/compound-button.api.md new file mode 100644 index 0000000000000..5b7121ceae770 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/compound-button.api.md @@ -0,0 +1,43 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { CompoundButtonProps as CompoundButtonProps_2 } from '@fluentui/react-headless-components-preview/compound-button'; +import { CompoundButtonSlots } from '@fluentui/react-headless-components-preview/compound-button'; +import type { CompoundButtonState as CompoundButtonState_2 } from '@fluentui/react-headless-components-preview/compound-button'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCompoundButton } from '@fluentui/react-headless-components-preview/compound-button'; +import { useCompoundButton } from '@fluentui/react-headless-components-preview/compound-button'; + +// @public +export const CompoundButton: ForwardRefComponent; + +// @public +export const compoundButtonClassNames: { + root: string; +}; + +// @public +export type CompoundButtonProps = CompoundButtonProps_2 & { + appearance?: ButtonAppearance; + shape?: ButtonShape; + size?: ButtonSize; +}; + +export { CompoundButtonSlots } + +// @public +export type CompoundButtonState = CompoundButtonState_2 & Required>; + +export { renderCompoundButton } + +export { useCompoundButton } + +// @public +export const useCompoundButtonStyles: (state: CompoundButtonState) => CompoundButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/dialog.api.md b/packages/react-components/react-windmod-preview/library/etc/dialog.api.md new file mode 100644 index 0000000000000..a75194b179cf9 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/dialog.api.md @@ -0,0 +1,224 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { DialogActionsProps as DialogActionsProps_2 } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogActionsSlots } from '@fluentui/react-headless-components-preview/dialog'; +import type { DialogActionsState as DialogActionsState_2 } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogBodyProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogBodySlots } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogBodyState } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogContextValue } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogContextValues } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogHeaderProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogHeaderSlots } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogHeaderState } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogModalType } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogOpenChangeData } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogOpenChangeEvent } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogOpenChangeEventHandler } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogState } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogSurfaceContextValue } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogSurfaceProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogSurfaceSlots } from '@fluentui/react-headless-components-preview/dialog'; +import type { DialogSurfaceState as DialogSurfaceState_2 } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTitleProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTitleSlots } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTitleState } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTriggerAction } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTriggerProps } from '@fluentui/react-headless-components-preview/dialog'; +import { DialogTriggerState } from '@fluentui/react-headless-components-preview/dialog'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type * as React_2 from 'react'; +import { renderDialog } from '@fluentui/react-headless-components-preview/dialog'; +import { renderDialogActions } from '@fluentui/react-headless-components-preview/dialog'; +import { renderDialogBody } from '@fluentui/react-headless-components-preview/dialog'; +import { renderDialogHeader } from '@fluentui/react-headless-components-preview/dialog'; +import { renderDialogSurface } from '@fluentui/react-headless-components-preview/dialog'; +import { renderDialogTitle } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialog } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogActions } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogBody } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogContext } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogContextValues } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogHeader } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogSurface } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogSurfaceContext } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogTitle } from '@fluentui/react-headless-components-preview/dialog'; +import { useDialogTrigger } from '@fluentui/react-headless-components-preview/dialog'; + +// @public +export const Dialog: React_2.FC; + +// @public +export const DialogActions: ForwardRefComponent; + +// @public +export const dialogActionsClassNames: { + root: string; +}; + +// @public +export type DialogActionsPosition = 'start' | 'end'; + +// @public +export type DialogActionsProps = DialogActionsProps_2 & { + position?: DialogActionsPosition; + fluid?: boolean; +}; + +export { DialogActionsSlots } + +// @public +export type DialogActionsState = DialogActionsState_2 & { + position: DialogActionsPosition; + fluid: boolean; +}; + +// @public +export const DialogBody: ForwardRefComponent; + +// @public +export const dialogBodyClassNames: { + root: string; +}; + +export { DialogBodyProps } + +export { DialogBodySlots } + +export { DialogBodyState } + +export { DialogContextValue } + +export { DialogContextValues } + +// @public +export const DialogHeader: ForwardRefComponent; + +// @public +export const dialogHeaderClassNames: { + root: string; +}; + +export { DialogHeaderProps } + +export { DialogHeaderSlots } + +export { DialogHeaderState } + +export { DialogModalType } + +export { DialogOpenChangeData } + +export { DialogOpenChangeEvent } + +export { DialogOpenChangeEventHandler } + +export { DialogProps } + +export { DialogState } + +// @public +export const DialogSurface: ForwardRefComponent; + +// @public +export const dialogSurfaceClassNames: { + root: string; +}; + +export { DialogSurfaceContextValue } + +export { DialogSurfaceProps } + +export { DialogSurfaceSlots } + +// @public +export type DialogSurfaceState = DialogSurfaceState_2 & { + nested: boolean; +}; + +// @public +export const DialogTitle: ForwardRefComponent; + +// @public +export const dialogTitleClassNames: { + root: string; +}; + +export { DialogTitleProps } + +export { DialogTitleSlots } + +export { DialogTitleState } + +// @public +export const DialogTrigger: React_2.FC; + +export { DialogTriggerAction } + +// @public +export const dialogTriggerClassNames: { + root: string; +}; + +export { DialogTriggerProps } + +export { DialogTriggerState } + +export { renderDialog } + +export { renderDialogActions } + +export { renderDialogBody } + +export { renderDialogHeader } + +export { renderDialogSurface } + +export { renderDialogTitle } + +export { useDialog } + +export { useDialogActions } + +// @public +export const useDialogActionsStyles: (state: DialogActionsState) => DialogActionsState; + +export { useDialogBody } + +// @public +export const useDialogBodyStyles: (state: DialogBodyState) => DialogBodyState; + +export { useDialogContext } + +export { useDialogContextValues } + +export { useDialogHeader } + +// @public +export const useDialogHeaderStyles: (state: DialogHeaderState) => DialogHeaderState; + +export { useDialogSurface } + +export { useDialogSurfaceContext } + +// @public +export const useDialogSurfaceStyles: (state: DialogSurfaceState) => DialogSurfaceState; + +export { useDialogTitle } + +// @public +export const useDialogTitleStyles: (state: DialogTitleState) => DialogTitleState; + +export { useDialogTrigger } + +// @public +export const useDialogTriggerStyles: (state: DialogTriggerState) => DialogTriggerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/divider.api.md b/packages/react-components/react-windmod-preview/library/etc/divider.api.md new file mode 100644 index 0000000000000..965c80efc9f87 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/divider.api.md @@ -0,0 +1,49 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { DividerProps as DividerProps_2 } from '@fluentui/react-headless-components-preview/divider'; +import { DividerSlots } from '@fluentui/react-headless-components-preview/divider'; +import type { DividerState as DividerState_2 } from '@fluentui/react-headless-components-preview/divider'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderDivider } from '@fluentui/react-headless-components-preview/divider'; +import { useDivider } from '@fluentui/react-headless-components-preview/divider'; + +// @public +export const Divider: ForwardRefComponent; + +// @public +export type DividerAlignContent = 'start' | 'center' | 'end'; + +// @public +export type DividerAppearance = 'brand' | 'default' | 'strong' | 'subtle'; + +// @public +export const dividerClassNames: { + root: string; +}; + +// @public +export type DividerProps = DividerProps_2 & { + alignContent?: DividerAlignContent; + appearance?: DividerAppearance; + inset?: boolean; +}; + +export { DividerSlots } + +// @public +export type DividerState = DividerState_2 & Required>; + +export { renderDivider } + +export { useDivider } + +// @public +export const useDividerStyles: (state: DividerState) => DividerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/drawer.api.md b/packages/react-components/react-windmod-preview/library/etc/drawer.api.md new file mode 100644 index 0000000000000..0a7c6e730a8e5 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/drawer.api.md @@ -0,0 +1,253 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { DrawerBodyProps } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerBodySlots } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerBodyState } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerContextValue } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerFooterProps } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerFooterSlots } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerFooterState } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderNavigationProps } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderNavigationSlots } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderNavigationState } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderProps } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderSlots } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderState } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderTitleProps } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderTitleSlots } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerHeaderTitleState } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerProvider } from '@fluentui/react-headless-components-preview/drawer'; +import { DrawerSlots } from '@fluentui/react-headless-components-preview/drawer'; +import type { DrawerState as DrawerState_2 } from '@fluentui/react-headless-components-preview/drawer'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InlineDrawerProps as InlineDrawerProps_2 } from '@fluentui/react-headless-components-preview/drawer'; +import { InlineDrawerSlots } from '@fluentui/react-headless-components-preview/drawer'; +import type { InlineDrawerState as InlineDrawerState_2 } from '@fluentui/react-headless-components-preview/drawer'; +import type { OverlayDrawerProps as OverlayDrawerProps_2 } from '@fluentui/react-headless-components-preview/drawer'; +import { OverlayDrawerSlots } from '@fluentui/react-headless-components-preview/drawer'; +import type { OverlayDrawerState as OverlayDrawerState_2 } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawer } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawerBody } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawerFooter } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawerHeader } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawerHeaderNavigation } from '@fluentui/react-headless-components-preview/drawer'; +import { renderDrawerHeaderTitle } from '@fluentui/react-headless-components-preview/drawer'; +import { renderInlineDrawer } from '@fluentui/react-headless-components-preview/drawer'; +import { renderOverlayDrawer } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawer } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerBody } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerContext } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerContextValue } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerFooter } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerHeader } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerHeaderNavigation } from '@fluentui/react-headless-components-preview/drawer'; +import { useDrawerHeaderTitle } from '@fluentui/react-headless-components-preview/drawer'; +import { useInlineDrawer } from '@fluentui/react-headless-components-preview/drawer'; +import { useOverlayDrawer } from '@fluentui/react-headless-components-preview/drawer'; + +// @public +export const Drawer: ForwardRefComponent; + +// @public +export const DrawerBody: ForwardRefComponent; + +// @public +export const drawerBodyClassNames: { + root: string; +}; + +export { DrawerBodyProps } + +export { DrawerBodySlots } + +export { DrawerBodyState } + +// @public +export const drawerClassNames: { + root: string; +}; + +export { DrawerContextValue } + +// @public +export const DrawerFooter: ForwardRefComponent; + +// @public +export const drawerFooterClassNames: { + root: string; +}; + +export { DrawerFooterProps } + +export { DrawerFooterSlots } + +export { DrawerFooterState } + +// @public +export const DrawerHeader: ForwardRefComponent; + +// @public +export const drawerHeaderClassNames: { + root: string; +}; + +// @public +export const DrawerHeaderNavigation: ForwardRefComponent; + +// @public +export const drawerHeaderNavigationClassNames: { + root: string; +}; + +export { DrawerHeaderNavigationProps } + +export { DrawerHeaderNavigationSlots } + +export { DrawerHeaderNavigationState } + +export { DrawerHeaderProps } + +export { DrawerHeaderSlots } + +export { DrawerHeaderState } + +// @public +export const DrawerHeaderTitle: ForwardRefComponent; + +// @public +export const drawerHeaderTitleClassNames: { + root: string; +}; + +export { DrawerHeaderTitleProps } + +export { DrawerHeaderTitleSlots } + +export { DrawerHeaderTitleState } + +// @public (undocumented) +export type DrawerProps = (OverlayDrawerProps & { + type?: 'overlay'; +}) | (InlineDrawerProps & { + type: 'inline'; +}); + +export { DrawerProvider } + +// @public +export type DrawerSize = 'small' | 'medium' | 'large' | 'full'; + +export { DrawerSlots } + +// @public (undocumented) +export type DrawerState = DrawerState_2; + +// @public +export const InlineDrawer: ForwardRefComponent; + +// @public +export const inlineDrawerClassNames: { + root: string; +}; + +// @public +export type InlineDrawerProps = InlineDrawerProps_2 & { + size?: DrawerSize; + separator?: boolean; +}; + +export { InlineDrawerSlots } + +// @public (undocumented) +export type InlineDrawerState = InlineDrawerState_2 & { + size: DrawerSize; + separator: boolean; +}; + +// @public +export const OverlayDrawer: ForwardRefComponent; + +// @public +export const overlayDrawerClassNames: { + root: string; +}; + +// @public +export type OverlayDrawerProps = OverlayDrawerProps_2 & { + size?: DrawerSize; +}; + +export { OverlayDrawerSlots } + +// @public (undocumented) +export type OverlayDrawerState = OverlayDrawerState_2 & { + size: DrawerSize; +}; + +export { renderDrawer } + +export { renderDrawerBody } + +export { renderDrawerFooter } + +export { renderDrawerHeader } + +export { renderDrawerHeaderNavigation } + +export { renderDrawerHeaderTitle } + +export { renderInlineDrawer } + +export { renderOverlayDrawer } + +export { useDrawer } + +export { useDrawerBody } + +// @public +export const useDrawerBodyStyles: (state: DrawerBodyState) => DrawerBodyState; + +export { useDrawerContext } + +export { useDrawerContextValue } + +export { useDrawerFooter } + +// @public +export const useDrawerFooterStyles: (state: DrawerFooterState) => DrawerFooterState; + +export { useDrawerHeader } + +export { useDrawerHeaderNavigation } + +// @public +export const useDrawerHeaderNavigationStyles: (state: DrawerHeaderNavigationState) => DrawerHeaderNavigationState; + +// @public +export const useDrawerHeaderStyles: (state: DrawerHeaderState) => DrawerHeaderState; + +export { useDrawerHeaderTitle } + +// @public +export const useDrawerHeaderTitleStyles: (state: DrawerHeaderTitleState) => DrawerHeaderTitleState; + +// @public +export const useDrawerStyles: (state: DrawerState) => DrawerState; + +export { useInlineDrawer } + +// @public +export const useInlineDrawerStyles: (state: InlineDrawerState) => InlineDrawerState; + +export { useOverlayDrawer } + +// @public +export const useOverlayDrawerStyles: (state: OverlayDrawerState) => OverlayDrawerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/dropdown.api.md b/packages/react-components/react-windmod-preview/library/etc/dropdown.api.md new file mode 100644 index 0000000000000..0165242c87d76 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/dropdown.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { DropdownProps as DropdownProps_2 } from '@fluentui/react-headless-components-preview/dropdown'; +import type { DropdownState as DropdownState_2 } from '@fluentui/react-headless-components-preview/dropdown'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderDropdown } from '@fluentui/react-headless-components-preview/dropdown'; +import { useDropdown } from '@fluentui/react-headless-components-preview/dropdown'; +import { useDropdownContextValues } from '@fluentui/react-headless-components-preview/dropdown'; + +// @public +export const Dropdown: ForwardRefComponent; + +// @public +export type DropdownAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const dropdownClassNames: { + root: string; +}; + +// @public +export type DropdownProps = DropdownProps_2 & { + appearance?: DropdownAppearance; + size?: DropdownSize; +}; + +// @public +export type DropdownSize = 'small' | 'medium' | 'large'; + +// @public +export type DropdownState = DropdownState_2 & Required>; + +export { renderDropdown } + +export { useDropdown } + +export { useDropdownContextValues } + +// @public +export const useDropdownStyles: (state: DropdownState) => DropdownState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/field.api.md b/packages/react-components/react-windmod-preview/library/etc/field.api.md new file mode 100644 index 0000000000000..54498b8511548 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/field.api.md @@ -0,0 +1,57 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { FieldProps as FieldProps_2 } from '@fluentui/react-headless-components-preview/field'; +import { FieldSlots } from '@fluentui/react-headless-components-preview/field'; +import type { FieldState as FieldState_2 } from '@fluentui/react-headless-components-preview/field'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderField } from '@fluentui/react-headless-components-preview/field'; +import { useField } from '@fluentui/react-headless-components-preview/field'; +import { useFieldContext } from '@fluentui/react-headless-components-preview/field'; +import { useFieldContextValues } from '@fluentui/react-headless-components-preview/field'; +import { useFieldControlProps } from '@fluentui/react-headless-components-preview/field'; + +// @public +export const Field: ForwardRefComponent; + +// @public +export const fieldClassNames: { + root: string; +}; + +// @public +export type FieldOrientation = 'vertical' | 'horizontal'; + +// @public +export type FieldProps = FieldProps_2 & { + orientation?: FieldOrientation; + size?: FieldSize; +}; + +// @public +export type FieldSize = 'small' | 'medium' | 'large'; + +export { FieldSlots } + +// @public +export type FieldState = FieldState_2 & Required>; + +export { renderField } + +export { useField } + +export { useFieldContext } + +export { useFieldContextValues } + +export { useFieldControlProps } + +// @public +export const useFieldStyles: (state: FieldState) => FieldState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/image.api.md b/packages/react-components/react-windmod-preview/library/etc/image.api.md new file mode 100644 index 0000000000000..33c308fb98d73 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/image.api.md @@ -0,0 +1,52 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ImageProps as ImageProps_2 } from '@fluentui/react-headless-components-preview/image'; +import { ImageSlots } from '@fluentui/react-headless-components-preview/image'; +import type { ImageState as ImageState_2 } from '@fluentui/react-headless-components-preview/image'; +import { renderImage } from '@fluentui/react-headless-components-preview/image'; +import { useImage } from '@fluentui/react-headless-components-preview/image'; + +// @public +const Image_2: ForwardRefComponent; +export { Image_2 as Image } + +// @public +export const imageClassNames: { + root: string; +}; + +// @public +export type ImageFit = 'none' | 'center' | 'contain' | 'cover' | 'default'; + +// @public +export type ImageProps = ImageProps_2 & { + block?: boolean; + bordered?: boolean; + fit?: ImageFit; + shadow?: boolean; + shape?: ImageShape; +}; + +// @public +export type ImageShape = 'square' | 'circular' | 'rounded'; + +export { ImageSlots } + +// @public +export type ImageState = ImageState_2 & Required>; + +export { renderImage } + +export { useImage } + +// @public +export const useImageStyles: (state: ImageState) => ImageState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/info-label.api.md b/packages/react-components/react-windmod-preview/library/etc/info-label.api.md new file mode 100644 index 0000000000000..fb15c43b20f09 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/info-label.api.md @@ -0,0 +1,81 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InfoButtonProps as InfoButtonProps_2 } from '@fluentui/react-headless-components-preview/info-label'; +import { InfoButtonSlots } from '@fluentui/react-headless-components-preview/info-label'; +import type { InfoButtonState as InfoButtonState_2 } from '@fluentui/react-headless-components-preview/info-label'; +import type { InfoLabelProps as InfoLabelProps_2 } from '@fluentui/react-headless-components-preview/info-label'; +import { InfoLabelSlots } from '@fluentui/react-headless-components-preview/info-label'; +import type { InfoLabelState as InfoLabelState_2 } from '@fluentui/react-headless-components-preview/info-label'; +import type { LabelProps as LabelProps_2 } from '@fluentui/react-headless-components-preview/label'; +import type { PopoverProps as PopoverProps_2 } from '@fluentui/react-headless-components-preview/popover'; +import { renderInfoButton } from '@fluentui/react-headless-components-preview/info-label'; +import { renderInfoLabel } from '@fluentui/react-headless-components-preview/info-label'; +import type { Slot } from '@fluentui/react-utilities'; +import { useInfoButton } from '@fluentui/react-headless-components-preview/info-label'; +import { useInfoLabel } from '@fluentui/react-headless-components-preview/info-label'; + +// @public +export const InfoButton: ForwardRefComponent; + +// @public +export const infoButtonClassNames: { + root: string; +}; + +// @public +export type InfoButtonProps = Omit & { + popover?: NonNullable>>>; + size?: InfoButtonSize; +}; + +// @public +export type InfoButtonSize = 'small' | 'medium' | 'large'; + +export { InfoButtonSlots } + +// @public +export type InfoButtonState = InfoButtonState_2 & Required>; + +// @public +export const InfoLabel: ForwardRefComponent; + +// @public +export const infoLabelClassNames: { + root: string; +}; + +// @public +export type InfoLabelProps = Omit & { + label?: NonNullable>; + infoButton?: Slot; + size?: LabelSize; + weight?: LabelWeight; +}; + +export { InfoLabelSlots } + +// @public +export type InfoLabelState = InfoLabelState_2 & Required>; + +export { renderInfoButton } + +export { renderInfoLabel } + +export { useInfoButton } + +// @public +export const useInfoButtonStyles: (state: InfoButtonState) => InfoButtonState; + +export { useInfoLabel } + +// @public +export const useInfoLabelStyles: (state: InfoLabelState) => InfoLabelState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/input.api.md b/packages/react-components/react-windmod-preview/library/etc/input.api.md new file mode 100644 index 0000000000000..09e3eae35a17f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/input.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InputProps as InputProps_2 } from '@fluentui/react-headless-components-preview/input'; +import { InputSlots } from '@fluentui/react-headless-components-preview/input'; +import type { InputState as InputState_2 } from '@fluentui/react-headless-components-preview/input'; +import { renderInput } from '@fluentui/react-headless-components-preview/input'; +import { useInput } from '@fluentui/react-headless-components-preview/input'; + +// @public +export const Input: ForwardRefComponent; + +// @public +export type InputAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const inputClassNames: { + root: string; +}; + +// @public +export type InputProps = InputProps_2 & { + appearance?: InputAppearance; + size?: InputSize; +}; + +// @public +export type InputSize = 'small' | 'medium' | 'large'; + +export { InputSlots } + +// @public +export type InputState = InputState_2 & Required>; + +export { renderInput } + +export { useInput } + +// @public +export const useInputStyles: (state: InputState) => InputState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/interaction-tag.api.md b/packages/react-components/react-windmod-preview/library/etc/interaction-tag.api.md new file mode 100644 index 0000000000000..c72a961c85baf --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/interaction-tag.api.md @@ -0,0 +1,130 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { InteractionTagContextValues } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { InteractionTagPrimaryContextValues } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagPrimaryProps as InteractionTagPrimaryProps_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { InteractionTagPrimarySlots } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagPrimaryState as InteractionTagPrimaryState_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagProps as InteractionTagProps_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagSecondaryProps as InteractionTagSecondaryProps_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { InteractionTagSecondarySlots } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagSecondaryState as InteractionTagSecondaryState_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { InteractionTagSlots } from '@fluentui/react-headless-components-preview/interaction-tag'; +import type { InteractionTagState as InteractionTagState_2 } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { renderInteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { renderInteractionTagPrimary } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { renderInteractionTagSecondary } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { useInteractionTag } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { useInteractionTagContextValues } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { useInteractionTagPrimary } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { useInteractionTagPrimaryContextValues } from '@fluentui/react-headless-components-preview/interaction-tag'; +import { useInteractionTagSecondary } from '@fluentui/react-headless-components-preview/interaction-tag'; + +// @public +export const InteractionTag: ForwardRefComponent; + +// @public +export type InteractionTagAppearance = TagAppearance; + +// @public +export const interactionTagClassNames: { + root: string; +}; + +export { InteractionTagContextValues } + +// @public +export const InteractionTagPrimary: ForwardRefComponent; + +// @public +export const interactionTagPrimaryClassNames: { + root: string; +}; + +export { InteractionTagPrimaryContextValues } + +// @public +export type InteractionTagPrimaryProps = InteractionTagPrimaryProps_2; + +export { InteractionTagPrimarySlots } + +// @public +export type InteractionTagPrimaryState = InteractionTagPrimaryState_2 & { + appearance: InteractionTagAppearance; + shape: InteractionTagShape; + size: InteractionTagSize; + avatarShape: AvatarShape; + avatarSize: AvatarSize; +}; + +// @public +export type InteractionTagProps = InteractionTagProps_2 & { + appearance?: InteractionTagAppearance; + shape?: InteractionTagShape; + size?: InteractionTagSize; +}; + +// @public +export const InteractionTagSecondary: ForwardRefComponent; + +// @public +export const interactionTagSecondaryClassNames: { + root: string; +}; + +// @public +export type InteractionTagSecondaryProps = InteractionTagSecondaryProps_2; + +export { InteractionTagSecondarySlots } + +// @public +export type InteractionTagSecondaryState = InteractionTagSecondaryState_2 & { + appearance: InteractionTagAppearance; + shape: InteractionTagShape; + size: InteractionTagSize; +}; + +// @public +export type InteractionTagShape = TagShape; + +// @public +export type InteractionTagSize = TagSize; + +export { InteractionTagSlots } + +// @public +export type InteractionTagState = InteractionTagState_2 & Required>; + +export { renderInteractionTag } + +export { renderInteractionTagPrimary } + +export { renderInteractionTagSecondary } + +export { useInteractionTag } + +export { useInteractionTagContextValues } + +export { useInteractionTagPrimary } + +export { useInteractionTagPrimaryContextValues } + +// @public +export const useInteractionTagPrimaryStyles: (state: InteractionTagPrimaryState) => InteractionTagPrimaryState; + +export { useInteractionTagSecondary } + +// @public +export const useInteractionTagSecondaryStyles: (state: InteractionTagSecondaryState) => InteractionTagSecondaryState; + +// @public +export const useInteractionTagStyles: (state: InteractionTagState) => InteractionTagState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/label.api.md b/packages/react-components/react-windmod-preview/library/etc/label.api.md new file mode 100644 index 0000000000000..817c4d2c35460 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/label.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { LabelProps as LabelProps_2 } from '@fluentui/react-headless-components-preview/label'; +import { LabelSlots } from '@fluentui/react-headless-components-preview/label'; +import type { LabelState as LabelState_2 } from '@fluentui/react-headless-components-preview/label'; +import { renderLabel } from '@fluentui/react-headless-components-preview/label'; +import { useLabel } from '@fluentui/react-headless-components-preview/label'; + +// @public +export const Label: ForwardRefComponent; + +// @public +export const labelClassNames: { + root: string; +}; + +// @public +export type LabelProps = LabelProps_2 & { + size?: LabelSize; + weight?: LabelWeight; +}; + +// @public +export type LabelSize = 'small' | 'medium' | 'large'; + +export { LabelSlots } + +// @public +export type LabelState = LabelState_2 & Required>; + +// @public +export type LabelWeight = 'regular' | 'semibold'; + +export { renderLabel } + +export { useLabel } + +// @public +export const useLabelStyles: (state: LabelState) => LabelState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/link.api.md b/packages/react-components/react-windmod-preview/library/etc/link.api.md new file mode 100644 index 0000000000000..a7faca404908f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/link.api.md @@ -0,0 +1,45 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { LinkProps as LinkProps_2 } from '@fluentui/react-headless-components-preview/link'; +import { LinkSlots } from '@fluentui/react-headless-components-preview/link'; +import type { LinkState as LinkState_2 } from '@fluentui/react-headless-components-preview/link'; +import { renderLink } from '@fluentui/react-headless-components-preview/link'; +import { useLink } from '@fluentui/react-headless-components-preview/link'; + +// @public +export const Link: ForwardRefComponent; + +// @public +export type LinkAppearance = 'default' | 'subtle'; + +// @public +export const linkClassNames: { + root: string; +}; + +// @public +export type LinkProps = LinkProps_2 & { + appearance?: LinkAppearance; + inline?: boolean; +}; + +export { LinkSlots } + +// @public +export type LinkState = LinkState_2 & Required>; + +export { renderLink } + +export { useLink } + +// @public +export const useLinkStyles: (state: LinkState) => LinkState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/menu-button.api.md b/packages/react-components/react-windmod-preview/library/etc/menu-button.api.md new file mode 100644 index 0000000000000..edff22494d3c6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/menu-button.api.md @@ -0,0 +1,43 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { MenuButtonProps as MenuButtonProps_2 } from '@fluentui/react-headless-components-preview/menu-button'; +import { MenuButtonSlots } from '@fluentui/react-headless-components-preview/menu-button'; +import type { MenuButtonState as MenuButtonState_2 } from '@fluentui/react-headless-components-preview/menu-button'; +import { renderMenuButton } from '@fluentui/react-headless-components-preview/menu-button'; +import { useMenuButton } from '@fluentui/react-headless-components-preview/menu-button'; + +// @public +export const MenuButton: ForwardRefComponent; + +// @public +export const menuButtonClassNames: { + root: string; +}; + +// @public +export type MenuButtonProps = MenuButtonProps_2 & { + appearance?: ButtonAppearance; + shape?: ButtonShape; + size?: ButtonSize; +}; + +export { MenuButtonSlots } + +// @public +export type MenuButtonState = MenuButtonState_2 & Required>; + +export { renderMenuButton } + +export { useMenuButton } + +// @public +export const useMenuButtonStyles: (state: MenuButtonState) => MenuButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/menu.api.md b/packages/react-components/react-windmod-preview/library/etc/menu.api.md new file mode 100644 index 0000000000000..092e259e6c0a9 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/menu.api.md @@ -0,0 +1,373 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { MenuContextValue } from '@fluentui/react-headless-components-preview/menu'; +import { MenuContextValues } from '@fluentui/react-headless-components-preview/menu'; +import { MenuDividerProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuDividerSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuDividerState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupContextValues } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupHeaderProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupHeaderSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupHeaderState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuGroupState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemCheckboxProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemCheckboxState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemLinkProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemLinkSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemLinkState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemRadioProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemRadioState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemSwitchProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemSwitchSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuItemSwitchState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuListProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuListSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuListState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuOpenChangeData } from '@fluentui/react-headless-components-preview/menu'; +import { MenuOpenEvent } from '@fluentui/react-headless-components-preview/menu'; +import { MenuPopoverProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuPopoverSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuPopoverState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuSplitGroupProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuSplitGroupSlots } from '@fluentui/react-headless-components-preview/menu'; +import { MenuSplitGroupState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuState } from '@fluentui/react-headless-components-preview/menu'; +import { MenuTriggerChildProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuTriggerProps } from '@fluentui/react-headless-components-preview/menu'; +import { MenuTriggerState } from '@fluentui/react-headless-components-preview/menu'; +import * as React_2 from 'react'; +import { renderMenu } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuDivider } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuGroup } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuGroupHeader } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuItem } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuItemCheckbox } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuItemLink } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuItemRadio } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuItemSwitch } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuList } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuPopover } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuSplitGroup } from '@fluentui/react-headless-components-preview/menu'; +import { renderMenuTrigger } from '@fluentui/react-headless-components-preview/menu'; +import { useMenu } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuContext } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuContextValues } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuDivider } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuGroup } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuGroupContextValues } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuGroupHeader } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuItem } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuItemCheckbox } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuItemLink } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuItemRadio } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuItemSwitch } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuList } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuListContextValues } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuPopover } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuSplitGroup } from '@fluentui/react-headless-components-preview/menu'; +import { useMenuTrigger } from '@fluentui/react-headless-components-preview/menu'; + +// @public +export const Menu: { + (props: MenuProps): JSXElement; + displayName: string; +}; + +export { MenuContextValue } + +export { MenuContextValues } + +// @public +export const MenuDivider: ForwardRefComponent; + +// @public +export const menuDividerClassNames: { + root: string; +}; + +export { MenuDividerProps } + +export { MenuDividerSlots } + +export { MenuDividerState } + +// @public +export const MenuGroup: ForwardRefComponent; + +// @public +export const menuGroupClassNames: { + root: string; +}; + +export { MenuGroupContextValues } + +// @public +export const MenuGroupHeader: ForwardRefComponent; + +// @public +export const menuGroupHeaderClassNames: { + root: string; +}; + +export { MenuGroupHeaderProps } + +export { MenuGroupHeaderSlots } + +export { MenuGroupHeaderState } + +export { MenuGroupProps } + +export { MenuGroupSlots } + +export { MenuGroupState } + +// @public +export const MenuItem: ForwardRefComponent; + +// @public +export const MenuItemCheckbox: ForwardRefComponent; + +// @public +export const menuItemCheckboxClassNames: { + root: string; +}; + +export { MenuItemCheckboxProps } + +export { MenuItemCheckboxState } + +// @public +export const menuItemClassNames: { + root: string; +}; + +// @internal (undocumented) +export const MenuItemContextProvider: React_2.Provider; + +// @internal +export type MenuItemContextValue = Partial & Partial; + +// @public +export const MenuItemLink: ForwardRefComponent; + +// @public +export const menuItemLinkClassNames: { + root: string; +}; + +export { MenuItemLinkProps } + +export { MenuItemLinkSlots } + +export { MenuItemLinkState } + +export { MenuItemProps } + +// @public +export const MenuItemRadio: ForwardRefComponent; + +// @public +export const menuItemRadioClassNames: { + root: string; +}; + +export { MenuItemRadioProps } + +export { MenuItemRadioState } + +export { MenuItemSlots } + +export { MenuItemState } + +// @public +export const MenuItemSwitch: ForwardRefComponent; + +// @public +export const menuItemSwitchClassNames: { + root: string; +}; + +export { MenuItemSwitchProps } + +export { MenuItemSwitchSlots } + +export { MenuItemSwitchState } + +// @public +export const MenuList: ForwardRefComponent; + +// @public +export const menuListClassNames: { + root: string; +}; + +export { MenuListProps } + +export { MenuListSlots } + +export { MenuListState } + +export { MenuOpenChangeData } + +export { MenuOpenEvent } + +// @public +export const MenuPopover: ForwardRefComponent; + +// @public +export const menuPopoverClassNames: { + root: string; +}; + +export { MenuPopoverProps } + +export { MenuPopoverSlots } + +export { MenuPopoverState } + +export { MenuProps } + +// @public +export const MenuSplitGroup: ForwardRefComponent; + +// @public +export const menuSplitGroupClassNames: { + root: string; +}; + +export { MenuSplitGroupProps } + +export { MenuSplitGroupSlots } + +export { MenuSplitGroupState } + +export { MenuState } + +// @public +export const MenuTrigger: React_2.FC; + +export { MenuTriggerChildProps } + +// @public +export const menuTriggerClassNames: { + root: string; +}; + +export { MenuTriggerProps } + +export { MenuTriggerState } + +export { renderMenu } + +export { renderMenuDivider } + +export { renderMenuGroup } + +export { renderMenuGroupHeader } + +export { renderMenuItem } + +export { renderMenuItemCheckbox } + +export { renderMenuItemLink } + +export { renderMenuItemRadio } + +export { renderMenuItemSwitch } + +export { renderMenuList } + +export { renderMenuPopover } + +export { renderMenuSplitGroup } + +export { renderMenuTrigger } + +export { useMenu } + +export { useMenuContext } + +export { useMenuContextValues } + +export { useMenuDivider } + +// @public +export const useMenuDividerStyles: (state: MenuDividerState) => MenuDividerState; + +export { useMenuGroup } + +export { useMenuGroupContextValues } + +export { useMenuGroupHeader } + +// @public +export const useMenuGroupHeaderStyles: (state: MenuGroupHeaderState) => MenuGroupHeaderState; + +// @public +export const useMenuGroupStyles: (state: MenuGroupState) => MenuGroupState; + +export { useMenuItem } + +export { useMenuItemCheckbox } + +// @public +export const useMenuItemCheckboxStyles: (state: MenuItemCheckboxState) => MenuItemCheckboxState; + +// @internal (undocumented) +export const useMenuItemContext: () => MenuItemContextValue; + +export { useMenuItemLink } + +// @public +export const useMenuItemLinkStyles: (state: MenuItemLinkState) => MenuItemLinkState; + +export { useMenuItemRadio } + +// @public +export const useMenuItemRadioStyles: (state: MenuItemRadioState) => MenuItemRadioState; + +// @public +export const useMenuItemStyles: (state: MenuItemState) => MenuItemState; + +export { useMenuItemSwitch } + +// @public +export const useMenuItemSwitchStyles: (state: MenuItemSwitchState) => MenuItemSwitchState; + +export { useMenuList } + +export { useMenuListContextValues } + +// @public +export const useMenuListStyles: (state: MenuListState) => MenuListState; + +export { useMenuPopover } + +// @public +export const useMenuPopoverStyles: (state: MenuPopoverState) => MenuPopoverState; + +export { useMenuSplitGroup } + +// @public +export const useMenuSplitGroupStyles: (state: MenuSplitGroupState) => MenuSplitGroupState; + +export { useMenuTrigger } + +// @public +export const useMenuTriggerStyles: (state: MenuTriggerState) => MenuTriggerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/message-bar.api.md b/packages/react-components/react-windmod-preview/library/etc/message-bar.api.md new file mode 100644 index 0000000000000..a7543a9b09559 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/message-bar.api.md @@ -0,0 +1,146 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { MessageBarActionsContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarActionsProps } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarActionsSlots } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarActionsState } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarBodyContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarBodyProps } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarBodySlots } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarBodyState } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarIntent } from '@fluentui/react-headless-components-preview/message-bar'; +import type { MessageBarProps as MessageBarProps_2 } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarSlots } from '@fluentui/react-headless-components-preview/message-bar'; +import type { MessageBarState as MessageBarState_2 } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarTitleProps } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarTitleSlots } from '@fluentui/react-headless-components-preview/message-bar'; +import { MessageBarTitleState } from '@fluentui/react-headless-components-preview/message-bar'; +import { renderMessageBar } from '@fluentui/react-headless-components-preview/message-bar'; +import { renderMessageBarActions } from '@fluentui/react-headless-components-preview/message-bar'; +import { renderMessageBarBody } from '@fluentui/react-headless-components-preview/message-bar'; +import { renderMessageBarTitle } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBar } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarActions } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarActionsContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarBody } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarBodyContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarContext } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarContextValues } from '@fluentui/react-headless-components-preview/message-bar'; +import { useMessageBarTitle } from '@fluentui/react-headless-components-preview/message-bar'; + +// @public +export const MessageBar: ForwardRefComponent; + +// @public +export const MessageBarActions: ForwardRefComponent; + +// @public +export const messageBarActionsClassNames: { + root: string; +}; + +export { MessageBarActionsContextValues } + +export { MessageBarActionsProps } + +export { MessageBarActionsSlots } + +export { MessageBarActionsState } + +// @public +export const MessageBarBody: ForwardRefComponent; + +// @public +export const messageBarBodyClassNames: { + root: string; +}; + +export { MessageBarBodyContextValues } + +export { MessageBarBodyProps } + +export { MessageBarBodySlots } + +export { MessageBarBodyState } + +// @public +export const messageBarClassNames: { + root: string; +}; + +export { MessageBarContextValues } + +export { MessageBarIntent } + +// @public +export type MessageBarProps = MessageBarProps_2 & { + shape?: MessageBarShape; +}; + +// @public +export type MessageBarShape = 'rounded' | 'square'; + +export { MessageBarSlots } + +// @public +export type MessageBarState = MessageBarState_2 & Required>; + +// @public +export const MessageBarTitle: ForwardRefComponent; + +// @public +export const messageBarTitleClassNames: { + root: string; +}; + +export { MessageBarTitleProps } + +export { MessageBarTitleSlots } + +export { MessageBarTitleState } + +export { renderMessageBar } + +export { renderMessageBarActions } + +export { renderMessageBarBody } + +export { renderMessageBarTitle } + +export { useMessageBar } + +export { useMessageBarActions } + +export { useMessageBarActionsContextValues } + +// @public +export const useMessageBarActionsStyles: (state: MessageBarActionsState) => MessageBarActionsState; + +export { useMessageBarBody } + +export { useMessageBarBodyContextValues } + +// @public +export const useMessageBarBodyStyles: (state: MessageBarBodyState) => MessageBarBodyState; + +export { useMessageBarContext } + +export { useMessageBarContextValues } + +// @public +export const useMessageBarStyles: (state: MessageBarState) => MessageBarState; + +export { useMessageBarTitle } + +// @public +export const useMessageBarTitleStyles: (state: MessageBarTitleState) => MessageBarTitleState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/nav.api.md b/packages/react-components/react-windmod-preview/library/etc/nav.api.md new file mode 100644 index 0000000000000..0c36021a8b15e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/nav.api.md @@ -0,0 +1,387 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { DividerProps as DividerProps_2 } from '@fluentui/react-headless-components-preview/divider'; +import type { DividerState as DividerState_2 } from '@fluentui/react-headless-components-preview/divider'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InlineDrawerProps as InlineDrawerProps_2 } from '@fluentui/react-headless-components-preview/drawer'; +import { NavCategoryContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryItemContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryItemProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryItemProvider } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryItemSlots } from '@fluentui/react-headless-components-preview/nav'; +import type { NavCategoryItemState as NavCategoryItemState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryProvider } from '@fluentui/react-headless-components-preview/nav'; +import { NavCategoryState } from '@fluentui/react-headless-components-preview/nav'; +import { NavContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { NavDividerProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavDividerSlots } from '@fluentui/react-headless-components-preview/nav'; +import type { NavDividerState as NavDividerState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerBodyProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerBodySlots } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerBodyState } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerFooterProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerFooterSlots } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerFooterState } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerHeaderProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerHeaderSlots } from '@fluentui/react-headless-components-preview/nav'; +import { NavDrawerHeaderState } from '@fluentui/react-headless-components-preview/nav'; +import type { NavDrawerState as NavDrawerState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavItemProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavItemSlots } from '@fluentui/react-headless-components-preview/nav'; +import type { NavItemState as NavItemState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavItemValue } from '@fluentui/react-headless-components-preview/nav'; +import type { NavProps as NavProps_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavProvider } from '@fluentui/react-headless-components-preview/nav'; +import { NavSectionHeaderProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavSectionHeaderSlots } from '@fluentui/react-headless-components-preview/nav'; +import { NavSectionHeaderState } from '@fluentui/react-headless-components-preview/nav'; +import { NavSlots } from '@fluentui/react-headless-components-preview/nav'; +import type { NavState as NavState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { NavSubItemGroupProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavSubItemGroupSlots } from '@fluentui/react-headless-components-preview/nav'; +import { NavSubItemGroupState } from '@fluentui/react-headless-components-preview/nav'; +import { NavSubItemProps } from '@fluentui/react-headless-components-preview/nav'; +import { NavSubItemSlots } from '@fluentui/react-headless-components-preview/nav'; +import type { NavSubItemState as NavSubItemState_2 } from '@fluentui/react-headless-components-preview/nav'; +import { OnNavItemSelectData } from '@fluentui/react-headless-components-preview/nav'; +import type { OverlayDrawerProps as OverlayDrawerProps_2 } from '@fluentui/react-headless-components-preview/drawer'; +import { renderNav } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavCategory } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavCategoryItem } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavDivider } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavDrawer } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavDrawerBody } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavDrawerFooter } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavDrawerHeader } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavItem } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavSectionHeader } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavSubItem } from '@fluentui/react-headless-components-preview/nav'; +import { renderNavSubItemGroup } from '@fluentui/react-headless-components-preview/nav'; +import { useNav } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategory } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategoryContext } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategoryContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategoryItem } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategoryItemContext } from '@fluentui/react-headless-components-preview/nav'; +import { useNavCategoryItemContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { useNavContext } from '@fluentui/react-headless-components-preview/nav'; +import { useNavContextValues } from '@fluentui/react-headless-components-preview/nav'; +import { useNavDivider } from '@fluentui/react-headless-components-preview/nav'; +import { useNavDrawer } from '@fluentui/react-headless-components-preview/nav'; +import { useNavDrawerBody } from '@fluentui/react-headless-components-preview/nav'; +import { useNavDrawerFooter } from '@fluentui/react-headless-components-preview/nav'; +import { useNavDrawerHeader } from '@fluentui/react-headless-components-preview/nav'; +import { useNavItem } from '@fluentui/react-headless-components-preview/nav'; +import { useNavSectionHeader } from '@fluentui/react-headless-components-preview/nav'; +import { useNavSubItem } from '@fluentui/react-headless-components-preview/nav'; +import { useNavSubItemGroup } from '@fluentui/react-headless-components-preview/nav'; + +// @public +export type DrawerSize = 'small' | 'medium' | 'large' | 'full'; + +// @public +export const Nav: ForwardRefComponent; + +// @public +export const NavCategory: ForwardRefComponent; + +export { NavCategoryContextValues } + +// @public +export const NavCategoryItem: ForwardRefComponent; + +// @public +export const navCategoryItemClassNames: { + root: string; +}; + +export { NavCategoryItemContextValues } + +export { NavCategoryItemProps } + +export { NavCategoryItemProvider } + +export { NavCategoryItemSlots } + +// @public +export type NavCategoryItemState = NavCategoryItemState_2 & { + density: NavDensity; + root: { + 'data-density'?: NavDensity; + }; +}; + +export { NavCategoryProps } + +export { NavCategoryProvider } + +export { NavCategoryState } + +// @public +export const navClassNames: { + root: string; +}; + +export { NavContextValues } + +// @public +export type NavDensity = 'small' | 'medium'; + +// @public +export const NavDivider: ForwardRefComponent; + +// @public +export const navDividerClassNames: { + root: string; +}; + +export { NavDividerProps } + +export { NavDividerSlots } + +// @public +export type NavDividerState = NavDividerState_2 & Required>; + +// @public +export const NavDrawer: ForwardRefComponent; + +// @public +export const NavDrawerBody: ForwardRefComponent; + +// @public +export const navDrawerBodyClassNames: { + root: string; +}; + +export { NavDrawerBodyProps } + +export { NavDrawerBodySlots } + +export { NavDrawerBodyState } + +// @public +export const navDrawerClassNames: { + root: string; +}; + +// @public +export const NavDrawerFooter: ForwardRefComponent; + +// @public +export const navDrawerFooterClassNames: { + root: string; +}; + +export { NavDrawerFooterProps } + +export { NavDrawerFooterSlots } + +export { NavDrawerFooterState } + +// @public +export const NavDrawerHeader: ForwardRefComponent; + +// @public +export const navDrawerHeaderClassNames: { + root: string; +}; + +export { NavDrawerHeaderProps } + +export { NavDrawerHeaderSlots } + +export { NavDrawerHeaderState } + +// @public +export type NavDrawerProps = DrawerProps & NavProps; + +// @public +export type NavDrawerState = NavDrawerState_2 & { + size?: DrawerSize; +}; + +// @public +export const NavItem: ForwardRefComponent; + +// @public +export const navItemClassNames: { + root: string; +}; + +export { NavItemProps } + +export { NavItemSlots } + +// @public +export type NavItemState = NavItemState_2 & { + density: NavDensity; + root: { + 'data-density'?: NavDensity; + }; +}; + +export { NavItemValue } + +// @public +export type NavProps = NavProps_2 & { + density?: NavDensity; +}; + +export { NavProvider } + +// @public +export const NavSectionHeader: ForwardRefComponent; + +// @public +export const navSectionHeaderClassNames: { + root: string; +}; + +export { NavSectionHeaderProps } + +export { NavSectionHeaderSlots } + +export { NavSectionHeaderState } + +export { NavSlots } + +// @public +export type NavState = NavState_2 & Required>; + +// @public +export const NavSubItem: ForwardRefComponent; + +// @public +export const navSubItemClassNames: { + root: string; +}; + +// @public +export const NavSubItemGroup: ForwardRefComponent; + +// @public +export const navSubItemGroupClassNames: { + root: string; +}; + +export { NavSubItemGroupProps } + +export { NavSubItemGroupSlots } + +export { NavSubItemGroupState } + +export { NavSubItemProps } + +export { NavSubItemSlots } + +// @public +export type NavSubItemState = NavSubItemState_2 & { + density: NavDensity; + root: { + 'data-density'?: NavDensity; + }; +}; + +export { OnNavItemSelectData } + +export { renderNav } + +export { renderNavCategory } + +export { renderNavCategoryItem } + +export { renderNavDivider } + +export { renderNavDrawer } + +export { renderNavDrawerBody } + +export { renderNavDrawerFooter } + +export { renderNavDrawerHeader } + +export { renderNavItem } + +export { renderNavSectionHeader } + +export { renderNavSubItem } + +export { renderNavSubItemGroup } + +export { useNav } + +export { useNavCategory } + +export { useNavCategoryContext } + +export { useNavCategoryContextValues } + +export { useNavCategoryItem } + +export { useNavCategoryItemContext } + +export { useNavCategoryItemContextValues } + +// @public +export const useNavCategoryItemStyles: (state: NavCategoryItemState) => NavCategoryItemState; + +export { useNavContext } + +export { useNavContextValues } + +export { useNavDivider } + +// @public +export const useNavDividerStyles: (state: NavDividerState) => NavDividerState; + +export { useNavDrawer } + +export { useNavDrawerBody } + +// @public +export const useNavDrawerBodyStyles: (state: NavDrawerBodyState) => NavDrawerBodyState; + +export { useNavDrawerFooter } + +// @public +export const useNavDrawerFooterStyles: (state: NavDrawerFooterState) => NavDrawerFooterState; + +export { useNavDrawerHeader } + +// @public +export const useNavDrawerHeaderStyles: (state: NavDrawerHeaderState) => NavDrawerHeaderState; + +// @public +export const useNavDrawerStyles: (state: NavDrawerState) => NavDrawerState; + +export { useNavItem } + +// @public +export const useNavItemStyles: (state: NavItemState) => NavItemState; + +export { useNavSectionHeader } + +// @public +export const useNavSectionHeaderStyles: (state: NavSectionHeaderState) => NavSectionHeaderState; + +// @public +export const useNavStyles: (state: NavState) => NavState; + +export { useNavSubItem } + +export { useNavSubItemGroup } + +// @public +export const useNavSubItemGroupStyles: (state: NavSubItemGroupState) => NavSubItemGroupState; + +// @public +export const useNavSubItemStyles: (state: NavSubItemState) => NavSubItemState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/persona.api.md b/packages/react-components/react-windmod-preview/library/etc/persona.api.md new file mode 100644 index 0000000000000..194d107d4e6bd --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/persona.api.md @@ -0,0 +1,57 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AvatarProps as AvatarProps_2 } from '@fluentui/react-headless-components-preview/avatar'; +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { PersonaProps as PersonaProps_2 } from '@fluentui/react-headless-components-preview/persona'; +import type { PersonaSlots as PersonaSlots_2 } from '@fluentui/react-headless-components-preview/persona'; +import type { PersonaState as PersonaState_2 } from '@fluentui/react-headless-components-preview/persona'; +import { renderPersona } from '@fluentui/react-headless-components-preview/persona'; +import type { Slot } from '@fluentui/react-utilities'; +import { usePersona } from '@fluentui/react-headless-components-preview/persona'; + +// @public +export const Persona: ForwardRefComponent; + +// @public +export const personaClassNames: { + root: string; +}; + +// @public +export type PersonaProps = ComponentProps & Pick & { + size?: PersonaSize; + textAlignment?: PersonaTextAlignment; +}; + +// @public +export type PersonaSize = 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large' | 'huge'; + +// @public +export type PersonaSlots = Omit & { + avatar?: Slot; +}; + +// @public +export type PersonaState = ComponentState & Pick & { + root: PersonaState_2['root']; +} & Required>; + +// @public +export type PersonaTextAlignment = 'center' | 'start'; + +export { renderPersona } + +export { usePersona } + +// @public +export const usePersonaStyles: (state: PersonaState) => PersonaState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/popover.api.md b/packages/react-components/react-windmod-preview/library/etc/popover.api.md new file mode 100644 index 0000000000000..017341123b42d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/popover.api.md @@ -0,0 +1,109 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { OnOpenChangeData } from '@fluentui/react-headless-components-preview/popover'; +import { OpenPopoverEvents } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverContextValue } from '@fluentui/react-headless-components-preview/popover'; +import type { PopoverProps as PopoverProps_2 } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverState } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverSurfaceProps } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverSurfaceSlots } from '@fluentui/react-headless-components-preview/popover'; +import type { PopoverSurfaceState as PopoverSurfaceState_2 } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverTriggerProps } from '@fluentui/react-headless-components-preview/popover'; +import { PopoverTriggerState } from '@fluentui/react-headless-components-preview/popover'; +import type * as React_2 from 'react'; +import { renderPopover } from '@fluentui/react-headless-components-preview/popover'; +import { renderPopoverSurface } from '@fluentui/react-headless-components-preview/popover'; +import { renderPopoverTrigger } from '@fluentui/react-headless-components-preview/popover'; +import { usePopover } from '@fluentui/react-headless-components-preview/popover'; +import { usePopoverContext } from '@fluentui/react-headless-components-preview/popover'; +import { usePopoverContextValues } from '@fluentui/react-headless-components-preview/popover'; +import { usePopoverSurface } from '@fluentui/react-headless-components-preview/popover'; +import { usePopoverTrigger } from '@fluentui/react-headless-components-preview/popover'; + +export { OnOpenChangeData } + +export { OpenPopoverEvents } + +// @public +export const Popover: { + (props: PopoverProps): JSXElement; + displayName: string; +}; + +// @public +export type PopoverAppearance = 'brand' | 'inverted'; + +export { PopoverContextValue } + +// @public +export type PopoverProps = PopoverProps_2 & { + appearance?: PopoverAppearance; + size?: PopoverSize; +}; + +// @public +export type PopoverSize = 'small' | 'medium' | 'large'; + +export { PopoverState } + +// @public +export const PopoverSurface: ForwardRefComponent; + +// @public +export const popoverSurfaceClassNames: { + root: string; +}; + +export { PopoverSurfaceProps } + +export { PopoverSurfaceSlots } + +// @public +export type PopoverSurfaceState = PopoverSurfaceState_2 & { + appearance?: PopoverAppearance; + size: PopoverSize; +}; + +// @public +export const PopoverTrigger: React_2.FC; + +// @public +export const popoverTriggerClassNames: { + root: string; +}; + +export { PopoverTriggerProps } + +export { PopoverTriggerState } + +export { renderPopover } + +export { renderPopoverSurface } + +export { renderPopoverTrigger } + +export { usePopover } + +export { usePopoverContext } + +export { usePopoverContextValues } + +export { usePopoverSurface } + +// @public +export const usePopoverSurfaceStyles: (state: PopoverSurfaceState) => PopoverSurfaceState; + +export { usePopoverTrigger } + +// @public +export const usePopoverTriggerStyles: (state: PopoverTriggerState) => PopoverTriggerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/positioning.api.md b/packages/react-components/react-windmod-preview/library/etc/positioning.api.md new file mode 100644 index 0000000000000..4a23c9f234593 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/positioning.api.md @@ -0,0 +1,46 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Alignment } from '@fluentui/react-headless-components-preview/positioning'; +import { ALIGNMENTS } from '@fluentui/react-headless-components-preview/positioning'; +import { getPlacementString } from '@fluentui/react-headless-components-preview/positioning'; +import { Position } from '@fluentui/react-headless-components-preview/positioning'; +import { PositioningImperativeRef } from '@fluentui/react-headless-components-preview/positioning'; +import { PositioningProps } from '@fluentui/react-headless-components-preview/positioning'; +import { PositioningReturn } from '@fluentui/react-headless-components-preview/positioning'; +import { PositioningShorthand } from '@fluentui/react-headless-components-preview/positioning'; +import { PositioningShorthandValue } from '@fluentui/react-headless-components-preview/positioning'; +import { POSITIONS } from '@fluentui/react-headless-components-preview/positioning'; +import { resolvePositioningShorthand } from '@fluentui/react-headless-components-preview/positioning'; +import { usePositioning } from '@fluentui/react-headless-components-preview/positioning'; + +export { Alignment } + +export { ALIGNMENTS } + +export { getPlacementString } + +export { Position } + +export { PositioningImperativeRef } + +export { PositioningProps } + +export { PositioningReturn } + +export { PositioningShorthand } + +export { PositioningShorthandValue } + +export { POSITIONS } + +export { resolvePositioningShorthand } + +export { usePositioning } + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/progress-bar.api.md b/packages/react-components/react-windmod-preview/library/etc/progress-bar.api.md new file mode 100644 index 0000000000000..dce8b6fb58b34 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/progress-bar.api.md @@ -0,0 +1,52 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ProgressBarProps as ProgressBarProps_2 } from '@fluentui/react-headless-components-preview/progress-bar'; +import { ProgressBarSlots } from '@fluentui/react-headless-components-preview/progress-bar'; +import type { ProgressBarState as ProgressBarState_2 } from '@fluentui/react-headless-components-preview/progress-bar'; +import { renderProgressBar } from '@fluentui/react-headless-components-preview/progress-bar'; +import { useProgressBar } from '@fluentui/react-headless-components-preview/progress-bar'; + +// @public +export const ProgressBar: ForwardRefComponent; + +// @public +export const progressBarClassNames: { + root: string; +}; + +// @public +export type ProgressBarColor = 'brand' | 'success' | 'warning' | 'error'; + +// @public +export type ProgressBarProps = ProgressBarProps_2 & { + color?: ProgressBarColor; + shape?: ProgressBarShape; + thickness?: ProgressBarThickness; +}; + +// @public +export type ProgressBarShape = 'rounded' | 'square'; + +export { ProgressBarSlots } + +// @public +export type ProgressBarState = ProgressBarState_2 & Required>; + +// @public +export type ProgressBarThickness = 'medium' | 'large'; + +export { renderProgressBar } + +export { useProgressBar } + +// @public +export const useProgressBarStyles: (state: ProgressBarState) => ProgressBarState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/provider.api.md b/packages/react-components/react-windmod-preview/library/etc/provider.api.md new file mode 100644 index 0000000000000..58ec767883d42 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/provider.api.md @@ -0,0 +1,75 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { Provider } from '@fluentui/react-headless-components-preview/provider'; +import * as React_2 from 'react'; +import { renderProvider } from '@fluentui/react-headless-components-preview/provider'; +import { teamsDarkThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { teamsDarkV21ThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { teamsHighContrastThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { teamsLightThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { teamsLightV21ThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { ThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { themeClassNames } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { useProvider } from '@fluentui/react-headless-components-preview/provider'; +import { webDarkThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; +import { webLightThemeClassName } from '@fluentui/react-tailwind-theme-preview/theme-class-names'; + +// @public +export const FluentProvider: React_2.ForwardRefExoticComponent & { + theme?: ThemeClassName | (string & {}); + dir?: "ltr" | "rtl"; + targetDocument?: Document; +} & React_2.RefAttributes>; + +// @public +export const fluentProviderClassNames: { + root: string; +}; + +// @public (undocumented) +export type FluentProviderProps = React_2.HTMLAttributes & { + theme?: ThemeClassName | (string & {}); + dir?: 'ltr' | 'rtl'; + targetDocument?: Document; +}; + +export { Provider } + +export { renderProvider } + +// @public +export const ScaleRegion: ForwardRefComponent; + +// @public (undocumented) +export type ScaleRegionProps = React_2.HTMLAttributes & { + scale?: number; +}; + +export { teamsDarkThemeClassName } + +export { teamsDarkV21ThemeClassName } + +export { teamsHighContrastThemeClassName } + +export { teamsLightThemeClassName } + +export { teamsLightV21ThemeClassName } + +export { ThemeClassName } + +export { themeClassNames } + +export { useProvider } + +export { webDarkThemeClassName } + +export { webLightThemeClassName } + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/radio-group.api.md b/packages/react-components/react-windmod-preview/library/etc/radio-group.api.md new file mode 100644 index 0000000000000..c27cb4ed3e7a9 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/radio-group.api.md @@ -0,0 +1,73 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { RadioGroupProps as RadioGroupProps_2 } from '@fluentui/react-headless-components-preview/radio-group'; +import { RadioGroupSlots } from '@fluentui/react-headless-components-preview/radio-group'; +import type { RadioGroupState as RadioGroupState_2 } from '@fluentui/react-headless-components-preview/radio-group'; +import { RadioProps } from '@fluentui/react-headless-components-preview/radio-group'; +import { RadioSlots } from '@fluentui/react-headless-components-preview/radio-group'; +import { RadioState } from '@fluentui/react-headless-components-preview/radio-group'; +import { renderRadio } from '@fluentui/react-headless-components-preview/radio-group'; +import { renderRadioGroup } from '@fluentui/react-headless-components-preview/radio-group'; +import { useRadio } from '@fluentui/react-headless-components-preview/radio-group'; +import { useRadioGroup } from '@fluentui/react-headless-components-preview/radio-group'; +import { useRadioGroupContextValues } from '@fluentui/react-headless-components-preview/radio-group'; + +// @public +export const Radio: ForwardRefComponent; + +// @public +export const radioClassNames: { + root: string; +}; + +// @public +export const RadioGroup: ForwardRefComponent; + +// @public +export const radioGroupClassNames: { + root: string; +}; + +// @public +export type RadioGroupLayout = 'vertical' | 'horizontal' | 'horizontal-stacked'; + +// @public +export type RadioGroupProps = RadioGroupProps_2 & { + layout?: RadioGroupLayout; +}; + +export { RadioGroupSlots } + +// @public +export type RadioGroupState = RadioGroupState_2 & Required>; + +export { RadioProps } + +export { RadioSlots } + +export { RadioState } + +export { renderRadio } + +export { renderRadioGroup } + +export { useRadio } + +export { useRadioGroup } + +export { useRadioGroupContextValues } + +// @public +export const useRadioGroupStyles: (state: RadioGroupState) => RadioGroupState; + +// @public +export const useRadioStyles: (state: RadioState) => RadioState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/rating-display.api.md b/packages/react-components/react-windmod-preview/library/etc/rating-display.api.md new file mode 100644 index 0000000000000..5cb75cfd10b73 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/rating-display.api.md @@ -0,0 +1,51 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { RatingDisplayProps as RatingDisplayProps_2 } from '@fluentui/react-headless-components-preview/rating-display'; +import { RatingDisplaySlots } from '@fluentui/react-headless-components-preview/rating-display'; +import type { RatingDisplayState as RatingDisplayState_2 } from '@fluentui/react-headless-components-preview/rating-display'; +import { renderRatingDisplay } from '@fluentui/react-headless-components-preview/rating-display'; +import { useRatingDisplay } from '@fluentui/react-headless-components-preview/rating-display'; +import { useRatingDisplayContextValues } from '@fluentui/react-headless-components-preview/rating-display'; + +// @public +export const RatingDisplay: ForwardRefComponent; + +// @public +export const ratingDisplayClassNames: { + root: string; +}; + +// @public +export type RatingDisplayColor = 'brand' | 'marigold' | 'neutral'; + +// @public +export type RatingDisplayProps = RatingDisplayProps_2 & { + color?: RatingDisplayColor; + size?: RatingDisplaySize; +}; + +// @public +export type RatingDisplaySize = 'small' | 'medium' | 'large' | 'extra-large'; + +export { RatingDisplaySlots } + +// @public +export type RatingDisplayState = RatingDisplayState_2 & Required>; + +export { renderRatingDisplay } + +export { useRatingDisplay } + +export { useRatingDisplayContextValues } + +// @public +export const useRatingDisplayStyles: (state: RatingDisplayState) => RatingDisplayState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/rating.api.md b/packages/react-components/react-windmod-preview/library/etc/rating.api.md new file mode 100644 index 0000000000000..f91539ff10895 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/rating.api.md @@ -0,0 +1,82 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { RatingItemProps as RatingItemProps_2 } from '@fluentui/react-headless-components-preview/rating'; +import { RatingItemSlots } from '@fluentui/react-headless-components-preview/rating'; +import type { RatingItemState as RatingItemState_2 } from '@fluentui/react-headless-components-preview/rating'; +import type { RatingProps as RatingProps_2 } from '@fluentui/react-headless-components-preview/rating'; +import { RatingSlots } from '@fluentui/react-headless-components-preview/rating'; +import type { RatingState as RatingState_2 } from '@fluentui/react-headless-components-preview/rating'; +import { renderRating } from '@fluentui/react-headless-components-preview/rating'; +import { renderRatingItem } from '@fluentui/react-headless-components-preview/rating'; +import { useRating } from '@fluentui/react-headless-components-preview/rating'; +import { useRatingContextValues } from '@fluentui/react-headless-components-preview/rating'; +import { useRatingItem } from '@fluentui/react-headless-components-preview/rating'; + +// @public +export const Rating: ForwardRefComponent; + +// @public +export const ratingClassNames: { + root: string; +}; + +// @public +export type RatingColor = 'brand' | 'marigold' | 'neutral'; + +// @public +export const RatingItem: ForwardRefComponent; + +// @public +export const ratingItemClassNames: { + root: string; +}; + +// @public +export type RatingItemProps = RatingItemProps_2 & { + color?: RatingDisplayColor; + size?: RatingDisplaySize; +}; + +export { RatingItemSlots } + +// @public +export type RatingItemState = RatingItemState_2 & Required>; + +// @public +export type RatingProps = RatingProps_2 & { + color?: RatingColor; + size?: RatingSize; +}; + +// @public +export type RatingSize = 'small' | 'medium' | 'large' | 'extra-large'; + +export { RatingSlots } + +// @public +export type RatingState = RatingState_2 & Required>; + +export { renderRating } + +export { renderRatingItem } + +export { useRating } + +export { useRatingContextValues } + +export { useRatingItem } + +// @public +export const useRatingItemStyles: (state: RatingItemState) => RatingItemState; + +// @public +export const useRatingStyles: (state: RatingState) => RatingState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/react-windmod-preview.api.md b/packages/react-components/react-windmod-preview/library/etc/react-windmod-preview.api.md new file mode 100644 index 0000000000000..c853feb8b9b6d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/react-windmod-preview.api.md @@ -0,0 +1,9 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/search-box.api.md b/packages/react-components/react-windmod-preview/library/etc/search-box.api.md new file mode 100644 index 0000000000000..4d4c4787911c2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/search-box.api.md @@ -0,0 +1,42 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSearchBox } from '@fluentui/react-headless-components-preview/search-box'; +import type { SearchBoxProps as SearchBoxProps_2 } from '@fluentui/react-headless-components-preview/search-box'; +import { SearchBoxSlots } from '@fluentui/react-headless-components-preview/search-box'; +import type { SearchBoxState as SearchBoxState_2 } from '@fluentui/react-headless-components-preview/search-box'; +import { useSearchBox } from '@fluentui/react-headless-components-preview/search-box'; + +export { renderSearchBox } + +// @public +export const SearchBox: ForwardRefComponent; + +// @public +export const searchBoxClassNames: { + root: string; +}; + +// @public +export type SearchBoxProps = SearchBoxProps_2 & { + appearance?: InputAppearance; + size?: InputSize; +}; + +export { SearchBoxSlots } + +// @public +export type SearchBoxState = SearchBoxState_2 & Required>; + +export { useSearchBox } + +// @public +export const useSearchBoxStyles: (state: SearchBoxState) => SearchBoxState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/select.api.md b/packages/react-components/react-windmod-preview/library/etc/select.api.md new file mode 100644 index 0000000000000..0d283303cc7b2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/select.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSelect } from '@fluentui/react-headless-components-preview/select'; +import type { SelectProps as SelectProps_2 } from '@fluentui/react-headless-components-preview/select'; +import { SelectSlots } from '@fluentui/react-headless-components-preview/select'; +import type { SelectState as SelectState_2 } from '@fluentui/react-headless-components-preview/select'; +import { useSelect } from '@fluentui/react-headless-components-preview/select'; + +export { renderSelect } + +// @public +export const Select: ForwardRefComponent; + +// @public +export type SelectAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const selectClassNames: { + root: string; +}; + +// @public +export type SelectProps = SelectProps_2 & { + appearance?: SelectAppearance; + size?: SelectSize; +}; + +// @public +export type SelectSize = 'small' | 'medium' | 'large'; + +export { SelectSlots } + +// @public +export type SelectState = SelectState_2 & Required>; + +export { useSelect } + +// @public +export const useSelectStyles: (state: SelectState) => SelectState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/skeleton.api.md b/packages/react-components/react-windmod-preview/library/etc/skeleton.api.md new file mode 100644 index 0000000000000..a1f4030bc06a8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/skeleton.api.md @@ -0,0 +1,90 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSkeleton } from '@fluentui/react-headless-components-preview/skeleton'; +import { renderSkeletonItem } from '@fluentui/react-headless-components-preview/skeleton'; +import type { SkeletonItemProps as SkeletonItemProps_2 } from '@fluentui/react-headless-components-preview/skeleton'; +import { SkeletonItemSlots } from '@fluentui/react-headless-components-preview/skeleton'; +import type { SkeletonItemState as SkeletonItemState_2 } from '@fluentui/react-headless-components-preview/skeleton'; +import type { SkeletonProps as SkeletonProps_2 } from '@fluentui/react-headless-components-preview/skeleton'; +import { SkeletonSlots } from '@fluentui/react-headless-components-preview/skeleton'; +import type { SkeletonState as SkeletonState_2 } from '@fluentui/react-headless-components-preview/skeleton'; +import { useSkeleton } from '@fluentui/react-headless-components-preview/skeleton'; +import { useSkeletonContextValues } from '@fluentui/react-headless-components-preview/skeleton'; +import { useSkeletonItem } from '@fluentui/react-headless-components-preview/skeleton'; + +export { renderSkeleton } + +export { renderSkeletonItem } + +// @public +export const Skeleton: ForwardRefComponent; + +// @public +export type SkeletonAnimation = 'wave' | 'pulse'; + +// @public +export type SkeletonAppearance = 'opaque' | 'translucent'; + +// @public +export const skeletonClassNames: { + root: string; +}; + +// @public +export const SkeletonItem: ForwardRefComponent; + +// @public +export const skeletonItemClassNames: { + root: string; +}; + +// @public +export type SkeletonItemProps = SkeletonItemProps_2 & { + animation?: SkeletonAnimation; + appearance?: SkeletonAppearance; + shape?: SkeletonItemShape; + size?: SkeletonItemSize; +}; + +// @public +export type SkeletonItemShape = 'circle' | 'rectangle' | 'square'; + +// @public +export type SkeletonItemSize = NonNullable; + +export { SkeletonItemSlots } + +// @public +export type SkeletonItemState = SkeletonItemState_2 & Required>; + +// @public +export type SkeletonProps = SkeletonProps_2 & { + animation?: SkeletonAnimation; + appearance?: SkeletonAppearance; +}; + +export { SkeletonSlots } + +// @public +export type SkeletonState = SkeletonState_2 & Required> & Pick; + +export { useSkeleton } + +export { useSkeletonContextValues } + +export { useSkeletonItem } + +// @public +export const useSkeletonItemStyles: (state: SkeletonItemState) => SkeletonItemState; + +// @public +export const useSkeletonStyles: (state: SkeletonState) => SkeletonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/slider.api.md b/packages/react-components/react-windmod-preview/library/etc/slider.api.md new file mode 100644 index 0000000000000..8541e42190fe3 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/slider.api.md @@ -0,0 +1,44 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSlider } from '@fluentui/react-headless-components-preview/slider'; +import type { SliderProps as SliderProps_2 } from '@fluentui/react-headless-components-preview/slider'; +import { SliderSlots } from '@fluentui/react-headless-components-preview/slider'; +import type { SliderState as SliderState_2 } from '@fluentui/react-headless-components-preview/slider'; +import { useSlider } from '@fluentui/react-headless-components-preview/slider'; + +export { renderSlider } + +// @public +export const Slider: ForwardRefComponent; + +// @public +export const sliderClassNames: { + root: string; +}; + +// @public +export type SliderProps = SliderProps_2 & { + size?: SliderSize; +}; + +// @public +export type SliderSize = 'small' | 'medium'; + +export { SliderSlots } + +// @public +export type SliderState = SliderState_2 & Required>; + +export { useSlider } + +// @public +export const useSliderStyles: (state: SliderState) => SliderState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/spin-button.api.md b/packages/react-components/react-windmod-preview/library/etc/spin-button.api.md new file mode 100644 index 0000000000000..d679d965f4774 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/spin-button.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSpinButton } from '@fluentui/react-headless-components-preview/spin-button'; +import type { SpinButtonProps as SpinButtonProps_2 } from '@fluentui/react-headless-components-preview/spin-button'; +import { SpinButtonSlots } from '@fluentui/react-headless-components-preview/spin-button'; +import type { SpinButtonState as SpinButtonState_2 } from '@fluentui/react-headless-components-preview/spin-button'; +import { useSpinButton } from '@fluentui/react-headless-components-preview/spin-button'; + +export { renderSpinButton } + +// @public +export const SpinButton: ForwardRefComponent; + +// @public +export type SpinButtonAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const spinButtonClassNames: { + root: string; +}; + +// @public +export type SpinButtonProps = SpinButtonProps_2 & { + appearance?: SpinButtonAppearance; + size?: SpinButtonSize; +}; + +// @public +export type SpinButtonSize = 'small' | 'medium'; + +export { SpinButtonSlots } + +// @public +export type SpinButtonState = SpinButtonState_2 & Required>; + +export { useSpinButton } + +// @public +export const useSpinButtonStyles: (state: SpinButtonState) => SpinButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/spinner.api.md b/packages/react-components/react-windmod-preview/library/etc/spinner.api.md new file mode 100644 index 0000000000000..38432255ba2c2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/spinner.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSpinner } from '@fluentui/react-headless-components-preview/spinner'; +import type { SpinnerProps as SpinnerProps_2 } from '@fluentui/react-headless-components-preview/spinner'; +import { SpinnerSlots } from '@fluentui/react-headless-components-preview/spinner'; +import type { SpinnerState as SpinnerState_2 } from '@fluentui/react-headless-components-preview/spinner'; +import { useSpinner } from '@fluentui/react-headless-components-preview/spinner'; + +export { renderSpinner } + +// @public +export const Spinner: ForwardRefComponent; + +// @public +export type SpinnerAppearance = 'primary' | 'inverted'; + +// @public +export const spinnerClassNames: { + root: string; +}; + +// @public +export type SpinnerProps = SpinnerProps_2 & { + appearance?: SpinnerAppearance; + size?: SpinnerSize; +}; + +// @public +export type SpinnerSize = 'extra-tiny' | 'tiny' | 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large' | 'huge'; + +export { SpinnerSlots } + +// @public +export type SpinnerState = SpinnerState_2 & Required>; + +export { useSpinner } + +// @public +export const useSpinnerStyles: (state: SpinnerState) => SpinnerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/split-button.api.md b/packages/react-components/react-windmod-preview/library/etc/split-button.api.md new file mode 100644 index 0000000000000..94c00fc5bc319 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/split-button.api.md @@ -0,0 +1,52 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ButtonProps as ButtonProps_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { MenuButtonProps as MenuButtonProps_2 } from '@fluentui/react-headless-components-preview/menu-button'; +import { renderSplitButton } from '@fluentui/react-headless-components-preview/split-button'; +import type { Slot } from '@fluentui/react-utilities'; +import type { SplitButtonProps as SplitButtonProps_2 } from '@fluentui/react-headless-components-preview/split-button'; +import type { SplitButtonSlots as SplitButtonSlots_2 } from '@fluentui/react-headless-components-preview/split-button'; +import type { SplitButtonState as SplitButtonState_2 } from '@fluentui/react-headless-components-preview/split-button'; +import { useSplitButton } from '@fluentui/react-headless-components-preview/split-button'; + +export { renderSplitButton } + +// @public +export const SplitButton: ForwardRefComponent; + +// @public +export const splitButtonClassNames: { + root: string; +}; + +// @public +export type SplitButtonProps = ComponentProps & Pick & { + appearance?: ButtonAppearance; + shape?: ButtonShape; + size?: ButtonSize; +}; + +// @public +export type SplitButtonSlots = Omit & { + menuButton?: Slot; + primaryActionButton?: Slot; +}; + +// @public +export type SplitButtonState = ComponentState & Pick & Required>; + +export { useSplitButton } + +// @public +export const useSplitButtonStyles: (state: SplitButtonState) => SplitButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/swatch-picker.api.md b/packages/react-components/react-windmod-preview/library/etc/swatch-picker.api.md new file mode 100644 index 0000000000000..1a5024e1e5dec --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/swatch-picker.api.md @@ -0,0 +1,181 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ColorSwatchProps as ColorSwatchProps_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { ColorSwatchSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { ColorSwatchState as ColorSwatchState_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { EmptySwatchProps as EmptySwatchProps_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { EmptySwatchSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { EmptySwatchState as EmptySwatchState_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { ImageSwatchProps as ImageSwatchProps_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { ImageSwatchSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { ImageSwatchState as ImageSwatchState_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { renderColorSwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { renderEmptySwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { renderImageSwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { renderSwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { renderSwatchPickerRow } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { SwatchPickerProps as SwatchPickerProps_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { SwatchPickerRowProps as SwatchPickerRowProps_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { SwatchPickerRowSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { SwatchPickerRowState as SwatchPickerRowState_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { SwatchPickerSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; +import type { SwatchPickerState as SwatchPickerState_2 } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useColorSwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useEmptySwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useImageSwatch } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useSwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useSwatchPickerContextValue } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useSwatchPickerContextValues } from '@fluentui/react-headless-components-preview/swatch-picker'; +import { useSwatchPickerRow } from '@fluentui/react-headless-components-preview/swatch-picker'; + +// @public +export const ColorSwatch: ForwardRefComponent; + +// @public +export const colorSwatchClassNames: { + root: string; +}; + +// @public +export type ColorSwatchProps = ColorSwatchProps_2 & { + size?: SwatchPickerSize; + shape?: SwatchPickerShape; +}; + +export { ColorSwatchSlots } + +// @public +export type ColorSwatchState = ColorSwatchState_2 & Required>; + +// @public +export const EmptySwatch: ForwardRefComponent; + +// @public +export const emptySwatchClassNames: { + root: string; +}; + +// @public +export type EmptySwatchProps = EmptySwatchProps_2 & { + size?: SwatchPickerSize; + shape?: SwatchPickerShape; +}; + +export { EmptySwatchSlots } + +// @public +export type EmptySwatchState = EmptySwatchState_2 & Required>; + +// @public +export const ImageSwatch: ForwardRefComponent; + +// @public +export const imageSwatchClassNames: { + root: string; +}; + +// @public +export type ImageSwatchProps = ImageSwatchProps_2; + +export { ImageSwatchSlots } + +// @public +export type ImageSwatchState = ImageSwatchState_2 & { + size: SwatchPickerSize; + shape: SwatchPickerShape; +}; + +export { renderColorSwatch } + +export { renderEmptySwatch } + +export { renderImageSwatch } + +export { renderSwatchPicker } + +export { renderSwatchPickerRow } + +// @public +export const SwatchPicker: ForwardRefComponent; + +// @public +export const swatchPickerClassNames: { + root: string; +}; + +// @public +export type SwatchPickerProps = SwatchPickerProps_2 & { + size?: SwatchPickerSize; + shape?: SwatchPickerShape; + spacing?: SwatchPickerSpacing; +}; + +// @public +export const SwatchPickerRow: ForwardRefComponent; + +// @public +export const swatchPickerRowClassNames: { + root: string; +}; + +// @public +export type SwatchPickerRowProps = SwatchPickerRowProps_2; + +export { SwatchPickerRowSlots } + +// @public +export type SwatchPickerRowState = SwatchPickerRowState_2 & { + spacing: SwatchPickerSpacing; +}; + +// @public +export type SwatchPickerShape = 'rounded' | 'square' | 'circular'; + +// @public +export type SwatchPickerSize = 'extra-small' | 'small' | 'medium' | 'large'; + +export { SwatchPickerSlots } + +// @public +export type SwatchPickerSpacing = 'small' | 'medium'; + +// @public +export type SwatchPickerState = SwatchPickerState_2 & Pick; + +export { useColorSwatch } + +// @public +export const useColorSwatchStyles: (state: ColorSwatchState) => ColorSwatchState; + +export { useEmptySwatch } + +// @public +export const useEmptySwatchStyles: (state: EmptySwatchState) => EmptySwatchState; + +export { useImageSwatch } + +// @public +export const useImageSwatchStyles: (state: ImageSwatchState) => ImageSwatchState; + +export { useSwatchPicker } + +export { useSwatchPickerContextValue } + +export { useSwatchPickerContextValues } + +export { useSwatchPickerRow } + +// @public +export const useSwatchPickerRowStyles: (state: SwatchPickerRowState) => SwatchPickerRowState; + +// @public +export const useSwatchPickerStyles: (state: SwatchPickerState) => SwatchPickerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/switch.api.md b/packages/react-components/react-windmod-preview/library/etc/switch.api.md new file mode 100644 index 0000000000000..caf3583c02de2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/switch.api.md @@ -0,0 +1,44 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderSwitch } from '@fluentui/react-headless-components-preview/switch'; +import type { SwitchProps as SwitchProps_2 } from '@fluentui/react-headless-components-preview/switch'; +import { SwitchSlots } from '@fluentui/react-headless-components-preview/switch'; +import type { SwitchState as SwitchState_2 } from '@fluentui/react-headless-components-preview/switch'; +import { useSwitch } from '@fluentui/react-headless-components-preview/switch'; + +export { renderSwitch } + +// @public +export const Switch: ForwardRefComponent; + +// @public +export const switchClassNames: { + root: string; +}; + +// @public +export type SwitchProps = SwitchProps_2 & { + size?: SwitchSize; +}; + +// @public +export type SwitchSize = 'small' | 'medium'; + +export { SwitchSlots } + +// @public +export type SwitchState = SwitchState_2 & Required>; + +export { useSwitch } + +// @public +export const useSwitchStyles: (state: SwitchState) => SwitchState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/tab-list.api.md b/packages/react-components/react-windmod-preview/library/etc/tab-list.api.md new file mode 100644 index 0000000000000..04cf4ef821117 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/tab-list.api.md @@ -0,0 +1,88 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderTab } from '@fluentui/react-headless-components-preview/tab-list'; +import { renderTabList } from '@fluentui/react-headless-components-preview/tab-list'; +import type { Slot } from '@fluentui/react-utilities'; +import type { TabListProps as TabListProps_2 } from '@fluentui/react-headless-components-preview/tab-list'; +import { TabListSlots } from '@fluentui/react-headless-components-preview/tab-list'; +import type { TabListState as TabListState_2 } from '@fluentui/react-headless-components-preview/tab-list'; +import type { TabProps as TabProps_2 } from '@fluentui/react-headless-components-preview/tab-list'; +import { TabSlots } from '@fluentui/react-headless-components-preview/tab-list'; +import type { TabState as TabState_2 } from '@fluentui/react-headless-components-preview/tab-list'; +import { TabValue } from '@fluentui/react-headless-components-preview/tab-list'; +import { useTab } from '@fluentui/react-headless-components-preview/tab-list'; +import { useTabList } from '@fluentui/react-headless-components-preview/tab-list'; +import { useTabListContextValues } from '@fluentui/react-headless-components-preview/tab-list'; + +export { renderTab } + +export { renderTabList } + +// @public +export const Tab: ForwardRefComponent; + +// @public +export type TabAppearance = 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular'; + +// @public +export const tabClassNames: { + root: string; +}; + +// @public +export const TabList: ForwardRefComponent; + +// @public +export const tabListClassNames: { + root: string; +}; + +// @public +export type TabListProps = TabListProps_2 & { + appearance?: TabAppearance; + reserveSelectedTabSpace?: boolean; + size?: TabSize; +}; + +export { TabListSlots } + +// @public +export type TabListState = TabListState_2 & Required>; + +// @public +export type TabProps = TabProps_2; + +// @public +export type TabSize = 'small' | 'medium' | 'large'; + +export { TabSlots } + +// @public +export type TabState = TabState_2 & Required> & Pick, 'contentReservedSpace'>; + +export { TabValue } + +export { useTab } + +// @public +export const useTabAnimatedIndicator: (state: TabState) => TabState; + +export { useTabList } + +export { useTabListContextValues } + +// @public +export const useTabListStyles: (state: TabListState) => TabListState; + +// @public +export const useTabStyles: (state: TabState) => TabState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/tag-group.api.md b/packages/react-components/react-windmod-preview/library/etc/tag-group.api.md new file mode 100644 index 0000000000000..5c7e6cb882ecb --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/tag-group.api.md @@ -0,0 +1,54 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderTagGroup } from '@fluentui/react-headless-components-preview/tag-group'; +import { TagGroupContextValues } from '@fluentui/react-headless-components-preview/tag-group'; +import type { TagGroupProps as TagGroupProps_2 } from '@fluentui/react-headless-components-preview/tag-group'; +import { TagGroupSlots } from '@fluentui/react-headless-components-preview/tag-group'; +import type { TagGroupState as TagGroupState_2 } from '@fluentui/react-headless-components-preview/tag-group'; +import { useTagGroup } from '@fluentui/react-headless-components-preview/tag-group'; +import { useTagGroupContextValues } from '@fluentui/react-headless-components-preview/tag-group'; + +export { renderTagGroup } + +// @public +export const TagGroup: ForwardRefComponent; + +// @public +export type TagGroupAppearance = TagAppearance; + +// @public +export const tagGroupClassNames: { + root: string; +}; + +export { TagGroupContextValues } + +// @public +export type TagGroupProps = TagGroupProps_2 & { + appearance?: TagGroupAppearance; + size?: TagGroupSize; +}; + +// @public +export type TagGroupSize = TagSize; + +export { TagGroupSlots } + +// @public +export type TagGroupState = TagGroupState_2 & Required>; + +export { useTagGroup } + +export { useTagGroupContextValues } + +// @public +export const useTagGroupStyles: (state: TagGroupState) => TagGroupState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/tag-picker.api.md b/packages/react-components/react-windmod-preview/library/etc/tag-picker.api.md new file mode 100644 index 0000000000000..7e57c81ba32ae --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/tag-picker.api.md @@ -0,0 +1,262 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { renderTagPicker } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerButton } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerControl } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerGroup } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerInput } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerList } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerOption } from '@fluentui/react-headless-components-preview/tag-picker'; +import { renderTagPickerOptionGroup } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerButtonProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerButtonSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerButtonState } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerContextValue } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerContextValues } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerControlInternalSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerControlProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerControlSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import type { TagPickerControlState as TagPickerControlState_2 } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerGroupProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerGroupSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import type { TagPickerGroupState as TagPickerGroupState_2 } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerInputProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerInputSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerInputState } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerListProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerListSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerListState } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOnOpenChangeData } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOnOptionSelectData } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionGroupProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionGroupSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionGroupState } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionProps } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerOptionState } from '@fluentui/react-headless-components-preview/tag-picker'; +import type { TagPickerProps as TagPickerProps_2 } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerSize } from '@fluentui/react-headless-components-preview/tag-picker'; +import { TagPickerSlots } from '@fluentui/react-headless-components-preview/tag-picker'; +import type { TagPickerState as TagPickerState_2 } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPicker } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerButton } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerContext_unstable } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerContextValues } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerControl } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerFilter } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerGroup } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerGroupContextValues } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerInput } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerList } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerOption } from '@fluentui/react-headless-components-preview/tag-picker'; +import { useTagPickerOptionGroup } from '@fluentui/react-headless-components-preview/tag-picker'; + +export { renderTagPicker } + +export { renderTagPickerButton } + +export { renderTagPickerControl } + +export { renderTagPickerGroup } + +export { renderTagPickerInput } + +export { renderTagPickerList } + +export { renderTagPickerOption } + +export { renderTagPickerOptionGroup } + +// @public +export const TagPicker: { + ({ appearance, size, ...rest }: TagPickerProps): JSXElement; + displayName: string; +}; + +// @public +export type TagPickerAppearance = 'outline' | 'underline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const TagPickerButton: ForwardRefComponent; + +// @public +export const tagPickerButtonClassNames: { + root: string; +}; + +export { TagPickerButtonProps } + +export { TagPickerButtonSlots } + +export { TagPickerButtonState } + +export { TagPickerContextValue } + +export { TagPickerContextValues } + +// @public +export const TagPickerControl: ForwardRefComponent; + +// @public +export const tagPickerControlClassNames: { + root: string; +}; + +export { TagPickerControlInternalSlots } + +export { TagPickerControlProps } + +export { TagPickerControlSlots } + +// @public +export type TagPickerControlState = TagPickerControlState_2 & { + appearance: TagPickerAppearance; + size: TagPickerSize; +}; + +// @public +export const TagPickerGroup: ForwardRefComponent; + +// @public +export const tagPickerGroupClassNames: { + root: string; +}; + +export { TagPickerGroupProps } + +export { TagPickerGroupSlots } + +// @public +export type TagPickerGroupState = TagPickerGroupState_2 & { + appearance: TagAppearance; + size: TagSize; + pickerSize: TagPickerSize; +}; + +// @public +export const TagPickerInput: ForwardRefComponent; + +// @public +export const tagPickerInputClassNames: { + root: string; +}; + +export { TagPickerInputProps } + +export { TagPickerInputSlots } + +export { TagPickerInputState } + +// @public +export const TagPickerList: ForwardRefComponent; + +// @public +export const tagPickerListClassNames: { + root: string; +}; + +export { TagPickerListProps } + +export { TagPickerListSlots } + +export { TagPickerListState } + +export { TagPickerOnOpenChangeData } + +export { TagPickerOnOptionSelectData } + +// @public +export const TagPickerOption: ForwardRefComponent; + +// @public +export const tagPickerOptionClassNames: { + root: string; +}; + +// @public +export const TagPickerOptionGroup: ForwardRefComponent; + +// @public +export const tagPickerOptionGroupClassNames: { + root: string; +}; + +export { TagPickerOptionGroupProps } + +export { TagPickerOptionGroupSlots } + +export { TagPickerOptionGroupState } + +export { TagPickerOptionProps } + +export { TagPickerOptionSlots } + +export { TagPickerOptionState } + +// @public +export type TagPickerProps = TagPickerProps_2 & { + appearance?: TagPickerAppearance; + size?: TagPickerState_2['size']; +}; + +export { TagPickerSize } + +export { TagPickerSlots } + +// @public +export type TagPickerState = TagPickerState_2; + +export { useTagPicker } + +export { useTagPickerButton } + +// @public +export const useTagPickerButtonStyles: (state: TagPickerButtonState) => TagPickerButtonState; + +export { useTagPickerContext_unstable } + +export { useTagPickerContextValues } + +export { useTagPickerControl } + +// @public +export const useTagPickerControlStyles: (state: TagPickerControlState) => TagPickerControlState; + +export { useTagPickerFilter } + +export { useTagPickerGroup } + +export { useTagPickerGroupContextValues } + +// @public +export const useTagPickerGroupStyles: (state: TagPickerGroupState) => TagPickerGroupState; + +export { useTagPickerInput } + +// @public +export const useTagPickerInputStyles: (state: TagPickerInputState) => TagPickerInputState; + +export { useTagPickerList } + +// @public +export const useTagPickerListStyles: (state: TagPickerListState) => TagPickerListState; + +export { useTagPickerOption } + +export { useTagPickerOptionGroup } + +// @public +export const useTagPickerOptionGroupStyles: (state: TagPickerOptionGroupState) => TagPickerOptionGroupState; + +// @public +export const useTagPickerOptionStyles: (state: TagPickerOptionState) => TagPickerOptionState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/tag.api.md b/packages/react-components/react-windmod-preview/library/etc/tag.api.md new file mode 100644 index 0000000000000..2d1b7c3c6fe54 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/tag.api.md @@ -0,0 +1,61 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderTag } from '@fluentui/react-headless-components-preview/tag'; +import { TagContextValues } from '@fluentui/react-headless-components-preview/tag'; +import type { TagProps as TagProps_2 } from '@fluentui/react-headless-components-preview/tag'; +import { TagSlots } from '@fluentui/react-headless-components-preview/tag'; +import type { TagState as TagState_2 } from '@fluentui/react-headless-components-preview/tag'; +import { useTag } from '@fluentui/react-headless-components-preview/tag'; +import { useTagContextValues } from '@fluentui/react-headless-components-preview/tag'; + +export { renderTag } + +// @public +export const Tag: ForwardRefComponent; + +// @public +export type TagAppearance = 'filled' | 'outline' | 'brand'; + +// @public +export const tagClassNames: { + root: string; +}; + +export { TagContextValues } + +// @public +export type TagProps = TagProps_2 & { + appearance?: TagAppearance; + shape?: TagShape; + size?: TagSize; +}; + +// @public +export type TagShape = 'rounded' | 'circular'; + +// @public +export type TagSize = 'extra-small' | 'small' | 'medium'; + +export { TagSlots } + +// @public +export type TagState = TagState_2 & Required> & { + avatarShape: AvatarShape; + avatarSize: AvatarSize; +}; + +export { useTag } + +export { useTagContextValues } + +// @public +export const useTagStyles: (state: TagState) => TagState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/teaching-popover.api.md b/packages/react-components/react-windmod-preview/library/etc/teaching-popover.api.md new file mode 100644 index 0000000000000..6f81c640b6301 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/teaching-popover.api.md @@ -0,0 +1,449 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ButtonProps as ButtonProps_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ButtonState as ButtonState_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ExtractSlotProps } from '@fluentui/react-utilities'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { NavButtonRenderFunction } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { PopoverSurfaceState as PopoverSurfaceState_2 } from '@fluentui/react-headless-components-preview/popover'; +import type * as React_2 from 'react'; +import { renderTeachingPopover } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverBody } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarousel } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselCard } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselFooter } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselFooterButton } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselNav } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselNavButton } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverCarouselPageCount } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverFooter } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverHeader } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverSurface } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverTitle } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { renderTeachingPopoverTrigger } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { Slot } from '@fluentui/react-utilities'; +import type { SlotComponentType } from '@fluentui/react-utilities'; +import { TeachingPopoverBaseBridgedContextValue } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverBodyProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverBodySlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverBodyState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselCardProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselCardSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselCardState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselContextValues } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselFooterButtonSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverCarouselFooterProps as TeachingPopoverCarouselFooterProps_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverCarouselFooterState as TeachingPopoverCarouselFooterState_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselNavButtonProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselNavButtonSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverCarouselNavButtonState as TeachingPopoverCarouselNavButtonState_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselNavProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselNavSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselNavState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselPageCountProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselPageCountRenderFunction } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselPageCountSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselPageCountState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverCarouselState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverContextValues } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverFooterProps as TeachingPopoverFooterProps_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverFooterState as TeachingPopoverFooterState_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverHeaderProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverHeaderSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverHeaderState as TeachingPopoverHeaderState_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverProps as TeachingPopoverProps_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverSurfaceProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverSurfaceSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverTitleProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverTitleSlots } from '@fluentui/react-headless-components-preview/teaching-popover'; +import type { TeachingPopoverTitleState as TeachingPopoverTitleState_2 } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverTriggerChildProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverTriggerProps } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { TeachingPopoverTriggerState } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopover } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverBody } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarousel } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselCard } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselContextValues } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselFooter } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselFooterButton } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselNav } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselNavButton } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverCarouselPageCount } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverContextValues } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverFooter } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverHeader } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverSurface } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverTitle } from '@fluentui/react-headless-components-preview/teaching-popover'; +import { useTeachingPopoverTrigger } from '@fluentui/react-headless-components-preview/teaching-popover'; + +export { NavButtonRenderFunction } + +// @public +export type PopoverAppearance = 'brand' | 'inverted'; + +// @public +export type PopoverSize = 'small' | 'medium' | 'large'; + +export { renderTeachingPopover } + +export { renderTeachingPopoverBody } + +export { renderTeachingPopoverCarousel } + +export { renderTeachingPopoverCarouselCard } + +export { renderTeachingPopoverCarouselFooter } + +export { renderTeachingPopoverCarouselFooterButton } + +export { renderTeachingPopoverCarouselNav } + +export { renderTeachingPopoverCarouselNavButton } + +export { renderTeachingPopoverCarouselPageCount } + +export { renderTeachingPopoverFooter } + +export { renderTeachingPopoverHeader } + +export { renderTeachingPopoverSurface } + +export { renderTeachingPopoverTitle } + +export { renderTeachingPopoverTrigger } + +// @public +export const TeachingPopover: { + (props: TeachingPopoverProps): JSXElement; + displayName: string; +}; + +export { TeachingPopoverBaseBridgedContextValue } + +// @public +export const TeachingPopoverBody: ForwardRefComponent; + +// @public +export const teachingPopoverBodyClassNames: { + root: string; +}; + +export { TeachingPopoverBodyProps } + +export { TeachingPopoverBodySlots } + +export { TeachingPopoverBodyState } + +// @public +export const TeachingPopoverCarousel: ForwardRefComponent; + +// @public +export const TeachingPopoverCarouselCard: ForwardRefComponent; + +// @public +export const teachingPopoverCarouselCardClassNames: { + root: string; +}; + +export { TeachingPopoverCarouselCardProps } + +export { TeachingPopoverCarouselCardSlots } + +export { TeachingPopoverCarouselCardState } + +// @public +export const teachingPopoverCarouselClassNames: { + root: string; +}; + +export { TeachingPopoverCarouselContextValues } + +// @public +export const TeachingPopoverCarouselFooter: ForwardRefComponent; + +// @public +export const TeachingPopoverCarouselFooterButton: ForwardRefComponent; + +// @public +export const teachingPopoverCarouselFooterButtonClassNames: { + root: string; +}; + +// @public +export type TeachingPopoverCarouselFooterButtonProps = ButtonProps & { + navType: 'next' | 'prev'; + altText: React_2.ReactNode; +}; + +// @public +export type TeachingPopoverCarouselFooterButtonSlot = SlotComponentType>; + +export { TeachingPopoverCarouselFooterButtonSlots } + +// @public +export type TeachingPopoverCarouselFooterButtonState = ButtonState & Pick & { + popoverAppearance?: PopoverAppearance; +}; + +// @public +export const teachingPopoverCarouselFooterClassNames: { + root: string; +}; + +// @public +export type TeachingPopoverCarouselFooterLayout = 'offset' | 'centered'; + +// @public +export type TeachingPopoverCarouselFooterProps = Omit & Omit & { + layout?: TeachingPopoverCarouselFooterLayout; +}; + +// @public +export type TeachingPopoverCarouselFooterSlots = { + root: NonNullable>; + previous?: Slot; + next: NonNullable>; +}; + +// @public +export type TeachingPopoverCarouselFooterState = Omit & Required> & { + previous?: TeachingPopoverCarouselFooterButtonSlot; + next: TeachingPopoverCarouselFooterButtonSlot; +}; + +// @public +export const TeachingPopoverCarouselNav: ForwardRefComponent; + +// @public +export const TeachingPopoverCarouselNavButton: ForwardRefComponent; + +// @public +export const teachingPopoverCarouselNavButtonClassNames: { + root: string; +}; + +export { TeachingPopoverCarouselNavButtonProps } + +export { TeachingPopoverCarouselNavButtonSlots } + +// @public +export type TeachingPopoverCarouselNavButtonState = TeachingPopoverCarouselNavButtonState_2 & { + appearance?: PopoverAppearance; +}; + +// @public +export const teachingPopoverCarouselNavClassNames: { + root: string; +}; + +export { TeachingPopoverCarouselNavProps } + +export { TeachingPopoverCarouselNavSlots } + +export { TeachingPopoverCarouselNavState } + +// @public +export const TeachingPopoverCarouselPageCount: ForwardRefComponent; + +// @public +export const teachingPopoverCarouselPageCountClassNames: { + root: string; +}; + +export { TeachingPopoverCarouselPageCountProps } + +export { TeachingPopoverCarouselPageCountRenderFunction } + +export { TeachingPopoverCarouselPageCountSlots } + +export { TeachingPopoverCarouselPageCountState } + +export { TeachingPopoverCarouselProps } + +export { TeachingPopoverCarouselSlots } + +export { TeachingPopoverCarouselState } + +export { TeachingPopoverContextValues } + +// @public +export const TeachingPopoverFooter: ForwardRefComponent; + +// @public +export type TeachingPopoverFooterButtonSlot = SlotComponentType>; + +// @public +export const teachingPopoverFooterClassNames: { + root: string; +}; + +// @public +export type TeachingPopoverFooterProps = TeachingPopoverFooterProps_2 & Partial; + +// @public +export type TeachingPopoverFooterSlots = { + primary: NonNullable>; + secondary?: Slot; +}; + +// @public +export type TeachingPopoverFooterState = TeachingPopoverFooterState_2 & { + appearance?: PopoverAppearance; + primary: TeachingPopoverFooterButtonSlot; + secondary?: TeachingPopoverFooterButtonSlot; +}; + +// @public +export const TeachingPopoverHeader: ForwardRefComponent; + +// @public +export const teachingPopoverHeaderClassNames: { + root: string; +}; + +export { TeachingPopoverHeaderProps } + +export { TeachingPopoverHeaderSlots } + +// @public +export type TeachingPopoverHeaderState = TeachingPopoverHeaderState_2 & { + appearance?: PopoverAppearance; +}; + +// @public +export type TeachingPopoverProps = TeachingPopoverProps_2 & { + appearance?: PopoverAppearance; + size?: PopoverSize; +}; + +export { TeachingPopoverState } + +// @public +export const TeachingPopoverSurface: ForwardRefComponent; + +// @public +export const teachingPopoverSurfaceClassNames: { + root: string; +}; + +export { TeachingPopoverSurfaceProps } + +export { TeachingPopoverSurfaceSlots } + +// @public +export type TeachingPopoverSurfaceState = PopoverSurfaceState; + +// @public +export const TeachingPopoverTitle: ForwardRefComponent; + +// @public +export const teachingPopoverTitleClassNames: { + root: string; +}; + +export { TeachingPopoverTitleProps } + +export { TeachingPopoverTitleSlots } + +// @public +export type TeachingPopoverTitleState = TeachingPopoverTitleState_2 & { + appearance?: PopoverAppearance; +}; + +// @public +export const TeachingPopoverTrigger: React_2.FC; + +export { TeachingPopoverTriggerChildProps } + +// @public +export const teachingPopoverTriggerClassNames: { + root: string; +}; + +export { TeachingPopoverTriggerProps } + +export { TeachingPopoverTriggerState } + +export { useTeachingPopover } + +export { useTeachingPopoverBody } + +// @public +export const useTeachingPopoverBodyStyles: (state: TeachingPopoverBodyState) => TeachingPopoverBodyState; + +export { useTeachingPopoverCarousel } + +export { useTeachingPopoverCarouselCard } + +// @public +export const useTeachingPopoverCarouselCardStyles: (state: TeachingPopoverCarouselCardState) => TeachingPopoverCarouselCardState; + +export { useTeachingPopoverCarouselContextValues } + +export { useTeachingPopoverCarouselFooter } + +export { useTeachingPopoverCarouselFooterButton } + +// @public +export const useTeachingPopoverCarouselFooterButtonStyles: (state: TeachingPopoverCarouselFooterButtonState) => TeachingPopoverCarouselFooterButtonState; + +// @public +export const useTeachingPopoverCarouselFooterStyles: (state: TeachingPopoverCarouselFooterState) => TeachingPopoverCarouselFooterState; + +export { useTeachingPopoverCarouselNav } + +export { useTeachingPopoverCarouselNavButton } + +// @public +export const useTeachingPopoverCarouselNavButtonStyles: (state: TeachingPopoverCarouselNavButtonState) => TeachingPopoverCarouselNavButtonState; + +// @public +export const useTeachingPopoverCarouselNavStyles: (state: TeachingPopoverCarouselNavState) => TeachingPopoverCarouselNavState; + +export { useTeachingPopoverCarouselPageCount } + +// @public +export const useTeachingPopoverCarouselPageCountStyles: (state: TeachingPopoverCarouselPageCountState) => TeachingPopoverCarouselPageCountState; + +// @public +export const useTeachingPopoverCarouselStyles: (state: TeachingPopoverCarouselState) => TeachingPopoverCarouselState; + +export { useTeachingPopoverContextValues } + +export { useTeachingPopoverFooter } + +// @public +export const useTeachingPopoverFooterStyles: (state: TeachingPopoverFooterState) => TeachingPopoverFooterState; + +export { useTeachingPopoverHeader } + +// @public +export const useTeachingPopoverHeaderStyles: (state: TeachingPopoverHeaderState) => TeachingPopoverHeaderState; + +export { useTeachingPopoverSurface } + +// @public +export const useTeachingPopoverSurfaceStyles: (state: TeachingPopoverSurfaceState) => TeachingPopoverSurfaceState; + +export { useTeachingPopoverTitle } + +// @public +export const useTeachingPopoverTitleStyles: (state: TeachingPopoverTitleState) => TeachingPopoverTitleState; + +export { useTeachingPopoverTrigger } + +// @public +export const useTeachingPopoverTriggerStyles: (state: TeachingPopoverTriggerState) => TeachingPopoverTriggerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/textarea.api.md b/packages/react-components/react-windmod-preview/library/etc/textarea.api.md new file mode 100644 index 0000000000000..2a24007ee9520 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/textarea.api.md @@ -0,0 +1,48 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderTextarea } from '@fluentui/react-headless-components-preview/textarea'; +import type { TextareaProps as TextareaProps_2 } from '@fluentui/react-headless-components-preview/textarea'; +import { TextareaSlots } from '@fluentui/react-headless-components-preview/textarea'; +import type { TextareaState as TextareaState_2 } from '@fluentui/react-headless-components-preview/textarea'; +import { useTextarea } from '@fluentui/react-headless-components-preview/textarea'; + +export { renderTextarea } + +// @public +export const Textarea: ForwardRefComponent; + +// @public +export type TextareaAppearance = 'outline' | 'filled-darker' | 'filled-lighter'; + +// @public +export const textareaClassNames: { + root: string; +}; + +// @public +export type TextareaProps = TextareaProps_2 & { + appearance?: TextareaAppearance; + size?: TextareaSize; +}; + +// @public +export type TextareaSize = 'small' | 'medium' | 'large'; + +export { TextareaSlots } + +// @public +export type TextareaState = TextareaState_2 & Required>; + +export { useTextarea } + +// @public +export const useTextareaStyles: (state: TextareaState) => TextareaState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/toast.api.md b/packages/react-components/react-windmod-preview/library/etc/toast.api.md new file mode 100644 index 0000000000000..a704a6009c06f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/toast.api.md @@ -0,0 +1,203 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type * as React_2 from 'react'; +import { renderToast } from '@fluentui/react-headless-components-preview/toast'; +import { renderToastBody } from '@fluentui/react-headless-components-preview/toast'; +import { renderToastContainer } from '@fluentui/react-headless-components-preview/toast'; +import { renderToaster } from '@fluentui/react-headless-components-preview/toast'; +import { renderToastFooter } from '@fluentui/react-headless-components-preview/toast'; +import { renderToastTitle } from '@fluentui/react-headless-components-preview/toast'; +import { ToastBodyProps } from '@fluentui/react-headless-components-preview/toast'; +import { ToastBodySlots } from '@fluentui/react-headless-components-preview/toast'; +import { ToastBodyState } from '@fluentui/react-headless-components-preview/toast'; +import { ToastChangeData } from '@fluentui/react-headless-components-preview/toast'; +import { ToastChangeHandler } from '@fluentui/react-headless-components-preview/toast'; +import { ToastContainer } from '@fluentui/react-headless-components-preview/toast'; +import { ToastContainerContextValue } from '@fluentui/react-headless-components-preview/toast'; +import { ToastContainerProps } from '@fluentui/react-headless-components-preview/toast'; +import { ToastContainerSlots } from '@fluentui/react-headless-components-preview/toast'; +import { ToastContainerState } from '@fluentui/react-headless-components-preview/toast'; +import { ToasterId } from '@fluentui/react-headless-components-preview/toast'; +import { ToasterProps } from '@fluentui/react-headless-components-preview/toast'; +import { ToasterState } from '@fluentui/react-headless-components-preview/toast'; +import { ToastFooterProps } from '@fluentui/react-headless-components-preview/toast'; +import { ToastFooterSlots } from '@fluentui/react-headless-components-preview/toast'; +import { ToastFooterState } from '@fluentui/react-headless-components-preview/toast'; +import { ToastId } from '@fluentui/react-headless-components-preview/toast'; +import { ToastImperativeRef } from '@fluentui/react-headless-components-preview/toast'; +import { ToastIntent } from '@fluentui/react-headless-components-preview/toast'; +import { ToastPoliteness } from '@fluentui/react-headless-components-preview/toast'; +import { ToastPosition } from '@fluentui/react-headless-components-preview/toast'; +import type { ToastProps as ToastProps_2 } from '@fluentui/react-headless-components-preview/toast'; +import { ToastSlots } from '@fluentui/react-headless-components-preview/toast'; +import type { ToastState as ToastState_2 } from '@fluentui/react-headless-components-preview/toast'; +import { ToastStatus } from '@fluentui/react-headless-components-preview/toast'; +import { ToastTitleProps } from '@fluentui/react-headless-components-preview/toast'; +import { ToastTitleSlots } from '@fluentui/react-headless-components-preview/toast'; +import { ToastTitleState } from '@fluentui/react-headless-components-preview/toast'; +import { useToast } from '@fluentui/react-headless-components-preview/toast'; +import { useToastBody } from '@fluentui/react-headless-components-preview/toast'; +import { useToastContainer } from '@fluentui/react-headless-components-preview/toast'; +import { useToastContainerContext } from '@fluentui/react-headless-components-preview/toast'; +import { useToastContainerContextValues } from '@fluentui/react-headless-components-preview/toast'; +import { useToastController } from '@fluentui/react-headless-components-preview/toast'; +import { useToaster } from '@fluentui/react-headless-components-preview/toast'; +import { useToastFooter } from '@fluentui/react-headless-components-preview/toast'; +import { useToastTitle } from '@fluentui/react-headless-components-preview/toast'; + +export { renderToast } + +export { renderToastBody } + +export { renderToastContainer } + +export { renderToaster } + +export { renderToastFooter } + +export { renderToastTitle } + +// @public +export const Toast: ForwardRefComponent; + +// @public +export type ToastAppearance = 'inverted'; + +// @public +export const ToastBody: ForwardRefComponent; + +// @public +export const toastBodyClassNames: { + root: string; +}; + +export { ToastBodyProps } + +export { ToastBodySlots } + +export { ToastBodyState } + +export { ToastChangeData } + +export { ToastChangeHandler } + +// @public +export const toastClassNames: { + root: string; +}; + +export { ToastContainer } + +export { ToastContainerContextValue } + +export { ToastContainerProps } + +export { ToastContainerSlots } + +export { ToastContainerState } + +// @public +export const Toaster: React_2.FC; + +// @public +export const toasterClassNames: { + root: string; +}; + +export { ToasterId } + +export { ToasterProps } + +export { ToasterState } + +// @public +export const ToastFooter: ForwardRefComponent; + +// @public +export const toastFooterClassNames: { + root: string; +}; + +export { ToastFooterProps } + +export { ToastFooterSlots } + +export { ToastFooterState } + +export { ToastId } + +export { ToastImperativeRef } + +export { ToastIntent } + +export { ToastPoliteness } + +export { ToastPosition } + +// @public +export type ToastProps = ToastProps_2 & { + appearance?: ToastAppearance; +}; + +export { ToastSlots } + +// @public +export type ToastState = ToastState_2 & Pick; + +export { ToastStatus } + +// @public +export const ToastTitle: ForwardRefComponent; + +// @public +export const toastTitleClassNames: { + root: string; +}; + +export { ToastTitleProps } + +export { ToastTitleSlots } + +export { ToastTitleState } + +export { useToast } + +export { useToastBody } + +// @public +export const useToastBodyStyles: (state: ToastBodyState) => ToastBodyState; + +export { useToastContainer } + +export { useToastContainerContext } + +export { useToastContainerContextValues } + +export { useToastController } + +export { useToaster } + +// @public +export const useToasterStyles: (state: ToasterState) => ToasterState; + +export { useToastFooter } + +// @public +export const useToastFooterStyles: (state: ToastFooterState) => ToastFooterState; + +// @public +export const useToastStyles: (state: ToastState) => ToastState; + +export { useToastTitle } + +// @public +export const useToastTitleStyles: (state: ToastTitleState) => ToastTitleState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/toggle-button.api.md b/packages/react-components/react-windmod-preview/library/etc/toggle-button.api.md new file mode 100644 index 0000000000000..1f5c36dfd4835 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/toggle-button.api.md @@ -0,0 +1,43 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderToggleButton } from '@fluentui/react-headless-components-preview/toggle-button'; +import type { ToggleButtonProps as ToggleButtonProps_2 } from '@fluentui/react-headless-components-preview/toggle-button'; +import { ToggleButtonSlots } from '@fluentui/react-headless-components-preview/toggle-button'; +import type { ToggleButtonState as ToggleButtonState_2 } from '@fluentui/react-headless-components-preview/toggle-button'; +import { useToggleButton } from '@fluentui/react-headless-components-preview/toggle-button'; + +export { renderToggleButton } + +// @public +export const ToggleButton: ForwardRefComponent; + +// @public +export const toggleButtonClassNames: { + root: string; +}; + +// @public +export type ToggleButtonProps = ToggleButtonProps_2 & { + appearance?: ButtonAppearance; + shape?: ButtonShape; + size?: ButtonSize; +}; + +export { ToggleButtonSlots } + +// @public +export type ToggleButtonState = ToggleButtonState_2 & Required>; + +export { useToggleButton } + +// @public +export const useToggleButtonStyles: (state: ToggleButtonState) => ToggleButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/toolbar.api.md b/packages/react-components/react-windmod-preview/library/etc/toolbar.api.md new file mode 100644 index 0000000000000..9980d7c7d0d91 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/toolbar.api.md @@ -0,0 +1,216 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ButtonProps as ButtonProps_2 } from '@fluentui/react-headless-components-preview/button'; +import type { ButtonState as ButtonState_2 } from '@fluentui/react-headless-components-preview/button'; +import type { DividerProps as DividerProps_2 } from '@fluentui/react-headless-components-preview/divider'; +import type { DividerState as DividerState_2 } from '@fluentui/react-headless-components-preview/divider'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderToolbar } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarButton } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarDivider } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarGroup } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarRadioButton } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarRadioGroup } from '@fluentui/react-headless-components-preview/toolbar'; +import { renderToolbarToggleButton } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarButtonProps as ToolbarButtonProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarButtonState as ToolbarButtonState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import { ToolbarContextValues } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarDividerProps as ToolbarDividerProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarDividerState as ToolbarDividerState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarGroupProps as ToolbarGroupProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarGroupState as ToolbarGroupState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarProps as ToolbarProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarRadioButtonProps as ToolbarRadioButtonProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarRadioButtonState as ToolbarRadioButtonState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarRadioGroupProps as ToolbarRadioGroupProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarRadioGroupState as ToolbarRadioGroupState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import { ToolbarSlots } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarState as ToolbarState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarToggleButtonProps as ToolbarToggleButtonProps_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import type { ToolbarToggleButtonState as ToolbarToggleButtonState_2 } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbar } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarButton } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarContext } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarContextValues } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarDivider } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarGroup } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarRadioButton } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarRadioGroup } from '@fluentui/react-headless-components-preview/toolbar'; +import { useToolbarToggleButton } from '@fluentui/react-headless-components-preview/toolbar'; + +export { renderToolbar } + +export { renderToolbarButton } + +export { renderToolbarDivider } + +export { renderToolbarGroup } + +export { renderToolbarRadioButton } + +export { renderToolbarRadioGroup } + +export { renderToolbarToggleButton } + +// @public +export const Toolbar: ForwardRefComponent; + +// @public +export const ToolbarButton: ForwardRefComponent; + +// @public +export type ToolbarButtonAppearance = 'primary' | 'subtle' | 'transparent'; + +// @public +export const toolbarButtonClassNames: { + root: string; +}; + +// @public +export type ToolbarButtonProps = ToolbarButtonProps_2 & { + appearance?: ToolbarButtonAppearance; +}; + +// @public +export type ToolbarButtonState = ToolbarButtonState_2 & Required> & Required>; + +// @public +export const toolbarClassNames: { + root: string; +}; + +export { ToolbarContextValues } + +// @public +export const ToolbarDivider: ForwardRefComponent; + +// @public +export const toolbarDividerClassNames: { + root: string; +}; + +// @public +export type ToolbarDividerProps = ToolbarDividerProps_2; + +// @public +export type ToolbarDividerState = ToolbarDividerState_2 & Required>; + +// @public +export const ToolbarGroup: ForwardRefComponent; + +// @public +export const toolbarGroupClassNames: { + root: string; +}; + +// @public +export type ToolbarGroupProps = ToolbarGroupProps_2; + +// @public +export type ToolbarGroupState = ToolbarGroupState_2; + +// @public +export type ToolbarProps = ToolbarProps_2 & { + size?: ToolbarSize; +}; + +// @public +export const ToolbarRadioButton: ForwardRefComponent; + +// @public +export const toolbarRadioButtonClassNames: { + root: string; +}; + +// @public +export type ToolbarRadioButtonProps = ToolbarRadioButtonProps_2 & { + appearance?: ToolbarButtonAppearance; +}; + +// @public +export type ToolbarRadioButtonState = ToolbarRadioButtonState_2 & Required> & Required>; + +// @public +export const ToolbarRadioGroup: ForwardRefComponent; + +// @public +export const toolbarRadioGroupClassNames: { + root: string; +}; + +// @public +export type ToolbarRadioGroupProps = ToolbarRadioGroupProps_2; + +// @public +export type ToolbarRadioGroupState = ToolbarRadioGroupState_2; + +// @public +export type ToolbarSize = 'small' | 'medium' | 'large'; + +export { ToolbarSlots } + +// @public +export type ToolbarState = ToolbarState_2 & Required>; + +// @public +export const ToolbarToggleButton: ForwardRefComponent; + +// @public +export const toolbarToggleButtonClassNames: { + root: string; +}; + +// @public +export type ToolbarToggleButtonProps = ToolbarToggleButtonProps_2 & { + appearance?: ToolbarButtonAppearance; +}; + +// @public +export type ToolbarToggleButtonState = ToolbarToggleButtonState_2 & Required> & Required>; + +export { useToolbar } + +export { useToolbarButton } + +// @public +export const useToolbarButtonStyles: (state: ToolbarButtonState) => ToolbarButtonState; + +export { useToolbarContext } + +export { useToolbarContextValues } + +export { useToolbarDivider } + +// @public +export const useToolbarDividerStyles: (state: ToolbarDividerState) => ToolbarDividerState; + +export { useToolbarGroup } + +// @public +export const useToolbarGroupStyles: (state: ToolbarGroupState) => ToolbarGroupState; + +export { useToolbarRadioButton } + +// @public +export const useToolbarRadioButtonStyles: (state: ToolbarRadioButtonState) => ToolbarRadioButtonState; + +export { useToolbarRadioGroup } + +// @public +export const useToolbarRadioGroupStyles: (state: ToolbarRadioGroupState) => ToolbarRadioGroupState; + +// @public +export const useToolbarStyles: (state: ToolbarState) => ToolbarState; + +export { useToolbarToggleButton } + +// @public +export const useToolbarToggleButtonStyles: (state: ToolbarToggleButtonState) => ToolbarToggleButtonState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/tooltip.api.md b/packages/react-components/react-windmod-preview/library/etc/tooltip.api.md new file mode 100644 index 0000000000000..585c00fcdfcce --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/tooltip.api.md @@ -0,0 +1,55 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { JSXElement } from '@fluentui/react-utilities'; +import { OnVisibleChangeData } from '@fluentui/react-headless-components-preview/tooltip'; +import { renderTooltip } from '@fluentui/react-headless-components-preview/tooltip'; +import type { TooltipProps as TooltipProps_2 } from '@fluentui/react-headless-components-preview/tooltip'; +import { TooltipSlots } from '@fluentui/react-headless-components-preview/tooltip'; +import type { TooltipState as TooltipState_2 } from '@fluentui/react-headless-components-preview/tooltip'; +import { TooltipTriggerProps } from '@fluentui/react-headless-components-preview/tooltip'; +import { useTooltip } from '@fluentui/react-headless-components-preview/tooltip'; + +export { OnVisibleChangeData } + +export { renderTooltip } + +// @public +export const Tooltip: { + (props: TooltipProps): JSXElement; + displayName: string; +}; + +// @public +export type TooltipAppearance = 'normal' | 'inverted'; + +// @public +export const tooltipClassNames: { + root: string; +}; + +// @public +export type TooltipProps = TooltipProps_2 & { + appearance?: TooltipAppearance; +}; + +export { TooltipSlots } + +// @public +export type TooltipState = TooltipState_2 & { + appearance: TooltipAppearance; +}; + +export { TooltipTriggerProps } + +export { useTooltip } + +// @public +export const useTooltipStyles: (state: TooltipState) => TooltipState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/etc/use-css-var-value.api.md b/packages/react-components/react-windmod-preview/library/etc/use-css-var-value.api.md new file mode 100644 index 0000000000000..8a2392931db54 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/etc/use-css-var-value.api.md @@ -0,0 +1,31 @@ +## API Report File for "@fluentui/react-windmod-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as React_2 from 'react'; + +// @public +export type CssVarValues> = { + [K in keyof T]: string | undefined; +}; + +// @public +export function invalidateCssVars(): void; + +// @public +export function useCssVarValue(variableName: string, elementRef: React_2.RefObject, options?: UseCssVarValueOptions): string | undefined; + +// @public +export function useCssVarValue>(variableNames: T, elementRef: React_2.RefObject, options?: UseCssVarValueOptions): CssVarValues; + +// @public +export interface UseCssVarValueOptions { + fallback?: string; + forceOnRender?: boolean; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-windmod-preview/library/jest.config.cjs b/packages/react-components/react-windmod-preview/library/jest.config.cjs new file mode 100644 index 0000000000000..8f38427e18654 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/jest.config.cjs @@ -0,0 +1,34 @@ +// @ts-check +/* eslint-disable */ + +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(join(__dirname, '.swcrc'), 'utf-8')); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-windmod-preview', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.cjs'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-windmod-preview/library/package.json b/packages/react-components/react-windmod-preview/library/package.json new file mode 100644 index 0000000000000..2260b197bb4af --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/package.json @@ -0,0 +1,616 @@ +{ + "name": "@fluentui/react-windmod-preview", + "version": "0.1.0", + "description": "Fluent UI React components styled with Tailwind v4 + CSS Modules ('windmod'). Composes @fluentui/react-headless-components-preview hooks (useX → useXStyles → renderX) with build-time-compiled CSS Modules that replicate the Griffel suite's visuals; theming is pure CSS via @fluentui/react-tailwind-theme-preview theme classes applied by FluentProvider.", + "type": "module", + "main": "lib-commonjs/index.cjs", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": [ + "**/*.css" + ], + "files": [ + "*.md", + "dist/*.d.ts", + "dist/*.d.cts", + "dist/base.css", + "dist/css", + "dist/styles.css", + "lib", + "lib-commonjs", + "skills", + "src/variants.css" + ], + "keywords": [ + "tanstack-intent" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "dependencies": { + "@fluentui/react-headless-components-preview": "^0.2.5", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.4.5", + "@fluentui/react-shared-contexts": "^9.26.3", + "@fluentui/react-tailwind-theme-preview": "^0.1.0", + "@fluentui/react-utilities": "^9.26.6", + "@swc/helpers": "^0.5.1", + "clsx": "^2.1.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + }, + "imports": { + "#theme": "./src/theme.css" + }, + "//exports": "HAND-AUTHORED ASSET SUBPATHS — ./styles.css and ./variants.css are CSS, not TypeScript, so the `export-maps-sync` Nx generator (tools/workspace-plugin, wired into CI as `yarn nx sync:check`) cannot derive them from src/*.ts. It rebuilds this whole map from project.json's metadata.exportMap on every `nx sync` and keeps only what that block declares, so both halves of the declaration are load bearing: `subpathEntryPoints: [\"src/*.ts\"]` regenerates `.` plus the 147 TypeScript subpaths, and `staticSubpaths: [\"./styles.css\", \"./variants.css\"]` tells the generator it does not own these two and must read them back verbatim from right here. Their paths therefore live in this file only — retarget either one below and a sync preserves the edit; remove one below while its key stays declared in project.json and the sync fails naming the key. Both are also shipped by the `files` array above (dist/styles.css, src/variants.css) — keep the two lists in step. ./variants.css is a documented public export: consumers point Tailwind's `@source` at it so the variant utilities survive their own build.", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./lib/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./lib-commonjs/index.cjs" + } + }, + "./accordion": { + "import": { + "types": "./dist/accordion.d.ts", + "default": "./lib/accordion.js" + }, + "require": { + "types": "./dist/accordion.d.cts", + "default": "./lib-commonjs/accordion.cjs" + } + }, + "./avatar": { + "import": { + "types": "./dist/avatar.d.ts", + "default": "./lib/avatar.js" + }, + "require": { + "types": "./dist/avatar.d.cts", + "default": "./lib-commonjs/avatar.cjs" + } + }, + "./avatar-group": { + "import": { + "types": "./dist/avatar-group.d.ts", + "default": "./lib/avatar-group.js" + }, + "require": { + "types": "./dist/avatar-group.d.cts", + "default": "./lib-commonjs/avatar-group.cjs" + } + }, + "./badge": { + "import": { + "types": "./dist/badge.d.ts", + "default": "./lib/badge.js" + }, + "require": { + "types": "./dist/badge.d.cts", + "default": "./lib-commonjs/badge.cjs" + } + }, + "./base.css": "./dist/base.css", + "./breadcrumb": { + "import": { + "types": "./dist/breadcrumb.d.ts", + "default": "./lib/breadcrumb.js" + }, + "require": { + "types": "./dist/breadcrumb.d.cts", + "default": "./lib-commonjs/breadcrumb.cjs" + } + }, + "./button": { + "import": { + "types": "./dist/button.d.ts", + "default": "./lib/button.js" + }, + "require": { + "types": "./dist/button.d.cts", + "default": "./lib-commonjs/button.cjs" + } + }, + "./card": { + "import": { + "types": "./dist/card.d.ts", + "default": "./lib/card.js" + }, + "require": { + "types": "./dist/card.d.cts", + "default": "./lib-commonjs/card.cjs" + } + }, + "./checkbox": { + "import": { + "types": "./dist/checkbox.d.ts", + "default": "./lib/checkbox.js" + }, + "require": { + "types": "./dist/checkbox.d.cts", + "default": "./lib-commonjs/checkbox.cjs" + } + }, + "./color-picker": { + "import": { + "types": "./dist/color-picker.d.ts", + "default": "./lib/color-picker.js" + }, + "require": { + "types": "./dist/color-picker.d.cts", + "default": "./lib-commonjs/color-picker.cjs" + } + }, + "./combobox": { + "import": { + "types": "./dist/combobox.d.ts", + "default": "./lib/combobox.js" + }, + "require": { + "types": "./dist/combobox.d.cts", + "default": "./lib-commonjs/combobox.cjs" + } + }, + "./compound-button": { + "import": { + "types": "./dist/compound-button.d.ts", + "default": "./lib/compound-button.js" + }, + "require": { + "types": "./dist/compound-button.d.cts", + "default": "./lib-commonjs/compound-button.cjs" + } + }, + "./css/*": "./dist/css/*", + "./dialog": { + "import": { + "types": "./dist/dialog.d.ts", + "default": "./lib/dialog.js" + }, + "require": { + "types": "./dist/dialog.d.cts", + "default": "./lib-commonjs/dialog.cjs" + } + }, + "./divider": { + "import": { + "types": "./dist/divider.d.ts", + "default": "./lib/divider.js" + }, + "require": { + "types": "./dist/divider.d.cts", + "default": "./lib-commonjs/divider.cjs" + } + }, + "./drawer": { + "import": { + "types": "./dist/drawer.d.ts", + "default": "./lib/drawer.js" + }, + "require": { + "types": "./dist/drawer.d.cts", + "default": "./lib-commonjs/drawer.cjs" + } + }, + "./dropdown": { + "import": { + "types": "./dist/dropdown.d.ts", + "default": "./lib/dropdown.js" + }, + "require": { + "types": "./dist/dropdown.d.cts", + "default": "./lib-commonjs/dropdown.cjs" + } + }, + "./field": { + "import": { + "types": "./dist/field.d.ts", + "default": "./lib/field.js" + }, + "require": { + "types": "./dist/field.d.cts", + "default": "./lib-commonjs/field.cjs" + } + }, + "./image": { + "import": { + "types": "./dist/image.d.ts", + "default": "./lib/image.js" + }, + "require": { + "types": "./dist/image.d.cts", + "default": "./lib-commonjs/image.cjs" + } + }, + "./info-label": { + "import": { + "types": "./dist/info-label.d.ts", + "default": "./lib/info-label.js" + }, + "require": { + "types": "./dist/info-label.d.cts", + "default": "./lib-commonjs/info-label.cjs" + } + }, + "./input": { + "import": { + "types": "./dist/input.d.ts", + "default": "./lib/input.js" + }, + "require": { + "types": "./dist/input.d.cts", + "default": "./lib-commonjs/input.cjs" + } + }, + "./interaction-tag": { + "import": { + "types": "./dist/interaction-tag.d.ts", + "default": "./lib/interaction-tag.js" + }, + "require": { + "types": "./dist/interaction-tag.d.cts", + "default": "./lib-commonjs/interaction-tag.cjs" + } + }, + "./label": { + "import": { + "types": "./dist/label.d.ts", + "default": "./lib/label.js" + }, + "require": { + "types": "./dist/label.d.cts", + "default": "./lib-commonjs/label.cjs" + } + }, + "./link": { + "import": { + "types": "./dist/link.d.ts", + "default": "./lib/link.js" + }, + "require": { + "types": "./dist/link.d.cts", + "default": "./lib-commonjs/link.cjs" + } + }, + "./menu": { + "import": { + "types": "./dist/menu.d.ts", + "default": "./lib/menu.js" + }, + "require": { + "types": "./dist/menu.d.cts", + "default": "./lib-commonjs/menu.cjs" + } + }, + "./menu-button": { + "import": { + "types": "./dist/menu-button.d.ts", + "default": "./lib/menu-button.js" + }, + "require": { + "types": "./dist/menu-button.d.cts", + "default": "./lib-commonjs/menu-button.cjs" + } + }, + "./message-bar": { + "import": { + "types": "./dist/message-bar.d.ts", + "default": "./lib/message-bar.js" + }, + "require": { + "types": "./dist/message-bar.d.cts", + "default": "./lib-commonjs/message-bar.cjs" + } + }, + "./nav": { + "import": { + "types": "./dist/nav.d.ts", + "default": "./lib/nav.js" + }, + "require": { + "types": "./dist/nav.d.cts", + "default": "./lib-commonjs/nav.cjs" + } + }, + "./persona": { + "import": { + "types": "./dist/persona.d.ts", + "default": "./lib/persona.js" + }, + "require": { + "types": "./dist/persona.d.cts", + "default": "./lib-commonjs/persona.cjs" + } + }, + "./popover": { + "import": { + "types": "./dist/popover.d.ts", + "default": "./lib/popover.js" + }, + "require": { + "types": "./dist/popover.d.cts", + "default": "./lib-commonjs/popover.cjs" + } + }, + "./positioning": { + "import": { + "types": "./dist/positioning.d.ts", + "default": "./lib/positioning.js" + }, + "require": { + "types": "./dist/positioning.d.cts", + "default": "./lib-commonjs/positioning.cjs" + } + }, + "./progress-bar": { + "import": { + "types": "./dist/progress-bar.d.ts", + "default": "./lib/progress-bar.js" + }, + "require": { + "types": "./dist/progress-bar.d.cts", + "default": "./lib-commonjs/progress-bar.cjs" + } + }, + "./provider": { + "import": { + "types": "./dist/provider.d.ts", + "default": "./lib/provider.js" + }, + "require": { + "types": "./dist/provider.d.cts", + "default": "./lib-commonjs/provider.cjs" + } + }, + "./radio-group": { + "import": { + "types": "./dist/radio-group.d.ts", + "default": "./lib/radio-group.js" + }, + "require": { + "types": "./dist/radio-group.d.cts", + "default": "./lib-commonjs/radio-group.cjs" + } + }, + "./rating": { + "import": { + "types": "./dist/rating.d.ts", + "default": "./lib/rating.js" + }, + "require": { + "types": "./dist/rating.d.cts", + "default": "./lib-commonjs/rating.cjs" + } + }, + "./rating-display": { + "import": { + "types": "./dist/rating-display.d.ts", + "default": "./lib/rating-display.js" + }, + "require": { + "types": "./dist/rating-display.d.cts", + "default": "./lib-commonjs/rating-display.cjs" + } + }, + "./search-box": { + "import": { + "types": "./dist/search-box.d.ts", + "default": "./lib/search-box.js" + }, + "require": { + "types": "./dist/search-box.d.cts", + "default": "./lib-commonjs/search-box.cjs" + } + }, + "./select": { + "import": { + "types": "./dist/select.d.ts", + "default": "./lib/select.js" + }, + "require": { + "types": "./dist/select.d.cts", + "default": "./lib-commonjs/select.cjs" + } + }, + "./skeleton": { + "import": { + "types": "./dist/skeleton.d.ts", + "default": "./lib/skeleton.js" + }, + "require": { + "types": "./dist/skeleton.d.cts", + "default": "./lib-commonjs/skeleton.cjs" + } + }, + "./slider": { + "import": { + "types": "./dist/slider.d.ts", + "default": "./lib/slider.js" + }, + "require": { + "types": "./dist/slider.d.cts", + "default": "./lib-commonjs/slider.cjs" + } + }, + "./spin-button": { + "import": { + "types": "./dist/spin-button.d.ts", + "default": "./lib/spin-button.js" + }, + "require": { + "types": "./dist/spin-button.d.cts", + "default": "./lib-commonjs/spin-button.cjs" + } + }, + "./spinner": { + "import": { + "types": "./dist/spinner.d.ts", + "default": "./lib/spinner.js" + }, + "require": { + "types": "./dist/spinner.d.cts", + "default": "./lib-commonjs/spinner.cjs" + } + }, + "./split-button": { + "import": { + "types": "./dist/split-button.d.ts", + "default": "./lib/split-button.js" + }, + "require": { + "types": "./dist/split-button.d.cts", + "default": "./lib-commonjs/split-button.cjs" + } + }, + "./styles.css": "./dist/styles.css", + "./swatch-picker": { + "import": { + "types": "./dist/swatch-picker.d.ts", + "default": "./lib/swatch-picker.js" + }, + "require": { + "types": "./dist/swatch-picker.d.cts", + "default": "./lib-commonjs/swatch-picker.cjs" + } + }, + "./switch": { + "import": { + "types": "./dist/switch.d.ts", + "default": "./lib/switch.js" + }, + "require": { + "types": "./dist/switch.d.cts", + "default": "./lib-commonjs/switch.cjs" + } + }, + "./tab-list": { + "import": { + "types": "./dist/tab-list.d.ts", + "default": "./lib/tab-list.js" + }, + "require": { + "types": "./dist/tab-list.d.cts", + "default": "./lib-commonjs/tab-list.cjs" + } + }, + "./tag": { + "import": { + "types": "./dist/tag.d.ts", + "default": "./lib/tag.js" + }, + "require": { + "types": "./dist/tag.d.cts", + "default": "./lib-commonjs/tag.cjs" + } + }, + "./tag-group": { + "import": { + "types": "./dist/tag-group.d.ts", + "default": "./lib/tag-group.js" + }, + "require": { + "types": "./dist/tag-group.d.cts", + "default": "./lib-commonjs/tag-group.cjs" + } + }, + "./tag-picker": { + "import": { + "types": "./dist/tag-picker.d.ts", + "default": "./lib/tag-picker.js" + }, + "require": { + "types": "./dist/tag-picker.d.cts", + "default": "./lib-commonjs/tag-picker.cjs" + } + }, + "./teaching-popover": { + "import": { + "types": "./dist/teaching-popover.d.ts", + "default": "./lib/teaching-popover.js" + }, + "require": { + "types": "./dist/teaching-popover.d.cts", + "default": "./lib-commonjs/teaching-popover.cjs" + } + }, + "./textarea": { + "import": { + "types": "./dist/textarea.d.ts", + "default": "./lib/textarea.js" + }, + "require": { + "types": "./dist/textarea.d.cts", + "default": "./lib-commonjs/textarea.cjs" + } + }, + "./toast": { + "import": { + "types": "./dist/toast.d.ts", + "default": "./lib/toast.js" + }, + "require": { + "types": "./dist/toast.d.cts", + "default": "./lib-commonjs/toast.cjs" + } + }, + "./toggle-button": { + "import": { + "types": "./dist/toggle-button.d.ts", + "default": "./lib/toggle-button.js" + }, + "require": { + "types": "./dist/toggle-button.d.cts", + "default": "./lib-commonjs/toggle-button.cjs" + } + }, + "./toolbar": { + "import": { + "types": "./dist/toolbar.d.ts", + "default": "./lib/toolbar.js" + }, + "require": { + "types": "./dist/toolbar.d.cts", + "default": "./lib-commonjs/toolbar.cjs" + } + }, + "./tooltip": { + "import": { + "types": "./dist/tooltip.d.ts", + "default": "./lib/tooltip.js" + }, + "require": { + "types": "./dist/tooltip.d.cts", + "default": "./lib-commonjs/tooltip.cjs" + } + }, + "./use-css-var-value": { + "import": { + "types": "./dist/use-css-var-value.d.ts", + "default": "./lib/use-css-var-value.js" + }, + "require": { + "types": "./dist/use-css-var-value.d.cts", + "default": "./lib-commonjs/use-css-var-value.cjs" + } + }, + "./variants.css": "./src/variants.css", + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-windmod-preview/library/project.json b/packages/react-components/react-windmod-preview/library/project.json new file mode 100644 index 0000000000000..2e5db468f7913 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/project.json @@ -0,0 +1,22 @@ +{ + "name": "react-windmod-preview", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-windmod-preview/library/src", + "tags": ["platform:web", "vNext", "react-windmod"], + "implicitDependencies": [], + "metadata": { + "exportMap": { + "root": true, + "subpathEntryPoints": ["src/*.ts"], + "staticSubpaths": ["./base.css", "./css/*", "./styles.css", "./variants.css"] + } + }, + "targets": { + "generate-api": { + "options": { + "exportSubpaths": true + } + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/README.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/README.md new file mode 100644 index 0000000000000..c1c4e6d50d371 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/README.md @@ -0,0 +1,163 @@ +# Fluent windmod + +Agent skill for styling with `@fluentui/react-windmod-preview` and +`@fluentui/react-tailwind-theme-preview` — the Fluent UI v9 visual contract rebuilt on Tailwind v4 + +CSS Modules instead of Griffel. + +Covers the override model (cascade layers, not style props), the public class and data-attribute +surface, the variant catalog, theme classes, the `--base-scale` density knob, and the authoring +conventions used inside the library itself. + +## Installation + +The skill ships inside the package. Point your agent at it: + +```markdown + + +When styling Fluent UI components, read +node_modules/@fluentui/react-windmod-preview/skills/fluentui-windmod/SKILL.md first. +``` + +Or link it into your project's skill directory: + +```sh +mkdir -p .claude/skills +ln -s ../../node_modules/@fluentui/react-windmod-preview/skills/fluentui-windmod .claude/skills/fluentui-windmod +``` + +Both routes are opt-in by design. No npm package can install an agent skill into your project +automatically, and one that tried would be writing into your agent's configuration without asking. +Shipping the skill in the tarball buys the thing that matters instead: it is versioned with the code +it documents, so `npm update` moves the guidance with the API rather than leaving a hand-copied guide +to rot. + +> **Preview.** This package is not published yet, and the skill tracks it. + +## Usage + +The skill activates when working with components imported from `@fluentui/react-windmod-preview`, when +editing `.module.css` files in a windmod project, or when migrating an app off +`@fluentui/react-components`. + +**Example prompts that trigger it:** + +``` +Style this Button with the Fluent design tokens +Override the card background for our brand +Why isn't my CSS overriding this component? +Switch the app to the dark theme +Port this page from @fluentui/react-components to windmod +Make the icon strike through when the button is disabled +``` + +## What's Included + +- **SKILL.md** — the rules: prohibitions, decision tests, the override model, the public class surface, + and the reference loading map +- **AGENTS.md** — quick reference: class surface, layer stack, variant vocabulary, token namespaces, + theme classes, subpaths, and a Griffel→windmod conversion table +- **references/** + - `setup.md` — install, the two stylesheets, the provider, Tailwind wiring + - `overriding.md` — the three override routes, what not to target, motion overrides + - `variant-catalog.md` — the full generic and component-specific vocabulary + - `tokens-and-scale.md` — token families, the density knob, what responds at runtime + - `griffel-deltas.md` — the differences that change how you style, test or debug + - `css-var-values.md` — `useCssVarValue` / `invalidateCssVars` + - `authoring-conventions.md` — contributor rules for changing styles inside the package + - `troubleshooting.md` — symptom-keyed fixes +- **assets/** — a worked consumer override, correct patterns with the anti-patterns beside them + +## Key Concepts + +### Cascade layers, not style props + +Every package style lives inside a `fui.*` cascade layer. Your CSS is unlayered, and unlayered CSS beats +layered CSS regardless of selector weight. Overriding a component is a one-class rule — no +`!important`, no specificity arms race. The corollary: **do not wrap your overrides in `@layer`.** + +There is no `useCustomStyleHook_unstable`, no `classNames` prop, and no `overrides_unstable`. + +### A two-class public surface + +Each component root carries `fui-button` (the identity class — safe for CSS and `querySelector`) and +`group/fui-button` (Tailwind's named-group class). Everything below the root is a hashed ident and a +`data-*` attribute. `buttonClassNames.root` is the **pair**, for `className`, never for a selector. + +### Group variants reach internals + +```tsx + +``` + +No group declaration of your own is needed — the marker is already on the root. + +### Theming is a class + +`theme` on `FluentProvider` takes a class name string, not a Griffel theme object. A custom theme is a +CSS class that redeclares the token custom properties. + +### One density knob + +`--base-scale: calc(1rem / 16px * var(--fui-scale, 1))` drives spacing, control heights, radii and the +whole type ramp. At a 16px root font size windmod is pixel-identical to Griffel — that is the parity +condition. `ScaleRegion` (from `/provider`) multiplies the factor for one subtree; steps are absolute +and nested regions replace, never compound. + +## Examples + +**Brand override** + +```css +/* app.css — unlayered, so it wins */ +.cta-button { + background-color: var(--color-brand-background); + color: var(--color-neutral-foreground-inverted); +} + +.cta-button:hover { + background-color: var(--color-brand-background-hover); +} +``` + +```tsx +import { Button } from '@fluentui/react-windmod-preview/button'; + +export const Cta = () => ; +``` + +**Custom theme** + +```css +.my-brand-theme { + --color-brand-background: #6b21a8; + --color-brand-background-hover: #7e22ce; + --color-brand-background-pressed: #581c87; +} +``` + +```tsx + +``` + +## Requirements + +- `@fluentui/react-windmod-preview` and `@fluentui/react-tailwind-theme-preview` installed +- Both stylesheets loaded, theme first +- Tailwind v4 only if you want to author your own utilities against windmod's variants — plain-CSS + consumers need no toolchain + +## Learn More + +- [SKILL.md](SKILL.md) — the full rule set +- [AGENTS.md](AGENTS.md) — quick reference +- [references/](references/) — detailed catalogs and guides +- The package's own `MIGRATION.md` — all fifty-nine deliberate differences from + `@fluentui/react-components` + +## Preview + +This package tracks `@fluentui/react-headless-components-preview`, which is itself in preview. APIs may +change without notice and coverage is limited to what the headless package ships. Not production-ready. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/SKILL.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/SKILL.md new file mode 100644 index 0000000000000..653a4b71226c6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/SKILL.md @@ -0,0 +1,551 @@ +--- +name: fluentui-windmod +description: Use when styling, theming or overriding Fluent UI components from @fluentui/react-windmod-preview or @fluentui/react-tailwind-theme-preview, when authoring or verifying component styles inside those packages, or when users say "style this Fluent component", "override the button styles", "theme this app", "switch to dark theme", "restyle windmod", "fui-button", "group variant", "cascade layer", or when working with .module.css files in a windmod project or migrating an app off @fluentui/react-components and Griffel. Provides the override model (cascade layers, not props), the public class and data-attribute surface, the variant catalog, theme class names, the base-scale density knob, and the authoring and verification conventions used inside the library itself. +license: MIT +metadata: + author: fluentui + version: '0.1.0' + library: '@fluentui/react-windmod-preview' + library_version: '0.1.0' +--- + +# Fluent windmod + +Expert knowledge for styling with `@fluentui/react-windmod-preview` — the Fluent v9 visual contract +rebuilt on **Tailwind v4 + CSS Modules** instead of Griffel. Styles are compiled at build time. There +is no runtime style injection, no `makeStyles`, and no `@griffel/react` in the dependency graph. + +Two audiences, one skill: + +- **Consumer** — using, extending and overriding windmod styles in an application. Start at + [The override model](#the-override-model). +- **Contributor** — authoring component styles inside the library. Load + [references/authoring-conventions.md](references/authoring-conventions.md); nothing else here + licenses a change to a `*.module.css` in the package. + +## NEVER Do When Styling with windmod + +- **NEVER wrap your overrides in `@layer`** — every package style lives inside a `fui.*` cascade + layer, and unlayered CSS beats layered CSS regardless of selector weight. Your plain + `.my-button { background-color: rebeccapurple; }` already wins. Putting it in a layer forfeits the + contest you win by default. +- **NEVER use `!important` to beat a component style** — there is nothing to beat. If a rule is not + applying, the cause is a layer mistake (see above) or the wrong selector, not specificity. Reach + for `!important` and you will also outrank the reduced-motion floor, which you do not want. +- **NEVER use `classNames.root` in a selector** — `buttonClassNames.root` is the space-separated + **pair** `"fui-button group/fui-button"`. It goes in `className`. `document.querySelector('.' + buttonClassNames.root)` + is an invalid selector. Query the identity class instead: `.fui-button`. +- **NEVER use PascalCase Fluent class names** — Griffel's `.fui-Button` is `.fui-button` here, and + there are no per-slot public classes at all. Griffel's `fui-Button__icon` has no equivalent. +- **NEVER target a component's internals by class** — every slot below the root carries a hashed + ident of the shape `fuicm---` that changes when the source changes. Reach + internals through the root's group variants and the published `data-*` attributes instead. +- **NEVER declare a `group` of your own just to use a group variant** — the marker `group/fui-` + is already on the root. `group-disabled/fui-button:line-through` works with no configuration. Adding + your own `group/name` is allowed, but only to disambiguate nested instances of the same component. +- **NEVER pass a theme object to `FluentProvider`** — `theme` takes a **class name string** + (`webDarkThemeClassName`), not Griffel's JS theme object. The custom properties are already in the + stylesheet. +- **NEVER reach for `useCustomStyleHook_unstable`, `overrides_unstable` or `customStyleHooks_unstable`** — + none of them exist in windmod, on any component. Restyle through CSS; cascade layers make that the + easier path anyway. +- **NEVER override a Fluent _spacing_ token at runtime and expect layout to move** — Tailwind's + `--spacing-*` namespace resolves at compile time, so `var(--spacingHorizontalM)` in Griffel is + `calc(var(--spacing) * 12)` here. Colour, typography and stroke tokens are still live `var()` + references and do respond. Use `--base-scale` for density. +- **NEVER set `--base-scale` on a provider or a subtree** — it and every scale derived from it are + declared at the document root, so descendants inherit the already-substituted stream and a + mid-tree override is inert. Scaling one subtree is `ScaleRegion`'s job (`/provider` export): + `` — absolute factors, nested regions replace rather than compound. +- **NEVER hardcode a palette value** — use the kebab-case Fluent theme tokens + (`var(--color-neutral-foreground-1)`, `bg-neutral-background-1`). The theme drops Tailwind's default + palette entirely, so a stray `text-red-500` fails the build rather than silently diverging. +- **NEVER author a `@custom-variant` in a component module** — the catalog is exactly two files. See + [The variant catalog](#the-variant-catalog). +- **NEVER assume `Tooltip` or `Popover` position correctly on Firefox or Safari** — the headless + positioning layer uses native CSS anchor positioning with no `@supports` guard, no feature + detection and no polyfill. On an engine without it the surface renders unpositioned at the viewport + origin. +- **NEVER assume a windmod component is a drop-in for its Griffel twin without checking the delta + list** — there are fifty-nine deliberate differences. The ones that bite while styling are in + [references/griffel-deltas.md](references/griffel-deltas.md). + +## Before You Style, Ask + +### Placement + +- **Does this belong in my own CSS, or in a slot's `className`?** Both work and both win — slot + `className` merges last on every slot, and your own stylesheet is unlayered. Use `className` for a + one-instance tweak; use a stylesheet class for anything reused. +- **Am I about to write a descendant selector into a component?** Prefer a group variant on the child + you actually control. Descendant selectors into hashed idents will break. + +### Targeting + +- **Is there a `data-*` attribute for this state?** Component roots publish their look props and state + as data attributes (`data-appearance`, `data-size`, `data-open`, `data-placement`, `data-intent`, …). + Select on those, not on inferred structure. +- **Is the state on the root or on a slot?** Group variants read the **root**. If the state lives on a + native input (checked, for example) the input carries `peer/fui-` and the sibling form + `peer-checked/fui-` applies. +- **Do I need a variant that does not exist?** Check both catalogs before inventing a selector — + ninety-five generic entries plus the component-specific file. + +### Theming + +- **Am I writing a light/dark branch by hand?** Do not. Apply a theme class and let the tokens move. +- **Am I building a custom theme?** Write a CSS class that redeclares the token custom properties and + pass its name: ``. `theme` accepts any string. + +### Density and scale + +- **Does my app set a non-16px root font size?** Then windmod controls will be larger or smaller than + the Griffel ones they replace — the whole UI scales, not just the type. Keep the root at 16px to + stay pixel-aligned with Griffel, or accept the coherent rescale deliberately. + +### Setup + +- **Is `@fluentui/react-tailwind-theme-preview/styles.css` imported, once, before my own CSS?** + Nothing renders correctly without it, and its position fixes how any layers of _yours_ sort against + `fui.*`. It does not affect an unlayered rule, which wins either way. +- **Is `@fluentui/react-windmod-preview/base.css` loaded once, ahead of everything else?** Component + CSS ships per component and every chunk assumes this root sheet (the layer order and the + `@property` registrations). Directly, or `@import`ed at the top of the app's own root stylesheet — + both work. +- **Is this a CommonJS or SSR build?** Then import `@fluentui/react-windmod-preview/styles.css` + instead — it bundles the root sheet and every component into one file. Only ESM gets the + per-component chunks as a side effect. + +## Setup Requirements + +**CRITICAL: two root stylesheets and one provider. Missing any of them produces symptoms that read +as styling bugs.** + +Component CSS itself needs no setup: each component's class map side-effect-imports its own chunk, +so the bundler ships exactly what the app uses. + +```sh +npm install @fluentui/react-windmod-preview @fluentui/react-tailwind-theme-preview +``` + +```js +// Once per document, BEFORE your own CSS. +// Palette, type ramp, spacing scale, the seven theme classes, and the fui.* layer order. +import '@fluentui/react-tailwind-theme-preview/styles.css'; + +// windmod's root sheet (~3 KB): the fui.* layer order and the global @property registrations that +// every component chunk assumes. The components arrive automatically, per component. +import '@fluentui/react-windmod-preview/base.css'; +``` + +If the app has its own root stylesheet, `@import` both at the TOP of it instead — that sheet loads +first, so ours transitively precedes everything: + +```css +@import '@fluentui/react-tailwind-theme-preview/styles.css'; +@import '@fluentui/react-windmod-preview/base.css'; +``` + +For CommonJS/SSR, or any time one file is simpler, swap `base.css` for +`@fluentui/react-windmod-preview/styles.css` — the aggregate carrying the root sheet plus all 131 +components. + +```tsx +import { Button } from '@fluentui/react-windmod-preview/button'; +import { FluentProvider, webDarkThemeClassName } from '@fluentui/react-windmod-preview/provider'; +import { Tooltip } from '@fluentui/react-windmod-preview/tooltip'; + +export const App = () => ( + + + + + +); +``` + +**Why the order matters.** The theme stylesheet declares the cascade-layer family, and layer order is +first-appearance. It is load-bearing for exactly two things: nothing resolves without the theme sheet, +because the components' `var()` references have nothing to read; and if _you_ declare layers of your +own, whichever sheet appeared first fixes their order against `fui.*`. It does **not** decide whether a +plain unlayered rule of yours wins. Unlayered author CSS outranks every layer in the author origin no +matter which sheet loaded first, so an override that is genuinely unlayered cannot be broken by import +order. + +**`FluentProvider` is a real element and it paints.** It renders a `div` carrying base typography, text +colour and `background-color: var(--color-neutral-background-1)`. If you wrap children in a provider +inside your own flex or grid container, the provider becomes the flex/grid item, not the children. If +you drop one onto a coloured surface it repaints that surface — set `background: transparent` via +`className` if that is not what you want. + +**If you use Tailwind yourself**, declare the `fui` layers before importing Tailwind so your own +utilities beat Fluent component styles: + +```css +@layer fui.theme, fui.base, fui.components, fui.utilities; +@import 'tailwindcss'; +``` + +Only `styles.css` and `theme-class-names` are consumable without a Tailwind toolchain. Everything +about authoring your own Tailwind against windmod's variants is in +[references/setup.md](references/setup.md). + +## How to Use + +This skill uses **progressive disclosure**. Work from the patterns below; load a reference only on its +trigger. + +**Installing, configuring, or wiring your own Tailwind build against windmod:** + +- **MANDATORY**: Load [references/setup.md](references/setup.md) when the user says "set up windmod", + "install", "configure Tailwind", "import the theme", or hits a build error naming `#theme`, + `@custom-variant`, or an unresolved token. +- **Do NOT Load**: griffel-deltas.md, css-var-values.md, authoring-conventions.md. + +**Overriding, extending or restyling a component in an app:** + +- **MANDATORY**: Load [references/overriding.md](references/overriding.md) when restyling a component, + targeting a slot, or when an override "is not applying". [assets/example-override.css](assets/example-override.css) + and [assets/example-override.tsx](assets/example-override.tsx) are the worked version of it, with the + anti-patterns written beside the correct ones. +- **Do NOT Load**: authoring-conventions.md (that is for changes inside the package), setup.md unless + the stylesheets are in question. + +**Choosing a variant, or selecting on component state:** + +- **MANDATORY**: Load [references/variant-catalog.md](references/variant-catalog.md) when composing + `group-/fui-` classes, when unsure whether a variant exists, or when adding one. +- **Do NOT Load**: setup.md unless the catalog is not resolving at all. + +**Choosing a token, or reasoning about sizing and density:** + +- **MANDATORY**: Load [references/tokens-and-scale.md](references/tokens-and-scale.md) when picking a + colour/typography/radius token, when layout is the wrong size, or when `--base-scale` comes up. +- **Do NOT Load**: variant-catalog.md unless also selecting on state. + +**Theming — switching themes, theming part of a page, or building a custom one:** + +- **MANDATORY**: Load [references/tokens-and-scale.md](references/tokens-and-scale.md), whose "Custom + themes" section is the whole story: what a theme class is, why a partial one is fine, and how nesting + a provider scopes a theme to a subtree. [Theming](#theming) above has the short version. +- **Do NOT Load**: overriding.md — a theme moves tokens, which is a different move from overriding a + rule, and its theming paragraph only repeats what you already have. + +**Porting an app off `@fluentui/react-components`, or explaining a behaviour difference:** + +- **MANDATORY**: Load [references/griffel-deltas.md](references/griffel-deltas.md) when the user is + migrating, when a snapshot or computed-style test broke, or when a component behaves differently + from the Griffel one. +- **Do NOT Load**: authoring-conventions.md. + +**Reading a resolved token value in JavaScript:** + +- **MANDATORY**: Load [references/css-var-values.md](references/css-var-values.md) when the task needs + a token's computed value at runtime (canvas, measurement, a third-party widget to theme). +- **Do NOT Load**: anything else. + +**Changing styles INSIDE the package (`packages/react-components/react-windmod-preview`), or +verifying one — VR scenes, mutation tables, probes, pixel allowances:** + +- **MANDATORY**: Load [references/authoring-conventions.md](references/authoring-conventions.md) + completely. The authoring rules are strict and violations are rejected, and the same file carries + the verification protocol: what the pixel gate actually asserts, what a pixel allowance requires, + and the probe and mutation-table rules that keep a "measured" claim honest. Before calling the + change done, check the diff against [references/failure-modes.md](references/failure-modes.md) — + the defect classes prior reviews caught, each with its detection grep. +- **Do NOT Load**: overriding.md — the consumer override model does not apply inside the library. + +**Diagnosing a style that will not apply:** + +- **MANDATORY**: Load [references/troubleshooting.md](references/troubleshooting.md). + +## The override model + +**Cascade layers, not props.** Every package style lives inside a `fui.*` layer. Your CSS is +unlayered, and unlayered author CSS outranks every layered declaration regardless of selector weight. +Overriding a component is a one-class rule with no `!important` and no specificity arms race. + +```css +/* wins over the component's own layered rules */ +.my-brand-button { + background-color: rebeccapurple; +} +``` + +```tsx + +``` + +The declared layer order, which the theme package owns: + +```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; +``` + +- `fui.base` — opens with Tailwind's preflight, then the theme's element resets (icon defaults); every component rule outranks both +- `fui.components.l1` — every component's own base styles (Button, Label, Input; also a + composition's rules for its own plain elements) +- `fui.components.l2` — rules overriding another component's `l1` styles (ToggleButton over Button) +- `fui.components.l3+` — rules overriding `l2` rules (deeper compositions) +- `fui.utilities` — Tailwind's utility layer; beats component styles, still loses to your unlayered CSS + +**Slot `className` merges last.** Every slot re-stacks its class list with the component's own classes +first and your incoming `className` last, so your declaration wins at equal specificity: + +```tsx + +``` + +Full worked patterns — per-slot overrides, confining a selector to a subtree, when to use a layer +deliberately — in [references/overriding.md](references/overriding.md). Scoping a _theme_ to a subtree +is a different mechanism and lives under [Theming](#theming). + +**When the task turns on what a component already does** — "drop the hover elevation", "match the +current focus ring" — read that component's own `src/components//.module.css`. It is shipped +source, it is short, and it is the only authority on the current state; the references here teach the +override mechanism, not per-component behaviour. Confirming that the thing you are removing exists beats +writing a defensive rule that quietly does nothing. + +## The public class surface + +Each component root carries **two** classes and nothing else that is public: + +```tsx +import { buttonClassNames } from '@fluentui/react-windmod-preview/button'; + +buttonClassNames.root; // "fui-button group/fui-button" +``` + +- `fui-button` — the identity class. Safe for your CSS and for `querySelector`, no escaping needed. +- `group/fui-button` — Tailwind's real named-group class, unchanged. + +```tsx +document.querySelectorAll('.fui-button'); // ✅ +document.querySelectorAll('.' + buttonClassNames.root); // ❌ invalid selector — it is a pair +``` + +The class-name records expose **one public key**, almost always `root`. The single exception is +`avatarGroupPopoverClassNames`, whose key is `triggerButton` — that component renders no root element of +its own. Internals use hashed idents and `data-*` state attributes. + +## Targeting internals with group variants + +A child inside a component targets the component's state directly, with no group declaration of your +own: + +```tsx + +``` + +The general form is `group-/fui-`, where `` is any entry in the variant +catalog. Because every catalog entry is authored `&:where([data-…])`, Tailwind rewrites each one into +its group form automatically — every variant composes. + +Component roots publish their look props and state as data attributes. Button, for example, stamps +`data-appearance`, `data-size` and `data-empty` from the styles hook, on top of the +`data-disabled` / `data-disabled-focusable` / `data-icon-only` / `data-icon-position` the headless hook +already stamps. + +Presence-style attributes are **presence-based**: select `[data-open]`, never `[data-open='true']`. +Enumerated ones select on the value: `[data-size='small']`. + +For a native input inside a composite control, the input carries `peer/fui-` and the sibling +form applies: `peer-checked/fui-switch:…`. + +## The variant catalog + +The catalog is exactly **two files**, both shipped, both importable: + +| File | Vocabulary | +| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `@fluentui/react-tailwind-theme-preview/css/variants.css` | generic — state, structure, positioning/orientation, size. Meaningful without knowing any component's API. 95 entries. | +| `@fluentui/react-windmod-preview/variants.css` | component-specific — component API values (`appearance-*`, `intent-*`, `text-position-*`, glyph variants), sectioned per component. | + +Every entry is the canonical flat form: + +```css +@custom-variant disabled (&:where([disabled], [data-disabled], :disabled)); +@custom-variant appearance-primary (&:where([data-appearance='primary'])); +@custom-variant intent-error (&:where([data-intent='error'])); +``` + +Two rules that follow from that shape: + +- **No union entries.** Comma-separate at the call site — `@variant hover, focus { … }` compiles to one + nested rule per variant. +- **No numeric `size-*` variants** — they collide with Tailwind's own utilities. + +To compose your own Tailwind against windmod's states, `@import` the two catalogs into your Tailwind +entry stylesheet. See [references/variant-catalog.md](references/variant-catalog.md) for the full +vocabulary and the import recipe. + +## Theming + +Seven themes ship as class names, plus a record and a type: + +```tsx +import { themeClassNames, type ThemeClassName } from '@fluentui/react-windmod-preview/provider'; + +export const pickTheme = (dark: boolean): ThemeClassName => + dark ? themeClassNames.webDarkTheme : themeClassNames.webLightTheme; +``` + +`webLightThemeClassName`, `webDarkThemeClassName`, `teamsLightThemeClassName`, +`teamsDarkThemeClassName`, `teamsHighContrastThemeClassName`, `teamsLightV21ThemeClassName`, +`teamsDarkV21ThemeClassName`. + +**A custom theme is a CSS class that redeclares the token custom properties.** `theme` accepts any +string, so your own class name works — in plain global CSS, since a CSS-Modules class would arrive +hashed. Redeclare only the tokens you are changing; the rest inherit. Do not port a Griffel theme object +— there is nothing to pass it to. Details in +[references/tokens-and-scale.md](references/tokens-and-scale.md). + +**To theme part of a page, nest a second provider.** Tokens are inherited custom properties, so the +inner provider redeclares them for its subtree and the rest of the app keeps the outer theme. + +```tsx + +
{content}
+ + + +
+``` + +Remember the provider is a real element: the inner one becomes the flex or grid item in your layout, not +the rail's children. + +**Anchored surfaces inherit from their DOM position.** `Tooltip`, `Popover` and the `AvatarGroup` +overflow surface are rendered inline and promoted to the native top layer, so they inherit theme +variables — and any other inherited property — from where they are written, not from a portal root. +A trigger outside every provider renders its surface with the document root's theme. Wrap the trigger +in a `FluentProvider` to control it. + +## Imports and subpaths + +**There is no root barrel** — `@fluentui/react-windmod-preview` exports nothing. Every component comes +from its **family** subpath, and the families are the ones +`@fluentui/react-headless-components-preview` already uses, so the two layers have the same shape. A +family is one kebab-case subpath exporting every part of that family: + +```tsx +import { Button } from '@fluentui/react-windmod-preview/button'; +import { Card, CardHeader, CardPreview } from '@fluentui/react-windmod-preview/card'; +import { Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-windmod-preview/menu'; +``` + +Rules of thumb when writing an import: + +- Look up the family, not the component. `CardHeader` → `./card`. `MenuItem` → `./menu`. `Tab` → + `./tab-list`. `Radio` → `./radio-group`. `Option` and `Listbox` → `./combobox`. `InfoButton` → + `./info-label`. `ColorArea`, `ColorSlider` and `AlphaSlider` → `./color-picker`. `ColorSwatch`, + `EmptySwatch` and `ImageSwatch` → `./swatch-picker`. `InlineDrawer` and `OverlayDrawer` → `./drawer`. + `NavDrawer` and its parts → `./nav`. `Toaster` → `./toast`. +- `FluentProvider` is at **`./provider`** (headless's name for the family), together with the seven + theme class-name constants, the `themeClassNames` record and the `ThemeClassName` type. +- One family, one import line, however many parts you use. + +Family JS still tree-shakes — unused siblings' code is dropped. Family **CSS** comes along, because each +class map side-effect-imports its own chunk and the barrel keeps the family's chunks reachable. That is +the deliberate trade: the styling cost is bounded by the family you reached for, not by the suite. + +Subpaths exporting no component: `./positioning` (the headless positioning primitives, re-exported) and +`./use-css-var-value`. Non-JavaScript subpaths: `./base.css`, `./styles.css`, `./css/*`, `./variants.css`. + +## Reading token values in JavaScript + +When a token's resolved value is needed at runtime — canvas, measurement, theming a third-party widget: + +```tsx +import { useCssVarValue } from '@fluentui/react-windmod-preview/use-css-var-value'; + +const ref = React.useRef(null); +const fg = useCssVarValue('--color-neutral-foreground-1', ref, { fallback: '#242424' }); +const { bg, radius } = useCssVarValue({ bg: '--color-neutral-background-1', radius: '--border-radius-medium' }, ref); +``` + +The read happens at the element's DOM position, so cascade, inheritance and theme scoping all apply. +Values are cached and re-read on invalidation. `invalidateCssVars()` from the same subpath is the +escape hatch for changes the observers cannot see. Details in +[references/css-var-values.md](references/css-var-values.md). + +## Common Issues + +### Issue: my override is not applying + +**Cause 1:** the override is inside an `@layer`. Layered CSS loses to unlayered CSS, and it loses to +`fui.utilities` too if the layer was declared earlier. + +**Fix:** take it out of the layer. + +**Cause 2:** the rule is layered without your having written `@layer` around it. `@layer components { … }` +is ordinary Tailwind idiom, and some frameworks and bundler CSS pipelines wrap imported global +stylesheets in a layer of their own. This is Cause 1 with nothing to see in your file, which is why it +outlives the first fix. + +**Fix:** look before theorising. DevTools' Styles pane labels each rule with the layer it landed in and +strikes through whatever lost, so one glance tells you which cause you actually have. Then unlayer the +rule, or move your layer after `fui.utilities` in the declaration. + +**Cause 3:** the selector targets a hashed ident or a Griffel-era class name (`.fui-Button`, +`.fui-Button__icon`). + +**Fix:** target `.fui-button` and reach internals through group variants and `data-*`. + +### Issue: `document.querySelector` throws on a class-name constant + +**Cause:** `xClassNames.root` is a space-separated pair, not a single class. + +**Fix:** query `.fui-` directly. Use the constant only in `className`. + +### The rest, in one line each + +Symptom, cause, and where the worked answer is. Every one of these is written out in +[references/troubleshooting.md](references/troubleshooting.md), which also covers the symptoms that only +show up in tests. + +| Symptom | Cause | Fix | +| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| a `group-…/fui-x` class does nothing | the name is in neither catalog, or your Tailwind build was never given the catalogs | `@import` both catalogs — [setup.md](references/setup.md) | +| everything is the wrong size | a non-16px root font size; `--base-scale` rides `calc(1rem / 16px)` and the whole UI follows it | keep `html { font-size: 16px }`, or accept the rescale — never patch it on a provider (scaling one subtree is `ScaleRegion`'s job) | +| overriding a spacing token changes nothing | Tailwind's `--spacing-*` resolves at compile time, so there is no live `var()` to move | use `--base-scale` for density, or set the property directly | +| `Tooltip`/`Popover` renders in the page corner | no CSS anchor positioning in that engine, and no fallback anywhere in the positioning layer | polyfill it, or keep those two on `@fluentui/react-components` | +| an animation stopped working | the theme's global `prefers-reduced-motion` floor — unlayered, 1ms, `animation-iteration-count: 1` | it is selector-less, so any rule of yours with a class already outranks it | +| a Tailwind class fails the build | the theme sets Tailwind's own palette, ramp, radii and shadows to `initial`, deliberately | use a Fluent token — [tokens-and-scale.md](references/tokens-and-scale.md) | +| a snapshot broke after migrating | computed `box-shadow` strings, `aria-modal`, lower-case class names | [griffel-deltas.md](references/griffel-deltas.md) | + +## Important Notes + +- **Tailwind's default theme is removed.** The theme sets `--color-*`, `--font-*`, `--text-*`, + `--radius-*`, `--shadow-*` and more to `initial`. A stray `text-red-500` or `rounded-lg` fails the + build rather than diverging from Fluent. Structural utilities (flex, grid, positioning) remain. +- **Preflight ships, at the head of `fui.base`.** The theme includes Tailwind's preflight as the + first content of `fui.base` — the same placement Tailwind itself gives it — below every component + layer, so every component rule (and yours, layered above or unlayered) outranks it. It is document-global: + markup of your own that relied on UA default styling (heading sizes, list markers, button font, + default margins) should be re-checked. Utilities still reach you only inlined through the + components' own `@apply`, unless you run Tailwind yourself. +- **The DOM and the props are Griffel's.** Element structure, slot names, roles and ARIA match, because + both libraries render through the same headless renderers. Props are the headless props plus the same + look props with the same defaults. +- **A local prop beats a context value.** Where Griffel lets a container's context win even over an + explicit prop, windmod gives the local prop priority and falls back to context. Callbacks compose + (context first, then local), `className` concatenates with yours last, `style` spreads the same way. +- **Computed style strings differ without any visual difference.** Tailwind's shadow and ring utilities + prepend fully transparent layers, so `getComputedStyle(el).boxShadow` returns a longer string than + Griffel's. Snapshot tests asserting on computed `box-shadow` need updating; nothing about the + rendering does. +- **Windmod containers for windmod children.** A windmod component reads the _windmod_ context, and + only a windmod container publishes it. A windmod `Tag` inside a _headless_ `TagGroup` falls back to + its own defaults. Griffel containers around windmod children work fine. +- **The parity contract is pixel-exact at 16px**, verified per component against the Griffel twin with + pixelmatch at threshold 0 — zero non-antialiasing differences, with the matcher's antialiasing + classifier absorbing sub-perceptual rasterization noise, and computed-style parity passes catching + what pixel counting cannot. The deliberate differences are enumerated, not incidental — see + [references/griffel-deltas.md](references/griffel-deltas.md). +- **This is a preview package.** It tracks `@fluentui/react-headless-components-preview`, which is + itself in preview. APIs may change without notice; coverage is limited to what the headless package + ships. Not production-ready. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/assets/example-override.css b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/assets/example-override.css new file mode 100644 index 0000000000000..1e47b1e8f9ddb --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/assets/example-override.css @@ -0,0 +1,156 @@ +/** + * example-override.css + * + * A consumer stylesheet overriding windmod components. Demonstrates the correct patterns: + * - unlayered rules, so they beat every fui.* layer with no !important + * - targeting the public identity class and published data-* attributes + * - moving a token rather than overriding a rule, where a token exists + * - motion that survives the reduced-motion floor by intent + * + * Loaded AFTER '@fluentui/react-tailwind-theme-preview/styles.css'. + */ + +/* --------------------------------------------------------------------------- + * 1. A custom theme is a class that redeclares token custom properties. + * Applied via , or to any element. + * ------------------------------------------------------------------------- */ + +.my-brand-theme { + --color-brand-background: #6b21a8; + --color-brand-background-hover: #7e22ce; + --color-brand-background-pressed: #581c87; + --color-compound-brand-background: #6b21a8; + --color-compound-brand-background-hover: #7e22ce; +} + +/* --------------------------------------------------------------------------- + * 2. Override by class. Unlayered, single class, no !important. + * ------------------------------------------------------------------------- */ + +.cta-button { + background-color: var(--color-brand-background); + color: var(--color-neutral-foreground-inverted); + border-radius: var(--radius-circular); +} + +.cta-button:hover { + background-color: var(--color-brand-background-hover); +} + +/* --------------------------------------------------------------------------- + * 3. Select on the identity class and published state attributes. + * Presence attributes select bare; enumerated ones select on their value. + * ------------------------------------------------------------------------- */ + +.fui-message-bar[data-intent='error'] { + border-inline-start-width: 4px; + border-inline-start-style: solid; + border-inline-start-color: var(--color-status-danger-border-1); +} + +.fui-button[data-size='small'] { + letter-spacing: 0.02em; +} + +.fui-popover-surface[data-open] { + box-shadow: var(--shadow-28); +} + +/* --------------------------------------------------------------------------- + * 4. Scope to a subtree with a wrapper class. + * ------------------------------------------------------------------------- */ + +.marketing-surface .fui-button { + border-radius: var(--radius-circular); +} + +/* --------------------------------------------------------------------------- + * 5. Essential motion. The reduced-motion floor is unlayered but SELECTOR-LESS, + * so a rule with a class in it already outranks it. + * ------------------------------------------------------------------------- */ + +.loading-pulse { + animation-duration: 1.2s; + animation-iteration-count: infinite; +} + +/* --------------------------------------------------------------------------- + * 6. App-global defaults that should still lose to your own page-level CSS. + * l4 and l5 are unclaimed by the library; l1-l3 are in use. + * ------------------------------------------------------------------------- */ + +@layer fui.components.l4 { + .fui-card { + border-radius: var(--radius-large); + } +} + +/** + * ANTI-PATTERNS TO AVOID + * + * ❌ Wrong — wrapping an override in a layer forfeits the contest you win by default: + * @layer app { + * .cta-button { background-color: var(--color-brand-background); } + * } + * + * ✅ Correct — unlayered: + * .cta-button { background-color: var(--color-brand-background); } + * + * + * ❌ Wrong — !important. There is nothing to beat, and it outranks the reduced-motion floor: + * .cta-button { background-color: rebeccapurple !important; } + * + * ✅ Correct — a plain declaration already wins: + * .cta-button { background-color: rebeccapurple; } + * + * + * ❌ Wrong — hashed idents are regenerated whenever the source changes: + * .fuicm-button-icon-a1b2c3 { color: red; } + * + * ✅ Correct — the slot's own className prop, or a group variant from a child: + * + +
+ ); +} + +function ShippedThemeExamples() { + return ( +
+ {/* Route 1 — your own class. Unlayered, so it wins outright. */} + + + {/* Route 2 — slot className. Your class lands last on every slot. */} + + + {/* Route 3 — group variant from a child you control. The marker is already on the + root, so no `group/name` declaration of your own is required. */} + + + {/* Group variants compose with any catalog entry, including the component-specific ones. */} + + Upload failed + + + {/* peer variants, for state that lives on a native input rather than the root. */} + Notifications} /> + + {/* Slot `style` works the same way — this is the documented fix for an unbounded listbox. */} + {/* */} + + + + +
+ ); +} + +function NestedCards() { + return ( + // Adding your own `group/name` is optional, and only for disambiguating nested instances + // of the SAME component. + + + dims with the outer card only + + + ); +} + +function TokenReader() { + const ref = React.useRef(null); + + // ✅ colour tokens read back as literals. Always pass a fallback — the hook returns it on the + // server and until the layout effect runs. + const fg = useCssVarValue('--color-neutral-foreground-1', ref, { fallback: '#242424' }); + + // Record form: the return mirrors the input's keys. It is a fresh object every render, so + // never use it as an effect dependency — read the slots. + const { bg, radius } = useCssVarValue({ bg: '--color-neutral-background-1', radius: '--radius-medium' }, ref, { + fallback: 'transparent', + }); + + // ⚠️ Spacing, text, stroke and --base-scale read back as UNEVALUATED calc() strings, because + // the theme leaves its knobs unregistered. For a resolved length, read a real property. + // useCssVarValue('--text-base-300', ref) → 'calc(14px * calc(1rem / 16px * 1))' + + return ( +
+ +
+ ); +} + +function ClassNameSurface() { + React.useEffect(() => { + // ✅ the identity class is a valid selector, no escaping needed + document.querySelectorAll('.fui-button'); + + // ❌ buttonClassNames.root is the PAIR "fui-button group/fui-button" — invalid as a selector + // document.querySelectorAll('.' + buttonClassNames.root); + }, []); + + // ✅ the constant belongs in className, where the pair is exactly what you want + return
; +} + +/** + * NOT AVAILABLE IN WINDMOD — Griffel escape hatches with no counterpart: + * + * + +); diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/authoring-conventions.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/authoring-conventions.md new file mode 100644 index 0000000000000..42ab57a75623d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/authoring-conventions.md @@ -0,0 +1,658 @@ +# Authoring conventions (contributors) + +**Load this only when changing styles INSIDE `packages/react-components/react-windmod-preview`.** The +consumer override model does not apply here — inside the library everything is layered, and these rules +are what keep the cascade predictable. + +`Button` and `Tooltip` are the reference implementations. Read them first and copy their patterns +exactly. + +**Where things are.** This file is long; jump to what you need. + +| Section | Read it when | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| [Composition](#composition) | wiring a component's hook, look props and context | +| [Re-slotting one component into another](#re-slotting-one-component-into-another) | one component becomes another's slot `elementType` | +| [The styles hook](#the-styles-hook) | writing or changing a `useStyles.ts` | +| [Markers](#markers) · [Data attributes](#data-attributes) | class-name records, group/peer markers, `data-*` stamps | +| [CSS authoring](#css-authoring) | touching any `*.module.css` — layers, blocks, tokens | +| [Code style](#code-style) · [Comments policy](#comments-policy) | before review | +| [Definition of done](#definition-of-done) | deciding whether the change is finished | +| [Verification](#verification) | the pixel gate, allowances, mutation tables, probes | +| [Scope](#scope) | deciding whether the feature is windmod's to add at all | +| [failure-modes.md](failure-modes.md) | before calling any style change done — the recurring defect classes and their detection greps | + +## Composition + +A styled component is: headless hook + local look-props + styles hook + headless render. + +```tsx +export const Button: ForwardRefComponent = React.forwardRef((props, ref) => { + const { + appearance = 'secondary', + shape = 'rounded', + size = 'medium', + ...rest + } = mergeContextProps(useButtonContext(), props); + + return renderButton( + useButtonStyles({ + ...useButton(rest, ref), + appearance, + shape, + size, + }), + ); +}) as ForwardRefComponent; +``` + +Look props (`appearance`/`shape`/`size`/…) live in the windmod layer with defaults matching Griffel's. +Everything else passes through to the headless hook untouched. Context is merged **before** +destructuring, so a container supplies the default while an explicit prop still wins. + +## Re-slotting one component into another + +A windmod component may be another's slot `elementType` wherever Griffel does the same — `Field` +swapping the windmod `Label` into `components.label`, `NavDrawer` re-binding its root onto `Drawer`. +The swap has **two channels, and both must move together.** + +```tsx +// ✅ both channels +return { + ...headless, + components: { ...headless.components, root: Drawer as NavDrawerState['components']['root'] }, + root: slot.always(headless.root, { elementType: Drawer }), +}; + +// ❌ elementType only — reverts in dev and jest, passes in production +return { ...headless, root: slot.always(headless.root, { elementType: Drawer }) }; +``` + +**Why.** `assertSlots` is not a dev-only warning. It iterates `state.components` and **writes the +declared element type back** onto any mismatched slot before it warns +(`react-utilities/src/compose/assertSlots.ts:64`). A +swap that moves `elementType` without moving the matching `components` entry is therefore silently +reverted under `NODE_ENV !== 'production'` — including in every jest test — while shipping correctly +to consumers. The bug is invisible exactly where you would look for it. + +Earlier swaps survived without the second channel only because their base hooks declare +`components: { root: 'div' }` alone; `assertSlots` short-circuits on `undefined` slots. Do not read +those as precedent. + +**Read `TagPickerList.tsx:20-28` before implementing** — it is the exact root-slot precedent, moving +both channels with a comment stating the same mechanism. `SplitButton.tsx:32-42` precedents the +enumerate-all-slots form; use the literal `components: { root: … }` when the headless slots type +declares exactly one slot, since it never _reads_ `components` and so needs no `no-deprecated` +disable. + +`slot.always` is immutable: it builds a fresh object and overwrites the element-type symbol the +headless hook set, while `resolveShorthand` returns an object value unchanged, so nothing of the +headless slot is lost and the render-function symbol survives the spread. + +## The styles hook + +**Immutable — returns a NEW state object. Never mutate.** + +```ts +export const buttonClassNames: { root: string } = { + root: componentMarkers('button'), +}; + +export const useButtonStyles = (state: ButtonState): ButtonState => { + const { appearance, shape, size } = state; + + const root: ButtonState['root'] & ButtonRootDataAttributes = { + ...state.root, + 'data-appearance': appearance, + 'data-size': size, + 'data-empty': !state.root.children || undefined, + className: clsx(buttonClassNames.root, styles.root, styles[appearance], styles[shape], state.root.className), + }; + + return { + ...state, + root, + icon: slotClasses(state.icon, styles.icon), + }; +}; +``` + +- **Note what is _not_ in that `clsx` call.** `appearance` and `shape` each contribute a class per + value; `size` is destructured, stamped as `data-size`, and then never composed. Sizes are almost + always attribute-driven — one `.root` block with `@variant size-*` nested inside it — so do not go + looking for a `styles.extraLarge` sibling class, and do not add one. Adding a size means adding a + `@variant` block to the existing `.root` (and to `.icon` where the glyph scales), not a new module + class. The same holds for any look prop whose values differ only in measurements. +- Named `useStyles.ts`. The filename must **not** contain a `.styles.` segment (Griffel's AOT + glob would pick it up). +- A styles hook that composes another component's styles hook must carry `'use client';` — + enforce-use-client fires on custom-hook calls. +- Class composition is always + `clsx(componentMarkers(''), styles.root, state.root.className)` — consumer className last. +- Slots go through `slotClasses(slot, ...classes)`, which returns a falsy slot unchanged rather than + materialising it. An optional slot the headless hook declined to build must stay absent. + +## Markers + +`utils/groupMarker.ts` is the only public class pair, and owns the ordering constraint: + +```ts +componentMarkers(name); // `fui-${name} group/fui-${name}` +peerMarker(name); // `peer/fui-${name}` +``` + +`fui-` must be **first**: jsdom's nwsapi builds its `:scope` polyfill (and therefore `:has()`) +from `classList[0]` run through the unescaped global `escape()`, which mishandles `/` and throws. + +`peerMarker` goes on **native input slots only** — where real pseudo-class state lives. Never on the +root of a composite control: a consumer's `peer-checked/fui-x` would silently match a root that is +never `:checked`. + +## Data attributes + +The styles hook stamps `data-*` for anything CSS must select on. Presence semantics: + +- JSX value is `true` when on — `X || undefined`, or `cond ? true : undefined` +- `undefined` removes the attribute from rendering +- never `? '' : undefined` + +CSS selectors stay **presence-based** (`[data-x]`, never `[data-x='true']`) unless targeting a specific +value (`data-size`, `data-color`, …). + +Headless already stamps its own state attributes — do not duplicate them. Headless-stamped attrs keep +the headless library's own `''` spelling; that is theirs, not ours. + +### Subtree token knobs: the grouped-selector mechanism + +A `:root`-declared token cannot be re-tuned mid-tree by overriding an input variable — descendants +inherit the already-substituted stream; a formula responds to a local knob only where its declaration +re-applies. The sanctioned shape (ScaleRegion is the precedent) is theme-owned: the knob's completion +class is **grouped into the invariant emission's selector** (`:root, :host, .fui-scale-region` — same +declarations, one more selector, a computed no-op at the root), and the knob value is derived from a +data attribute on the same element via typed attr() coercion +(`--fui-scale: attr(data-fui-scale type(), 1)` — the same coercion `data-size` uses, see +[variant-catalog.md](variant-catalog.md)). Attribute-derived knobs are absolute per element: nested +regions replace the factor, never compound it. The component stamps the class, the attribute, and the +ambient theme class (theme-owned formulas re-substitute only where the theme class re-applies). + +## CSS authoring + +### Layers + +- **All** component styles live in `fui.components.l`. +- Tailwind's preflight ships at the **head of `fui.base`** — the first content of that layer, so + every component rule outranks it by layer order and the theme's other element resets beat it by + source order (or, for the zero-specificity icon defaults, by the theme's own guard rule). Never + lean on a preflight value: a component declares what its rendering depends on rather than + inheriting the reset's answer. +- `fui.base` belongs to the theme, for the preflight and global element resets **only**. Components + never author into it. +- **The level is assigned per RULE, not per file.** A rule styling the component's own DOM — its root, + its plain-element slots — is a base style and sits at `l1`, no matter what the rest of the file does. + A rule that overrides ANOTHER component's styles (a slot rendering a windmod Label, Button, + Listbox, …) sits one level above the overridden rules — `l2` over an `l1` base, `l3` over an `l2` + one. Layer order carries the override, **never** cross-file source order. Never park a rule higher + than the rule it overrides requires; wholesale-levelling a file grows the stack for nothing + (Field's root is a plain div and sits at `l1` even though its label block, overriding the windmod + Label, sits at `l2`). +- **One `@layer` block per level per `module.css`** — a mixed file carries at most one block for each + level it uses. Same-element rules must share a level so block order keeps arbitrating between them; + only rules for disjoint elements may sit at different levels. +- **Never** write an `@layer` ORDER statement anywhere except the theme package, which is its sole + owner. + +Layer assignments are conventions, not hard rules: the library claims as deep as composition requires, +and the declared stack may expand if headroom is needed. + +#### The ladder is unconditional — and that is the trap + +Griffel resolves a composition by **sheet order _and_ selector specificity**, and the two frequently +disagree. windmod resolves it by layer alone: every declaration an `l2` module authors beats the `l1` +base it composes over, in every state, regardless of weight. + +So a rule that Griffel writes but never applies **must not be ported**: + +- `AvatarGroup` — Griffel merges its focus indicator unconditionally, and that `[data-fui-focus-visible]` + selector (0,2,0) outranks the pie bucket (0,1,0). Porting the pie bucket alone left a pie overflow + trigger with **no focus ring at all**. Its `forced-colors` block is the mirror image: a bare class + (0,1,0) that Griffel's hover bucket (0,2,0) beats, so authored last in a layer it painted + `CanvasText` where Griffel paints the hover colour. +- The `TeachingPopover` carousel footer — Griffel's brand buckets author the narrow `:hover:active` + while windmod's `hover-active` variant is the wide `:hover:active, :active:focus-visible`. At + `:active:focus-visible` Griffel paints the plain Button appearance and a verbatim port paints brand + blue over it. + +**`!important` inverts the whole ladder.** An important declaration in a lower layer beats a normal +one in a higher layer, so a single `!important` anywhere in a base turns the ladder upside down and +forces every override above it to escalate too. The library carries none, by design. + +### Author what Griffel computes, never what Griffel writes + +The parity target is the **computed** style of the Griffel twin. Source is a lead, not the answer; +where they disagree, the browser is right. + +- `NavDrawerFooter` — Griffel writes a **five-value** `padding` shorthand. Five values are invalid at + computed-value time, so the property resolves to its initial value and the footer computes + `0px 0px 0px 0px`, wiping `DrawerFooter`'s reset rather than replacing it. windmod authors the zero, + because that is what ships. ✅ measure → author `0`; ❌ read the source → author the intended values + → guaranteed VR diff. +- Dead declarations are common: `Dropdown`'s and `Combobox`'s icon `fontSize` is overridden by all + three size buckets, so windmod authors the three bucket values and nothing else. + +Where a plan cannot predict the winner, a browser probe on the **Griffel side only**, reading +`getComputedStyle` across the full state cross-product, is the binding measurement — and it blocks the +module. Force `:hover` / `:active` through CDP `CSS.forcePseudoState`, and when a state is a _pair_ +(`:active:focus-visible`) verify the pair actually latched before trusting the row. Record the dead +declarations you skipped; a reviewer must be able to see the omission was deliberate. + +### One block per class + +Each module class is authored **exactly once** — resting look first inside the block, then its variants +nested in cascade order. A class never appears as two sibling blocks. Cascade _between_ classes is +carried by block order within the layer; every catalog variant is `:where()`-flat, so equal specificity +is guaranteed and source order decides. + +```css +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply …; + + transition-property: background, border, color; + + @variant hover { + @apply …; + } + @variant hover-active { + @apply …; + } + @variant size-small { + @apply …; + } + @variant disabled, disabled-focusable { + @apply …; + @variant hover { + @apply …; + } + } + @variant focus-visible { + @apply fui-focus-ring; + } + } +} +``` + +Every module opens with `@reference '#theme'` — one line, resolved through `library/package.json`'s +`"imports"."#theme"`, which points at `library/src/theme.css`, which composes the theme package's +catalog with the package's own variants file. + +### Children are styled directly, never through nested descendant selectors + +Every slot gets its own module class with its own top-level block, and parent state reaches it via group +variants: + +```css +.required { + @variant group-disabled/fui-label { … } +} +``` + +Nested `& .child` selectors inside a root state block are allowed **only** when the target has no module +class at all (raw icon glyph elements). A local class inside `:not()` is an exclusion filter, not a +target, and satisfies the rule. Where Tailwind lacks a combinator primitive, a plain CSS sibling +selector in the module is the sanctioned escape hatch. + +### Tailwind-max + +Use utilities for everything feasible, including `border-(length:--spacing-thin)`, `rotate-(--var)`, +`size-(--var)`, `me-(--var)`, `border-[Highlight]` (system colours), `forced-color-adjust-none`. Only +complex `var()`-fallback chains, clip-paths and multi-drop-shadows stay as raw CSS declarations. + +**No arbitrary-property utilities.** A utility that is _entirely_ a square-bracketed `property:value` +pair — `[text-align:unset]`, `[grid-area:content]` — is banned; write the plain declaration instead. The +compiled output is identical and the bracket form only obscures. Arbitrary **values** on a real utility +prefix (`border-[Highlight]`, `text-[length:…]`) remain fine. + +### Naming + +Module.css local class names are **kebab-case** (`.ring-thicker`, `.shadow-16`), never camelCase — the +DOM shows kebab. The class-map generator aliases each kebab key to its camelCase form, so styles hooks +keep idiomatic dot access (`styles.ringThicker`). Hooks and tests never use bracket-string access. + +### Variants + +The catalog is two files and **variants are never defined in a component module** — see +[variant-catalog.md](variant-catalog.md) for the canonical form, the split test, and the minimality +rules. If a needed variant is missing, add it in `&:where([data-…])` form to whichever catalog it +belongs to, one selector per entry. + +### Pseudo-elements: `content` comes free + +Tailwind's `before`/`after` variants already emit `content: var(--tw-content)` into the rule they +open, and `dist/base.css` registers `@property --tw-content { syntax: "*"; inherits: false; +initial-value: "" }`. A `content-['']` inside such a block therefore writes `--tw-content` the value +it already holds. **Never author `content-['']` merely to make a pseudo-element exist** — the variant +did that. Drop it and the rule keeps its injected `content`, resolving to `""` off the initial value. + +```css +/* ❌ redundant — the variant already supplies `content: var(--tw-content)`, initial-value "" */ +@variant after { + @apply absolute inset-0 content-['']; +} + +/* ✅ */ +@variant after { + @apply absolute inset-0; +} +``` + +**The exception class — an explicit `content` is required whenever it changes the default:** + +- **A different value.** `content-['_']` (MessageBarTitle's literal space), `content-['·_']` + (RatingDisplay's separator), `content: unset` (Input `.disabled`, suppressing the focus underline). +- **Suppression, then restoration — the load-bearing pair.** A block writes `content-none` to keep a + pseudo-element out of the box tree, and a later block writes `content-['']` to put it back. + `--tw-content: none` lands **on the same pseudo-element**, so it outranks the `@property` initial + value and the restoring `content-['']` is doing real work — deleting it leaves the element + unpainted. `Avatar.module.css` is the canonical case (`.root` neutralises both pseudo-elements; + `.ring` and `.shadow` each set theirs back), and `Tab.module.css` repeats the shape for its pending + and selection indicators. Both carry a header comment saying so; keep it with the code. +- **A raw `&::before` / `&::after` selector.** No variant, so no injection, so the `content` is the + only one there. Used when the variant's automatic `content` would itself be the bug — see + `Radio.module.css`, whose indicator dot would otherwise render in every state. + +`Divider.module.css`'s header comment is the canonical statement of the mechanism; read it before +adding a `content` anywhere. + +### Tokens + +Kebab-case theme tokens only. Never hardcode a palette value — compare against the Griffel source to +find the right token. + +### Line-height never rides without font-size + +- Never author a `leading-*` without a `text-*` on the same element. A leading token is a unitless + ratio derived against one font-size step — `--leading-base-300` is `calc(20 / 14)` — so authored + alone it multiplies whatever font-size inheritance delivers and the line box drifts off the ramp. + Pair the token with its own step (`text-base-300 leading-base-300`). Where a rule needs a ramp + line-height over a **different** authored font-size, spell the ratio as the self-documenting + fraction `leading-[calc(target/ownFontSize)]` in the same rule as the font-size. `leading-[0]` is + the one exemption — zero computes the same against any font-size (Spinner's root, matching + Griffel's bare `lineHeight: 0`). +- Native interactive elements (`button`, `input`, `select`, `textarea`) author their `text-*` step + even when nothing in the box visibly depends on it. Preflight resets them to `font: inherit`, so + the component declares its metrics rather than riding the surrounding context — `Tab.module.css` + and `InfoButton.module.css` roots are the precedent. + +### Transition-property parity + +Spell `transition-property` **exactly as the Griffel source authors it** — shorthands and order +verbatim, e.g. Button's `background, border, color` — never an expanded longhand list. Computed +`transition-property` preserves the author's spelling, so expansion is a real computed-value divergence +and also narrows behaviour (Griffel's `border` would animate width and style changes; a longhand list +would not). Verify per module with a computed-style equality check. + +### Identical variant bodies: delete before collapsing + +Sibling `@variant` blocks with identical bodies collapse to a comma list — `@variant hover, +hover-active { … }` compiles to one nested rule per member in list order, byte-identical to the +separate blocks, with per-member specificity (never a `:is()` wrap). Collapse only CONSECUTIVE +siblings: hoisting a block across an intervening sibling that touches any of the same properties +reorders the cascade. + +Before collapsing, test the null hypothesis: a nested `hover`/`hover-active`/`focus` arm that only +re-asserts declarations its parent state block already carries is usually DEAD — every catalog +variant is `:where()`-flat, so the parent base (later in source than the interaction blocks it +outranks) already wins on order alone. Delete such arms rather than collapsing them, and prove the +deletion with a computed-style A/B across the pseudo-state × forced-colors matrix. The identity +custom variant (`@custom-variant self (&)`) that would let a base join a comma list compiles +safely, but no shipped site has needed it — a re-assertion arm that measures redundant is deleted +instead. + +### Icon variants + +Glyph styling never uses a raw `svg` type selector. Every `@fluentui/react-icons` icon stamps +`data-fui-icon` (SVG icons: empty string; font icons: `"font"`), so the generic `icon` variant +(`(& > :where([data-fui-icon]))`) selects any direct-child Fluent glyph regardless of bundling — +it is the spelling for shaping a single glyph (`display`, `text-icon-*`, `flex-none`): + +```css +.icon { + @variant icon { + @apply block; + } +} +``` + +Note it matches font icons too, and it does NOT match a consumer's arbitrary `` — that +behavioural delta vs Griffel's `& svg` is documented in MIGRATION.md (stamp `data-fui-icon` to +opt in). + +The filled/regular glyph swap selects on `data-fui-icon-variant`, stamped by the headless +`bundleIcon` on each of the two sibling glyphs it renders directly inside the slot element. The +`icon-filled` / `icon-regular` variants are direct-child selectors applied to the class that owns +the glyphs — no descendant wrapper: + +```css +.icon { + @variant group-hover/fui-button { + @variant icon-filled { + @apply inline; + } + + @variant icon-regular { + @apply hidden; + } + } +} +``` + +Copy `Button.module.css`. Two constraints: + +- **State-guard placement.** A group-state guard wraps the icon variants from the OWNING class's + block, and that class must be a **descendant** of the group root. On a block anchored on the group + root itself the compiled group check fails silently — verify with a computed-style probe before + anchoring a guarded swap on a root class. +- **Direct-child reach.** `icon`, `icon-filled` and `icon-regular` reach only glyphs that are + direct children of the anchor. Where the glyphs sit deeper (InteractionTagPrimary anchors on root + appearance classes while the glyphs live inside the tag icon slot), use the glyph-self spellings + `variant-filled` / `variant-regular` inside a `& *` wrapper — the one remaining permitted + nested-selector form for glyphs. Never widen a catalog variant to descendant reach for such a + site. A consumer who wraps their icon in an extra element takes the glyphs out of direct-child + reach by design. + +## Code style + +- **No nested ternaries.** Never chain `? :`. A single non-nested ternary is fine. +- **Bucket ladders are condition-key lookups.** Three or more mutually exclusive branches selecting a + class become an object literal keyed by each condition coerced to `1`/`0`, indexed by `1`. The `+()` + is load-bearing: TS rejects a bare boolean computed key (TS2464). Two-branch selections are a plain + ternary, not a lookup. + + ```ts + // ✅ every range written in full, so the keys partition the domain + const textClass = (size: AvatarSize) => + ({ + [+(size <= 24)]: styles.text100, + [+(size > 24 && size <= 28)]: styles.text200, + [+(size > 28)]: styles.text300, + })[1]; + ``` + + ```ts + // ❌ cumulative boundaries and an implied else + const textClass = (size: AvatarSize) => + ({ + [+(size <= 24)]: styles.text100, + [+(size <= 28)]: styles.text200, // overlaps the first key — at size 20 BOTH are 1 and the + })[1]; // later one silently wins; and sizes above 28 fall out as undefined + ``` + + Two gotchas, both fatal and both silent: + - **Mutual exclusivity.** Unlike an if-return chain, no branch shadows a later one — every key is + evaluated. Two true conditions both write key `1` and the last one wins. Spell out both bounds of + every range (`size > 24 && size <= 28`), never the cumulative `size <= 28` an if-chain allowed. + - **Explicit final bucket.** There is no `else`. The last bucket needs its own condition, and every + member of an enum domain needs its own key — including the ones whose value is `undefined`. Miss + one and the lookup returns `undefined` at runtime while TS still types it `string`. + + An intentionally class-less bucket is written out with an `undefined` value and a comment saying why + (`useAvatarStyles`' 32–40 base bucket). TS then types the helper `string | undefined` on its own — + no `satisfies` or return annotation is needed to keep the lookup honest. + +- **Component shape**: a value used exactly once earns no intermediate const — destructure `props` + directly in the parameter list when the body never references `props` itself, and pass the state object + literal inline into the styles hook. A value used twice or more stays a named const. +- Match the existing files exactly: import order, `type Props`, forwardRef pattern, and the **family + barrel** (`src/.ts` re-exports the windmod component plus the headless building blocks for + that family). `library/src/index.ts` stays `export {}` — there is no root barrel. +- Adding a component means adding its exports to its family's `src/.ts`. That is the whole + registration: `nx sync` derives the export map from `src/*.ts`, and `generate-api` follows it. A + component in a **new** family adds one `src/.ts` file; nothing else is hand-edited. +- `isConformant` reads the component→family map straight out of `src/*.ts`, so a component added to the + wrong family (or to none) fails its `has-top-level-file` assertion rather than checking a wrong subpath. + +## Comments policy + +- Comments are **standalone statements**. Never reference decisions, reviews, or "we chose". +- State only constraints the code cannot show. No "what the next line does", no provenance. +- Minimal: fewer comments than code, no duplication across files — one canonical location, others may + reference it by file ("see useButtonStyles"). + +## Definition of done + +1. A VR scene added: shared `VrScene.tsx` in `stories/src/vr/`, rendered by both a windmod and a + Griffel story, re-exported from `vr/index.stories.tsx`, with a scene entry appended to the harness's + `scenes.json`. +2. The gates script fully green — build, type-check, lint and test for both packages, then a static + storybook build and the pixel diff against Griffel. + +## Verification + +**Where these rules live, and where the runner does not.** The VR scenes are checked in — +`stories/src/vr/` carries a `VrScene.tsx` plus its Griffel and windmod story pair per component, +and that is what a runner captures. The pixel-diff runner itself is **not** in this package: no +`pixelmatch` reference, no `scenes.json`, no gate script exists under +`packages/react-components/react-windmod-preview`. The recorded numbers and the allowance rows in +`library/MIGRATION.md` come from a harness pointed at this package from outside it. + +So do not spend a cycle hunting for a command that is not there. What you can run here are the four +nx targets — `build`, `type-check`, `lint`, `test` — plus `generate-api`. Everything below is the +standard the parity work is held to and the protocol for reporting it honestly; treat a claim you cannot +currently reproduce as exactly that, and say so rather than implying a gate you did not run. + +### The pixel gate + +**Zero means zero, under a stated rule.** The runner diffs the two captures with pixelmatch at +threshold 0 and the default `includeAA: false`, so a scene passes at zero when it has **no +non-antialiasing differences** — pixelmatch's antialiasing classifier absorbs sub-perceptual +rasterization noise along edges. That is the gate, and it is the number to quote; it is not "no bytes +differ". The computed-style parity passes are what catch the class of divergence pixel counting +cannot see, which is why both run. + +There is no tolerance band on top of it. If a diff is genuinely impossible to close, document it and +stop — never loosen the threshold. + +### The allowance protocol + +A scene may carry an explicit `allowedStrictDiff` only under all four of these: + +1. **A control first.** A reproducing control — Griffel-vs-Griffel, or self-vs-self — must bound the + residue as browser noise. Build the control _before_ proposing the allowance, not to justify one + already written. +2. **An identity control is the strong form.** Force the reference's own compositing conditions onto + itself and diff that against the gate's reference capture. When it reproduces the residue + pixel-for-pixel (full overlap, zero control-only and zero gate-only pixels) the CSS is proven + exactly correct and the diff is pure rasterization. +3. **Decomposed, not just bounded.** Every allowance names a specific mechanism and a pixel count per + mechanism. A diff that does not decompose the documented way fails the scene **even when it sits + under the ceiling**. An allowance is never a tolerance band. +4. **No self-granting.** Ratification is a reviewer's call, never the implementer's — measure, + decompose and recommend. Raising a ceiling to make one's own cycle pass is the failure this rule + exists to prevent. A residue with no bounding control, or one exceeding its control, is a real + diff: stop the cycle. + +A `--disable-gpu` re-run is evidence, not a cleaner baseline. Some scenes get **noisier** without the +GPU; where they do, the GPU number stays authoritative and the no-GPU row is recorded as what it is. +A no-GPU number is never the GPU number minus a component. + +If a plan flags a specific cell as an open risk, confirm that cell is actually in the captured set +before trusting a zero — a band that omits the flagged cell cannot adjudicate the risk it raised. + +### Mutation testing — name the harness per row + +**Spec quality bar: mutation-tested.** Deleting any class from a `clsx` call, breaking any stamped +attribute, or mutating pass-through state must each fail at least one test — verified by **running** +the mutation, not by reading the tests. Every D1 glyph consumer additionally carries a frozen-state +test through the shared test-only `testing/freezeState.ts` helper. + +Two honesty rules make the table mean something: + +- **Every row names its harness** — `jest`, `browser probe`, or `VR`. A CSS-only mutation has no + possible jest guard, and a table that files it under jest is claiming coverage that does not exist. + ❌ "M18 — fails the spec" when the change is invisible to jsdom. ✅ "M18 — `browser probe`; computed + `min-width` reverts to 96px". +- **Assert the green baseline first, and label the unkillable.** A row that cannot fail by + construction (a no-op mutation, a declaration with no observable effect) is recorded as + **unkillable by construction** with the reason. Quietly dropping it inflates the kill rate. + +### Probe hygiene + +- **Browser-behaviour claims are measured**, never asserted from byte inspection of source or compiled + CSS. Every claim labelled "measured" must point at a probe artifact that exists — script _and_ + recorded output. Citing a run that was never recorded is fabrication even when the conclusion turns + out to be true. +- **Never trust a piped exit code.** A pipeline's status is the _last_ command's, so + `nx run …:build | tail` reports `tail`'s success and hides a failed pre-task — once leaving a stale + `dist/` that polluted a CSS diff with ~22 phantom removals. Check per-step exit codes, or the + harness's `report.json`, never the tail. The same trap has bitten builds and sweeps alike. +- **Prettier reads `.gitignore` as an ignore path.** A plain `prettier --check` on anything under + `.scratch/` therefore passes _without reading the file_. Probe runs need `--ignore-path `. And a formatting verdict on a probe copy never transfers to the shipped module: the Tailwind + plugin sorts against the resolved entry stylesheet, and a probe's `@reference` line differs by + construction. Check the **shipped** `module.css`. +- **Calibrate ident-hash probes** against a shipped component's real `lib/**/*.module.css.js` hash + before trusting a prediction. The digest seeds on the path relative to `library/src` (no `src/` + prefix). Enumerate module files **recursively** — a one-level glob silently misses nested pair + components — and count `@keyframes` names, which postcss-modules scopes as locals too. +- **Type probes resolve through built `.d.ts`.** A tsconfig without explicit `paths` resolves + `@fluentui/*` through `node_modules` symlinks to each package's build output, so a source-type edit + is **inert** until that package (and any re-exporting package) rebuilds. State the resolution mode + in the artifact. Related: `@ts-expect-error` inside a multi-line JSX comment does not suppress — + pins must be single-line. +- `generate-api` replays a stale nx cache after a subpath is added. Run it with `--skip-nx-cache` and + verify the new `etc/.api.md` was actually emitted. + +### The jest ident hazard + +`generateTestIdent` drops the component token, so under jest a composed component's local class names +collide with its composee's **as strings** — `Field`'s `.root` and `Label`'s `.root` compare equal — +while the real built idents differ. Bare class-name equality is therefore not a test. + +```tsx +// ❌ passes for the wrong reason under jest +expect(field.className).toContain(labelStyles.root); + +// ✅ structural, or by count +expect(field.querySelectorAll(`.${styles.root}`)).toHaveLength(2); +``` + +Every re-slot composition must disambiguate **structurally** — by element, by marker pair, by +occurrence count — never by a bare class-name comparison. + +## Scope + +Replicate **only** what the headless component ships. Do not invent props or features the Griffel +component has but headless lacks; do not add speculative abstractions. + +**The generic catalog is vocabulary, not permission.** `variants.css` defines `size-huge` and +`size-full` because _some_ component needs them; that a variant name resolves says nothing about whether +the component in front of you may adopt it. Before adding a value to any look-prop union, read the prop's +type on the Griffel twin in `packages/react-components/react-` — if Griffel's `size` stops at +`extra-large`, so does windmod's, however available `size-huge` looks. This is the easiest way to write +a change that compiles, renders, passes review-by-eyeball, and is still out of scope. + +Check the value does not already exist before planning to add it. Sizes and appearances arrived with +their components rather than one at a time, so the union in `.types.ts` is the fastest answer to +"is this already here", and it is a cheaper read than the CSS. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/css-var-values.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/css-var-values.md new file mode 100644 index 0000000000000..667208c464ea1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/css-var-values.md @@ -0,0 +1,132 @@ +# Reading token values in JavaScript + +`useCssVarValue` reads the computed value of one or more CSS custom properties at an element's DOM +position, and re-reads them when the values could have changed. Use it when a token's resolved value is +needed at runtime — canvas drawing, measurement, theming a third-party widget. + +```tsx +import { useCssVarValue, invalidateCssVars } from '@fluentui/react-windmod-preview/use-css-var-value'; +``` + +## Signatures + +```ts +useCssVarValue( + variableName: string, + elementRef: React.RefObject, + options?: UseCssVarValueOptions, +): string | undefined; + +useCssVarValue>( + variableNames: T, + elementRef: React.RefObject, + options?: UseCssVarValueOptions, +): CssVarValues; // { [K in keyof T]: string | undefined } +``` + +```ts +interface UseCssVarValueOptions { + /** Returned before mount (SSR and the first client render) and whenever the variable has no + * computed value at the element. For the record form it fills each unresolved slot. */ + fallback?: string; + /** Reads the DOM on every committed render instead of consulting the cache. */ + forceOnRender?: boolean; +} +``` + +Variable names include their leading dashes. + +## Usage + +```tsx +const ref = React.useRef(null); + +// single +const fg = useCssVarValue('--color-neutral-foreground-1', ref, { fallback: '#242424' }); + +// record — the return mirrors the input's keys +const { bg, radius } = useCssVarValue({ bg: '--color-neutral-background-1', radius: '--radius-medium' }, ref); + +return
; +``` + +The read happens through `getComputedStyle(element).getPropertyValue(name)` **at the consuming +element**, so cascade, inheritance and theme scoping all apply — the value is whatever CSS would hand +to a `var()` reference on that element. That is why it takes a ref rather than reading `:root`: a token +inside a nested provider resolves to that provider's theme. + +## What re-reads + +- the nearest `FluentProvider`'s `className` or inline `style` changing — from a prop or from an + external `classList`/`style` mutation +- **any ancestor provider** changing (a nested provider only shields its subtree for tokens it + redeclares itself) +- a `style` mutation on `documentElement` in any observed document +- `invalidateCssVars()` +- `options.forceOnRender`, on every committed render + +Values are memoized per `(element, variable)` in a `WeakMap`, tagged with the scope and root versions +they were read under, so a hit is taken only while every input is unchanged. A record read is N +independent entries and shares them with any single-name read of the same variable at the same element. + +## The one thing that will surprise you + +**Not every token reads back as a usable value.** The theme deliberately leaves its knobs unregistered +(no `@property`), so a custom property's computed value is the specified token stream with `var()` +substituted and **`calc()` not evaluated**. Measured over all 472 declared tokens: + +| Family | Reads as a literal | Reads as a `calc()` string | +| -------------- | ------------------ | -------------------------- | +| `--color-*` | 366 | 0 | +| `--shadow-*` | 12 | 0 | +| `--radius-*` | 11 | 0 | +| `--leading-*` | 5 | 5 | +| `--ease-*` | 9 | 0 | +| `--duration-*` | 8 | 0 | +| `--font-*` | 7 | 0 | +| `--spacing-*` | 2 | 25 | +| `--text-*` | 0 | 17 | +| `--stroke-*` | 0 | 4 | +| `--base-scale` | 0 | 1 | +| **Total** | **420** | **52** | + +So colour, shadow, radius, ease, duration and font read as usable values — `#242424`, `150ms`, +`12px`, `cubic-bezier(0.9, 0.1, 1, 0.2)`. Text, spacing, stroke and `--base-scale` read as unevaluated +strings such as `calc(14px * calc(1rem / 16px * 1))`, invariant under both a theme change and a root +font-size change. Leading splits down the middle of the ramp's arithmetic: the finite ratios read as +unitless numbers (`1.4`) and the repeating ones as unevaluated division strings (`calc(20 / 14)`) — +either way a **ratio**, never a length. A length is +`calc(var(--text-base-300) * var(--leading-base-300))`. + +The practical consequence: **this hook is for colour and other literal tokens.** For a resolved _length_ +you want `getComputedStyle` on a real property, not on the token. + +A theme-class change moves 315 tokens and every one of them is a literal — so for exactly the tokens +theme switching changes, the value is real and scope-correct. + +## Gotchas + +- **The returned record is not a stable identity.** It is a fresh object on every render even when no + slot changed. Never use it as an effect dependency — read the slots. +- **SSR-safe**: on the server, and until the layout effect runs, the hook returns `options.fallback`. + Always pass one for anything that renders. +- **A bare root `font-size` change reports nothing.** Zoom controls that change only the root font size + move no unregistered token; the invalidation fires and the re-read returns the same string. A zoom + control that sets a custom property inline **is** observable. +- **Residual staleness**: a provider's version only advances while that provider is mounted. A mutation + at an element while no provider is mounted, followed by a remount at that element, can serve a stale + cached value. `invalidateCssVars()` is the escape hatch. +- **`forceOnRender` writes the shared cache.** A forced consumer refreshes the `(element, variable)` + entry for every other consumer of the same pair, so a plain peer's value can move without any + invalidation event. The value is read at the current versions, so it is fresher, never wronger. +- **jsdom**: `getComputedStyle` resolves custom properties set as **inline styles** but does not cascade + them from stylesheets. In unit tests, set the variable with `element.style.setProperty(...)`. + +## `invalidateCssVars()` + +```ts +invalidateCssVars(): void; +``` + +Invalidates every mounted `useCssVarValue` consumer, in every document. The escape hatch for anything +the provider and root observers cannot see. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/failure-modes.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/failure-modes.md new file mode 100644 index 0000000000000..711e1a1966cdc --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/failure-modes.md @@ -0,0 +1,30 @@ +# Failure modes (contributors) + +Recurring defect classes found in review of this package's own styling corpus. Every one of these +shipped at least once and was swept back out. Check your diff against this table before calling a +style change done; when one instance of a class is found, sweep the whole corpus for the CLASS — +never just the literal string that exposed it. + +| Defect class | Wrong form | Right form | Detection | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Raw pseudo-element selectors | `&::before { content: ''; … }` | `@variant before { … }` — the variant supplies `content: var(--tw-content)`; add `content-['…']` only to change the default. Load-bearing exceptions stay raw and commented: Radio's `&::after` (the variant's content would paint the dot in every state), Dropdown's `&:has(.clear-button:focus)::after` | `grep -rn '&::' src/components --include='*.module.css'` — every hit needs a justification comment | +| Redundant pseudo `content` | `@variant after { @apply content-['']; … }` restating the default | omit `content-*` unless the value differs from `''` | grep `content-\[''\]` and check each against the variant's built-in content | +| Hand-rolled negated variants | authoring `@custom-variant not-scroll-state-none (…)` in a catalog | Tailwind derives `not-` automatically from the positive entry — custom AND media variants alike (`not-forced-colors` works from the built-in) | grep the catalogs for `not-` custom-variant entries; try the automatic form in a compile probe first | +| Raw media queries where a variant exists | `@media (forced-colors: none)`, `@media screen and (prefers-reduced-motion: reduce)` | `@variant not-forced-colors`, `@variant motion-reduce` | `grep -rn '@media' src/components --include='*.module.css'` — only the Dialog-family Griffel breakpoints are justified | +| Wholesale per-file layer levels | whole module on `fui.components.l2` because one rule overrides another component | assign the level PER RULE: base styles `l1`, cross-component overrides `l2+` | in each module, ask of every rule "whose styles does this override?" — a file-wide level is the smell | +| Var-reference utilities where a named binding exists | `border-(length:--spacing-thin)`, `duration-(--duration-faster)`, `ease-(--ease-easy-ease)` | the bound named utility: `border-thin`, `duration-faster`, `ease-easy-ease`; if the token exists but no utility binds it, add the binding in the theme (alias the token into the utility's namespace), then use the name | `grep -rn -- '-(--\|(length:--' src/components --include='*.module.css'` and check each token for a binding | +| Raw `svg` type selectors for glyph styling | `& svg { … }`, `& > svg { … }`, `& * { @apply hidden; }` wrapper blocks | the direct-child icon variants: generic `icon` (`(& > :where([data-fui-icon]))` — every Fluent icon stamps the attribute, bundled or not) for glyph shaping; `icon-filled` / `icon-regular` for the bundled-pair swap. Exception: glyphs that are NOT direct children of any anchor class stay on the glyph-self spellings with a comment (InteractionTagPrimary) — never widen a variant to descendant reach | `grep -rn '& svg\|& > svg\|& \*' src/components --include='*.module.css'` | +| Duplicated variant bodies | identical `@apply` repeated under sibling variants, or re-assertion arms under hover/hover-active | FIRST test the null hypothesis: delete the arms and A/B the computed state matrix — source order + layers usually already resolve it (39 of 39 re-assertion arms in one sweep were deletable). Only a site that measurably needs the body gets the comma list: `@variant disabled, disabled-focusable { … }` | normalize innermost block bodies and count repeats (census script); per site, delete-and-diff before collapsing | +| Multiple `@apply` per block | two `@apply` statements in one rule | one `@apply` per block position; raw declarations sit beside it as plain lines | brace-depth awk/grep for a second `@apply` at the same depth | +| Raw declarations with a utility equivalent | `align-items: start`, `justify-self: end`, `transform: scale(0.5)`, `rounded-[50%]` justified by "the utility emits a different value" | the idiomatic utility (`items-start`, `justify-self-end`, `scale-50`, `rounded-full`) — computed-EQUIVALENCE is the bar, not byte-equivalence: convert whenever layout is identical, verify per site (compiled diff + probe where the census notes an emitted difference) | grep raw declarations against the utility map; any justification comment claiming byte divergence is itself the finding | +| Leading without an authored font-size | `leading-base-300` in a rule with no `text-*`/font-size (the Tab-root shape) | pair the leading with its own `text-*` step in the same rule — leading tokens are unitless ratios, so an unpaired one multiplies whatever font-size inheritance delivers (pre-preflight this rode the UA's 13.3333px button constant; under preflight, `font: inherit`). Exemptions: `leading-[0]` (zero is ratio-invariant) and a ladder whose font-size is authored on the component's own root (Avatar) | census every `leading-*` rule for a font-size-authoring class in the same rule (`text-*` including numeric `text-`) — an unpaired hit needs a justification comment | +| Literal-token sweeps | fixing the one spelling the review named (`text-[length:calc(20px*…)]`) and stopping | sweep the defect CLASS: every construct with the same shape, corpus-wide, before the item closes | after any correction, write the class-level grep and run it — the item is not done while it returns hits | +| Stale-tarball VR signature | debugging icon-scene diffs (InfoButton glyph missing/wrong) as a style regression | the local icons tarball is stale — `yarn install` first, then re-run the sweep | icon scenes failing together while non-icon scenes pass | +| Piped exit codes | `run.mjs … \| tee log` then trusting `$?` (the pipe's exit, not the runner's) | `&&`-sequence the steps and adjudicate from the runner's `report.json`, not the shell status | any verification chain containing a pipe — re-check what its exit code actually reports | +| Orphaned background verifications | agent ends its turn "waiting" on a spawned check | wait in-turn for the result, or collect it on resume — a spawned task is not a completed task; results of children that finish after the turn ends are lost | any final message containing "waiting for" is the failure | +| "Dead" `@supports` fallbacks | deleting `@supports not (aspect-ratio)` blocks as unreachable under the browser floor | a bare property name is not a valid declaration test, so the negation evaluates TRUE and the "fallback" renders on every load (TeachingPopoverBody precedent: −14,135 px parity when removed). Browser-floor deletions apply only to guards with valid, truthy conditions — verify evaluation empirically | any `@supports not ()` with no `: value` — assume live until a rendered probe proves otherwise | +| `unset` restating the initial value | porting Griffel's `alignItems: 'unset'` / `margin: 'unset'` undo into a module whose own base never authors the property | an undo rule must target what the WINDMOD base authors, not what Griffel's base authored. For a non-inherited property with no other author rule on the element, `unset` computes the initial value the element already had — delete it (NavDrawerBody/NavDrawerHeader precedent, computed-parity proven). For INHERITED properties (`text-align`) `unset` means `inherit` and undoes UA sheets — those are live | for each `unset`, name the author rule it overrides on the same element; inherited-property unsets are live by default, non-inherited ones with no named victim are dead | +| Identity keyframe frames | `0%`/`100%` frames spelling `transform: rotate(0deg)` when the element's own `transform` is `none` | omit frames the browser synthesizes from the underlying computed value — `none` interpolates as the identity (Spinner precedent; SkeletonItem's `wave` was already the to-only form). A mid-timeline frame restating a value is NOT this class: tail-rotate's 50% `rotate(0deg)` shapes a non-uniform swing and measured load-bearing | any from/to frame whose value equals the element's underlying computed value — prove with a paused-animation computed-transform A/B before deleting | + +Related: [authoring-conventions.md](authoring-conventions.md) carries the positive rules these +defects violate, and its Verification section defines the probes and gates named above. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/griffel-deltas.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/griffel-deltas.md new file mode 100644 index 0000000000000..a011251f7710c --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/griffel-deltas.md @@ -0,0 +1,170 @@ +# Differences from `@fluentui/react-components` + +The package's `MIGRATION.md` enumerates **fifty-nine** deliberate differences in full. This is the +routing layer: the ones that change how you style, test or debug, grouped by what they break. Load the +full document from +`node_modules/@fluentui/react-windmod-preview/MIGRATION.md` when a specific delta needs its detail. + +## What is identical + +Start here, because it is most of the surface: + +- **The DOM** — element structure, slot names, `role`s and ARIA wiring, because both libraries render + through the same headless renderers. +- **The props** — the headless props plus the same look props Griffel takes (`appearance`, `shape`, + `size`, `orientation`, …) with the same defaults. +- **The pixels**, at a 16px root font size. Rest-state rendering is byte-identical across the shipped + matrix, including forced-colors rules, RTL and the icon glyph swaps, verified at a strict-zero pixel + threshold. +- **Slot `className` merging** — your class lands last on every slot. +- **Context behaviour** — components read their own contexts the way Griffel's do (`ButtonContext`, + `LinkContext`, `AvatarContext`, `FieldContext`, …). + +## Setup and API + +| # | Delta | What to do | +| --- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `theme` is a **class name**, not a theme object | `theme={webDarkThemeClassName}`. Port a custom theme object to a CSS class. | +| 2 | The theme stylesheet is a separate, required import | `import '@fluentui/react-tailwind-theme-preview/styles.css'` before your own CSS. | +| 3 | The provider is a real element **and it paints** | It is the flex/grid item, not its children; it repaints coloured surfaces. `background: transparent` via `className` if unwanted. | +| 4 | No `useCustomStyleHook_unstable` | Restyle through CSS. | +| 5 | Public classes are lower-case; `classNames.root` is a **pair** | `.fui-button`, not `.fui-Button`. No per-slot classes at all. | +| 6 | No root barrel — every import names a family | `CardHeader` from `'…/card'`, `MenuItem` from `'…/menu'`. Names unchanged; paths change. | +| 7 | `Input`/`Textarea` drop the deprecated shadow appearances | Map `filled-*-shadow` to its non-shadow twin. | +| 8 | `required` renders no asterisk on `Checkbox`/`Switch` | Wrap in a `Field` with `required` if you need the `*`. The attribute still reaches the input. | + +## The styling model + +| # | Delta | What to do | +| --- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| 9 | Everything scales with the root font size | Keep `html { font-size: 16px }` for Griffel parity. `--base-scale` lives at the document root. | +| 10 | **Cascade layers replace specificity juggling** | Your unlayered one-class rule wins. Do **not** wrap overrides in `@layer`. | +| 11 | Spacing tokens are inlined at build time | Runtime spacing-token overrides do nothing. Use `--base-scale`, or set the property directly. | +| 12 | Some computed style strings differ with no visual difference | Update snapshot tests asserting on computed `box-shadow`. | +| 13 | Text alignment is logical, not physical | Only diverges in a `dir`-flipped subtree inside an opposite-direction provider. | +| 22 | **`prefers-reduced-motion` is suppressed globally**, not per component | One unlayered floor rule. Your classed rules already outrank it. | +| 58 | **Tailwind's preflight ships**, at the head of the `fui.base` layer | Document-global reset; any rule you author outranks it. Re-check markup that relied on UA defaults. | +| 59 | **Leading tokens are unitless ratios**, not lengths | A `--leading-*` read is a number; multiply by the paired `--text-*` for a length. Descendants inherit the ratio, not a px box. | + +Delta 22 in detail, because it changes behaviour you may be measuring. The floor sets 1ms durations and +delays on `*`, `*::before`, `*::after` plus `animation-iteration-count: 1`, with one carve-out: the +Spinner's rotation stays at **1.8s, linear, infinite** — Griffel's own under-reduce value, because a +loading indicator that does not move stops being one. + +Measured differences under emulated `reduce`: + +| Motion | Griffel | windmod | +| ----------------------------------------- | ----------------- | ------- | +| `ProgressBar` determinate width | tweens over 0.3s | jumps | +| `AccordionHeader` chevron | rotates over 0.2s | jumps | +| `Nav` row background | 0.1s | instant | +| `NavItem` selection indicator + icon swap | 0.1s keyframes | instant | + +And the one consequence to guard against: the bare `*` means `transition-property` defaults to `all`, +so under the preference **every** element carries a 1ms transition. `transitionend` now fires where it +previously would not. Do not key logic on a `transitionend` arriving only for elements you styled. + +## Focus and keyboard + +| # | Delta | What to do | +| --- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 14 | Focus rings follow the browser (`:focus-visible`), not keyborg | Two edges where native is more generous: keyboard-then-click keeps the ring; click-then-arrow shows it. Neither is a defect. Update visual tests that capture focus. | +| 15 | Arrow-key navigation comes from `focusgroup`, not tabster | Needs browser support or a polyfill. **`Nav` publishes none** — one Tab stop per row instead of one for the whole nav. | +| 16 | `Accordion`'s `navigation` and `BreadcrumbButton`'s `focusMode` do nothing | Absent from the headless surface; TypeScript rejects them. | +| 17 | `TagGroup` does not restore focus after dismiss | Move focus yourself in `onDismiss` before the tag unmounts. | + +## Motion and mounting + +| # | Delta | What to do | +| --- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 18 | `Accordion` has no collapse animation | — | +| 19 | **`Accordion` keeps closed panels mounted — state persists** | React state, uncontrolled inputs, scroll position and timers survive close/open. If you relied on unmount to reset a form, reset explicitly or key children on the open state. | +| 20 | `Nav` has no collapse motion, and an open sub-item group **clips** rather than scrolls | Add `overflow-y: auto` yourself if you height-constrain a `NavSubItemGroup`. | +| 21 | `MessageBar` has no group animation and does not `announce()` | `MessageBarGroup` is not shipped. Announce yourself, or nest in a Griffel provider. | +| 36 | Popover surfaces have no enter motion | Resting render is unchanged. | + +## Anchoring and the top layer + +These are the ones most likely to surprise a styling change. + +| # | Delta | What to do | +| --- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 27 | **Anchored components require CSS anchor positioning, with no fallback** | No `@supports` guard, no polyfill, no floating-ui path. On Firefox and Safari today the surface renders unpositioned at the viewport origin. Polyfill, or keep `Tooltip`/`Popover` on `@fluentui/react-components`. | +| 28 | `Tooltip`'s arrow sits differently on corner placements | ~−36.6px / +35.0px on aligned placements. Edge-centred placements are unaffected. | +| 37 | A surface outside every provider reads the **document root's** theme | It is rendered inline and promoted to the top layer, so it inherits from its DOM position. Wrap the trigger in a `FluentProvider`. | +| 38 | **The surface inherits arbitrary CSS from its DOM ancestors** | `letter-spacing`, `text-transform`, `font-variant`, a wrapper's `color` — all reach it, where a Griffel portal saw none of it. Set those properties explicitly on the surface if you relied on portal isolation. | +| 35 | A focus-trapping surface carries no `aria-modal` | `dialog:modal` already conveys modality. Update assertions. | +| 40 | `InfoButton` has no `inline` prop; its popover is always top-layer | Its default placement is aligned, so delta 28's arrow displacement is the out-of-the-box appearance. | + +## Composition and context + +| # | Delta | What to do | +| --- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 23 | **A local prop beats a context value** | Griffel lets context win even over an explicit prop. If you relied on a container overriding explicit sizes, remove the explicit prop. Callbacks compose (context first), `className` concatenates with yours last. | +| 24 | `Field` does not push `size` into its control | Pass `size` to both. Measured gap: 8px control height, 2px font size. | +| 25 | A nested `Field` renders its label with the outer Field's orientation and size | Avoid nesting, or set the inner label explicitly. | +| 26 | `Card` selection is not focus-aware | Supply `shouldRestrictTriggerAction` yourself if the card contains interactive content. | +| 46 | `Combobox`/`Dropdown` listboxes are not clamped to the space around the trigger | `` | + +## Places windmod renders _more correctly_ than Griffel + +Kept rather than matched, because copying would have imported a defect. Listed so a pixel-diffing audit +is not surprised. + +- **47** — a current `BreadcrumbButton` keeps its hover and press styling (Griffel's cascade lets a later + rule beat the current-item styling it evidently intends). +- **48** — pressing a `Card` styles the card, not its descendants (Griffel's compiled selector is + `.card:hover, .card :active` — a stray descendant combinator). +- **52** — `layout="offset"` right-alignment reaches only the footer's own children (Griffel's is a + descendant combinator). +- **53** — `appearance={undefined}` on a carousel footer button no longer clobbers the derived appearance. +- **54** — `disabledFocusable` on a carousel footer button actually blocks the click. + +## What is not shipped + +| Not shipped | Why | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `Text` and the typography components, `Table`/`DataGrid`, `Tree`, `Carousel`, `Virtualizer`, `List` | no headless counterpart | +| `MessageBarGroup` | no headless counterpart, no visual contract to reskin | +| `Overflow` and family | **scoped out permanently** — it is renderless, and a component with no skin has nothing to reskin | +| `Persona`'s `presence` slot and `presenceOnly` prop | the headless surface omits both | + +All of them compose over windmod components without a shim — Griffel-styled containers around +windmod-styled children, and neither library's CSS fights the other. Keep importing them from +`@fluentui/react-components`: + +```tsx +import { Overflow, OverflowItem, useOverflowMenu } from '@fluentui/react-components'; +import { Button } from '@fluentui/react-windmod-preview/button'; +``` + +`Overflow`'s whole family ships two token-free declarations — +`[data-overflowing] { display: none }` and `[data-overflow-menu] { flex-shrink: 0 }` — which is exactly +why they are as correct over windmod-styled items as over Griffel-styled ones. The cascade win is +structural: the component's own `display: inline-flex` is layered, and those two rules are unlayered. + +## Mixing the two libraries + +- **Griffel container around windmod children** — works. +- **Headless (unstyled) container around windmod children** — has gaps. A windmod component reads the + _windmod_ context and only a windmod container publishes it. A windmod `Tag` inside a headless + `TagGroup` falls back to `filled`/`medium`; a windmod `NavItem` inside a headless `Nav` gets + `density: 'medium'` regardless. + +Use windmod containers for windmod children. + +## Known issues affecting both libraries + +Shared upstream code, so windmod is at exact parity. Consumer-actionable, not caused by migrating: + +- `InteractionTagPrimary` silently loses selection when you pass `onClick` (the base hook spreads + `...props` after its own merged handler — `onClick={undefined}` loses both). +- `BreadcrumbButton`'s `as` prop is mis-parsed — any truthy `as` yields an ``. Set `href`, omit `as`. +- `SwatchPicker` and `SwatchPickerRow` discard a consumer `style`. Use `className`. +- A vertical `AlphaSlider` reports a horizontal orientation to assistive technology. +- `ColorArea`'s progress custom properties cannot be overridden by a consumer (the two slider hooks + spread consumer-wins; `ColorArea` spreads hook-wins). +- `SpinButton`'s held-mouse spin does not report its spin state. +- A disabled `ColorSwatch` loses its 1px border on hover. +- A `MenuSplitGroup` whose halves are different element types loses its seam styling (`:nth-of-type` + counts by element type). Keep both halves on the same element type. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/overriding.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/overriding.md new file mode 100644 index 0000000000000..a47cbb101f3af --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/overriding.md @@ -0,0 +1,257 @@ +# Overriding and extending windmod styles + +The consumer override model. Read this when restyling a component, targeting a slot, or when an +override "is not applying". + +## The mechanism: cascade layers + +Every package style lives inside a `fui.*` cascade layer. **Unlayered author CSS beats layered author +CSS regardless of selector weight.** Your stylesheet is unlayered, so it wins — with a single class, +no `!important`, no specificity arms race. + +```css +/* wins over the component's own layered rules */ +.my-brand-button { + background-color: rebeccapurple; +} +``` + +```tsx + +``` + +This is the whole story for most overrides. The corollary is the one thing to remember: + +```css +/* ❌ now loses — you forfeited the contest you win by default */ +@layer app { + .my-brand-button { + background-color: rebeccapurple; + } +} +``` + +### The declared order + +```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; +``` + +`fui.utilities` (Tailwind's utility layer) beats component styles and still loses to your unlayered +CSS. If you run Tailwind yourself and want _your_ utilities to beat Fluent's, declare the family before +importing Tailwind — see [setup.md](setup.md). + +Use a `fui.components.l4` / `l5` layer deliberately only when you want your override to lose to your +own unlayered CSS — app-global defaults that a page-level rule should still be able to beat. + +## Route 1: your own class + +Best for anything reused. Target the identity class, or your own class on the component. + +```css +/* on the identity class — every Button in the app */ +.fui-button { + letter-spacing: 0.01em; +} + +/* a selector confined to a subtree — for scoping a *theme* to one, nest a provider instead */ +.marketing-surface .fui-button { + border-radius: 999px; +} + +/* on a class you add — one component */ +.cta-button { + background-color: rebeccapurple; + color: white; +} +``` + +```tsx + +``` + +Selecting on published state is safe and stable: + +```css +.fui-message-bar[data-intent='error'] { + border-inline-start-width: 4px; +} +.fui-button[data-size='small'] { + text-transform: uppercase; +} +.fui-popover-surface[data-open] { + box-shadow: var(--shadow-28); +} +``` + +Presence attributes select **bare** — `[data-open]`, never `[data-open='true']`. Enumerated attributes +select on their value. + +## Route 2: slot `className` + +Every slot re-stacks its class list with the component's own classes first and your incoming +`className` last, so your declaration wins at equal specificity. + +```tsx + + + +``` + +This is the only sanctioned route to a slot. There is no `classNames` prop, and no per-slot public +class. + +## Route 3: group variants, from a child you control + +A child inside a component targets the component's state directly, with no group declaration of your +own — the marker `group/fui-` is already on the root. + +```tsx + + + + + + +); +``` + +`theme` takes a **class name string**, not a Griffel theme object — but it is equally required. +Griffel makes you `import { webDarkTheme }` and pass it, and leaves tokens unset without one; here +you import `themes/web-dark.css` and pass its class. Same two steps, same absence of a default. + +Any string works, so a custom theme is a class of your own that redeclares the token custom +properties: + +```tsx + +``` + +```css +.my-brand-theme { + --color-brand-background: #6b21a8; + --color-brand-background-hover: #7e22ce; + /* … the rest of the token set you are changing */ +} +``` + +A theme class is custom properties only, so it may be applied to **any** element — you do not need a +provider to scope a theme, though you do need one for the base typography and background. With no +baked default, a custom class that declares the full token set is a standalone theme; one that +declares only overrides is applied alongside a shipped class +(`theme="fui-theme-web-light my-brand-theme"`) and wins by source order in the shared `fui.theme` +layer. + +### The provider is a real element and it paints + +It renders a `div` carrying base typography, text colour and +`background-color: var(--color-neutral-background-1)`. + +- **Layout.** A provider inside your own flex or grid container becomes the flex/grid item — its + children do not. Griffel's provider has always been a real div, so an app migrating from + `@fluentui/react-components` already accounts for this. +- **Paint.** A provider dropped onto a coloured surface repaints it. Pass + `className` with `background: transparent` if that is not what you want. + +`targetDocument` and `dir` are supported. `customStyleHooks_unstable`, `overrides_unstable` and +`applyStylesToPortals` are not. + +## Family imports + +**There is no root barrel** — `@fluentui/react-windmod-preview` exports nothing. Every component comes +from its **family** subpath, kebab-case, and the families are the ones +`@fluentui/react-headless-components-preview` already uses. A family exports every part of that family: + +```tsx +import { Button } from '@fluentui/react-windmod-preview/button'; +import { Card, CardHeader, CardPreview } from '@fluentui/react-windmod-preview/card'; +import { Dialog, DialogSurface, DialogTitle, DialogActions } from '@fluentui/react-windmod-preview/dialog'; +``` + +**52 family subpaths** carry components. The ones whose name is not simply the component's own: +`CardHeader`/`CardFooter`/`CardPreview` → `./card` · `Tab` → `./tab-list` · `Radio` → `./radio-group` · +`Option`/`OptionGroup`/`Listbox` → `./combobox` · `InfoButton` → `./info-label` · +`ColorArea`/`ColorSlider`/`AlphaSlider` → `./color-picker` · +`ColorSwatch`/`EmptySwatch`/`ImageSwatch`/`SwatchPickerRow` → `./swatch-picker` · +`InlineDrawer`/`OverlayDrawer` and every `Drawer*` part → `./drawer` · every `Nav*` **and** every +`NavDrawer*` part → `./nav` · `Toaster` → `./toast` · `FluentProvider` → `./provider` (with the theme +class names) · every `Menu*`, `Toolbar*`, `TagPicker*`, `TeachingPopover*`, `MessageBar*`, +`Breadcrumb*`, `Accordion*`, `AvatarGroup*`, `InteractionTag*`, `Popover*` and `Skeleton*` part → its +family root. + +Family JS tree-shakes; family CSS comes along, by design. + +Subpaths that export no component: + +| Subpath | What it is | +| --------------------- | ------------------------------------------------ | +| `./positioning` | the headless positioning primitives, re-exported | +| `./use-css-var-value` | `useCssVarValue` / `invalidateCssVars` | + +Non-JavaScript subpaths: + +| Subpath | What it is | +| ---------------- | ------------------------------------------------------------- | +| `./base.css` | the root stylesheet — layer order + `@property` registrations | +| `./styles.css` | the batteries-included monolith | +| `./css/*` | the individual per-component chunks | +| `./variants.css` | the component-specific variant catalog, for your own Tailwind | + +## Plain-CSS consumers: no setup at all + +If you are not running Tailwind, you need nothing beyond the two stylesheet imports. Your CSS is +unlayered and beats every `fui.*` layer by default: + +```css +.my-brand-button { + background-color: rebeccapurple; +} +``` + +That is the whole override story. See [overriding.md](overriding.md). + +## Tailwind consumers + +### Make your own utilities beat Fluent's + +Cascade layer order is first-appearance, so declare the `fui` family **before** importing Tailwind: + +```css +@layer fui.theme, fui.base, fui.components, fui.utilities; +@import 'tailwindcss'; +``` + +Without that line, Tailwind's own layers are declared first and `fui.utilities` outranks them. +Copy the list exactly — layer order is first-appearance, so omitting or reordering a name here +would let the theme sheet's own statement introduce it in a different position than every +component chunk was compiled against. (Tailwind's preflight ships inside `fui.base`, at its +head, so it needs no name of its own.) + +### Compose against windmod's states + +To write `group-disabled/fui-button:…` in your own Tailwind, your build has to know those variants +exist. Both catalogs are shipped source and importable: + +```css +/* your Tailwind entry stylesheet */ +@layer fui.theme, fui.base, fui.components, fui.utilities; +@import 'tailwindcss'; + +/* the generic vocabulary — state, structure, positioning, size */ +@import '@fluentui/react-tailwind-theme-preview/css/variants.css'; + +/* the component-specific vocabulary — appearance-*, intent-*, … */ +@import '@fluentui/react-windmod-preview/variants.css'; +``` + +Both files are pure `@custom-variant` declarations and emit no CSS of their own. + +> **Verify this recipe against your bundler before relying on it.** The exports resolve +> (`./variants.css` and `./css/*` are both declared, and both files are in their packages' `files` +> arrays), but the import order and whether your Tailwind entry is processed by the same PostCSS pass +> as your app CSS is build-specific. If a variant compiles to nothing, this is the first thing to +> check. + +### The token layer, if you want Fluent tokens in your own utilities + +`@fluentui/react-tailwind-theme-preview` (the bare specifier) resolves to `css/index.css` — Tailwind +**source**, not plain CSS. It is the reference target the library's own modules use, and it is +deliberately unlike an app Tailwind setup: + +- **preflight ships, at the head of `fui.base`** — Tailwind's preflight is imported as the first + content of `fui.base` (the same placement Tailwind itself gives it), so every component rule — + the library's and yours — outranks it, and the rest of the layer's element resets beat it by + source order; components author over a normalized base instead of inheriting UA quirks +- tokens register via `@theme inline`, so `var(--token)` substitutes into each utility and values stay + per-element custom properties +- Tailwind's default palette, type ramp, radii and shadows are set to `initial` + +Note that `css/index.css` is theme-less, like the emitted `base.css` it compiles to — referencing it +registers every token NAME but gives none of them a value. A Tailwind build that also wants the +values imports the theme sources it needs alongside. + +Individual layers are available at `@fluentui/react-tailwind-theme-preview/css/*` — `index.css`, +`tokens.css`, `themes/.css`, `themes.css` (the all-seven aggregate), `variants.css`, +`utilities.css` — for advanced setups. + +Only `base.css`, `themes/*.css`, `styles.css` and `theme-class-names` are consumable **without** a +Tailwind toolchain. + +## What the layers are for + +```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; +``` + +`fui.base` opens with Tailwind's preflight, followed by the theme's global element resets (the +headless icon defaults). Every component rule — and any rule of yours, layered above or unlayered — +outranks the reset; within the layer the resets that follow preflight beat it by source order. + +The theme package's README describes `l3`–`l5` as available for app-global, per-page and one-off +overrides that should still lose to your unlayered CSS. + +> **Caveat, verified in the current tree:** `fui.components.l3` is no longer empty — four component +> modules author into it (deeper compositions such as ToolbarToggleButton over ToggleButton over +> Button). `l4` and `l5` are still unused. Treat `l4`/`l5` as the safe consumer levels, and prefer +> plain unlayered CSS, which needs no coordination at all. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/tokens-and-scale.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/tokens-and-scale.md new file mode 100644 index 0000000000000..5fc2ec5c2c256 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/tokens-and-scale.md @@ -0,0 +1,208 @@ +# Tokens, sizing and the density knob + +Read this when picking a token, when layout is the wrong size, or when `--base-scale` comes up. + +## The rule + +**Never hardcode a palette value.** Use the kebab-case Fluent theme tokens. The theme drops Tailwind's +default palette, type ramp, radii and shadows entirely — `--color-*`, `--font-*`, `--text-*`, +`--font-weight-*`, `--tracking-*`, `--leading-*`, `--radius-*`, `--shadow-*`, `--inset-shadow-*`, +`--drop-shadow-*`, `--blur-*` and `--animate-*` are all set to `initial` — so a stray `text-red-500` or +`rounded-lg` **fails the build** rather than silently diverging from Fluent. Structural utilities (flex, +grid, positioning, sizing) remain. + +Two ways to reach a token, both valid: + +```css +/* utility form */ +@apply rounded-medium bg-neutral-background-1 text-neutral-foreground-1; + +/* custom-property form, for anything a utility cannot spell */ +box-shadow: 0 0 0 2px var(--color-stroke-focus-2) inset; +``` + +Tokens register through `@theme inline`, which substitutes `var(--token)` into each utility — so values +stay per-element custom properties and continue to respond to theme scoping. (A non-inline `@theme` +alias would freeze resolution at `:root`.) + +## Colour + +| Family | Shape | +| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| `--color-neutral-background-1…8` | plus `-hover`, `-pressed`, `-selected`, `-disabled`, `-static`, `-inverted`, `-alpha` | +| `--color-neutral-foreground-1…5` | plus `-hover`, `-pressed`, `-selected`, `-static`, `-disabled`, `-inverted`, and the `-brand-*` and `-link-*` sub-families | +| `--color-neutral-card-background` | plus `-hover`, `-pressed`, `-selected`, `-disabled` | +| `--color-neutral-stroke-*` | the stroke family | +| `--color-brand-background` | plus `-2`, `-3-static`, `-4-static`, `-hover`, `-pressed`, `-selected`, `-inverted*`, `-static` | +| `--color-brand-foreground-1`, `-2`, `-link`, `-inverted`, `-on-light` | each with state suffixes | +| `--color-brand-stroke-1`, `-2` | plus `-contrast`, `-hover`, `-pressed` | +| `--color-compound-brand-background\|foreground-1\|stroke` | plus `-hover`, `-pressed` | +| `--color-status-*` | success / warning / danger / severe families | +| `--color-stroke-focus-1`, `-2` | the focus indicator colours | +| `--color-background-overlay`, `--color-scrollbar-*`, `--color-subtle-*`, `--color-transparent-*` | | +| `--color-palette-*` | the raw ramp — prefer a semantic token above | + +Utility prefixes are the Tailwind ones: `bg-neutral-background-1`, `text-neutral-foreground-1`, +`border-neutral-stroke-1`. + +**Picking a colour token** — the same four questions each time: + +1. **Purpose** — background (`bg-`), foreground (`text-`), or stroke (`border-`)? +2. **Family** — neutral, brand, compound-brand, or status? +3. **Emphasis** — the numeric step (`-1` is the most prominent surface / strongest text) +4. **State** — resting, `-hover`, `-pressed`, `-selected`, `-disabled` + +> "I need the background of a secondary button in its pressed state" +> → purpose: background → family: neutral → step: 1 → state: pressed +> → `--color-neutral-background-1-pressed` / `bg-neutral-background-1-pressed` + +When matching a Griffel component, read the Griffel source for the token it names rather than guessing +from the rendered colour. + +## Typography + +``` +--text-base-100 … --text-base-600 font sizes +--text-hero-700 … --text-hero-1000 +--leading-base-100 … --leading-base-600 paired line heights (unitless ratios) +--leading-hero-700 … --leading-hero-1000 +--text-icon-12|16|20|24|28|32|48 glyph sizes +--font-base | --font-monospace | --font-numeric +--font-weight-regular|medium|semibold|bold +``` + +Sizes and line heights are **paired by number** — `text-base-300` goes with `leading-base-300`. Use the +pair; picking a line height from a different step is how type drifts off the ramp. A leading token is a +**unitless ratio** of its paired step (`--leading-base-300` is `calc(20 / 14)`), so it is not a length: +it multiplies the element's own font-size, descendants inherit the ratio rather than a px box, and a +JS read wanting a length must multiply by the paired `--text-*` token. + +The icon glyph family has no Fluent token equivalent — the sizes come from the icon set itself, so they +are registered by the theme rather than generated from Fluent's token set. + +## Spacing + +``` +--spacing-horizontal-none|xxs|xs|s|s-nudge|m|m-nudge|l|xl|xxl|xxxl +--spacing-vertical-none|xxs|xs|s|s-nudge|m|m-nudge|l|xl|xxl|xxxl +--spacing-thin|thick|thicker|thickest stroke widths +``` + +Utility form: `px-horizontal-m`, `py-vertical-s`, `border-(length:--spacing-thin)`. + +A **numeric axis** also exists — `p-12`, `gap-8` — reading as px and computing through `--base-scale`. +Both forms share the one density knob. Prefer the named Fluent steps; the numeric axis is for values the +named scale does not carry. + +## Radius, shadow, motion, stacking + +``` +--radius-none|small|medium|large|x-large|circular +--radius-2-x-large … --radius-6-x-large +--shadow-2|4|8|16|28|64 each with a -brand twin +--transition-duration-ultra-fast|faster|fast|normal|gentle|slow|slower|ultra-slow + = 50, 100, 150, 200, 250, 300, 400, 500 ms + utility form: duration-faster +--ease-linear | --ease-easy-ease | --ease-easy-ease-max +--ease-accelerate-min|mid|max | --ease-decelerate-min|mid|max +--z-index-background|content|overlay|popup|messages|floating|priority|debug +``` + +Two custom utilities the theme adds on top: `fui-focus-outline` and `fui-focus-ring`. + +## The density knob + +```css +--base-scale: calc(1rem / 16px * var(--fui-scale, 1)); +--spacing: calc(1px * var(--base-scale)); +--text-base-300: calc(14px * var(--base-scale)); +--radius-medium: calc(4px * var(--base-scale)); +``` + +Griffel writes literal pixels. windmod puts **spacing, control heights, radii, shadows and the whole +type ramp** on one knob. (`--radius-none` is 0 and `--radius-circular` is the 10000px fully-round +sentinel the browser clamps to half the box — neither meaningfully scales, so both stay literal.) + +- At a 16px root font size the two libraries are identical. That is the parity condition, and it is what + the pixel gate verifies. +- Change `html { font-size: … }` and the whole windmod UI scales coherently, where Griffel's would only + move its text. That is an improvement, but it is a **difference**: an app with a non-16px root font + size will see windmod controls larger or smaller than the Griffel ones they replace. +- **`--base-scale` and everything derived from it are declared at the document root.** A raw custom-property + override has to go there too — setting `--base-scale` on a provider or a subtree does nothing, because + descendants inherit the already-substituted token stream. To scale ONE subtree, use the `ScaleRegion` + component (`/provider` export): it stamps the theme package's `.fui-scale-region` class, which is grouped + into the `:root, :host` invariant emission so every scale-riding formula re-substitutes at the region, and + it sets the region's `--fui-scale` factor from `data-fui-scale` via typed attr(). Factors are absolute — + nested regions replace, never compound. + +A handful of literals stay fixed by design where Griffel is also fixed: stroke widths (borders must not +thin with layout density) and a few 1px nudges. + +## What responds at runtime, and what does not + +| Token family | Runtime override? | +| ------------------------ | ---------------------------- | +| colour | ✅ live `var()` reference | +| typography | ✅ live | +| stroke / radius / shadow | ✅ live | +| **spacing** | ❌ **inlined at build time** | + +Tailwind's `--spacing-*` namespace resolves at compile time, so a rule that reads +`var(--spacingHorizontalM)` in Griffel reads `calc(var(--spacing) * 12)` in windmod. Redeclaring a +Fluent _spacing_ token at runtime therefore moves nothing. + +For density, use `--base-scale` at the document root. For a one-off, set the property you actually want +on your own unlayered class. + +## Custom themes + +A theme is a CSS class that redeclares token custom properties. `FluentProvider`'s `theme` accepts any +string: + +```css +.my-brand-theme { + --color-brand-background: #6b21a8; + --color-brand-background-hover: #7e22ce; + --color-brand-background-pressed: #581c87; + --color-compound-brand-background: #6b21a8; +} +``` + +```tsx + +``` + +The seven shipped themes are exactly this — generated classes carrying custom properties only, which is +why a theme class can be applied to any element, not just a provider. The `:root, :host` defaults are +web light. + +**A partial theme is the normal case.** You redeclare the handful of tokens you are actually changing; +every token you leave out inherits from the theme stacked above, by ordinary cascade. There is no +"complete theme" checklist to satisfy — a four-token brand class layered over `webLightThemeClassName` +is a legitimate theme. + +**Write it as plain global CSS.** `theme` takes a literal class-name string, so a CSS-Modules class +would arrive hashed and select nothing. + +**To theme one part of a page, nest a second `FluentProvider` around it** with the inner `theme`. Tokens +are inherited custom properties, so the inner provider redeclares them for its subtree and the rest of +the app keeps the outer theme. A bare `
` moves the tokens too, but only the +provider paints the base background and typography — and, being a real element, the provider becomes the +flex or grid item in your layout. + +Do not port a Griffel theme object; there is nothing to pass it to. + +## Computed-value gotchas + +- **`box-shadow` strings are longer.** Tailwind's shadow and ring utilities prepend fully transparent + layers, so `getComputedStyle(el).boxShadow` returns more than Griffel's `var(--shadow4)` — same + painted result. `shadow-none` computes to five transparent layers rather than the literal `none`. + Snapshot tests asserting on computed `box-shadow` need updating; nothing about the rendering does. +- **Opacity modifiers mix in oklab.** Tailwind's default interpolation space is oklab, so a translucent + colour computes as `oklab(… / 0.3)` where Griffel writes `color-mix(in srgb, … )`. Verified to cost + nothing in pixels on the surfaces it appears on. +- **`text-align` is logical.** The provider sets `text-align: start` where Griffel compiles a + `left`/`right` pair. Identical whenever an element's direction matches the provider's; the one + divergence is a `dir`-flipped subtree inside a provider of the opposite direction, where windmod + follows the subtree (correct i18n) and Griffel stays physically left. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/troubleshooting.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/troubleshooting.md new file mode 100644 index 0000000000000..db34efc500c9f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/troubleshooting.md @@ -0,0 +1,258 @@ +# Troubleshooting + +Keyed on the symptom. Each entry gives the cause and the fix. + +## "My override is not applying" + +The most common problem, and almost always one of four things. + +**Cause 1 — the override is inside an `@layer`.** +Unlayered author CSS beats layered author CSS regardless of selector weight. Putting your rule in a +layer forfeits the contest you win by default, and if your layer was declared before `fui.utilities` it +loses to Tailwind's utilities too. + +```css +/* ❌ */ +@layer app { + .cta { + background: rebeccapurple; + } +} +/* ✅ */ +.cta { + background: rebeccapurple; +} +``` + +**Cause 2 — the rule is layered and you never wrote `@layer`.** +This is Cause 1 with nothing to find in your own file, which is why it survives the first fix and is the +version that actually reaches a bug report. `@layer components { … }` is ordinary Tailwind idiom that +predates windmod in most codebases; some frameworks and bundler CSS pipelines also wrap imported global +stylesheets in a layer of their own. + +```css +/* ❌ a house convention, not a windmod decision — and it still loses */ +@layer components { + .cta { + background: rebeccapurple; + } +} +``` + +Fix: unlayer the rule, or declare your own layer after `fui.utilities`: + +```css +@layer fui.theme, fui.base, fui.components, fui.utilities, app; +``` + +**How to tell Cause 1 from Cause 2 in ten seconds.** Do not reason about it — look. DevTools' Styles +pane labels every matched rule with the layer it landed in (unlayered rules carry no label) and strikes +through the declarations that lost. If your rule shows a layer name you did not write, it is Cause 2. If +it is unlayered and still struck through, the winner is another unlayered rule of yours and this is an +ordinary specificity question, not a windmod one. + +**A note on import order, because it is the usual suspect and it is usually innocent here.** Loading +`@fluentui/react-tailwind-theme-preview/styles.css` first is genuinely required — without it the +components' `var()` references resolve to nothing — and first-appearance order settles how _your_ named +layers sort against `fui.*`. But it cannot break a plain unlayered override: unlayered author CSS +outranks every layer in the origin regardless of which sheet loaded first. If your rule is unlayered and +not applying, import order is not the reason. Keep looking. + +**Cause 3 — the selector targets something that is not public.** +A hashed ident (`fuicm-button-icon-a1b2c3`), a Griffel-era class (`.fui-Button`, `.fui-Button__icon`), +or a structural position (`.fui-button > span:nth-child(2)`). + +Fix: target `.fui-`, a published `data-*`, or a class you added. Reach internals through +group variants. + +**Cause 4 — you are inside the library.** +Package styles are all layered. The consumer override model does not apply; see +[authoring-conventions.md](authoring-conventions.md). + +## "`document.querySelector` throws on a class-name constant" + +**Cause:** `xClassNames.root` is the space-separated **pair** `"fui-button group/fui-button"`, not a +single class. `'.' + pair` is an invalid selector. + +**Fix:** query the identity class. Use the constant only in `className`. + +```tsx +document.querySelectorAll('.fui-button'); // ✅ +document.querySelectorAll('.' + buttonClassNames.root); // ❌ +``` + +## "A `group-…/fui-x` class does nothing" + +**Cause 1:** the variant does not exist under that name. Check both catalogs — see +[variant-catalog.md](variant-catalog.md). + +**Cause 2:** your Tailwind build has not been given the catalogs, so the class compiled to nothing. + +**Fix:** import both into your Tailwind entry stylesheet: + +```css +@import '@fluentui/react-tailwind-theme-preview/css/variants.css'; +@import '@fluentui/react-windmod-preview/variants.css'; +``` + +**Cause 3:** you used a numeric size variant. `size-16` reads as a Tailwind utility and is deliberately +not in the catalog. Numeric `data-size` is coerced instead: +`calc(attr(data-size type()) * 1px)`. + +**Cause 4:** the state is on a native input, not on the root. Use the sibling form — +`peer-checked/fui-switch:` — not the group form. + +## "A Tailwind class fails the build" (`text-red-500`, `rounded-lg`, `shadow-md`) + +**Cause:** the theme sets `--color-*`, `--font-*`, `--text-*`, `--font-weight-*`, `--tracking-*`, +`--leading-*`, `--radius-*`, `--shadow-*` and more to `initial`, deliberately, so a value outside the +Fluent system fails loudly instead of diverging silently. + +**Fix:** use a Fluent token — `text-neutral-foreground-1`, `rounded-medium`, `shadow-16`. See +[tokens-and-scale.md](tokens-and-scale.md). Structural utilities (flex, grid, positioning) still work. + +## "Nothing is styled at all" + +**Cause 1:** `@fluentui/react-tailwind-theme-preview/styles.css` is not imported. The components' `var()` +references resolve to nothing. + +**Cause 2:** CommonJS or an SSR setup that drops the ESM side-effect import of the component +chunks. + +**Fix:** import `@fluentui/react-windmod-preview/styles.css` explicitly — the aggregate carrying +the root sheet and every component in one file. + +## "Borders and shadows are missing, or the wrong component wins" + +**Cause:** `@fluentui/react-windmod-preview/base.css` is not loaded. Component CSS ships per +component; the root sheet carries the two things that cannot live in a chunk — the `@property` +registrations that give Tailwind's `--tw-*` variables their initial values (missing ⇒ borders, +shadows and rings lose their values), and the cascade-layer order (missing ⇒ layers resolve in +whatever order chunks happened to load, so a composed component can lose to its own base). + +A development build logs a warning naming this. It looks for the layer-order declaration rather +than a specific stylesheet URL, so it does not false-positive when `base.css` is `@import`ed into +the app's own root stylesheet. + +**Fix:** load it once, ahead of everything else: + +```js +import '@fluentui/react-windmod-preview/base.css'; +``` + +or at the top of the app's own root stylesheet: + +```css +@import '@fluentui/react-windmod-preview/base.css'; +``` + +Loading `@fluentui/react-windmod-preview/styles.css` instead also fixes it — it inlines the root +sheet. + +## "Everything is the wrong size" + +**Cause:** the app sets a non-16px root font size. `--base-scale` is +`calc(1rem / 16px * var(--fui-scale, 1))`, and spacing, control heights, radii and the whole type ramp +ride it — so the entire UI rescales coherently, away from Griffel's fixed pixels. + +**Fix:** keep `html { font-size: 16px }` for Griffel parity, or accept the rescale deliberately. Do not +try to correct it with a raw `--base-scale` override on a provider — the declaration lives at the +**document root** and an override has to go there too. If the goal is scaling one subtree (not +correcting the document), that is what the `ScaleRegion` component is for. + +## "Overriding a spacing token changes nothing" + +**Cause:** Tailwind's `--spacing-*` namespace resolves at compile time, so `var(--spacingHorizontalM)` +is `calc(var(--spacing) * 12)` in the compiled output. There is no live reference to redeclare. + +**Fix:** use `--base-scale` at the document root for density, or set the property you want directly on +your own unlayered class. Colour, typography and stroke tokens _are_ live and do respond. + +## "`Tooltip` / `Popover` renders in the corner of the page" + +**Cause:** the browser does not support CSS anchor positioning. The headless positioning layer uses it +with no `@supports` guard, no feature detection, no polyfill and no floating-ui fallback. As of today +that means Firefox and Safari, and the failure is total on those engines. + +**Fix:** polyfill CSS anchor positioning, or keep `Tooltip` and `Popover` on +`@fluentui/react-components` — Griffel containers compose over windmod children without trouble. + +## "A popover surface picked up styling from an ancestor" + +**Cause:** the surface is rendered inline and promoted to the native top layer, so it inherits from its +DOM position — `letter-spacing`, `text-transform`, `font-variant`, a wrapper's `color`. A Griffel +portalled surface saw none of that. + +**Fix:** set those properties explicitly on the surface. Related: a trigger outside every +`FluentProvider` renders its surface with the **document root's** theme; wrap the trigger in a provider +to control it. + +## "An animation stopped working" + +**Cause:** the theme ships a global `prefers-reduced-motion` floor — one **unlayered** rule setting 1ms +durations and delays on `*`, `*::before`, `*::after`, with `animation-iteration-count: 1`. + +**Fix:** the floor is unlayered but **selector-less**, so any rule of yours with a class in it already +outranks it. Re-declare the duration from unlayered CSS or inline. There is no `!important` to fight. + +```css +.loading-pulse { + animation-duration: 1.2s; + animation-iteration-count: infinite; +} +``` + +## "`transitionend` is firing for elements I never styled" + +**Cause:** under `prefers-reduced-motion: reduce`, the floor's bare `*` selector leaves +`transition-property` at `all`, so every element in the document carries a 1ms transition on every +animatable property. + +**Fix:** do not key logic on a `transitionend` arriving only for elements you styled. Filter on the +target, or on `propertyName`. + +## "A snapshot test broke after migrating" + +Likely one of: + +- **computed `box-shadow`** — Tailwind's shadow and ring utilities prepend fully transparent layers, so + the string is longer. `shadow-none` computes to five transparent layers, not `none`. Same painted + result. +- **`aria-modal`** — a focus-trapping popover surface no longer carries it; the native `` opened + with `showModal()` conveys modality via `dialog:modal`. +- **`AvatarGroup` overflow button** — carries `aria-haspopup="true"` where Griffel carries a + `data-tabster` focus-restorer. +- **`InfoButton` trigger** — carries `aria-haspopup="true"` always and `aria-details` while open. +- **`role="navigation"` on `NavDrawer`** — it is on the drawer **body**, not the root. +- **class names** — lower-case (`fui-button`), no per-slot classes, hashed internals. + +## "A component behaves differently from its Griffel twin" + +Check [griffel-deltas.md](griffel-deltas.md) first — there are fifty-nine documented differences and this +is very likely one of them. The high-traffic ones: closed `Accordion` panels stay mounted (state +persists), a local prop beats a context value, `Field` does not push `size` into its control, +`TagGroup` does not restore focus after a dismiss, `Card` selection is not focus-aware. + +## "Styles collide in a jest test" + +**Cause:** the test-ident generator drops the component token, so a composed component's locals collide +with its composee's as strings (a composed `Field`'s `.root` equals `Label`'s `.root`). The built idents +differ; only the jest ones collide. + +**Fix:** disambiguate structurally in tests, never by bare class-name equality. + +## "`useCssVarValue` returns a `calc()` string" + +**Cause:** the theme leaves its knobs unregistered (no `@property`), so a custom property's computed +value is the token stream with `var()` substituted and `calc()` **not** evaluated. Spacing, text, stroke +and `--base-scale` all read this way (47 of 472 tokens). + +**Fix:** the hook is for colour and other literal tokens. For a resolved length, read a real property +with `getComputedStyle`, not the token. See [css-var-values.md](css-var-values.md). + +## "`useCssVarValue` returns the fallback in a unit test" + +**Cause:** jsdom's `getComputedStyle` resolves custom properties set as **inline styles** but does not +cascade them from stylesheets. + +**Fix:** set the variable with `element.style.setProperty('--x', '…')` in the test. diff --git a/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/variant-catalog.md b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/variant-catalog.md new file mode 100644 index 0000000000000..c54ae57e635e0 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/skills/fluentui-windmod/references/variant-catalog.md @@ -0,0 +1,275 @@ +# The variant catalog + +The state vocabulary. Read this when composing `group-/fui-` classes, when unsure +whether a variant exists, or when adding one. + +## Two files, and only two + +Variants are **never** defined in a component module. The catalog is exactly two files, both shipped +and both importable: + +| File | Vocabulary | Entries | +| --------------------------------------------------------- | ---------------------------------------------------------------------- | ------- | +| `@fluentui/react-tailwind-theme-preview/css/variants.css` | **generic** — meaningful without knowing any component's API | 95 | +| `@fluentui/react-windmod-preview/variants.css` | **component-specific** — component API values, sectioned per component | ~55 | + +The split test: _would this name mean anything to someone who has never seen this component's props?_ +`disabled`, `placement-above`, `size-small` — yes, generic. `appearance-filled-alternative`, +`text-position-below`, `toaster-position-top-end` — no, component-specific. + +## The canonical form + +Every entry is authored the same way: + +```css +@custom-variant disabled (&:where([disabled], [data-disabled], :disabled)); +@custom-variant appearance-primary (&:where([data-appearance='primary'])); +@custom-variant intent-error (&:where([data-intent='error'])); +``` + +`&:where(…)` does three things at once: + +1. **Zero added specificity** — every catalog variant is flat, so cascade order between rules is decided + by source order, not by an accidental specificity win. +2. **Automatic group composition** — Tailwind rewrites a `&:where(…)` entry into its + `group-/` form for free. An **ancestor-form** entry silently stops group-composing; + that is why the form is mandatory rather than stylistic. +3. **Predictable overrides** — a consumer's unlayered single-class rule always wins. + +Two rules follow: + +- **No union entries.** Comma-separate at the call site — `@variant hover, focus { … }` compiles to one + nested rule per variant. A union entry would be one rule matching either, which composes differently. +- **No numeric `size-*` variants.** `size-16` would read as a Tailwind utility. Numeric `data-size` + values are coerced instead: `calc(attr(data-size type()) * 1px)`. + +At-rule variants such as `forced-colors` cannot take a group prefix — nest the group variant inside +them instead. + +## Generic catalog + +### Direction and orientation + +``` +rtl :dir(rtl) — computed, correct under nesting +ltr :dir(ltr) +vertical [data-orientation='vertical'] +horizontal [data-orientation='horizontal'] +vertical-flag [data-vertical] — the boolean spelling (Slider, Toolbar) +``` + +The headless library spells vertical orientation two ways; they are separate entries, one spelling each. + +### Placement (anchored surfaces) + +``` +placement-above|below|before|after prefix match on the main axis +placement-start|end|top|bottom suffix match on the alignment +placement-above-centered|below-centered +placement-before-centered|after-centered exact, suffix-less +``` + +### Alignment and layout + +``` +align-content-start|center|end +layout-vertical|horizontal|horizontal-stacked|singleline|multiline|grid +label-above|after|before|below data-label-position, always stamped — select on value +``` + +### Generic state + +``` +disabled [disabled], [data-disabled], :disabled +enabled the negation +checked / not-checked +checked-mixed / not-checked-mixed +selected / not-selected +multiselect +current [data-current], [aria-current] not 'false' +open [open], [data-open], :open +expanded +invalid +required +read-only +placeholder-shown +indeterminate / not-indeterminate +submenu-open +``` + +### The disabled family + +Four entries, in narrowing order — pick the narrowest that fits: + +``` +disabled-focusable [data-disabled-focusable] +disabled-native :disabled — native form controls only +disabled [disabled], [data-disabled], :disabled +disabled-control adds [aria-disabled='true'] +enabled-control the negation of disabled-control +``` + +`disabled-native` exists because a component that renders as either a `button` or an `a` needs to reach +the button form alone — `disabled` and `disabled-control` also match the `data-disabled` an anchor +carries. + +### Interaction + +``` +hover :hover +focus :focus +focused / not-focused [data-focused] +active :active +hover-active :hover:active, :active:focus-visible +at-rest :not(:hover, :active:focus-visible) +``` + +### Focus visibility + +``` +focus-visible [data-fui-focus-visible], :focus-visible +focus-within :focus-within — permissive +not-focus-within +focus-within-visible :has(:focus-visible) — keyboard-only, native modality heuristic +activedescendant-focusvisible — the react-aria active-descendant ring +``` + +`focus-within-visible` is the keyboard-only ring: measured equivalent to Griffel's keyborg gate for +non-text controls (no match on mouse click, match on keyboard and on a mouse-then-keyboard modality +switch). Text-editable descendants always match — which is the behaviour text controls want anyway. + +### Size + +``` +size-extra-tiny size-tiny size-extra-small size-small size-medium +size-large size-extra-large size-huge size-full +``` + +A multi-component vocabulary; not every component uses every step. + +### Icons + +``` +icon-only / not-icon-only [data-icon-only] +with-icon [data-icon-position] present +icon-before / icon-after the position value +icon-present / not-icon-present [data-icon] +``` + +### Content presence + +Two readings, deliberately on different attributes, because one element sometimes needs both: + +``` +empty / not-empty [data-empty], :empty — falsiness reading +content-empty / not-content-empty [data-content-empty] — nullish reading +with-description / not-with-description +``` + +`empty` mirrors "no JSX children" with a `:empty` fallback for unstamped elements. `content-empty` +mirrors "children is null or undefined", so a falsy-but-rendered `0` or `''` still counts as present. +Griffel needs both readings on one element (CompoundButton gates its icon margin nullishly, Button's +gate is falsiness), so they cannot share an attribute. + +### Per-component booleans + +``` +inset interactive inline multiline content-before content-after +has-actions / not-has-actions +``` + +## Component-specific catalog + +Sectioned by owning component, in +`@fluentui/react-windmod-preview/variants.css`: + +``` +Accordion expand-icon-position-start|end +Button appearance-secondary|primary|outline|subtle|transparent +Card appearance-filled-alternative (outline, subtle shared with Button) +icons icon — any direct-child Fluent glyph ([data-fui-icon], SVG or font) +bundleIcon icon-filled icon-regular — the glyph swap, direct-child form + variant-filled variant-regular — glyph-self form for deeper-nested glyphs +ColorSlider channel-saturation channel-value (hue is the resting look, no entry) +Dialog modal-type-non-modal nested +Drawer position-start|end|bottom + scroll-state-none|top|middle|bottom, not-scroll-state-none + separator +Field validate-state-error|warning|success +MessageBar/Toast intent-info|success|warning|error +Nav density-small (medium is the base look) +Persona text-position-after|before|below, text-alignment-center +Skeleton animation-pulse appearance-translucent shape-circle shape-square +SpinButton spin-active +Spinner/Toast appearance-inverted +SwatchPicker spacing-small shape-rounded shape-circular +TabList appearance-subtle-circular appearance-filled-circular animating +Toaster toaster-position-top|top-start|top-end|bottom|bottom-start|bottom-end +ToggleButton accessible +``` + +Note the deliberate minimality: **a resting look gets no entry.** `hue` on ColorSlider, `medium` on Nav +density, `start` on Persona text position — each is the base state that everything else overrides, so +there is nothing to select. Similarly `toaster-position-*` is prefixed because a bare `position-start` +already belongs to Drawer and means something else. + +## Using the catalog + +### From a child, via a group variant + +```tsx + + + + + +``` + +Form: `group-/fui-`. No group declaration of your own is required. + +### From a sibling, via a peer variant + +Where the real state lives on a native input — `Switch`, `Radio`, `Checkbox` — the input slot carries +`peer/fui-`: + +```tsx +On +``` + +Never emit a peer marker on the root of a composite control: a consumer's +`peer-checked/fui-x` would silently match a root that is never `:checked`. + +### From your own Tailwind + +Import both catalogs into your Tailwind entry stylesheet so your build knows the variants exist: + +```css +@import '@fluentui/react-tailwind-theme-preview/css/variants.css'; +@import '@fluentui/react-windmod-preview/variants.css'; +``` + +Both files are pure `@custom-variant` declarations and emit no CSS. See [setup.md](setup.md) for the +full entry-stylesheet shape and the caveat about build wiring. + +## If a variant is missing + +**Consumers:** you do not need one. Write the plain attribute selector in your own unlayered CSS — +`.fui-button[data-appearance='primary'] { … }` — which wins outright anyway. + +**Contributors:** add it in the canonical `&:where([data-…])` form, to whichever catalog it belongs to +(generic vs. component-specific), one selector per entry. Keep each catalog minimal. See +[authoring-conventions.md](authoring-conventions.md). + +An entry is vocabulary, not a usage index — having no in-repo consumer is not by itself a reason to +delete one. Three kinds stay unconsumed on purpose: + +- names that **shadow a Tailwind built-in** whose selector is narrower and not `:where()`-flat + (`invalid`, `required`, `read-only`, `ltr`) — dropping the entry silently substitutes the built-in +- the **unused half of a polarity pair** (`focused`, `not-selected`, `not-indeterminate`, `has-actions`) +- the **unstyled member of a live enumeration** (`layout-horizontal`, `layout-singleline`, + `scroll-state-none`) + +Delete an entry only when its selector can match nothing the tree renders, or when it can never be +false. diff --git a/packages/react-components/react-windmod-preview/library/src/accordion.ts b/packages/react-components/react-windmod-preview/library/src/accordion.ts new file mode 100644 index 0000000000000..a5311ac3847f4 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/accordion.ts @@ -0,0 +1,32 @@ +export { Accordion, accordionClassNames, useAccordionStyles } from './components/Accordion'; +export type { AccordionContextValues, AccordionProps, AccordionSlots, AccordionState } from './components/Accordion'; + +export { AccordionHeader, accordionHeaderClassNames, useAccordionHeaderStyles } from './components/AccordionHeader'; +export type { + AccordionHeaderProps, + AccordionHeaderSize, + AccordionHeaderSlots, + AccordionHeaderState, +} from './components/AccordionHeader'; + +export { AccordionItem, accordionItemClassNames, useAccordionItemStyles } from './components/AccordionItem'; +export type { AccordionItemProps, AccordionItemSlots, AccordionItemState } from './components/AccordionItem'; + +export { AccordionPanel, accordionPanelClassNames, useAccordionPanelStyles } from './components/AccordionPanel'; +export type { AccordionPanelProps, AccordionPanelSlots, AccordionPanelState } from './components/AccordionPanel'; + +/** Headless building blocks, re-exported for consumers composing their own Accordion. */ +export { + renderAccordion, + renderAccordionHeader, + renderAccordionItem, + renderAccordionPanel, + useAccordion, + useAccordionContext, + useAccordionContextValues, + useAccordionHeader, + useAccordionHeaderContextValues, + useAccordionItem, + useAccordionItemContextValues, + useAccordionPanel, +} from '@fluentui/react-headless-components-preview/accordion'; diff --git a/packages/react-components/react-windmod-preview/library/src/avatar-group.ts b/packages/react-components/react-windmod-preview/library/src/avatar-group.ts new file mode 100644 index 0000000000000..4070441b44c8e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/avatar-group.ts @@ -0,0 +1,38 @@ +export { AvatarGroup, avatarGroupClassNames, useAvatarGroupStyles } from './components/AvatarGroup'; +export type { AvatarGroupProps, AvatarGroupSlots, AvatarGroupState } from './components/AvatarGroup'; + +export { AvatarGroupItem, avatarGroupItemClassNames, useAvatarGroupItemStyles } from './components/AvatarGroupItem'; +export type { AvatarGroupItemProps, AvatarGroupItemSlots, AvatarGroupItemState } from './components/AvatarGroupItem'; + +export { + AvatarGroupPopover, + avatarGroupPopoverClassNames, + useAvatarGroupPopoverStyles, +} from './components/AvatarGroupPopover'; +export type { + AvatarGroupPopoverPopoverProps, + AvatarGroupPopoverProps, + AvatarGroupPopoverSlots, + AvatarGroupPopoverState, +} from './components/AvatarGroupPopover'; + +/** Headless building blocks, re-exported for consumers composing their own AvatarGroup. + * `useAvatarGroupPopoverContextValues` is withheld: paired with `renderAvatarGroupPopover` it + * produces Griffel-divergent overflow sizing — see AvatarGroupPopover.tsx. */ +export { + AvatarGroupProvider, + partitionAvatarGroupItems, + renderAvatarGroup, + renderAvatarGroupItem, + renderAvatarGroupPopover, + useAvatarGroup, + useAvatarGroupContext, + useAvatarGroupItem, + useAvatarGroupPopover, +} from '@fluentui/react-headless-components-preview/avatar-group'; +export type { + AvatarGroupContextValue, + AvatarGroupContextValues, + PartitionAvatarGroupItems, + PartitionAvatarGroupItemsOptions, +} from '@fluentui/react-headless-components-preview/avatar-group'; diff --git a/packages/react-components/react-windmod-preview/library/src/avatar.ts b/packages/react-components/react-windmod-preview/library/src/avatar.ts new file mode 100644 index 0000000000000..512279c61a4a1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/avatar.ts @@ -0,0 +1,15 @@ +export { Avatar, avatarClassNames, useAvatarStyles } from './components/Avatar'; +export type { + AvatarActive, + AvatarActiveAppearance, + AvatarColor, + AvatarNamedColor, + AvatarProps, + AvatarShape, + AvatarSize, + AvatarSlots, + AvatarState, +} from './components/Avatar'; + +/** Headless building blocks, re-exported for consumers composing their own Avatar. */ +export { renderAvatar, useAvatar } from '@fluentui/react-headless-components-preview/avatar'; diff --git a/packages/react-components/react-windmod-preview/library/src/badge.ts b/packages/react-components/react-windmod-preview/library/src/badge.ts new file mode 100644 index 0000000000000..b0ed6c8dcde17 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/badge.ts @@ -0,0 +1,13 @@ +export { Badge, badgeClassNames, useBadgeStyles } from './components/Badge'; +export type { + BadgeAppearance, + BadgeColor, + BadgeProps, + BadgeShape, + BadgeSize, + BadgeSlots, + BadgeState, +} from './components/Badge'; + +/** Headless building blocks, re-exported for consumers composing their own Badge. */ +export { renderBadge, useBadge } from '@fluentui/react-headless-components-preview/badge'; diff --git a/packages/react-components/react-windmod-preview/library/src/breadcrumb.ts b/packages/react-components/react-windmod-preview/library/src/breadcrumb.ts new file mode 100644 index 0000000000000..babca8921db09 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/breadcrumb.ts @@ -0,0 +1,43 @@ +export { Breadcrumb, breadcrumbClassNames, useBreadcrumbStyles } from './components/Breadcrumb'; +export type { + BreadcrumbContextValues, + BreadcrumbProps, + BreadcrumbSize, + BreadcrumbSlots, + BreadcrumbState, +} from './components/Breadcrumb'; + +export { BreadcrumbButton, breadcrumbButtonClassNames, useBreadcrumbButtonStyles } from './components/BreadcrumbButton'; +export type { + BreadcrumbButtonProps, + BreadcrumbButtonSlots, + BreadcrumbButtonState, +} from './components/BreadcrumbButton'; + +export { + BreadcrumbDivider, + breadcrumbDividerClassNames, + useBreadcrumbDividerStyles, +} from './components/BreadcrumbDivider'; +export type { + BreadcrumbDividerProps, + BreadcrumbDividerSlots, + BreadcrumbDividerState, +} from './components/BreadcrumbDivider'; + +export { BreadcrumbItem, breadcrumbItemClassNames, useBreadcrumbItemStyles } from './components/BreadcrumbItem'; +export type { BreadcrumbItemProps, BreadcrumbItemSlots, BreadcrumbItemState } from './components/BreadcrumbItem'; + +/** Headless building blocks, re-exported for consumers composing their own Breadcrumb. */ +export { + renderBreadcrumb, + renderBreadcrumbButton, + renderBreadcrumbDivider, + renderBreadcrumbItem, + useBreadcrumb, + useBreadcrumbButton, + useBreadcrumbContext, + useBreadcrumbContextValues, + useBreadcrumbDivider, + useBreadcrumbItem, +} from '@fluentui/react-headless-components-preview/breadcrumb'; diff --git a/packages/react-components/react-windmod-preview/library/src/button.ts b/packages/react-components/react-windmod-preview/library/src/button.ts new file mode 100644 index 0000000000000..79b49f03b5d8c --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/button.ts @@ -0,0 +1,12 @@ +export { Button, buttonClassNames, useButtonStyles } from './components/Button'; +export type { + ButtonAppearance, + ButtonProps, + ButtonShape, + ButtonSize, + ButtonSlots, + ButtonState, +} from './components/Button'; + +/** Headless building blocks, re-exported for consumers composing their own Button. */ +export { renderButton, useButton } from '@fluentui/react-headless-components-preview/button'; diff --git a/packages/react-components/react-windmod-preview/library/src/card.ts b/packages/react-components/react-windmod-preview/library/src/card.ts new file mode 100644 index 0000000000000..43de4dc38fc6f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/card.ts @@ -0,0 +1,34 @@ +export { Card, cardClassNames, useCardStyles } from './components/Card'; +export type { + CardAppearance, + CardContextValue, + CardOnSelectionChangeEvent, + CardOrientation, + CardProps, + CardSize, + CardSlots, + CardState, +} from './components/Card'; + +export { CardFooter, cardFooterClassNames, useCardFooterStyles } from './components/CardFooter'; +export type { CardFooterProps, CardFooterSlots, CardFooterState } from './components/CardFooter'; + +export { CardHeader, cardHeaderClassNames, useCardHeaderStyles } from './components/CardHeader'; +export type { CardHeaderProps, CardHeaderSlots, CardHeaderState } from './components/CardHeader'; + +export { CardPreview, cardPreviewClassNames, useCardPreviewStyles } from './components/CardPreview'; +export type { CardPreviewProps, CardPreviewSlots, CardPreviewState } from './components/CardPreview'; + +/** Headless building blocks, re-exported for consumers composing their own Card. */ +export { + renderCard, + renderCardFooter, + renderCardHeader, + renderCardPreview, + useCard, + useCardContext, + useCardContextValue, + useCardFooter, + useCardHeader, + useCardPreview, +} from '@fluentui/react-headless-components-preview/card'; diff --git a/packages/react-components/react-windmod-preview/library/src/checkbox.ts b/packages/react-components/react-windmod-preview/library/src/checkbox.ts new file mode 100644 index 0000000000000..31fc6546d625d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/checkbox.ts @@ -0,0 +1,5 @@ +export { Checkbox, checkboxClassNames, useCheckboxStyles } from './components/Checkbox'; +export type { CheckboxProps, CheckboxShape, CheckboxSize, CheckboxSlots, CheckboxState } from './components/Checkbox'; + +/** Headless building blocks, re-exported for consumers composing their own Checkbox. */ +export { renderCheckbox, useCheckbox } from '@fluentui/react-headless-components-preview/checkbox'; diff --git a/packages/react-components/react-windmod-preview/library/src/color-picker.ts b/packages/react-components/react-windmod-preview/library/src/color-picker.ts new file mode 100644 index 0000000000000..57ee6725de512 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/color-picker.ts @@ -0,0 +1,26 @@ +export { ColorPicker, colorPickerClassNames, useColorPickerStyles } from './components/ColorPicker'; +export type { ColorPickerProps, ColorPickerShape, ColorPickerSlots, ColorPickerState } from './components/ColorPicker'; + +export { AlphaSlider, alphaSliderClassNames, useAlphaSliderStyles } from './components/AlphaSlider'; +export type { AlphaSliderProps, AlphaSliderSlots, AlphaSliderState } from './components/AlphaSlider'; + +export { ColorArea, colorAreaClassNames, useColorAreaStyles } from './components/ColorArea'; +export type { ColorAreaProps, ColorAreaSlots, ColorAreaState } from './components/ColorArea'; + +export { ColorSlider, colorSliderClassNames, useColorSliderStyles } from './components/ColorSlider'; +export type { ColorSliderProps, ColorSliderSlots, ColorSliderState } from './components/ColorSlider'; + +/** Headless building blocks, re-exported for consumers composing their own ColorPicker. */ +export { + renderAlphaSlider, + renderColorArea, + renderColorPicker, + renderColorSlider, + useAlphaSlider, + useColorArea, + useColorPicker, + useColorPickerContextValue, + useColorPickerContextValues, + useColorSlider, +} from '@fluentui/react-headless-components-preview/color-picker'; +export type { ColorPickerContextValue } from '@fluentui/react-headless-components-preview/color-picker'; diff --git a/packages/react-components/react-windmod-preview/library/src/combobox.ts b/packages/react-components/react-windmod-preview/library/src/combobox.ts new file mode 100644 index 0000000000000..3bc8100a356c0 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/combobox.ts @@ -0,0 +1,32 @@ +export { Combobox, comboboxClassNames, useComboboxStyles } from './components/Combobox'; +export type { + ComboboxAppearance, + ComboboxProps, + ComboboxSize, + ComboboxSlots, + ComboboxState, +} from './components/Combobox'; + +export { Listbox, listboxClassNames, useListboxStyles } from './components/Listbox'; +export type { ListboxContextValues, ListboxProps, ListboxSlots, ListboxState } from './components/Listbox'; + +export { Option, optionClassNames, useOptionStyles } from './components/Option'; +export type { OptionProps, OptionSlots, OptionState } from './components/Option'; + +export { OptionGroup, optionGroupClassNames, useOptionGroupStyles } from './components/OptionGroup'; +export type { OptionGroupProps, OptionGroupSlots, OptionGroupState } from './components/OptionGroup'; + +/** Headless building blocks, re-exported for consumers composing their own Combobox. */ +export { + renderCombobox, + renderListbox, + renderOption, + renderOptionGroup, + useCombobox, + useComboboxContextValues, + useComboboxFilter, + useListbox, + useListboxContextValues, + useOption, + useOptionGroup, +} from '@fluentui/react-headless-components-preview/combobox'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 0000000000000..df2e45b10afc6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.test.tsx @@ -0,0 +1,171 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { AccordionHeader } from '../AccordionHeader/AccordionHeader'; +import { AccordionItem } from '../AccordionItem/AccordionItem'; +import { AccordionPanel } from '../AccordionPanel/AccordionPanel'; +import { Accordion } from './Accordion'; +import type { AccordionState } from './Accordion.types'; +import { accordionClassNames, useAccordionStyles } from './useAccordionStyles'; + +// Every state the component hands to the headless context-values helper, in call order. +const mockContextValuesStates: { root?: { className?: string } }[] = []; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/accordion', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/accordion'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAccordion: (...args: Parameters) => deepFreezeState(actual.useAccordion(...args)), + useAccordionContextValues: (state: { root?: { className?: string } }) => { + mockContextValuesStates.push(state); + + return actual.useAccordionContextValues(state); + }, + }; +}); + +const item = (value: string) => ( + + {value} + body {value} + +); + +const renderAccordionTree = (props: React.ComponentProps = {}) => { + const result = render( + + {item('a')} + {item('b')} + , + ); + + return { + ...result, + toggle: (value: string) => fireEvent.click(result.getByTestId(`header-${value}`).querySelector('button')!), + isOpen: (value: string) => result.getByTestId(`item-${value}`).hasAttribute('data-open'), + }; +}; + +describe('Accordion', () => { + beforeEach(() => { + mockContextValuesStates.length = 0; + }); + + isConformant({ + Component: Accordion, + displayName: 'Accordion', + }); + + it('stamps its marker pair, slash-free class first', () => { + // This component ships no stylesheet of its own — the base typography and text colour every + // panel paints come from FluentProvider — so the marker set is the whole class contract. + const { getByTestId } = renderAccordionTree(); + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-accordion'); + expect(root).toHaveClass('group/fui-accordion'); + expect(root.classList[0]).toBe('fui-accordion'); + expect(accordionClassNames.root).toBe('fui-accordion group/fui-accordion'); + }); + + it.each([ + ['data-collapsible', { collapsible: true }], + ['data-multiple', { multiple: true }], + ] as const)('carries the headless %s stamp', (attribute, props) => { + const stamped = renderAccordionTree(props); + + expect(stamped.getByTestId('root').getAttribute(attribute)).toBe(''); + stamped.unmount(); + + expect(renderAccordionTree().getByTestId('root').hasAttribute(attribute)).toBe(false); + }); + + it('opens the items named by defaultOpenItems on mount', () => { + const tree = renderAccordionTree({ defaultOpenItems: 'b' }); + + expect(tree.isOpen('a')).toBe(false); + expect(tree.isOpen('b')).toBe(true); + }); + + it('opens one item at a time by default', () => { + const tree = renderAccordionTree(); + + tree.toggle('a'); + tree.toggle('b'); + + expect(tree.isOpen('a')).toBe(false); + expect(tree.isOpen('b')).toBe(true); + }); + + it('opens several items with multiple', () => { + const tree = renderAccordionTree({ multiple: true }); + + tree.toggle('a'); + tree.toggle('b'); + + expect(tree.isOpen('a')).toBe(true); + expect(tree.isOpen('b')).toBe(true); + }); + + it('closes the last open item with collapsible', () => { + const tree = renderAccordionTree({ collapsible: true, defaultOpenItems: 'a' }); + + tree.toggle('a'); + + expect(tree.isOpen('a')).toBe(false); + }); + + it('honours a controlled openItems and does not self-toggle', () => { + const onToggle = jest.fn(); + const tree = renderAccordionTree({ openItems: 'a', onToggle }); + + expect(tree.isOpen('a')).toBe(true); + + tree.toggle('b'); + + expect(onToggle).toHaveBeenCalledTimes(1); + expect(tree.isOpen('b')).toBe(false); + expect(tree.isOpen('a')).toBe(true); + }); + + it('builds its context values from the styled state', () => { + renderAccordionTree(); + + expect(mockContextValuesStates[mockContextValuesStates.length - 1].root!.className).toContain('fui-accordion'); + }); + + it('passes consumer props and the ref through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + {item('a')} + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('acc'); + expect(root.getAttribute('aria-label')).toBe('Sections'); + expect(root).toHaveClass('consumer'); + expect(root.style.zIndex).toBe('6'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'div' }, + root: { as: 'div', className: 'consumer' }, + } as unknown as AccordionState; + + const styled = useAccordionStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(state.root.className).toBe('consumer'); + expect(styled.root.className).toContain('consumer'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.tsx b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000000000..26f77cddbe142 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.tsx @@ -0,0 +1,25 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderAccordion, + useAccordion, + useAccordionContextValues, +} from '@fluentui/react-headless-components-preview/accordion'; + +import type { AccordionProps } from './Accordion.types'; +import { useAccordionStyles } from './useAccordionStyles'; + +/** + * An Accordion is a set of collapsible sections. Windmod Accordion: the headless accordion + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Accordion: ForwardRefComponent = React.forwardRef((props, ref) => { + const styled = useAccordionStyles(useAccordion(props, ref)); + + return renderAccordion(styled, useAccordionContextValues(styled)); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +Accordion.displayName = 'Accordion'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.types.ts new file mode 100644 index 0000000000000..06f2497a76d05 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Accordion/Accordion.types.ts @@ -0,0 +1,12 @@ +import type { + AccordionProps as AccordionHeadlessProps, + AccordionState as AccordionHeadlessState, +} from '@fluentui/react-headless-components-preview/accordion'; + +export type { AccordionContextValues, AccordionSlots } from '@fluentui/react-headless-components-preview/accordion'; + +/** Windmod Accordion props. The headless surface carries every prop; windmod adds no look props. */ +export type AccordionProps = AccordionHeadlessProps; + +/** Windmod Accordion state. */ +export type AccordionState = AccordionHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Accordion/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Accordion/index.ts new file mode 100644 index 0000000000000..fef0f7b2540b3 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Accordion/index.ts @@ -0,0 +1,3 @@ +export { Accordion } from './Accordion'; +export { accordionClassNames, useAccordionStyles } from './useAccordionStyles'; +export type { AccordionContextValues, AccordionProps, AccordionSlots, AccordionState } from './Accordion.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Accordion/useAccordionStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Accordion/useAccordionStyles.ts new file mode 100644 index 0000000000000..051503b200c86 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Accordion/useAccordionStyles.ts @@ -0,0 +1,19 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { AccordionState } from './Accordion.types'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const accordionClassNames: { root: string } = { + root: componentMarkers('accordion'), +}; + +/** + * Applies the visual contract, returning new state. Griffel's Accordion authors no rules at all, + * and the base typography and text colour every panel paints come from FluentProvider, so this + * component ships no stylesheet of its own. + */ +export const useAccordionStyles = (state: AccordionState): AccordionState => ({ + ...state, + root: { ...state.root, className: clsx(accordionClassNames.root, state.root.className) }, +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.module.css b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.module.css new file mode 100644 index 0000000000000..0584b9bbdeca4 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.module.css @@ -0,0 +1,94 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply m-0 rounded-medium bg-transparent-background text-neutral-foreground-1; + + @variant inline { + @apply inline-block; + } + + @variant disabled { + @apply bg-none text-neutral-foreground-disabled; + } + } + + /* Griffel's button reset also sets font-family, font-size, line-height and padding; each of + those is overwritten below at every size, so only the surviving reset values are authored. */ + .button { + @apply relative box-border flex min-h-44 w-full cursor-pointer items-center overflow-visible border-0 bg-inherit py-0 ps-horizontal-m-nudge pe-horizontal-m font-base text-base-300 leading-base-300 font-regular text-inherit outline-none; + text-align: unset; + -webkit-appearance: button; + + @variant group-size-small/fui-accordion-header { + @apply min-h-32 text-base-200 leading-[calc(20/12)]; + } + + @variant group-size-large/fui-accordion-header { + @apply text-base-400 leading-base-400; + } + + @variant group-size-extra-large/fui-accordion-header { + @apply text-base-500 leading-base-500; + } + + @variant group-inline/fui-accordion-header { + @apply inline-flex; + } + + @variant group-expand-icon-position-end/fui-accordion-header { + @apply pe-horizontal-m-nudge; + + @variant group-not-icon-present/fui-accordion-header { + @apply ps-horizontal-m; + } + } + + @variant group-disabled/fui-accordion-header { + @apply cursor-not-allowed; + } + + @variant focus-visible { + @apply fui-focus-outline; + } + } + + .expand-icon { + @apply flex h-full items-center text-base-500 leading-base-500; + + @variant group-expand-icon-position-start/fui-accordion-header { + @apply pe-horizontal-s; + } + + @variant group-expand-icon-position-end/fui-accordion-header { + @apply flex-1 justify-end ps-horizontal-s; + } + + /* The chevron carries no module class of its own, so it is reached through the slot. Direct + children only: a descendant selector would rotate an svg's own path a second time. The four + rotations are equal-specificity, so their source order is the cascade. */ + & > * { + @apply rotate-0 transition-[rotate] duration-normal ease-[ease-out]; + + @variant rtl { + @apply rotate-180; + } + + @variant group-open/fui-accordion-header { + @apply rotate-90; + } + + @variant group-expand-icon-position-end/fui-accordion-header { + @apply rotate-90; + + @variant group-open/fui-accordion-header { + @apply -rotate-90; + } + } + } + } + + .icon { + @apply flex h-full items-center pe-horizontal-s text-base-500 leading-base-500; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.test.tsx new file mode 100644 index 0000000000000..c8649ebcb7d52 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.test.tsx @@ -0,0 +1,222 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { Accordion } from '../Accordion/Accordion'; +import { AccordionItem } from '../AccordionItem/AccordionItem'; +import { AccordionHeader } from './AccordionHeader'; +import type { AccordionHeaderProps, AccordionHeaderState } from './AccordionHeader.types'; +import { accordionHeaderClassNames, useAccordionHeaderStyles } from './useAccordionHeaderStyles'; + +import styles from './AccordionHeader.module.css'; + +// Every state the component hands to the headless context-values helper, in call order. +const mockContextValuesStates: { size?: string; inline?: boolean }[] = []; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/accordion', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/accordion'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAccordionHeader: (...args: Parameters) => + deepFreezeState(actual.useAccordionHeader(...args)), + useAccordionHeaderContextValues: (state: { size?: string; inline?: boolean }) => { + mockContextValuesStates.push(state); + + return actual.useAccordionHeaderContextValues(state); + }, + }; +}); + +const sizes = ['small', 'medium', 'large', 'extra-large'] as const; + +// Read structurally, never by class query: a dropped module class must be able to fail a test +// rather than hide the element it was dropped from. +const parts = (root: HTMLElement) => { + const button = root.querySelector('button')!; + + return { + root, + button, + expandIcon: button.querySelector('span'), + icon: button.querySelector('div'), + }; +}; + +const renderHeader = (props: AccordionHeaderProps = {}) => { + const { container } = render(Section); + + return parts(container.firstElementChild as HTMLElement); +}; + +describe('AccordionHeader', () => { + beforeEach(() => { + mockContextValuesStates.length = 0; + }); + + isConformant({ + Component: AccordionHeader, + displayName: 'AccordionHeader', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { root } = renderHeader(); + + expect(root).toHaveClass('fui-accordion-header'); + expect(root).toHaveClass('group/fui-accordion-header'); + expect(root.classList[0]).toBe('fui-accordion-header'); + expect(accordionHeaderClassNames.root).toBe('fui-accordion-header group/fui-accordion-header'); + }); + + it('decorates the root, button and expandIcon slots with their own classes', () => { + const { root, button, expandIcon } = renderHeader(); + + expect(root).toHaveClass(styles.root); + expect(button).toHaveClass(styles.button); + expect(expandIcon).toHaveClass(styles.expandIcon); + }); + + it('decorates the icon slot when one is supplied', () => { + const { icon } = renderHeader({ icon: }); + + expect(icon).toHaveClass(styles.icon); + }); + + it.each(sizes)('stamps data-size=%s', size => { + expect(renderHeader({ size }).root.getAttribute('data-size')).toBe(size); + }); + + it('defaults data-size to medium', () => { + expect(renderHeader().root.getAttribute('data-size')).toBe('medium'); + }); + + it('stamps data-inline only when inline', () => { + expect(renderHeader({ inline: true }).root.getAttribute('data-inline')).toBe('true'); + expect(renderHeader().root.hasAttribute('data-inline')).toBe(false); + }); + + it('stamps data-icon only when an icon slot resolved', () => { + expect(renderHeader({ icon: }).root.getAttribute('data-icon')).toBe('true'); + expect(renderHeader().root.hasAttribute('data-icon')).toBe(false); + expect(renderHeader({ icon: null }).root.hasAttribute('data-icon')).toBe(false); + }); + + it('feeds the look props to the header context through the styled state', () => { + renderHeader({ size: 'large', inline: true }); + + // The headless state omits both, so children would read `undefined` if the unstyled state + // reached the helper instead. + expect(mockContextValuesStates[mockContextValuesStates.length - 1]).toMatchObject({ + size: 'large', + inline: true, + }); + }); + + it('defaults the size it publishes to the header context', () => { + renderHeader(); + + expect(mockContextValuesStates[mockContextValuesStates.length - 1]).toMatchObject({ size: 'medium' }); + }); + + it('restores one chevron glyph, carrying no inline rotation of its own', () => { + const { expandIcon } = renderHeader(); + const glyphs = expandIcon!.querySelectorAll('svg'); + + expect(glyphs).toHaveLength(1); + // The rotation is a stylesheet rule keyed off the root, not an inline transform. + expect(glyphs[0].hasAttribute('style')).toBe(false); + }); + + it('restores the glyph as the only element child of the slot', () => { + // The rotation rule targets direct children only, so a second element child would rotate too. + expect(renderHeader().expandIcon!.children).toHaveLength(1); + }); + + it('keeps a consumer expandIcon child instead of the chevron', () => { + const { expandIcon } = renderHeader({ expandIcon: { children: } }); + + expect(expandIcon!.querySelectorAll('svg')).toHaveLength(0); + expect(expandIcon!.querySelector('b')).not.toBeNull(); + }); + + it('renders no expandIcon slot when it is suppressed', () => { + expect(renderHeader({ expandIcon: null }).expandIcon).toBeNull(); + }); + + it('carries the headless stamps the styles hook must not rebuild', () => { + expect(renderHeader().root.getAttribute('data-expand-icon-position')).toBe('start'); + expect(renderHeader({ expandIconPosition: 'end' }).root.getAttribute('data-expand-icon-position')).toBe('end'); + }); + + it('takes open and disabled from the item context', () => { + const { getByTestId } = render( + + + Open + + + Disabled + + , + ); + + expect(getByTestId('open').getAttribute('data-open')).toBe(''); + expect(getByTestId('disabled').getAttribute('data-disabled')).toBe(''); + expect(getByTestId('disabled').querySelector('button')!.disabled).toBe(true); + }); + + it('keeps consumer class names on every slot', () => { + const { root, button, expandIcon, icon } = renderHeader({ + className: 'consumer-root', + button: { className: 'consumer-button' }, + expandIcon: { className: 'consumer-expand' }, + icon: { className: 'consumer-icon', children: }, + }); + + expect(root).toHaveClass('consumer-root'); + expect(button).toHaveClass('consumer-button'); + expect(expandIcon).toHaveClass('consumer-expand'); + expect(icon).toHaveClass('consumer-icon'); + }); + + it('passes consumer props and the ref through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Section + , + ); + + const root = getByTestId('root'); + + expect(root.tagName).toBe('H3'); + expect(root.id).toBe('hdr'); + expect(root.getAttribute('aria-label')).toBe('Section'); + expect(root.style.zIndex).toBe('3'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'div', button: 'button', expandIcon: 'span', icon: 'div' }, + root: { as: 'div', className: 'consumer' }, + button: { as: 'button', className: 'consumer-button' }, + expandIcon: { as: 'span', className: 'consumer-expand' }, + icon: { as: 'div', className: 'consumer-icon' }, + inline: false, + size: 'medium', + } as unknown as AccordionHeaderState; + + const styled = useAccordionHeaderStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.button).not.toBe(state.button); + expect(styled.expandIcon).not.toBe(state.expandIcon); + expect(styled.icon).not.toBe(state.icon); + expect(state.root.className).toBe('consumer'); + expect(state.button.className).toBe('consumer-button'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.tsx new file mode 100644 index 0000000000000..2ebf591ef2d6a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderAccordionHeader, + useAccordionHeader, + useAccordionHeaderContextValues, +} from '@fluentui/react-headless-components-preview/accordion'; +import { ChevronRightRegular } from '@fluentui/react-icons/headless/svg/chevron-right'; + +import type { AccordionHeaderProps } from './AccordionHeader.types'; +import { useAccordionHeaderStyles } from './useAccordionHeaderStyles'; + +/** + * An AccordionHeader is the button that toggles one AccordionItem. Windmod AccordionHeader: the + * headless header decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const AccordionHeader: ForwardRefComponent = React.forwardRef( + ({ inline = false, size = 'medium', ...rest }, ref) => { + const base = useAccordionHeader(rest, ref); + + // The headless surface builds the expand-icon slot but leaves it empty. The glyph carries no + // rotation of its own: which way it points is a function of open state, icon position and + // direction, all of which the stylesheet reads off the header root. + const styled = useAccordionHeaderStyles({ + ...base, + inline, + size, + expandIcon: base.expandIcon && { + ...base.expandIcon, + children: base.expandIcon.children ?? , + }, + }); + + // The context values are built from the styled state: the headless state omits `size`, so + // children reading the header context see `undefined` unless windmod feeds it back in. + return renderAccordionHeader(styled, useAccordionHeaderContextValues(styled)); + // Casting is required due to lack of distributive union to support union on @types/react + }, +) as ForwardRefComponent; + +AccordionHeader.displayName = 'AccordionHeader'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.types.ts new file mode 100644 index 0000000000000..0d7ba83095df1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/AccordionHeader.types.ts @@ -0,0 +1,27 @@ +import type { + AccordionHeaderProps as AccordionHeaderHeadlessProps, + AccordionHeaderState as AccordionHeaderHeadlessState, +} from '@fluentui/react-headless-components-preview/accordion'; + +export type { AccordionHeaderSlots } from '@fluentui/react-headless-components-preview/accordion'; + +/** Size of an AccordionHeader. `'medium'` is the base look. */ +export type AccordionHeaderSize = 'small' | 'medium' | 'large' | 'extra-large'; + +/** + * Windmod AccordionHeader props: the headless header plus the look props the headless surface + * deliberately omits. + */ +export type AccordionHeaderProps = AccordionHeaderHeadlessProps & { + /** + * Lays the header out inline rather than as a block. + * @default false + */ + inline?: boolean; + /** @default 'medium' */ + size?: AccordionHeaderSize; +}; + +/** Windmod AccordionHeader state: headless state plus the resolved look props. */ +export type AccordionHeaderState = AccordionHeaderHeadlessState & + Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/index.ts new file mode 100644 index 0000000000000..3e706951cc76a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/index.ts @@ -0,0 +1,8 @@ +export { AccordionHeader } from './AccordionHeader'; +export { accordionHeaderClassNames, useAccordionHeaderStyles } from './useAccordionHeaderStyles'; +export type { + AccordionHeaderProps, + AccordionHeaderSize, + AccordionHeaderSlots, + AccordionHeaderState, +} from './AccordionHeader.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/useAccordionHeaderStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/useAccordionHeaderStyles.ts new file mode 100644 index 0000000000000..9dd9e254dd44a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionHeader/useAccordionHeaderStyles.ts @@ -0,0 +1,38 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { AccordionHeaderState } from './AccordionHeader.types'; + +import styles from './AccordionHeader.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const accordionHeaderClassNames: { root: string } = { + root: componentMarkers('accordion-header'), +}; + +type AccordionHeaderRootDataAttributes = { + 'data-icon'?: true; + 'data-inline'?: true; + 'data-size'?: AccordionHeaderState['size']; +}; + +/** Applies the visual contract, returning new state. */ +export const useAccordionHeaderStyles = (state: AccordionHeaderState): AccordionHeaderState => { + const root: AccordionHeaderState['root'] & AccordionHeaderRootDataAttributes = { + ...state.root, + // The end-position button reclaims the icon slot's leading padding when no icon is supplied. + 'data-icon': state.icon ? true : undefined, + 'data-inline': state.inline || undefined, + 'data-size': state.size, + className: clsx(accordionHeaderClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + button: slotClasses(state.button, styles.button), + expandIcon: slotClasses(state.expandIcon, styles.expandIcon), + icon: slotClasses(state.icon, styles.icon), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.test.tsx new file mode 100644 index 0000000000000..557819d0b4a2a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.test.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { Accordion } from '../Accordion/Accordion'; +import { AccordionHeader } from '../AccordionHeader/AccordionHeader'; +import { AccordionPanel } from '../AccordionPanel/AccordionPanel'; +import { AccordionItem } from './AccordionItem'; +import type { AccordionItemState } from './AccordionItem.types'; +import { accordionItemClassNames, useAccordionItemStyles } from './useAccordionItemStyles'; + +// Every state the component hands to the headless context-values helper, in call order. +const mockContextValuesStates: { root?: { className?: string } }[] = []; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/accordion', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/accordion'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAccordionItem: (...args: Parameters) => + deepFreezeState(actual.useAccordionItem(...args)), + useAccordionItemContextValues: (state: { root?: { className?: string } }) => { + mockContextValuesStates.push(state); + + return actual.useAccordionItemContextValues(state); + }, + }; +}); + +const renderTrio = () => + render( + + + Section + Body + + , + ); + +describe('AccordionItem', () => { + beforeEach(() => { + mockContextValuesStates.length = 0; + }); + + isConformant({ + Component: AccordionItem, + displayName: 'AccordionItem', + requiredProps: { value: 'a' } as never, + }); + + it('stamps its marker pair, slash-free class first', () => { + // This component ships no stylesheet of its own: the marker set is the only class-shaped + // evidence that its styles hook still runs. + const { getByTestId } = renderTrio(); + const root = getByTestId('item'); + + expect(root).toHaveClass('fui-accordion-item'); + expect(root).toHaveClass('group/fui-accordion-item'); + expect(root.classList[0]).toBe('fui-accordion-item'); + expect(accordionItemClassNames.root).toBe('fui-accordion-item group/fui-accordion-item'); + }); + + it('carries the headless open and disabled stamps', () => { + const { getByTestId } = render( + + + Open + + + Disabled + + , + ); + + expect(getByTestId('open').getAttribute('data-open')).toBe(''); + expect(getByTestId('disabled').getAttribute('data-disabled')).toBe(''); + expect(getByTestId('disabled').hasAttribute('data-open')).toBe(false); + }); + + it('drives its header and panel from one toggle', () => { + const { getByTestId } = renderTrio(); + + expect(getByTestId('item').hasAttribute('data-open')).toBe(false); + + fireEvent.click(getByTestId('header').querySelector('button')!); + + for (const id of ['item', 'header', 'panel']) { + expect(getByTestId(id).getAttribute('data-open')).toBe(''); + } + }); + + it('does not toggle a disabled item', () => { + const { getByTestId } = render( + + + Section + + , + ); + + fireEvent.click(getByTestId('header').querySelector('button')!); + + expect(getByTestId('item').hasAttribute('data-open')).toBe(false); + }); + + it('builds its context values from the styled state', () => { + renderTrio(); + + expect(mockContextValuesStates[mockContextValuesStates.length - 1].root!.className).toContain('fui-accordion-item'); + }); + + it('passes consumer props and the ref through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Section + , + ); + + const root = getByTestId('item'); + + expect(root.id).toBe('itm'); + expect(root).toHaveClass('consumer'); + expect(root.style.zIndex).toBe('5'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'div' }, + root: { as: 'div', className: 'consumer' }, + } as unknown as AccordionItemState; + + const styled = useAccordionItemStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(state.root.className).toBe('consumer'); + expect(styled.root.className).toContain('consumer'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.tsx new file mode 100644 index 0000000000000..4bd9f7efef831 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.tsx @@ -0,0 +1,25 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderAccordionItem, + useAccordionItem, + useAccordionItemContextValues, +} from '@fluentui/react-headless-components-preview/accordion'; + +import type { AccordionItemProps } from './AccordionItem.types'; +import { useAccordionItemStyles } from './useAccordionItemStyles'; + +/** + * An AccordionItem pairs one header with one panel. Windmod AccordionItem: the headless item + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const AccordionItem: ForwardRefComponent = React.forwardRef((props, ref) => { + const styled = useAccordionItemStyles(useAccordionItem(props, ref)); + + return renderAccordionItem(styled, useAccordionItemContextValues(styled)); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +AccordionItem.displayName = 'AccordionItem'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.types.ts new file mode 100644 index 0000000000000..54a0226d8695f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/AccordionItem.types.ts @@ -0,0 +1,12 @@ +import type { + AccordionItemProps as AccordionItemHeadlessProps, + AccordionItemState as AccordionItemHeadlessState, +} from '@fluentui/react-headless-components-preview/accordion'; + +export type { AccordionItemSlots } from '@fluentui/react-headless-components-preview/accordion'; + +/** Windmod AccordionItem props. The headless surface carries every prop; windmod adds no look props. */ +export type AccordionItemProps = AccordionItemHeadlessProps; + +/** Windmod AccordionItem state. */ +export type AccordionItemState = AccordionItemHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/index.ts new file mode 100644 index 0000000000000..081e457c63b71 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/index.ts @@ -0,0 +1,3 @@ +export { AccordionItem } from './AccordionItem'; +export { accordionItemClassNames, useAccordionItemStyles } from './useAccordionItemStyles'; +export type { AccordionItemProps, AccordionItemSlots, AccordionItemState } from './AccordionItem.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/useAccordionItemStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/useAccordionItemStyles.ts new file mode 100644 index 0000000000000..af02c4dcdbdab --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionItem/useAccordionItemStyles.ts @@ -0,0 +1,18 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { AccordionItemState } from './AccordionItem.types'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const accordionItemClassNames: { root: string } = { + root: componentMarkers('accordion-item'), +}; + +/** + * Applies the visual contract, returning new state. Griffel's AccordionItem authors no rules, so + * there is no stylesheet of its own — the marker pair is the whole contract. + */ +export const useAccordionItemStyles = (state: AccordionItemState): AccordionItemState => ({ + ...state, + root: { ...state.root, className: clsx(accordionItemClassNames.root, state.root.className) }, +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.module.css b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.module.css new file mode 100644 index 0000000000000..cc37306ca5bc3 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.module.css @@ -0,0 +1,14 @@ +@reference '#theme'; + +@layer fui.components.l1 { + /* A closed panel stays mounted here: the headless panel drops the presence wrapper that + carried both the collapse animation and its unmount, keeping only tabIndex and inert. + State inside a closed panel therefore survives a close/open cycle. */ + .root { + @apply mx-horizontal-m my-0 hidden; + + @variant open { + @apply block; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.test.tsx new file mode 100644 index 0000000000000..c99aa135feb91 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.test.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { Accordion } from '../Accordion/Accordion'; +import { AccordionHeader } from '../AccordionHeader/AccordionHeader'; +import { AccordionItem } from '../AccordionItem/AccordionItem'; +import { AccordionPanel } from './AccordionPanel'; +import type { AccordionPanelState } from './AccordionPanel.types'; +import { accordionPanelClassNames, useAccordionPanelStyles } from './useAccordionPanelStyles'; + +import styles from './AccordionPanel.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/accordion', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/accordion'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAccordionPanel: (...args: Parameters) => + deepFreezeState(actual.useAccordionPanel(...args)), + }; +}); + +const renderItem = (open: boolean) => + render( + + + Section + + + + + , + ); + +describe('AccordionPanel', () => { + isConformant({ + Component: AccordionPanel, + displayName: 'AccordionPanel', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { container } = render(Body); + const root = container.firstElementChild as HTMLElement; + + expect(root).toHaveClass('fui-accordion-panel'); + expect(root).toHaveClass('group/fui-accordion-panel'); + expect(root.classList[0]).toBe('fui-accordion-panel'); + expect(accordionPanelClassNames.root).toBe('fui-accordion-panel group/fui-accordion-panel'); + }); + + it('renders the root in both states and carries its class in both', () => { + // Griffel unmounts a closed panel; the headless panel stays mounted, so the closed look is a + // stylesheet rule on this class. Both halves of that must hold. + for (const open of [false, true]) { + const { getByTestId, unmount } = renderItem(open); + + expect(getByTestId('panel')).toHaveClass(styles.root); + unmount(); + } + }); + + it('carries data-open only when the item is open', () => { + const closed = renderItem(false); + + expect(closed.getByTestId('panel').hasAttribute('data-open')).toBe(false); + closed.unmount(); + + expect(renderItem(true).getByTestId('panel').getAttribute('data-open')).toBe(''); + }); + + it('keeps the closed panel inert rather than unmounted', () => { + const panel = renderItem(false).getByTestId('panel'); + + expect(panel.getAttribute('tabindex')).toBe('-1'); + expect(panel.hasAttribute('inert')).toBe(true); + }); + + it('keeps panel content mounted across a close and reopen', () => { + // The mount contract Griffel's collapse motion carried is gone with the motion: state inside a + // closed panel survives here where Griffel would have destroyed it. + const { getByTestId } = renderItem(true); + + fireEvent.change(getByTestId('field'), { target: { value: 'typed' } }); + fireEvent.click(getByTestId('header').querySelector('button')!); + fireEvent.click(getByTestId('header').querySelector('button')!); + + expect((getByTestId('field') as HTMLInputElement).value).toBe('typed'); + }); + + it('passes consumer props and the ref through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Body + , + ); + + const root = getByTestId('panel'); + + expect(root.id).toBe('pnl'); + expect(root).toHaveClass('consumer'); + expect(root.style.zIndex).toBe('4'); + expect(root.textContent).toBe('Body'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'div' }, + root: { as: 'div', className: 'consumer' }, + } as unknown as AccordionPanelState; + + const styled = useAccordionPanelStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(state.root.className).toBe('consumer'); + expect(styled.root.className).toContain('consumer'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.tsx b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.tsx new file mode 100644 index 0000000000000..8f774123fd366 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAccordionPanel, useAccordionPanel } from '@fluentui/react-headless-components-preview/accordion'; + +import type { AccordionPanelProps } from './AccordionPanel.types'; +import { useAccordionPanelStyles } from './useAccordionPanelStyles'; + +/** + * An AccordionPanel is the content an AccordionHeader reveals. Windmod AccordionPanel: the + * headless panel decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const AccordionPanel: ForwardRefComponent = React.forwardRef( + (props, ref) => renderAccordionPanel(useAccordionPanelStyles(useAccordionPanel(props, ref))), + // Casting is required due to lack of distributive union to support union on @types/react +) as ForwardRefComponent; + +AccordionPanel.displayName = 'AccordionPanel'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.types.ts new file mode 100644 index 0000000000000..2787bee4ee9a0 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/AccordionPanel.types.ts @@ -0,0 +1,12 @@ +import type { + AccordionPanelProps as AccordionPanelHeadlessProps, + AccordionPanelState as AccordionPanelHeadlessState, +} from '@fluentui/react-headless-components-preview/accordion'; + +export type { AccordionPanelSlots } from '@fluentui/react-headless-components-preview/accordion'; + +/** Windmod AccordionPanel props. The headless surface carries every prop; windmod adds no look props. */ +export type AccordionPanelProps = AccordionPanelHeadlessProps; + +/** Windmod AccordionPanel state. */ +export type AccordionPanelState = AccordionPanelHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/index.ts new file mode 100644 index 0000000000000..d76caaf1a03e5 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/index.ts @@ -0,0 +1,3 @@ +export { AccordionPanel } from './AccordionPanel'; +export { accordionPanelClassNames, useAccordionPanelStyles } from './useAccordionPanelStyles'; +export type { AccordionPanelProps, AccordionPanelSlots, AccordionPanelState } from './AccordionPanel.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/useAccordionPanelStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/useAccordionPanelStyles.ts new file mode 100644 index 0000000000000..b72bb43e318c8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AccordionPanel/useAccordionPanelStyles.ts @@ -0,0 +1,17 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { AccordionPanelState } from './AccordionPanel.types'; + +import styles from './AccordionPanel.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const accordionPanelClassNames: { root: string } = { + root: componentMarkers('accordion-panel'), +}; + +/** Applies the visual contract, returning new state. */ +export const useAccordionPanelStyles = (state: AccordionPanelState): AccordionPanelState => ({ + ...state, + root: { ...state.root, className: clsx(accordionPanelClassNames.root, styles.root, state.root.className) }, +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.module.css b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.module.css new file mode 100644 index 0000000000000..a044808ea5eaa --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.module.css @@ -0,0 +1,28 @@ +@reference '#theme'; + +/* l2: every class here overrides a ColorSlider l1 class on the same element. */ +@layer fui.components.l2 { + /* The checkerboard is the 10x10 PNG Griffel loads from a CDN, inlined byte-identically as a + data URI so the rail never depends on a network fetch. */ + .rail { + @apply border-thin border-solid border-neutral-stroke-1; + + background-image: + linear-gradient(var(--fui-AlphaSlider--direction), transparent, var(--fui-AlphaSlider__rail--color)), + url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJUlEQVQYV2N89erVfwY0ICYmxoguxjgUFKI7GsTH5m4M3w1ChQC1/Ca8i2n1WgAAAABJRU5ErkJggg=='); + } + + .thumb { + @apply start-(--fui-AlphaSlider--progress) bg-neutral-background-1; + + @variant before { + @apply bg-(--fui-AlphaSlider__thumb--color); + } + + /* start-auto is required: this layer's unconditional inset-inline-start outranks the l1 + vertical block that would otherwise clear it. */ + @variant group-vertical/fui-color-slider { + @apply start-auto bottom-(--fui-AlphaSlider--progress); + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.test.tsx new file mode 100644 index 0000000000000..57868a0cc826b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.test.tsx @@ -0,0 +1,198 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; +import colorSliderStyles from '../ColorSlider/ColorSlider.module.css'; +import { AlphaSlider } from './AlphaSlider'; +import type { AlphaSliderState } from './AlphaSlider.types'; +import { alphaSliderClassNames, useAlphaSliderStyles } from './useAlphaSliderStyles'; + +import styles from './AlphaSlider.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/color-picker', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/color-picker'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAlphaSlider: (...args: Parameters) => + deepFreezeState(actual.useAlphaSlider(...args)), + }; +}); + +const teal = { a: 0.5, h: 180, s: 0.5, v: 0.6 }; + +const slotsOf = (root: HTMLElement) => { + const [input, rail, thumb] = Array.from(root.children) as HTMLElement[]; + + return { input: input as HTMLInputElement, rail, root, thumb }; +}; + +const renderAlphaSlider = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return slotsOf(container.firstElementChild as HTMLElement); +}; + +describe('AlphaSlider', () => { + isConformant({ + Component: AlphaSlider, + displayName: 'AlphaSlider', + primarySlot: 'input', + }); + + it('stamps both marker pairs on the root, its own first', () => { + const { root } = renderAlphaSlider(); + + expect(root).toHaveClass('fui-alpha-slider'); + expect(root).toHaveClass('group/fui-alpha-slider'); + expect(root).toHaveClass('fui-color-slider'); + expect(root).toHaveClass('group/fui-color-slider'); + expect(root.classList[0]).toBe('fui-alpha-slider'); + expect(alphaSliderClassNames.root).toBe('fui-alpha-slider group/fui-alpha-slider'); + }); + + it('layers its own rail and thumb classes over the composed ColorSlider ones', () => { + const { rail, root, thumb } = renderAlphaSlider(); + + expect(styles.rail).toBe(colorSliderStyles.rail); + expect(classOccurrences(rail, styles.rail)).toBe(2); + expect(classOccurrences(thumb, styles.thumb)).toBe(2); + expect(classOccurrences(root, colorSliderStyles.root)).toBe(1); + }); + + it('inherits the composed input class and peer marker untouched', () => { + const { input } = renderAlphaSlider(); + + expect(input).toHaveClass(colorSliderStyles.input); + expect(input.classList[0]).toBe(colorSliderStyles.input); + expect(input.type).toBe('range'); + + // Exactly one peer name: a second one would let a consumer's peer-* variant match a slot + // that never carries the state the name implies — see peerMarker. + expect(Array.from(input.classList).filter(name => name.startsWith('peer/'))).toEqual(['peer/fui-color-slider']); + }); + + it('stamps data-shape on the root through the composed styles hook', () => { + expect(renderAlphaSlider().root.getAttribute('data-shape')).toBe('rounded'); + + (['rounded', 'square'] as const).forEach(shape => { + expect(renderAlphaSlider({ shape }).root.getAttribute('data-shape')).toBe(shape); + }); + }); + + it('takes the shape from an enclosing ColorPicker, its own prop winning', () => { + const inherited = render( + + + , + ); + const overridden = render( + + + , + ); + + const rootOf = (result: ReturnType) => + (result.container.firstElementChild as HTMLElement).firstElementChild as HTMLElement; + + expect(rootOf(inherited).getAttribute('data-shape')).toBe('square'); + expect(rootOf(overridden).getAttribute('data-shape')).toBe('rounded'); + }); + + it('leaves the headless stamps alone', () => { + const { root } = renderAlphaSlider(); + + expect(root.getAttribute('data-orientation')).toBe('horizontal'); + // AlphaSlider has no `channel`, so the headless stamp is structurally always absent. + expect(root.hasAttribute('data-channel')).toBe(false); + expect(root.hasAttribute('data-transparency')).toBe(false); + + expect(renderAlphaSlider({ vertical: true }).root.getAttribute('data-orientation')).toBe('vertical'); + expect(renderAlphaSlider({ transparency: true }).root.getAttribute('data-transparency')).toBe(''); + expect(renderAlphaSlider({ transparency: false }).root.hasAttribute('data-transparency')).toBe(false); + }); + + it('leaves the root inline custom properties exactly as the headless hook wrote them', () => { + const { root } = renderAlphaSlider({ color: teal, style: { marginTop: 3 } }); + const style = root.getAttribute('style')!; + + expect(root.style.getPropertyValue('--fui-AlphaSlider--progress')).toBe('50%'); + expect(root.style.getPropertyValue('--fui-AlphaSlider--direction')).not.toBe(''); + expect(root.style.getPropertyValue('--fui-AlphaSlider__thumb--color')).not.toBe(''); + expect(root.style.getPropertyValue('--fui-AlphaSlider__rail--color')).not.toBe(''); + expect(root.style.marginTop).toBe('3px'); + expect(style.indexOf('--fui-AlphaSlider--direction')).toBeLessThan(style.indexOf('margin-top')); + }); + + it('reports an alpha change from the input', () => { + const onChange = jest.fn(); + const { input } = renderAlphaSlider({ color: teal, onChange }); + + fireEvent.change(input, { target: { value: '80' } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][1].color).toEqual(expect.objectContaining({ a: 0.8 })); + }); + + it('keeps a consumer className on every slot exactly once', () => { + const { input, rail, root, thumb } = renderAlphaSlider({ + className: 'consumer', + input: { className: 'i' }, + rail: { className: 'r' }, + thumb: { className: 't' }, + }); + + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(classOccurrences(rail, 'r')).toBe(1); + expect(classOccurrences(thumb, 't')).toBe(1); + expect(classOccurrences(input, 'i')).toBe(1); + }); + + it('lands native props on the primary slot', () => { + const ref = React.createRef(); + const { input, root } = renderAlphaSlider({ + 'aria-label': 'alpha', + 'data-testid': 'probe', + id: 'field', + ref, + } as React.ComponentProps); + + expect(input.id).toBe('field'); + expect(input.getAttribute('data-testid')).toBe('probe'); + expect(input.getAttribute('aria-label')).toBe('alpha'); + expect(ref.current).toBe(input); + expect(root.hasAttribute('data-testid')).toBe(false); + }); + + it('does not mutate the state it is given, nor the one it composes', () => { + const state = { + components: { input: 'input', rail: 'div', root: 'div', thumb: 'div' }, + input: { className: 'native' }, + rail: { className: 'consumer-rail' }, + root: { className: 'consumer', style: { '--fui-AlphaSlider--progress': '50%' } }, + shape: 'square', + thumb: { className: 'consumer-thumb' }, + } as unknown as AlphaSliderState; + + const styled = useAlphaSliderStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.rail).not.toBe(state.rail); + expect(styled.thumb).not.toBe(state.thumb); + + expect(state.root.className).toBe('consumer'); + expect(state.rail.className).toBe('consumer-rail'); + expect(state.thumb.className).toBe('consumer-thumb'); + expect(state.input.className).toBe('native'); + expect('data-shape' in state.root).toBe(false); + + expect(styled.root.style).toBe(state.root.style); + expect(styled.root.className).toContain(alphaSliderClassNames.root); + expect(styled.input.className).toContain('native'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.tsx b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.tsx new file mode 100644 index 0000000000000..0ce6f1fabf394 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.tsx @@ -0,0 +1,35 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderAlphaSlider, + useAlphaSlider, + useColorPickerContextValue, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { AlphaSliderProps } from './AlphaSlider.types'; +import { useAlphaSliderStyles } from './useAlphaSliderStyles'; + +/** + * An AlphaSlider picks the alpha channel of a colour. Windmod AlphaSlider: the headless slider + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + * + * The per-instance colour and geometry are inline custom properties the headless base hook writes — + * see `ColorArea` for the rule the windmod layer follows around them. + */ +export const AlphaSlider: ForwardRefComponent = React.forwardRef( + ({ shape: shapeProp, ...rest }, ref) => { + const shapeFromContext = useColorPickerContextValue(ctx => ctx.shape); + + return renderAlphaSlider( + useAlphaSliderStyles({ + ...useAlphaSlider(rest, ref), + // The trailing fallback, not the context default, is what resolves `shape` — see `ColorArea`. + shape: shapeProp ?? shapeFromContext ?? 'rounded', + }), + ); + }, +); + +AlphaSlider.displayName = 'AlphaSlider'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts new file mode 100644 index 0000000000000..b7be2c1850513 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/AlphaSlider.types.ts @@ -0,0 +1,20 @@ +import type { + AlphaSliderProps as AlphaSliderHeadlessProps, + AlphaSliderState as AlphaSliderHeadlessState, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorPickerShape } from '../ColorPicker/ColorPicker.types'; + +export type { AlphaSliderSlots } from '@fluentui/react-headless-components-preview/color-picker'; + +/** + * Windmod AlphaSlider props: the headless slider plus the look prop the headless surface + * deliberately omits. + */ +export type AlphaSliderProps = AlphaSliderHeadlessProps & { + /** @default the enclosing ColorPicker's shape, then 'rounded' */ + shape?: ColorPickerShape; +}; + +/** Windmod AlphaSlider state: headless state plus the resolved look prop. */ +export type AlphaSliderState = AlphaSliderHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/index.ts new file mode 100644 index 0000000000000..b04fe6ef8fd6d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/index.ts @@ -0,0 +1,3 @@ +export { AlphaSlider } from './AlphaSlider'; +export { alphaSliderClassNames, useAlphaSliderStyles } from './useAlphaSliderStyles'; +export type { AlphaSliderProps, AlphaSliderSlots, AlphaSliderState } from './AlphaSlider.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/useAlphaSliderStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/useAlphaSliderStyles.ts new file mode 100644 index 0000000000000..97e941e475a85 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AlphaSlider/useAlphaSliderStyles.ts @@ -0,0 +1,33 @@ +'use client'; + +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import { useColorSliderStyles } from '../ColorSlider/useColorSliderStyles'; +import type { AlphaSliderState } from './AlphaSlider.types'; + +import styles from './AlphaSlider.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const alphaSliderClassNames: { root: string } = { + root: componentMarkers('alpha-slider'), +}; + +/** + * Applies the visual contract on top of ColorSlider's, returning new state. ColorSlider's rules + * select through `group/fui-color-slider` and its input carries the peer marker, so the root must + * keep ColorSlider's marker pair alongside its own and the input passes through untouched. + * `data-shape` is stamped by the composed hook. + */ +export const useAlphaSliderStyles = (state: AlphaSliderState): AlphaSliderState => { + const { input, rail, root, thumb } = useColorSliderStyles(state); + + return { + ...state, + root: { ...root, className: clsx(alphaSliderClassNames.root, root.className) }, + rail: slotClasses(rail, styles.rail), + thumb: slotClasses(thumb, styles.thumb), + input, + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.module.css b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.module.css new file mode 100644 index 0000000000000..c56b7db2c0ff7 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.module.css @@ -0,0 +1,366 @@ +@reference '#theme'; + +/* Block order carries the cascade: the text-size, square-radius, ring, shadow and inactive + blocks all override `.root` at equal specificity, and `.inactive` comes last because it + collapses the ring and shadow it is combined with. The colour blocks declare only custom + properties, which the slot classes read, so their position carries no cascade. + + The before/after variants emit `content: var(--tw-content)` unconditionally, so `.root` must + neutralise it with `content-none` or both pseudo-elements would paint on every Avatar + instead of only under `.ring` / `.shadow`. `.root` sets `--tw-content: none` ON THE SAME + PSEUDO-ELEMENT, which outranks the `@property` initial value, so `.ring` and `.shadow` each + have to set `--tw-content` back — a depth block's own `content: var(--tw-content)` would + otherwise resolve to `none` and paint nothing. */ + +@layer fui.components.l1 { + .root { + --fui-avatar-size: calc(attr(data-size type(), 32) * 1px); + + @apply relative inline-block size-(--fui-avatar-size) shrink-0 rounded-circular align-middle font-base text-base-300 font-semibold; + + @variant before, after { + @apply absolute inset-0 -z-1 rounded-[inherit] content-none; + + margin: calc(-2 * var(--fui-avatar-ring-width, 0px)); + transition-property: margin, opacity; + transition-timing-function: var(--ease-easy-ease-max), var(--ease-linear); + transition-duration: var(--duration-ultra-slow), var(--duration-slower); + } + + @variant before { + @apply border-(length:--fui-avatar-ring-width) border-solid; + } + } + + .text-caption-2-strong { + @apply text-base-100 leading-[calc(20/10)]; + } + + .text-caption-1-strong { + @apply text-base-200 leading-[calc(20/12)]; + } + + .text-subtitle-2 { + @apply text-base-400 leading-[calc(20/16)]; + } + + .text-subtitle-1 { + @apply text-base-500 leading-[calc(20/20)]; + } + + .text-title-3 { + @apply text-base-600 leading-[calc(20/24)]; + } + + .square-small { + @apply rounded-small; + } + + .square-medium { + @apply rounded-medium; + } + + .square-large { + @apply rounded-large; + } + + .square-x-large { + @apply rounded-x-large; + } + + .active-or-inactive { + transform: perspective(1px); + transition-property: transform, opacity; + transition-timing-function: var(--ease-easy-ease-max), var(--ease-linear); + transition-duration: var(--duration-ultra-slow), var(--duration-faster); + } + + .ring { + @variant before { + @apply text-(--fui-avatar-ring-color) content-['']; + } + } + + .ring-thick { + --fui-avatar-ring-width: var(--stroke-width-thick); + } + + .ring-thicker { + --fui-avatar-ring-width: var(--stroke-width-thicker); + } + + .ring-thickest { + --fui-avatar-ring-width: var(--stroke-width-thickest); + } + + .shadow { + @variant after { + @apply content-['']; + } + } + + .shadow-4 { + @variant after { + @apply shadow-4; + } + } + + .shadow-8 { + @variant after { + @apply shadow-8; + } + } + + .shadow-16 { + @variant after { + @apply shadow-16; + } + } + + .shadow-28 { + @variant after { + @apply shadow-28; + } + } + + .inactive { + @apply opacity-80; + + transform: scale(0.875); + transition-timing-function: var(--ease-decelerate-min), var(--ease-linear); + + @variant before, after { + @apply m-0 opacity-0; + + transition-timing-function: var(--ease-decelerate-min), var(--ease-linear); + } + } + + .neutral { + --fui-avatar-foreground: var(--color-neutral-foreground-3); + --fui-avatar-background: var(--color-neutral-background-6); + --fui-avatar-ring-color: var(--color-brand-stroke-1); + } + + .brand { + --fui-avatar-foreground: var(--color-neutral-foreground-static-inverted); + --fui-avatar-background: var(--color-brand-background-static); + --fui-avatar-ring-color: var(--color-brand-stroke-1); + } + + .dark-red { + --fui-avatar-foreground: var(--color-palette-dark-red-foreground-2); + --fui-avatar-background: var(--color-palette-dark-red-background-2); + --fui-avatar-ring-color: var(--color-palette-dark-red-border-active); + } + + .cranberry { + --fui-avatar-foreground: var(--color-palette-cranberry-foreground-2); + --fui-avatar-background: var(--color-palette-cranberry-background-2); + --fui-avatar-ring-color: var(--color-palette-cranberry-border-active); + } + + .red { + --fui-avatar-foreground: var(--color-palette-red-foreground-2); + --fui-avatar-background: var(--color-palette-red-background-2); + --fui-avatar-ring-color: var(--color-palette-red-border-active); + } + + .pumpkin { + --fui-avatar-foreground: var(--color-palette-pumpkin-foreground-2); + --fui-avatar-background: var(--color-palette-pumpkin-background-2); + --fui-avatar-ring-color: var(--color-palette-pumpkin-border-active); + } + + .peach { + --fui-avatar-foreground: var(--color-palette-peach-foreground-2); + --fui-avatar-background: var(--color-palette-peach-background-2); + --fui-avatar-ring-color: var(--color-palette-peach-border-active); + } + + .marigold { + --fui-avatar-foreground: var(--color-palette-marigold-foreground-2); + --fui-avatar-background: var(--color-palette-marigold-background-2); + --fui-avatar-ring-color: var(--color-palette-marigold-border-active); + } + + .gold { + --fui-avatar-foreground: var(--color-palette-gold-foreground-2); + --fui-avatar-background: var(--color-palette-gold-background-2); + --fui-avatar-ring-color: var(--color-palette-gold-border-active); + } + + .brass { + --fui-avatar-foreground: var(--color-palette-brass-foreground-2); + --fui-avatar-background: var(--color-palette-brass-background-2); + --fui-avatar-ring-color: var(--color-palette-brass-border-active); + } + + .brown { + --fui-avatar-foreground: var(--color-palette-brown-foreground-2); + --fui-avatar-background: var(--color-palette-brown-background-2); + --fui-avatar-ring-color: var(--color-palette-brown-border-active); + } + + .forest { + --fui-avatar-foreground: var(--color-palette-forest-foreground-2); + --fui-avatar-background: var(--color-palette-forest-background-2); + --fui-avatar-ring-color: var(--color-palette-forest-border-active); + } + + .seafoam { + --fui-avatar-foreground: var(--color-palette-seafoam-foreground-2); + --fui-avatar-background: var(--color-palette-seafoam-background-2); + --fui-avatar-ring-color: var(--color-palette-seafoam-border-active); + } + + .dark-green { + --fui-avatar-foreground: var(--color-palette-dark-green-foreground-2); + --fui-avatar-background: var(--color-palette-dark-green-background-2); + --fui-avatar-ring-color: var(--color-palette-dark-green-border-active); + } + + .light-teal { + --fui-avatar-foreground: var(--color-palette-light-teal-foreground-2); + --fui-avatar-background: var(--color-palette-light-teal-background-2); + --fui-avatar-ring-color: var(--color-palette-light-teal-border-active); + } + + .teal { + --fui-avatar-foreground: var(--color-palette-teal-foreground-2); + --fui-avatar-background: var(--color-palette-teal-background-2); + --fui-avatar-ring-color: var(--color-palette-teal-border-active); + } + + .steel { + --fui-avatar-foreground: var(--color-palette-steel-foreground-2); + --fui-avatar-background: var(--color-palette-steel-background-2); + --fui-avatar-ring-color: var(--color-palette-steel-border-active); + } + + .blue { + --fui-avatar-foreground: var(--color-palette-blue-foreground-2); + --fui-avatar-background: var(--color-palette-blue-background-2); + --fui-avatar-ring-color: var(--color-palette-blue-border-active); + } + + .royal-blue { + --fui-avatar-foreground: var(--color-palette-royal-blue-foreground-2); + --fui-avatar-background: var(--color-palette-royal-blue-background-2); + --fui-avatar-ring-color: var(--color-palette-royal-blue-border-active); + } + + .cornflower { + --fui-avatar-foreground: var(--color-palette-cornflower-foreground-2); + --fui-avatar-background: var(--color-palette-cornflower-background-2); + --fui-avatar-ring-color: var(--color-palette-cornflower-border-active); + } + + .navy { + --fui-avatar-foreground: var(--color-palette-navy-foreground-2); + --fui-avatar-background: var(--color-palette-navy-background-2); + --fui-avatar-ring-color: var(--color-palette-navy-border-active); + } + + .lavender { + --fui-avatar-foreground: var(--color-palette-lavender-foreground-2); + --fui-avatar-background: var(--color-palette-lavender-background-2); + --fui-avatar-ring-color: var(--color-palette-lavender-border-active); + } + + .purple { + --fui-avatar-foreground: var(--color-palette-purple-foreground-2); + --fui-avatar-background: var(--color-palette-purple-background-2); + --fui-avatar-ring-color: var(--color-palette-purple-border-active); + } + + .grape { + --fui-avatar-foreground: var(--color-palette-grape-foreground-2); + --fui-avatar-background: var(--color-palette-grape-background-2); + --fui-avatar-ring-color: var(--color-palette-grape-border-active); + } + + .lilac { + --fui-avatar-foreground: var(--color-palette-lilac-foreground-2); + --fui-avatar-background: var(--color-palette-lilac-background-2); + --fui-avatar-ring-color: var(--color-palette-lilac-border-active); + } + + .pink { + --fui-avatar-foreground: var(--color-palette-pink-foreground-2); + --fui-avatar-background: var(--color-palette-pink-background-2); + --fui-avatar-ring-color: var(--color-palette-pink-border-active); + } + + .magenta { + --fui-avatar-foreground: var(--color-palette-magenta-foreground-2); + --fui-avatar-background: var(--color-palette-magenta-background-2); + --fui-avatar-ring-color: var(--color-palette-magenta-border-active); + } + + .plum { + --fui-avatar-foreground: var(--color-palette-plum-foreground-2); + --fui-avatar-background: var(--color-palette-plum-background-2); + --fui-avatar-ring-color: var(--color-palette-plum-border-active); + } + + .beige { + --fui-avatar-foreground: var(--color-palette-beige-foreground-2); + --fui-avatar-background: var(--color-palette-beige-background-2); + --fui-avatar-ring-color: var(--color-palette-beige-border-active); + } + + .mink { + --fui-avatar-foreground: var(--color-palette-mink-foreground-2); + --fui-avatar-background: var(--color-palette-mink-background-2); + --fui-avatar-ring-color: var(--color-palette-mink-border-active); + } + + .platinum { + --fui-avatar-foreground: var(--color-palette-platinum-foreground-2); + --fui-avatar-background: var(--color-palette-platinum-background-2); + --fui-avatar-ring-color: var(--color-palette-platinum-border-active); + } + + .anchor { + --fui-avatar-foreground: var(--color-palette-anchor-foreground-2); + --fui-avatar-background: var(--color-palette-anchor-background-2); + --fui-avatar-ring-color: var(--color-palette-anchor-border-active); + } + + .image { + @apply absolute start-0 top-0 size-full rounded-[inherit] bg-(--fui-avatar-background) object-cover align-top text-(--fui-avatar-foreground); + } + + .icon-initials { + @apply absolute start-0 top-0 box-border flex size-full items-center justify-center rounded-[inherit] border-thin border-solid border-transparent-stroke bg-(--fui-avatar-background) text-center leading-none text-(--fui-avatar-foreground) select-none; + } + + .icon-12 { + @apply text-icon-12; + } + + .icon-16 { + @apply text-icon-16; + } + + .icon-20 { + @apply text-icon-20; + } + + .icon-24 { + @apply text-icon-24; + } + + .icon-28 { + @apply text-icon-28; + } + + .icon-32 { + @apply text-icon-32; + } + + .icon-48 { + @apply text-icon-48; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 0000000000000..1d2bb77b0e1de --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.test.tsx @@ -0,0 +1,527 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { Provider } from '@fluentui/react-headless-components-preview/provider'; +import { PersonRegular } from '@fluentui/react-icons/headless/svg/person'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { Avatar } from './Avatar'; +import type { AvatarNamedColor, AvatarSize, AvatarState } from './Avatar.types'; +import { avatarClassNames, useAvatarStyles } from './useAvatarStyles'; + +import styles from './Avatar.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/avatar', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/avatar'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAvatar: (...args: Parameters) => deepFreezeState(actual.useAvatar(...args)), + }; +}); + +const sizes: AvatarSize[] = [16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 96, 120, 128]; + +// The icon slot is the only aria-hidden span the root owns directly; the initials slot carries a +// generated id and the active label element is hidden rather than aria-hidden. +const parts = (root: HTMLElement) => ({ + root, + initials: root.querySelector('[id$="__initials"]'), + icon: + Array.from(root.children).find( + (child): child is HTMLElement => child.tagName === 'SPAN' && child.getAttribute('aria-hidden') === 'true', + ) ?? null, + image: root.querySelector('img'), + activeLabel: root.querySelector('span[hidden]'), +}); + +const renderAvatar = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return parts(container.firstElementChild as HTMLElement); +}; + +const personPath = () => { + const { container } = render(); + + return container.querySelector('path')!.getAttribute('d'); +}; + +const pathOf = (element: Element | null) => element?.querySelector('svg path')?.getAttribute('d') ?? null; + +describe('Avatar', () => { + isConformant({ + Component: Avatar, + displayName: 'Avatar', + }); + + it('stamps the marker pair on the root', () => { + const { root } = renderAvatar(); + + expect(root).toHaveClass('fui-avatar'); + expect(root).toHaveClass('group/fui-avatar'); + expect(root.classList[0]).toBe('fui-avatar'); + expect(avatarClassNames.root).toBe('fui-avatar group/fui-avatar'); + }); + + it('stamps the numeric size on the root and defaults it to 32', () => { + expect(renderAvatar().root.getAttribute('data-size')).toBe('32'); + + sizes.forEach(size => { + expect(renderAvatar({ size }).root.getAttribute('data-size')).toBe(String(size)); + }); + }); + + it('carries the root module class and keeps the consumer className exactly once', () => { + const { root } = renderAvatar({ className: 'consumer' }); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass('consumer'); + + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(classOccurrences(root, styles.root)).toBe(1); + }); + + it('picks the text bucket by size, with no class in the base band', () => { + const expected: Array<[AvatarSize, string | undefined]> = [ + [24, styles.textCaption2Strong], + [28, styles.textCaption1Strong], + [32, undefined], + [40, undefined], + [56, styles.textSubtitle2], + [64, styles.textSubtitle1], + [96, styles.textSubtitle1], + [120, styles.textTitle3], + ]; + const all = [ + styles.textCaption2Strong, + styles.textCaption1Strong, + styles.textSubtitle2, + styles.textSubtitle1, + styles.textTitle3, + ]; + + expected.forEach(([size, className]) => { + const { root } = renderAvatar({ size }); + + all.forEach(candidate => { + if (candidate === className) { + expect(root).toHaveClass(candidate); + } else { + expect(root).not.toHaveClass(candidate); + } + }); + }); + }); + + it('picks the square radius bucket by size, and applies none when circular', () => { + const expected: Array<[AvatarSize, string]> = [ + [24, styles.squareSmall], + [28, styles.squareMedium], + [48, styles.squareMedium], + [56, styles.squareLarge], + [72, styles.squareLarge], + [96, styles.squareXLarge], + ]; + const all = [styles.squareSmall, styles.squareMedium, styles.squareLarge, styles.squareXLarge]; + + expected.forEach(([size, className]) => { + const { root } = renderAvatar({ shape: 'square', size }); + + expect(root).toHaveClass(className); + all.filter(candidate => candidate !== className).forEach(candidate => expect(root).not.toHaveClass(candidate)); + }); + + sizes.forEach(size => { + const { root } = renderAvatar({ size }); + + all.forEach(candidate => expect(root).not.toHaveClass(candidate)); + }); + }); + + it('picks the ring width bucket by size, and rings only for ring and ring-shadow', () => { + const expected: Array<[AvatarSize, string]> = [ + [48, styles.ringThick], + [56, styles.ringThicker], + [64, styles.ringThicker], + [72, styles.ringThickest], + ]; + const all = [styles.ringThick, styles.ringThicker, styles.ringThickest]; + + expected.forEach(([size, className]) => { + const { root } = renderAvatar({ active: 'active', size }); + + expect(root).toHaveClass(styles.ring); + expect(root).toHaveClass(className); + all.filter(candidate => candidate !== className).forEach(candidate => expect(root).not.toHaveClass(candidate)); + }); + + expect(renderAvatar({ active: 'active', activeAppearance: 'ring-shadow' }).root).toHaveClass(styles.ring); + expect(renderAvatar({ active: 'active', activeAppearance: 'shadow' }).root).not.toHaveClass(styles.ring); + expect(renderAvatar({ activeAppearance: 'ring' }).root).not.toHaveClass(styles.ring); + }); + + it('picks the shadow depth bucket by size, and shadows only for shadow and ring-shadow', () => { + const expected: Array<[AvatarSize, string]> = [ + [28, styles.shadow4], + [32, styles.shadow8], + [48, styles.shadow8], + [56, styles.shadow16], + [64, styles.shadow16], + [72, styles.shadow28], + ]; + const all = [styles.shadow4, styles.shadow8, styles.shadow16, styles.shadow28]; + + expected.forEach(([size, className]) => { + const { root } = renderAvatar({ active: 'active', activeAppearance: 'shadow', size }); + + expect(root).toHaveClass(styles.shadow); + expect(root).toHaveClass(className); + all.filter(candidate => candidate !== className).forEach(candidate => expect(root).not.toHaveClass(candidate)); + }); + + expect(renderAvatar({ active: 'active', activeAppearance: 'ring-shadow' }).root).toHaveClass(styles.shadow); + expect(renderAvatar({ active: 'active', activeAppearance: 'ring' }).root).not.toHaveClass(styles.shadow); + expect(renderAvatar({ activeAppearance: 'shadow' }).root).not.toHaveClass(styles.shadow); + }); + + it('animates for both active and inactive, and fades for inactive alone', () => { + expect(renderAvatar({ active: 'active' }).root).toHaveClass(styles.activeOrInactive); + expect(renderAvatar({ active: 'inactive' }).root).toHaveClass(styles.activeOrInactive); + expect(renderAvatar().root).not.toHaveClass(styles.activeOrInactive); + + expect(renderAvatar({ active: 'inactive' }).root).toHaveClass(styles.inactive); + expect(renderAvatar({ active: 'active' }).root).not.toHaveClass(styles.inactive); + expect(renderAvatar().root).not.toHaveClass(styles.inactive); + }); + + it('applies the colour class, defaulting to neutral', () => { + expect(renderAvatar().root).toHaveClass(styles.neutral); + + (['brand', 'dark-red', 'seafoam', 'navy', 'anchor'] as const).forEach(color => { + const { root } = renderAvatar({ color }); + + expect(root).toHaveClass(styles[color]); + expect(root).not.toHaveClass(styles.neutral); + }); + }); + + it('resolves colorful through the hash Griffel uses, deterministically', () => { + // Measured against @fluentui/react-avatar's own rendered colour classes. + const expected: Array<[string, AvatarNamedColor]> = [ + ['Katri Athokas', 'lilac'], + ['Elvia Atkins', 'gold'], + ['Cameron Evans', 'dark-red'], + ['Wanda Howard', 'blue'], + ['Mona Kane', 'light-teal'], + ['Robert Tolbert', 'anchor'], + ]; + + expected.forEach(([name, color]) => { + expect(renderAvatar({ color: 'colorful', name }).root).toHaveClass(styles[color]); + expect(renderAvatar({ color: 'colorful', name }).root).toHaveClass(styles[color]); + }); + + // A name with no derivable initials still colours the icon-bearing Avatar. + expect(renderAvatar({ color: 'colorful', name: '山田 太郎' }).root).toHaveClass(styles.navy); + expect(renderAvatar({ color: 'colorful' }).root).toHaveClass(styles.darkRed); + }); + + it('seeds the colorful hash from idForColor in preference to name', () => { + expect(renderAvatar({ color: 'colorful', idForColor: 'abc' }).root).toHaveClass(styles.beige); + expect(renderAvatar({ color: 'colorful', idForColor: 'user-42' }).root).toHaveClass(styles.mink); + expect(renderAvatar({ color: 'colorful', idForColor: 'abc', name: 'Katri Athokas' }).root).toHaveClass( + styles.beige, + ); + expect(renderAvatar({ color: 'colorful', name: 'Katri Athokas' }).root).toHaveClass(styles.lilac); + }); + + it('seeds the hash from an empty idForColor rather than falling through to name', () => { + // The fallback is nullish-coalescing, so an explicit empty string is a seed in its own right — + // it must not be treated as absent the way undefined is. + expect(renderAvatar({ color: 'colorful', idForColor: '', name: 'Katri Athokas' }).root).toHaveClass(styles.darkRed); + expect(renderAvatar({ color: 'colorful', name: 'Katri Athokas' }).root).not.toHaveClass(styles.darkRed); + }); + + it('restores the person glyph across every icon shorthand shape', () => { + const person = personPath(); + + expect(pathOf(renderAvatar().icon)).toBe(person); + expect(pathOf(renderAvatar({ icon: { className: 'x' } }).icon)).toBe(person); + expect(renderAvatar({ icon: { className: 'x' } }).icon).toHaveClass('x'); + + const element = renderAvatar({ icon: }); + + expect(element.icon!.querySelector('[data-consumer]')).not.toBeNull(); + expect(element.icon!.querySelector('svg')).toBeNull(); + + expect(renderAvatar({ icon: 'TXT' }).icon!.textContent).toBe('TXT'); + expect( + renderAvatar({ icon: { children: } }).icon!.querySelector('[data-consumer]'), + ).not.toBeNull(); + + expect(renderAvatar({ icon: null }).icon).toBeNull(); + }); + + it('falls back to the glyph for icon children of null or undefined', () => { + // The uniform glyph rule is nullish-coalescing, so both blank spellings take the default — + // the two inputs where windmod does not reproduce the Griffel Avatar's empty . + expect(pathOf(renderAvatar({ icon: { children: null } }).icon)).toBe(personPath()); + expect(pathOf(renderAvatar({ icon: { children: undefined } }).icon)).toBe(personPath()); + }); + + it('treats falsy-but-present icon children as consumer content', () => { + ([0, '', false] as const).forEach(children => { + expect(renderAvatar({ icon: { children } }).icon!.querySelector('svg')).toBeNull(); + }); + + expect(renderAvatar({ icon: { children: 0 } }).icon!.textContent).toBe('0'); + }); + + it('restores the glyph on the icon the failed-image fallback reveals', () => { + const { root, image } = renderAvatar({ image: { src: 'broken.png' } }); + + expect(parts(root).icon).toBeNull(); + + fireEvent.error(image!); + + expect(pathOf(parts(root).icon)).toBe(personPath()); + }); + + it('truncates derived initials to one code point at size 16 only', () => { + expect(renderAvatar({ name: 'Katri Athokas', size: 16 }).initials!.textContent).toBe('K'); + expect(renderAvatar({ name: 'Katri Athokas', size: 20 }).initials!.textContent).toBe('KA'); + expect(renderAvatar({ name: 'Katri Athokas' }).initials!.textContent).toBe('KA'); + + // A consumer's own initials content is not derived, so it is never truncated. + expect(renderAvatar({ initials: 'ZZ', size: 16 }).initials!.textContent).toBe('ZZ'); + expect(renderAvatar({ initials: { children: 'ZZ' }, size: 16 }).initials!.textContent).toBe('ZZ'); + + // A childless shorthand still leaves the derivation in place. + const childless = renderAvatar({ initials: { className: 'x' }, name: 'Katri Athokas', size: 16 }); + + expect(childless.initials!.textContent).toBe('K'); + expect(childless.initials).toHaveClass('x'); + }); + + it('truncates to the true first initial under an RTL provider, not the first code point of the swapped pair', () => { + // The headless base already swapped the pair for RTL (getInitials.ts), so taking the leading + // code point of that swapped string would show the second initial, not the first. + const { container } = render( + + + , + ); + const root = container.querySelector('[role="img"]') as HTMLElement; + + expect(root.querySelector('[id$="__initials"]')!.textContent).toBe('K'); + }); + + it('drives the image → initials → icon cascade off the real load and error events', () => { + const withName = renderAvatar({ image: { src: 'x.png' }, name: 'Katri Athokas' }); + + expect(withName.initials!.textContent).toBe('KA'); + expect(withName.image!.hasAttribute('hidden')).toBe(false); + expect(withName.icon).toBeNull(); + + fireEvent.error(withName.image!); + + expect(parts(withName.root).image!.hasAttribute('hidden')).toBe(true); + expect(parts(withName.root).initials!.textContent).toBe('KA'); + + fireEvent.load(withName.image!); + + expect(parts(withName.root).image!.hasAttribute('hidden')).toBe(false); + + const withoutName = renderAvatar({ image: { src: 'x.png' } }); + + expect(withoutName.initials).toBeNull(); + expect(withoutName.icon).toBeNull(); + + fireEvent.error(withoutName.image!); + + expect(parts(withoutName.root).icon).not.toBeNull(); + + fireEvent.load(withoutName.image!); + + expect(parts(withoutName.root).icon).toBeNull(); + }); + + it('renders no image element when the shorthand carries no src', () => { + expect(renderAvatar({ image: { alt: 'x' } }).image).toBeNull(); + expect(renderAvatar({ image: { src: 'x.png' } }).image).not.toBeNull(); + }); + + it('decorates each slot with its own classes, and picks the icon size bucket', () => { + const image = renderAvatar({ image: { src: 'x.png' } }); + + expect(image.image).toHaveClass(styles.image); + expect(image.image).not.toHaveClass(styles.iconInitials); + + const initials = renderAvatar({ name: 'Katri Athokas' }); + + expect(initials.initials).toHaveClass(styles.iconInitials); + expect(initials.initials).not.toHaveClass(styles.image); + + const expected: Array<[AvatarSize, string]> = [ + [16, styles.icon12], + [20, styles.icon16], + [24, styles.icon16], + [28, styles.icon20], + [40, styles.icon20], + [48, styles.icon24], + [56, styles.icon28], + [64, styles.icon32], + [72, styles.icon32], + [96, styles.icon48], + ]; + const all = [ + styles.icon12, + styles.icon16, + styles.icon20, + styles.icon24, + styles.icon28, + styles.icon32, + styles.icon48, + ]; + + expected.forEach(([size, className]) => { + const { icon } = renderAvatar({ size }); + + expect(icon).toHaveClass(styles.iconInitials); + expect(icon).toHaveClass(className); + all.filter(candidate => candidate !== className).forEach(candidate => expect(icon).not.toHaveClass(candidate)); + }); + }); + + it('appends the active state to the accessible name', () => { + const labelled = renderAvatar({ active: 'active', name: 'Katri Athokas' }); + + expect(labelled.root.getAttribute('aria-label')).toBe('Katri Athokas active'); + expect(labelled.activeLabel).toBeNull(); + + expect(renderAvatar({ active: 'inactive', name: 'Katri Athokas' }).root.getAttribute('aria-label')).toBe( + 'Katri Athokas inactive', + ); + + const labelledBy = renderAvatar({ active: 'inactive', initials: 'ZZ' }); + const tokens = labelledBy.root.getAttribute('aria-labelledby')!.split(' '); + + expect(labelledBy.activeLabel).not.toBeNull(); + expect(labelledBy.activeLabel!.textContent).toBe('inactive'); + expect(tokens).toHaveLength(2); + expect(tokens[0]).toBe(labelledBy.initials!.id); + expect(tokens[1]).toBe(labelledBy.activeLabel!.id); + expect(labelledBy.root.children[labelledBy.root.children.length - 1]).toBe(labelledBy.activeLabel); + + expect(renderAvatar({ active: 'active', initials: 'ZZ' }).activeLabel!.textContent).toBe('active'); + }); + + it('leaves a consumer-supplied accessible name untouched', () => { + const withLabel = renderAvatar({ + 'aria-label': 'Mine', + active: 'active', + name: 'Katri Athokas', + } as React.ComponentProps); + + expect(withLabel.root.getAttribute('aria-label')).toBe('Mine'); + expect(withLabel.activeLabel).toBeNull(); + + const withLabelledBy = renderAvatar({ + 'aria-labelledby': 'outside', + active: 'inactive', + initials: 'ZZ', + } as React.ComponentProps); + + expect(withLabelledBy.root.getAttribute('aria-labelledby')).toBe('outside'); + expect(withLabelledBy.activeLabel).toBeNull(); + }); + + it('adds no active element or name suffix when the state is unset', () => { + const { root, activeLabel } = renderAvatar({ name: 'Katri Athokas' }); + + expect(root.getAttribute('aria-label')).toBe('Katri Athokas'); + expect(activeLabel).toBeNull(); + }); + + it('lands native props on the root and forwards the ref to it', () => { + const ref = React.createRef(); + const { container } = render( + )} + ref={ref} + />, + ); + const root = container.firstElementChild as HTMLElement; + + expect(root.id).toBe('my-id'); + expect(root.getAttribute('data-testid')).toBe('t'); + expect(root.getAttribute('role')).toBe('img'); + expect(root).toHaveClass('consumer'); + expect(root.style.margin).toBe('4px'); + expect(ref.current).toBe(root); + expect(root.tagName).toBe('SPAN'); + }); + + it('does not mutate the state it is given', () => { + const state = { + active: 'inactive', + activeAppearance: 'ring-shadow', + color: 'brand', + components: { root: 'span', initials: 'span', icon: 'span', image: 'img' }, + icon: { className: 'consumer-icon' }, + image: { className: 'consumer-image' }, + initials: { className: 'consumer-initials' }, + root: { className: 'consumer' }, + shape: 'square', + size: 64, + } as unknown as AvatarState; + + const styled = useAvatarStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.icon).not.toBe(state.icon); + expect(styled.image).not.toBe(state.image); + expect(styled.initials).not.toBe(state.initials); + + expect(state.root.className).toBe('consumer'); + expect(state.icon!.className).toBe('consumer-icon'); + expect(state.image!.className).toBe('consumer-image'); + expect(state.initials!.className).toBe('consumer-initials'); + expect('data-size' in state.root).toBe(false); + + const rootClasses = styled.root.className!.split(/\s+/); + + expect(rootClasses).toContain(styles.root); + expect(rootClasses).toContain(styles.squareLarge); + expect(rootClasses).toContain(styles.ring); + expect(rootClasses).toContain(styles.ringThicker); + expect(rootClasses).toContain(styles.shadow); + expect(rootClasses).toContain(styles.shadow16); + expect(rootClasses).toContain(styles.inactive); + expect(rootClasses).toContain(styles.brand); + expect(rootClasses).toContain('consumer'); + expect(styled.icon!.className!.split(/\s+/)).toContain(styles.icon32); + }); + + it('renders the full pipeline against a frozen headless state without throwing', () => { + expect(() => renderAvatar()).not.toThrow(); + expect(() => renderAvatar({ name: 'Katri Athokas', size: 16 })).not.toThrow(); + expect(() => renderAvatar({ color: 'colorful', image: { src: 'x.png' }, name: 'Katri Athokas' })).not.toThrow(); + expect(() => renderAvatar({ active: 'inactive', initials: 'ZZ' })).not.toThrow(); + expect(() => renderAvatar({ icon: null })).not.toThrow(); + + sizes.forEach(size => { + (['ring', 'shadow', 'ring-shadow'] as const).forEach(activeAppearance => { + expect(() => + renderAvatar({ active: 'active', activeAppearance, name: 'Katri Athokas', shape: 'square', size }), + ).not.toThrow(); + }); + }); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.tsx b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.tsx new file mode 100644 index 0000000000000..3c19a11e893db --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.tsx @@ -0,0 +1,159 @@ +'use client'; + +import * as React from 'react'; +import { slot } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAvatar, useAvatar, useAvatarContext } from '@fluentui/react-headless-components-preview/avatar'; +import { useProviderContext } from '@fluentui/react-headless-components-preview/provider'; +import { PersonRegular } from '@fluentui/react-icons/headless/svg/person'; + +import { mergeContextProps } from '../../utils/mergeContextProps'; +import type { AvatarNamedColor, AvatarProps, AvatarState } from './Avatar.types'; +import { useAvatarStyles } from './useAvatarStyles'; + +// Hash order is load-bearing: a name must resolve to the same colour it does in the Griffel suite. +const AVATAR_COLORS: AvatarNamedColor[] = [ + 'dark-red', + 'cranberry', + 'red', + 'pumpkin', + 'peach', + 'marigold', + 'gold', + 'brass', + 'brown', + 'forest', + 'seafoam', + 'dark-green', + 'light-teal', + 'teal', + 'steel', + 'blue', + 'royal-blue', + 'cornflower', + 'navy', + 'lavender', + 'purple', + 'grape', + 'lilac', + 'pink', + 'magenta', + 'plum', + 'beige', + 'mink', + 'platinum', + 'anchor', +]; + +const getHashCode = (str: string): number => { + let hashCode = 0; + + for (let len: number = str.length - 1; len >= 0; len--) { + const ch = str.charCodeAt(len); + const shift = len % 8; + + // eslint-disable-next-line no-bitwise + hashCode ^= (ch << shift) + (ch >> (8 - shift)); + } + + return hashCode; +}; + +const ACTIVE_STRINGS = { active: 'active', inactive: 'inactive' } as const; + +const firstInitial = (initials: string, rtl: boolean) => { + const codePoints = [...initials]; + + return (rtl ? codePoints[codePoints.length - 1] : codePoints[0]) ?? ''; +}; + +/** + * An Avatar shows a person or entity as an image, their initials, or a generic icon. Windmod + * Avatar: the headless avatar decorated with the Fluent visual contract (Tailwind v4 + CSS + * Modules). + */ +export const Avatar: ForwardRefComponent = React.forwardRef((props, ref) => { + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-avatar's styled useAvatar, AvatarContext read included: a + // container such as Tag publishes both `shape` and `size`, and Griffel spells the resolution as + // the destructuring defaults `size = contextSize ?? 32` / `shape = contextShape ?? 'circular'` + // (react-avatar useAvatar.tsx:19-21). Those fire only when the prop is absent, so Griffel's + // precedence is prop, then context, then constant — the order mergeContextProps reproduces, not + // the context-first order the spelling reads like. The published shape is disjoint from the + // `props.*` reads further down (`initials`, `name`, `aria-label`, `aria-labelledby`), so those + // keep reading `props` and are unaffected by the merge. + const { + active = 'unset', + activeAppearance = 'ring', + color: propColor = 'neutral', + idForColor, + shape = 'circular', + size = 32, + ...rest + } = mergeContextProps(useAvatarContext(), props); + + const { dir } = useProviderContext(); + const base = useAvatar(rest, ref); + + // The headless icon slot ships no glyph of its own, and it is the last rung of the image → + // initials → icon cascade, so an icon-only Avatar would otherwise be an empty coloured circle. + // The slot exists only when there is neither an initials nor a visible image, and that decision + // is made inside the hook, so the restoration has to run on the resolved state. Consumer + // children always win; `icon={null}` still removes the slot. + const icon: AvatarState['icon'] = base.icon && { ...base.icon, children: base.icon.children ?? }; + + // At 16px only one initial fits. The base derived both from `name`, so the truncation reruns + // here against the same direction the base read: the rtl branch of getInitials swaps the pair, + // which puts the leading initial last. + const derived = slot.resolveShorthand(props.initials)?.children === undefined; + const initials: AvatarState['initials'] = + base.initials && size <= 16 && derived && typeof base.initials.children === 'string' + ? { ...base.initials, children: firstInitial(base.initials.children, dir === 'rtl') } + : base.initials; + + // `active` is a windmod look prop, so the accessible name it contributes has no other home: + // the base always returns the element undefined, and the renderer already places it last inside + // the root. A consumer-supplied name suppresses both branches, as it does in the base. + let root = base.root; + let activeAriaLabelElement = base.activeAriaLabelElement; + + if (props['aria-label'] === undefined && props['aria-labelledby'] === undefined && active !== 'unset') { + const activeText = ACTIVE_STRINGS[active]; + + if (root['aria-labelledby']) { + const activeId = root.id + '__active'; + + root = { ...root, 'aria-labelledby': root['aria-labelledby'] + ' ' + activeId }; + activeAriaLabelElement = ( + + ); + } else if (root['aria-label']) { + root = { ...root, 'aria-label': root['aria-label'] + ' ' + activeText }; + } + } + + const color: AvatarState['color'] = + propColor === 'colorful' + ? AVATAR_COLORS[getHashCode(idForColor ?? props.name ?? '') % AVATAR_COLORS.length] + : propColor; + + return renderAvatar( + useAvatarStyles({ + ...base, + root, + icon, + initials, + activeAriaLabelElement, + active, + activeAppearance, + color, + shape, + size, + }), + ); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +Avatar.displayName = 'Avatar'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.types.ts new file mode 100644 index 0000000000000..8815f41d76768 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/Avatar.types.ts @@ -0,0 +1,79 @@ +import type { + AvatarProps as AvatarHeadlessProps, + AvatarState as AvatarHeadlessState, +} from '@fluentui/react-headless-components-preview/avatar'; + +export type { AvatarSlots } from '@fluentui/react-headless-components-preview/avatar'; + +/** The 30 named colours the `colorful` hash picks from, in hash order. */ +export type AvatarNamedColor = + | 'dark-red' + | 'cranberry' + | 'red' + | 'pumpkin' + | 'peach' + | 'marigold' + | 'gold' + | 'brass' + | 'brown' + | 'forest' + | 'seafoam' + | 'dark-green' + | 'light-teal' + | 'teal' + | 'steel' + | 'blue' + | 'royal-blue' + | 'cornflower' + | 'navy' + | 'lavender' + | 'purple' + | 'grape' + | 'lilac' + | 'pink' + | 'magenta' + | 'plum' + | 'beige' + | 'mink' + | 'platinum' + | 'anchor'; + +/** Whether the Avatar is decorated to read as active, faded to read as inactive, or neither. */ +export type AvatarActive = 'active' | 'inactive' | 'unset'; + +/** Which decoration the active state draws. */ +export type AvatarActiveAppearance = 'ring' | 'shadow' | 'ring-shadow'; + +/** `colorful` is resolved to a named colour before it reaches the state. */ +export type AvatarColor = 'neutral' | 'brand' | 'colorful' | AvatarNamedColor; + +/** The square shape picks its corner radius from the size, so the two axes are not independent. */ +export type AvatarShape = 'circular' | 'square'; + +/** Both the box edge in pixels and the bucket the text, radius, ring and shadow are picked from. */ +export type AvatarSize = 16 | 20 | 24 | 28 | 32 | 36 | 40 | 48 | 56 | 64 | 72 | 96 | 120 | 128; + +/** + * Windmod Avatar props: the headless avatar plus the look props the headless surface + * deliberately omits (they exist purely to select styles). + */ +export type AvatarProps = AvatarHeadlessProps & { + /** @default 'unset' */ + active?: AvatarActive; + /** @default 'ring' */ + activeAppearance?: AvatarActiveAppearance; + /** @default 'neutral' */ + color?: AvatarColor; + /** Seeds the `colorful` hash in place of `name`. */ + idForColor?: string | undefined; + /** @default 'circular' */ + shape?: AvatarShape; + /** @default 32 */ + size?: AvatarSize; +}; + +/** Windmod Avatar state: headless state plus the resolved look props. */ +export type AvatarState = AvatarHeadlessState & + Required> & { + color: Exclude; + }; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Avatar/index.ts new file mode 100644 index 0000000000000..959f0f15c491f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/index.ts @@ -0,0 +1,13 @@ +export { Avatar } from './Avatar'; +export { avatarClassNames, useAvatarStyles } from './useAvatarStyles'; +export type { + AvatarActive, + AvatarActiveAppearance, + AvatarColor, + AvatarNamedColor, + AvatarProps, + AvatarShape, + AvatarSize, + AvatarSlots, + AvatarState, +} from './Avatar.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Avatar/useAvatarStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Avatar/useAvatarStyles.ts new file mode 100644 index 0000000000000..50493d931a5c1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Avatar/useAvatarStyles.ts @@ -0,0 +1,110 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { AvatarSize, AvatarState } from './Avatar.types'; + +import styles from './Avatar.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const avatarClassNames: { root: string } = { + root: componentMarkers('avatar'), +}; + +type AvatarRootDataAttributes = { + 'data-size'?: AvatarState['size']; +}; + +// Every size-dependent value except the box edge is a bucket, so the fourteen sizes collapse to +// these five ladders plus the root's typed attr(). Each range is written in full and both its +// boundaries are inclusive, so the keys partition AvatarSize with no implied else. `+()` coerces +// a condition to 1 or 0 because TS rejects a bare boolean computed key (TS2464). +const textClass = (size: AvatarSize) => + ({ + [+(size <= 24)]: styles.textCaption2Strong, + [+(size > 24 && size <= 28)]: styles.textCaption1Strong, + // 32–40 is Griffel's base bucket: it names no font of its own, so no class is merged. + [+(size > 28 && size <= 40)]: undefined, + [+(size > 40 && size <= 56)]: styles.textSubtitle2, + [+(size > 56 && size <= 96)]: styles.textSubtitle1, + [+(size > 96)]: styles.textTitle3, + })[1]; + +const squareClass = (size: AvatarSize) => + ({ + [+(size <= 24)]: styles.squareSmall, + [+(size > 24 && size <= 48)]: styles.squareMedium, + [+(size > 48 && size <= 72)]: styles.squareLarge, + [+(size > 72)]: styles.squareXLarge, + })[1]; + +const ringClass = (size: AvatarSize) => + ({ + [+(size <= 48)]: styles.ringThick, + [+(size > 48 && size <= 64)]: styles.ringThicker, + [+(size > 64)]: styles.ringThickest, + })[1]; + +const shadowClass = (size: AvatarSize) => + ({ + [+(size <= 28)]: styles.shadow4, + [+(size > 28 && size <= 48)]: styles.shadow8, + [+(size > 48 && size <= 64)]: styles.shadow16, + [+(size > 64)]: styles.shadow28, + })[1]; + +const iconClass = (size: AvatarSize) => + ({ + [+(size <= 16)]: styles.icon12, + [+(size > 16 && size <= 24)]: styles.icon16, + [+(size > 24 && size <= 40)]: styles.icon20, + [+(size > 40 && size <= 48)]: styles.icon24, + [+(size > 48 && size <= 56)]: styles.icon28, + [+(size > 56 && size <= 72)]: styles.icon32, + [+(size > 72)]: styles.icon48, + })[1]; + +/** + * Applies the visual contract, returning new state. The headless hook stamps no attributes at + * all; `data-size` is the only one added here, because the root's box edge is the one value CSS + * cannot bucket. Shape, active state, appearance and colour stay module-class axes — each is + * resolved in JS before the class list is built, and block order carries their cascade. + * + * The colour class sits on the root and declares custom properties alone; the image, initials and + * icon slots read them. Painting the colours on the root itself would give it a background it + * must not have. + */ +export const useAvatarStyles = (state: AvatarState): AvatarState => { + const { active, activeAppearance, color, shape, size } = state; + // True when the `active` prop is set at all, either value: that is what enables the + // activity-indicator machinery. Named for Griffel's own bucket (`activeOrInactive`, its + // useAvatarStyles.styles.ts) so the flag and the class it gates read as one vocabulary. + const isActiveOrInactive = active === 'active' || active === 'inactive'; + const ring = isActiveOrInactive && (activeAppearance === 'ring' || activeAppearance === 'ring-shadow'); + const shadow = isActiveOrInactive && (activeAppearance === 'shadow' || activeAppearance === 'ring-shadow'); + + const root: AvatarState['root'] & AvatarRootDataAttributes = { + ...state.root, + 'data-size': size, + className: clsx( + avatarClassNames.root, + styles.root, + textClass(size), + shape === 'square' && squareClass(size), + isActiveOrInactive && styles.activeOrInactive, + ring && [styles.ring, ringClass(size)], + shadow && [styles.shadow, shadowClass(size)], + active === 'inactive' && styles.inactive, + styles[color], + state.root.className, + ), + }; + + return { + ...state, + root, + image: slotClasses(state.image, styles.image), + initials: slotClasses(state.initials, styles.iconInitials), + icon: slotClasses(state.icon, styles.iconInitials, iconClass(size)), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.module.css b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.module.css new file mode 100644 index 0000000000000..719f588ade6ba --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.module.css @@ -0,0 +1,21 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply relative inline-flex; + } + + /* The box exists only in the pie layout — the spread and stack layouts size themselves from + their children. */ + .pie { + --fui-avatar-group-size: calc(attr(data-size type(), 32) * 1px); + + @apply size-(--fui-avatar-group-size) bg-transparent-stroke; + + clip-path: circle(50%); + + @variant forced-colors { + @apply bg-[CanvasText]; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.test.tsx new file mode 100644 index 0000000000000..d0ee346f056c0 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.test.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { AvatarGroupItem } from '../AvatarGroupItem'; +import { AvatarGroup } from './AvatarGroup'; +import { avatarGroupClassNames } from './useAvatarGroupStyles'; + +import styles from './AvatarGroup.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/avatar-group', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/avatar-group'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAvatarGroup: (...args: Parameters) => + deepFreezeState(actual.useAvatarGroup(...args)), + }; +}); + +const renderGroup = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return container.firstElementChild as HTMLElement; +}; + +describe('AvatarGroup', () => { + isConformant({ + Component: AvatarGroup, + displayName: 'AvatarGroup', + }); + + it('stamps the marker pair on the root, in order', () => { + const root = renderGroup(); + + expect(avatarGroupClassNames.root).toBe('fui-avatar-group group/fui-avatar-group'); + expect(root).toHaveClass('fui-avatar-group'); + expect(root).toHaveClass('group/fui-avatar-group'); + expect(root.classList[0]).toBe('fui-avatar-group'); + expect(root.classList[1]).toBe('group/fui-avatar-group'); + }); + + it('defaults size to 32 and stamps it', () => { + expect(renderGroup().getAttribute('data-size')).toBe('32'); + expect(renderGroup({ size: 56 }).getAttribute('data-size')).toBe('56'); + }); + + it('leaves the headless data-layout stamp alone and adds no second layout attribute', () => { + const root = renderGroup({ layout: 'stack' }); + + expect(root.getAttribute('data-layout')).toBe('stack'); + expect( + root + .getAttributeNames() + .filter(name => name.startsWith('data-')) + .sort(), + ).toEqual(['data-layout', 'data-size']); + }); + + it('applies the pie class only under layout="pie"', () => { + expect(renderGroup({ layout: 'pie' }).classList).toContain(styles.pie); + expect(renderGroup({ layout: 'spread' }).classList).not.toContain(styles.pie); + expect(renderGroup({ layout: 'stack' }).classList).not.toContain(styles.pie); + }); + + it('publishes both layout and size on the context', () => { + // The headless context value carries `layout` alone; without the windmod republish every + // child falls back to the 32px default. Read structurally, through a real child. + const { container } = render( + + + , + ); + const item = container.querySelector('.fui-avatar-group-item'); + + expect(item?.getAttribute('data-size')).toBe('56'); + expect(item?.querySelector('.fui-avatar')?.getAttribute('data-size')).toBe('56'); + }); + + it('passes everything else through to the headless hook untouched', () => { + const root = renderGroup({ id: 'group-1', 'aria-label': 'Team' }); + + expect(root.id).toBe('group-1'); + expect(root.getAttribute('role')).toBe('group'); + expect(root.getAttribute('aria-label')).toBe('Team'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.tsx new file mode 100644 index 0000000000000..f784c91600681 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.tsx @@ -0,0 +1,30 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderAvatarGroup, useAvatarGroup } from '@fluentui/react-headless-components-preview/avatar-group'; + +import type { AvatarGroupProps } from './AvatarGroup.types'; +import { useAvatarGroupStyles } from './useAvatarGroupStyles'; + +/** + * An AvatarGroup displays a set of people or entities as a spread, stack or pie of Avatars. + * Windmod AvatarGroup: the headless avatar group decorated with the Fluent visual contract + * (Tailwind v4 + CSS Modules). + */ +export const AvatarGroup: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-avatar's styled useAvatarGroup. + ({ size = 32, ...rest }, ref) => { + const state = useAvatarGroup(rest, ref); + + // The headless context value carries `layout` alone; both the items and the overflow trigger + // pick their whole size ladder off `size`, so windmod publishes its own value in place of + // useAvatarGroupContextValues. + const avatarGroup = React.useMemo(() => ({ layout: state.layout, size }), [state.layout, size]); + + return renderAvatarGroup(useAvatarGroupStyles({ ...state, size }), { avatarGroup }); + }, +); + +AvatarGroup.displayName = 'AvatarGroup'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts new file mode 100644 index 0000000000000..a5c337dc77d84 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts @@ -0,0 +1,20 @@ +import type { + AvatarGroupProps as AvatarGroupHeadlessProps, + AvatarGroupState as AvatarGroupHeadlessState, +} from '@fluentui/react-headless-components-preview/avatar-group'; + +import type { AvatarSize } from '../Avatar'; + +export type { AvatarGroupSlots } from '@fluentui/react-headless-components-preview/avatar-group'; + +/** + * Windmod AvatarGroup props: the headless avatar group plus the look prop the headless surface + * deliberately omits (it exists purely to select styles, and it reaches every child by context). + */ +export type AvatarGroupProps = AvatarGroupHeadlessProps & { + /** @default 32 */ + size?: AvatarSize; +}; + +/** Windmod AvatarGroup state: headless state plus the resolved look prop. */ +export type AvatarGroupState = AvatarGroupHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/index.ts new file mode 100644 index 0000000000000..c4e0cb52c4b68 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/index.ts @@ -0,0 +1,3 @@ +export { AvatarGroup } from './AvatarGroup'; +export { avatarGroupClassNames, useAvatarGroupStyles } from './useAvatarGroupStyles'; +export type { AvatarGroupProps, AvatarGroupSlots, AvatarGroupState } from './AvatarGroup.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/useAvatarGroupStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/useAvatarGroupStyles.ts new file mode 100644 index 0000000000000..f491a6e7f300d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroup/useAvatarGroupStyles.ts @@ -0,0 +1,32 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { AvatarGroupState } from './AvatarGroup.types'; + +import styles from './AvatarGroup.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const avatarGroupClassNames: { root: string } = { + root: componentMarkers('avatar-group'), +}; + +type AvatarGroupRootDataAttributes = { + 'data-size'?: AvatarGroupState['size']; +}; + +/** + * Applies the visual contract, returning new state. `data-layout` is already stamped by the + * headless hook, so `data-size` is the only attribute added here — the pie box edge is the one + * value CSS cannot bucket. + */ +export const useAvatarGroupStyles = (state: AvatarGroupState): AvatarGroupState => { + const { layout, size } = state; + + const root: AvatarGroupState['root'] & AvatarGroupRootDataAttributes = { + ...state.root, + 'data-size': size, + className: clsx(avatarGroupClassNames.root, styles.root, layout === 'pie' && styles.pie, state.root.className), + }; + + return { ...state, root }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.module.css b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.module.css new file mode 100644 index 0000000000000..c92e46d8779d7 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.module.css @@ -0,0 +1,163 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply relative inline-flex shrink-0 items-center; + } + + .non-overflow-item { + --fui-avatar-group-item-size: calc(attr(data-size type(), 32) * 1px); + + @apply size-(--fui-avatar-group-item-size) rounded-circular; + } + + .overflow-item { + @apply px-horizontal-xs py-vertical-xs; + } + + .overflow-label { + @apply ms-horizontal-s font-base text-base-300 leading-base-300 font-regular text-neutral-foreground-1; + } + + .stack-thick { + box-shadow: 0 0 0 var(--stroke-width-thick) var(--color-neutral-background-2); + } + + .stack-thicker { + box-shadow: 0 0 0 var(--stroke-width-thicker) var(--color-neutral-background-2); + } + + .stack-thickest { + box-shadow: 0 0 0 var(--stroke-width-thickest) var(--color-neutral-background-2); + } + + /* A single logical margin reproduces the physical left/right pair Griffel picks between by + reading `dir`. */ + .stack-gap-xxs { + @variant not-first { + @apply -ms-horizontal-xxs; + } + } + + .stack-gap-xs { + @variant not-first { + @apply -ms-horizontal-xs; + } + } + + .stack-gap-s { + @variant not-first { + @apply -ms-horizontal-s; + } + } + + .stack-gap-l { + @variant not-first { + @apply -ms-horizontal-l; + } + } + + .spread-gap-s { + @variant not-first { + @apply ms-horizontal-s; + } + } + + .spread-gap-m-nudge { + @variant not-first { + @apply ms-horizontal-m-nudge; + } + } + + .spread-gap-l { + @variant not-first { + @apply ms-horizontal-l; + } + } + + .spread-gap-xl { + @variant not-first { + @apply ms-horizontal-xl; + } + } + + .pie { + @apply absolute; + } + + /* The RTL half mirrors the four clip-paths and moves the two three-slice quadrants back to the + inline start; `transform-origin` has no RTL twin on Griffel and stays physically top-left. */ + .pie-slices { + &:nth-of-type(1):nth-last-of-type(2), + &:nth-of-type(1):nth-last-of-type(3) { + @apply -start-1/4; + + clip-path: inset(0 calc(25% + (var(--fui-avatar-group-item-divider-width) / 2)) 0 25%); + } + + &:nth-of-type(2):nth-last-of-type(1) { + @apply start-1/4; + + clip-path: inset(0 25% 0 calc(25% + (var(--fui-avatar-group-item-divider-width) / 2))); + } + + &:nth-of-type(2):nth-last-of-type(2) { + @apply start-1/2 origin-top-left scale-50; + + clip-path: inset(0 0 var(--fui-avatar-group-item-divider-width) var(--fui-avatar-group-item-divider-width)); + } + + &:nth-of-type(3):nth-last-of-type(1) { + @apply start-1/2 top-1/2 origin-top-left scale-50; + + clip-path: inset(var(--fui-avatar-group-item-divider-width) 0 0 var(--fui-avatar-group-item-divider-width)); + } + + @variant rtl { + &:nth-of-type(1):nth-last-of-type(2), + &:nth-of-type(1):nth-last-of-type(3) { + clip-path: inset(0 25% 0 calc(25% + (var(--fui-avatar-group-item-divider-width) / 2))); + } + + &:nth-of-type(2):nth-last-of-type(1) { + clip-path: inset(0 calc(25% + (var(--fui-avatar-group-item-divider-width) / 2)) 0 25%); + } + + &:nth-of-type(2):nth-last-of-type(2) { + @apply start-0; + + clip-path: inset(0 var(--fui-avatar-group-item-divider-width) var(--fui-avatar-group-item-divider-width) 0); + } + + &:nth-of-type(3):nth-last-of-type(1) { + @apply start-0; + + clip-path: inset(var(--fui-avatar-group-item-divider-width) var(--fui-avatar-group-item-divider-width) 0 0); + } + } + } + + .pie-divider-thick { + --fui-avatar-group-item-divider-width: var(--stroke-width-thick); + } + + .pie-divider-thicker { + --fui-avatar-group-item-divider-width: var(--stroke-width-thicker); + } + + .pie-divider-thickest { + --fui-avatar-group-item-divider-width: var(--stroke-width-thickest); + } +} + +@layer fui.components.l2 { + /* The avatar slot renders a windmod Avatar; these blocks override its own l1 root + (`position: relative`, `rounded-circular`), so they sit one level above it. */ + .avatar-non-overflow { + @apply absolute; + } + + .avatar-pie { + @apply rounded-none; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx new file mode 100644 index 0000000000000..c9c283d3f984b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx @@ -0,0 +1,210 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { AvatarGroupProvider } from '@fluentui/react-headless-components-preview/avatar-group'; + +import { isConformant } from '../../testing/isConformant'; +import { AvatarGroup } from '../AvatarGroup'; +import type { AvatarGroupProps } from '../AvatarGroup'; +import { AvatarGroupItem } from './AvatarGroupItem'; +import { avatarGroupItemClassNames } from './useAvatarGroupItemStyles'; + +import styles from './AvatarGroupItem.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/avatar-group', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/avatar-group'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAvatarGroupItem: (...args: Parameters) => + deepFreezeState(actual.useAvatarGroupItem(...args)), + }; +}); + +type ItemProps = React.ComponentProps; + +// Queried by the marker class, never by a module ident: this module's `root` and AvatarGroup's +// `root` are the same string under the jest ident proxy while the built idents differ. +const renderItem = (groupProps: AvatarGroupProps = {}, itemProps: ItemProps = {}) => { + const { container } = render( + + + , + ); + const root = container.querySelector(`.${avatarGroupItemClassNames.root.split(' ')[0]}`)!; + + return { root, coin: root.querySelector('.fui-avatar')! }; +}; + +const renderOverflowItem = (itemProps: ItemProps = {}) => { + const { container } = render( + + + , + ); + + return container.firstElementChild as HTMLElement; +}; + +describe('AvatarGroupItem', () => { + isConformant({ + Component: AvatarGroupItem, + displayName: 'AvatarGroupItem', + requiredProps: { name: 'Ada Lovelace' }, + // The ref lands on the avatar, which is the primary slot on both libraries — the root exists + // only to carry the layout ladder. + primarySlot: 'avatar', + }); + + it('stamps the marker pair on the root, in order', () => { + const { root } = renderItem(); + + expect(avatarGroupItemClassNames.root).toBe('fui-avatar-group-item group/fui-avatar-group-item'); + expect(root).toHaveClass('fui-avatar-group-item'); + expect(root).toHaveClass('group/fui-avatar-group-item'); + expect(root.classList[0]).toBe('fui-avatar-group-item'); + expect(root.classList[1]).toBe('group/fui-avatar-group-item'); + }); + + it('reads size off the group context and reaches both the root and the avatar', () => { + const { root, coin } = renderItem({ size: 72 }); + + expect(root.getAttribute('data-size')).toBe('72'); + expect(coin.getAttribute('data-size')).toBe('72'); + }); + + describe('the avatar slot', () => { + it('renders the windmod Avatar rather than the headless one', () => { + // The swap is only real when the slot's elementType changes too — a `components` swap is + // read at render by assertSlots alone, inside a development-only guard. + expect(renderItem().coin).not.toBeNull(); + }); + + it('renders the windmod Avatar in production mode, where assertSlots does not run', () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + try { + expect(renderItem().coin).not.toBeNull(); + } finally { + process.env.NODE_ENV = previous; + } + }); + + it('warns nothing in development, so the components map and the element type agree', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + renderItem(); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('defaults color to colorful, hashing distinct names to distinct classes', () => { + const ada = renderItem({}, { name: 'Ada Lovelace' }).coin.className; + const grace = renderItem({}, { name: 'Grace Hopper' }).coin.className; + + expect(ada).not.toBe(grace); + }); + + it('lets an explicit color win over the colorful default', () => { + const brand = renderItem({}, { color: 'brand' }).coin.className; + const colorful = renderItem({}, { name: 'Ada Lovelace' }).coin.className; + + expect(brand).not.toBe(colorful); + }); + + it('seeds the colorful hash from idForColor when supplied', () => { + const seeded = renderItem({}, { name: 'Ada Lovelace', idForColor: 'Grace Hopper' }).coin.className; + const byName = renderItem({}, { name: 'Grace Hopper' }).coin.className; + + expect(seeded).toBe(byName); + }); + }); + + describe('the overflow form', () => { + it('renders an li carrying the name label', () => { + const root = renderOverflowItem(); + + expect(root.tagName).toBe('LI'); + expect(root.classList).toContain(styles.overflowItem); + expect(root.querySelector(`.${styles.overflowLabel}`)?.textContent).toBe('Ada Lovelace'); + }); + + it('renders a div without the label when inline', () => { + const { root } = renderItem(); + + expect(root.tagName).toBe('DIV'); + expect(root.classList).toContain(styles.nonOverflowItem); + expect(root.querySelector(`.${styles.overflowLabel}`)).toBeNull(); + }); + }); + + describe('the stack ladders', () => { + it.each([ + [48, styles.stackThick], + [56, styles.stackThicker], + [64, styles.stackThicker], + [72, styles.stackThickest], + ] as const)('picks the ring for size %s', (size, expected) => { + expect(renderItem({ layout: 'stack', size }).root.classList).toContain(expected); + }); + + it.each([ + [20, styles.stackGapXxs], + [24, styles.stackGapXs], + [48, styles.stackGapS], + [96, styles.stackGapL], + ] as const)('picks the gap for size %s', (size, expected) => { + expect(renderItem({ layout: 'stack', size }).root.classList).toContain(expected); + }); + }); + + describe('the spread ladder', () => { + it.each([ + [16, styles.spreadGapS], + [20, styles.spreadGapMNudge], + [24, styles.spreadGapMNudge], + [32, styles.spreadGapL], + [64, styles.spreadGapXl], + ] as const)('picks the gap for size %s', (size, expected) => { + expect(renderItem({ layout: 'spread', size }).root.classList).toContain(expected); + }); + }); + + describe('the pie layout', () => { + it.each([ + [48, styles.pieDividerThick], + [56, styles.pieDividerThicker], + [72, styles.pieDividerThickest], + ] as const)('picks the divider width for size %s', (size, expected) => { + expect(renderItem({ layout: 'pie', size }).root.classList).toContain(expected); + }); + + it('applies the slice geometry and the pie avatar class', () => { + const { root, coin } = renderItem({ layout: 'pie' }); + + expect(root.classList).toContain(styles.pieSlices); + expect(root.classList).toContain(styles.pie); + expect(coin.classList).toContain(styles.avatarPie); + }); + + it('takes no gap class from the shared ladder', () => { + const { root } = renderItem({ layout: 'pie', size: 32 }); + + expect(root.classList).not.toContain(styles.spreadGapL); + expect(root.classList).not.toContain(styles.stackGapXs); + }); + }); + + it('warns exactly once when rendered outside a group', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + render(); + + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith('AvatarGroupItem must only be used inside an AvatarGroup component.'); + warn.mockRestore(); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.tsx new file mode 100644 index 0000000000000..d4056d07cab3f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.tsx @@ -0,0 +1,47 @@ +'use client'; + +import * as React from 'react'; +import { slot } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderAvatarGroupItem, + useAvatarGroupContext, + useAvatarGroupItem, +} from '@fluentui/react-headless-components-preview/avatar-group'; + +import { Avatar } from '../Avatar'; +import type { AvatarGroupItemProps } from './AvatarGroupItem.types'; +import { useAvatarGroupItemStyles } from './useAvatarGroupItemStyles'; + +/** + * An AvatarGroupItem is one member of an AvatarGroup, rendered as a coin inline or as a named row + * inside the overflow popover. Windmod AvatarGroupItem: the headless item decorated with the + * Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const AvatarGroupItem: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them, and its + // types explicitly omit these two. Defaults mirror @fluentui/react-avatar's styled + // useAvatarGroupItem. `avatar` stays in `rest`: the headless base hook forwards it into the + // avatar slot rather than onto the root, so lifting it out would drop a consumer's shorthand. + ({ color = 'colorful', idForColor, ...rest }, ref) => { + // The group publishes its size on the context; the fallback matches Griffel's own constant. + const size = useAvatarGroupContext(ctx => ctx.size) ?? 32; + const base = useAvatarGroupItem(rest, ref); + + // The avatar slot is re-created with the windmod element type — see Persona.tsx for what the + // headless Avatar cannot carry, and Combobox.tsx for why the components swap alone is not + // enough. The look defaults sit ahead of the resolved slot so consumer props still win. + return renderAvatarGroupItem( + useAvatarGroupItemStyles({ + ...base, + // eslint-disable-next-line @typescript-eslint/no-deprecated -- reading base.components to keep every other slot's element type + components: { ...base.components, avatar: Avatar }, + avatar: slot.always({ size, color, idForColor, ...base.avatar }, { elementType: Avatar }), + size, + }), + ); + }, + // Casting is required due to lack of distributive union to support union on @types/react +) as ForwardRefComponent; + +AvatarGroupItem.displayName = 'AvatarGroupItem'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.types.ts new file mode 100644 index 0000000000000..95e9d7ffa0583 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/AvatarGroupItem.types.ts @@ -0,0 +1,33 @@ +import type { ComponentState, Slot } from '@fluentui/react-utilities'; +import type { + AvatarGroupItemProps as AvatarGroupItemHeadlessProps, + AvatarGroupItemSlots as AvatarGroupItemHeadlessSlots, + AvatarGroupItemState as AvatarGroupItemHeadlessState, +} from '@fluentui/react-headless-components-preview/avatar-group'; + +import type { Avatar, AvatarColor, AvatarSize } from '../Avatar'; + +/** + * The headless slots type the avatar against the headless Avatar, which carries none of the look + * props windmod's Avatar adds and does not narrow `color` — see PersonaSlots. + */ +export type AvatarGroupItemSlots = Omit & { + avatar: NonNullable>; +}; + +/** + * Windmod AvatarGroupItem props: the headless item plus the two avatar look props the headless + * surface deliberately omits. + */ +export type AvatarGroupItemProps = AvatarGroupItemHeadlessProps & { + /** @default 'colorful' */ + color?: AvatarColor; + /** Seeds the `colorful` hash in place of the avatar's `name`. */ + idForColor?: string | undefined; +}; + +/** Windmod AvatarGroupItem state: headless state plus the size read off the group context. */ +export type AvatarGroupItemState = ComponentState & + Pick & { + size: AvatarSize; + }; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/groupChildClasses.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/groupChildClasses.ts new file mode 100644 index 0000000000000..cbda3b5c32e1e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/groupChildClasses.ts @@ -0,0 +1,51 @@ +import { clsx } from 'clsx'; + +import type { AvatarSize } from '../Avatar'; +import type { AvatarGroupProps } from '../AvatarGroup/AvatarGroup.types'; + +import styles from './AvatarGroupItem.module.css'; + +// Each upper boundary is exclusive, and the ring and gap ladders do not share one. Every range is +// written in full so the keys partition AvatarSize with no implied else; `+()` coerces a condition +// to 1 or 0 because TS rejects a bare boolean computed key (TS2464). +const stackRingClass = (size: AvatarSize) => + ({ + [+(size < 56)]: styles.stackThick, + [+(size >= 56 && size < 72)]: styles.stackThicker, + [+(size >= 72)]: styles.stackThickest, + })[1]; + +const stackGapClass = (size: AvatarSize) => + ({ + [+(size < 24)]: styles.stackGapXxs, + [+(size >= 24 && size < 48)]: styles.stackGapXs, + [+(size >= 48 && size < 96)]: styles.stackGapS, + [+(size >= 96)]: styles.stackGapL, + })[1]; + +// Four classes, not five: Griffel's `m` bucket sits between the `mNudge` and `l` boundaries and +// is unreachable for every size. +const spreadGapClass = (size: AvatarSize) => + ({ + [+(size < 20)]: styles.spreadGapS, + [+(size >= 20 && size < 32)]: styles.spreadGapMNudge, + [+(size >= 32 && size < 64)]: styles.spreadGapL, + [+(size >= 64)]: styles.spreadGapXl, + })[1]; + +/** + * Spacing and ring a direct child of an AvatarGroup carries for its layout and size. A plain + * function rather than a hook: no shared state exists, only conditional class assembly. The pie + * layout spaces its children by clip geometry instead and takes nothing from here. + * + * The overflow trigger button is a group child too, so AvatarGroupPopover calls this as well. + */ +export const groupChildClasses = (layout: AvatarGroupProps['layout'], size: AvatarSize): string => { + if (layout === 'stack') { + return clsx(stackRingClass(size), stackGapClass(size)); + } + if (layout === 'spread') { + return clsx(spreadGapClass(size)); + } + return ''; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/index.ts new file mode 100644 index 0000000000000..a59b730b1e17b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/index.ts @@ -0,0 +1,3 @@ +export { AvatarGroupItem } from './AvatarGroupItem'; +export { avatarGroupItemClassNames, useAvatarGroupItemStyles } from './useAvatarGroupItemStyles'; +export type { AvatarGroupItemProps, AvatarGroupItemSlots, AvatarGroupItemState } from './AvatarGroupItem.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts new file mode 100644 index 0000000000000..fb7b2ea8f4394 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupItem/useAvatarGroupItemStyles.ts @@ -0,0 +1,64 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { AvatarSize } from '../Avatar'; +import type { AvatarGroupItemState } from './AvatarGroupItem.types'; +import { groupChildClasses } from './groupChildClasses'; + +import styles from './AvatarGroupItem.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const avatarGroupItemClassNames: { root: string } = { + root: componentMarkers('avatar-group-item'), +}; + +type AvatarGroupItemRootDataAttributes = { + 'data-size'?: AvatarGroupItemState['size']; +}; + +// Shares its boundaries with the stack ring ladder but never its class: one emits a box-shadow, +// the other a custom property, and they are never applied together. +const pieDividerClass = (size: AvatarSize) => + ({ + [+(size < 56)]: styles.pieDividerThick, + [+(size >= 56 && size < 72)]: styles.pieDividerThicker, + [+(size >= 72)]: styles.pieDividerThickest, + })[1]; + +/** + * Applies the visual contract, returning new state. The headless hook stamps no attributes at + * all; `data-size` is the only one added here, because the box edge is the one value CSS cannot + * bucket. + */ +export const useAvatarGroupItemStyles = (state: AvatarGroupItemState): AvatarGroupItemState => { + const { isOverflowItem, layout, size } = state; + + const root: AvatarGroupItemState['root'] & AvatarGroupItemRootDataAttributes = { + ...state.root, + 'data-size': size, + className: clsx( + avatarGroupItemClassNames.root, + styles.root, + isOverflowItem + ? styles.overflowItem + : [ + styles.nonOverflowItem, + groupChildClasses(layout, size), + layout === 'pie' && [styles.pie, pieDividerClass(size), styles.pieSlices], + ], + state.root.className, + ), + }; + + return { + ...state, + root, + avatar: slotClasses( + state.avatar, + !isOverflowItem && styles.avatarNonOverflow, + layout === 'pie' && styles.avatarPie, + ), + overflowLabel: slotClasses(state.overflowLabel, styles.overflowLabel), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.module.css b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.module.css new file mode 100644 index 0000000000000..9c80ad541daf0 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.module.css @@ -0,0 +1,141 @@ +@reference '#theme'; + +/* Block order carries the whole cascade. Every catalog variant is `:where()`-flat, so every block + inside `.trigger-button` sits at equal specificity and source order alone decides. Griffel does + NOT settle these by source order — it settles them by sheet order AND selector specificity, and + the two disagree in three places, so the order below is that resolution written out rather than + a transcription of Griffel's bucket order: + + - the four border-width classes are authored BEFORE `.trigger-button` so its focus-visible + block still wins the width; Griffel's focus indicator is an attribute selector and outranks + the plain width bucket. + - forced-colors sits ABOVE focus-visible, hover and active and below selected. Griffel's + forced-colors sheet is inserted last, which beats the base and selected buckets, but its + rule is a bare class and loses to the attribute and pseudo-class selectors of the other + three. + - `.trigger-button-pie` is a later class and beats every variant inside `.trigger-button`, + which is what Griffel gets by omitting the states and selected buckets under pie. It has to + re-assert BOTH the forced-colors border and the focus ring: Griffel merges its focus + indicator unconditionally, pie included, and that indicator outranks the pie bucket, so a + pie trigger without the re-assertion would show no focus ring at all. */ +@layer fui.components.l1 { + .content { + @apply m-0 flex list-none flex-col p-0; + } + + .border-thin { + @apply border-thin; + } + + .border-thick { + @apply border-thick; + } + + .border-thicker { + @apply border-thicker; + } + + .border-thickest { + @apply border-thickest; + } + + .trigger-button { + --fui-avatar-group-popover-size: calc(attr(data-size type(), 32) * 1px); + + @apply relative inline-flex size-(--fui-avatar-group-popover-size) shrink-0 items-center justify-center rounded-circular border-solid border-neutral-stroke-1 bg-neutral-background-1 p-0 text-neutral-foreground-1; + + @variant selected { + @apply border-neutral-stroke-1-selected bg-neutral-background-1-selected text-neutral-foreground-1-selected; + } + + @variant forced-colors { + @apply border-[CanvasText]; + } + + @variant focus-visible { + @apply border-thick border-solid border-stroke-focus-2 outline-none; + } + + @variant hover { + @apply border-neutral-stroke-1-hover bg-neutral-background-1-hover text-neutral-foreground-1-hover; + } + + @variant active { + @apply border-neutral-stroke-1-pressed bg-neutral-background-1-pressed text-neutral-foreground-1-pressed; + } + } + + .trigger-button-pie { + @apply border-transparent-stroke bg-transparent-background text-transparent; + + @variant forced-colors { + @apply border-[CanvasText]; + } + + @variant focus-visible { + @apply border-thick border-solid border-stroke-focus-2 outline-none; + } + } + + .text-caption-2-strong { + @apply font-base text-base-100 leading-base-100 font-semibold; + } + + .text-caption-1-strong { + @apply font-base text-base-200 leading-base-200 font-semibold; + } + + .text-body-1-strong { + @apply font-base text-base-300 leading-base-300 font-semibold; + } + + .text-subtitle-2 { + @apply font-base text-base-400 leading-base-400 font-semibold; + } + + .text-subtitle-1 { + @apply font-base text-base-500 leading-base-500 font-semibold; + } + + .text-title-3 { + @apply font-base text-base-600 leading-base-600 font-semibold; + } + + .icon-12 { + @apply text-icon-12; + } + + .icon-16 { + @apply text-icon-16; + } + + .icon-20 { + @apply text-icon-20; + } + + .icon-24 { + @apply text-icon-24; + } + + .icon-28 { + @apply text-icon-28; + } + + .icon-32 { + @apply text-icon-32; + } + + .icon-48 { + @apply text-icon-48; + } +} + +@layer fui.components.l2 { + /* Overrides the windmod PopoverSurface's own l1 padding and sizing. + The three lengths are Griffel's own numbers, authored against the content-box the popover + surface computes on both libraries. A box-sizing override here would fold the 8px padding and + the 1px border into all three and render the surface 18px narrower and shorter than Griffel's. */ + .popover-surface { + @apply max-h-220 min-h-80 w-220 overflow-x-hidden overflow-y-scroll px-horizontal-s py-vertical-s; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.test.tsx new file mode 100644 index 0000000000000..ddfa2b951a56a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.test.tsx @@ -0,0 +1,283 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { AvatarGroup } from '../AvatarGroup'; +import type { AvatarGroupProps } from '../AvatarGroup'; +import { AvatarGroupItem } from '../AvatarGroupItem'; +import itemStyles from '../AvatarGroupItem/AvatarGroupItem.module.css'; +import tooltipStyles from '../Tooltip/Tooltip.module.css'; +import { AvatarGroupPopover } from './AvatarGroupPopover'; +import { avatarGroupPopoverClassNames } from './useAvatarGroupPopoverStyles'; + +import styles from './AvatarGroupPopover.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +// The same mock records what AvatarGroupPopover hands the headless hook, which is where the resolved +// indicator and the materialised glyph land. +jest.mock('@fluentui/react-headless-components-preview/avatar-group', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/avatar-group'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useAvatarGroupPopover: (...args: Parameters) => { + hookProps.push(args[0] as HeadlessPopoverProps); + + return deepFreezeState(actual.useAvatarGroupPopover(...args)); + }, + }; +}); + +type HeadlessPopoverProps = { indicator?: 'count' | 'icon'; triggerButton?: { children?: React.ReactNode } }; +const hookProps: HeadlessPopoverProps[] = []; + +beforeEach(() => { + hookProps.length = 0; +}); + +type PopoverProps = React.ComponentProps; + +const members = ['Ada Lovelace', 'Grace Hopper']; + +const renderPopover = (groupProps: AvatarGroupProps = {}, popoverProps: Partial = {}) => { + const { container } = render( + + + {members.map(name => ( + + ))} + + , + ); + + return { + container, + trigger: container.querySelector('.fui-avatar-group-popover')!, + surface: container.querySelector('[data-popover-surface]'), + }; +}; + +/** What AvatarGroupPopover handed the headless hook on its first render. */ +const handedOver = () => hookProps[0]; + +describe('AvatarGroupPopover', () => { + isConformant({ + Component: AvatarGroupPopover, + displayName: 'AvatarGroupPopover', + requiredProps: { children: } as never, + // AvatarGroupPopover renders no element of its own — the render's outermost node is the + // headless Popover, which renders no DOM. The marker pair sits on the trigger button. + disabledTests: ['component-handles-ref', 'component-has-root-ref', 'component-handles-classname'], + }); + + it('stamps the marker pair on the trigger button, in order', () => { + const { trigger } = renderPopover(); + + expect(avatarGroupPopoverClassNames.triggerButton).toBe('fui-avatar-group-popover group/fui-avatar-group-popover'); + expect(trigger.tagName).toBe('BUTTON'); + expect(trigger).toHaveClass('fui-avatar-group-popover'); + expect(trigger).toHaveClass('group/fui-avatar-group-popover'); + expect(trigger.classList[0]).toBe('fui-avatar-group-popover'); + expect(trigger.classList[1]).toBe('group/fui-avatar-group-popover'); + }); + + it.each([16, 20, 24, 32] as const)('stamps data-size %s on the trigger button', size => { + expect(renderPopover({ size }).trigger.getAttribute('data-size')).toBe(String(size)); + }); + + describe('the size-keyed indicator default', () => { + it.each([16, 20] as const)('resolves to the glyph below 24 (size %s)', size => { + const { trigger } = renderPopover({ size }); + + expect(handedOver().indicator).toBe('icon'); + expect(trigger.querySelector('svg[data-fui-icon]')).not.toBeNull(); + expect(trigger.textContent).toBe(''); + }); + + it.each([24, 32] as const)('resolves to the count from 24 up (size %s)', size => { + const { trigger } = renderPopover({ size }); + + expect(handedOver().indicator).toBe('count'); + expect(trigger.textContent).toBe('+2'); + expect(trigger.querySelector('svg[data-fui-icon]')).toBeNull(); + }); + + it('lets an explicit indicator override the default', () => { + const { trigger } = renderPopover({ size: 72 }, { indicator: 'icon' }); + + expect(trigger.querySelector('svg[data-fui-icon]')).not.toBeNull(); + expect(trigger.textContent).toBe(''); + }); + }); + + describe('the default glyph', () => { + it('never lets the headless ellipsis string reach the DOM', () => { + // The headless hook assigns '...' when it finds the children nullish, so the glyph has to + // be materialised before the hook rather than restored after it. + const { trigger } = renderPopover({ size: 16 }); + + expect(trigger.textContent).not.toContain('...'); + expect(trigger.childElementCount).toBe(1); + expect(trigger.firstElementChild?.tagName.toLowerCase()).toBe('svg'); + }); + + it('lets consumer children win', () => { + const { trigger } = renderPopover({ size: 16 }, { triggerButton: { children: 'ZZZ' } }); + + expect(trigger.textContent).toBe('ZZZ'); + expect(trigger.querySelector('svg[data-fui-icon]')).toBeNull(); + }); + + it('renders an empty button under the pie layout', () => { + const { trigger } = renderPopover({ layout: 'pie', size: 16 }); + + expect(trigger.textContent).toBe(''); + expect(trigger.childElementCount).toBe(0); + expect(trigger.classList).toContain(styles.triggerButtonPie); + }); + + it('leaves the count alone', () => { + expect(renderPopover({ size: 32 }).trigger.querySelector('svg')).toBeNull(); + }); + }); + + describe('the ladders', () => { + it.each([ + [32, styles.borderThin], + [36, styles.borderThick], + [48, styles.borderThick], + [56, styles.borderThicker], + [72, styles.borderThickest], + ] as const)('picks the border width for size %s', (size, expected) => { + expect(renderPopover({ size }).trigger.classList).toContain(expected); + }); + + it.each([ + [24, styles.textCaption2Strong], + [28, styles.textCaption1Strong], + [40, styles.textBody1Strong], + [56, styles.textSubtitle2], + [96, styles.textSubtitle1], + [120, styles.textTitle3], + ] as const)('picks the count typography for size %s', (size, expected) => { + expect(renderPopover({ size }).trigger.classList).toContain(expected); + }); + + it.each([ + [16, styles.icon12], + [24, styles.icon16], + [40, styles.icon20], + [48, styles.icon24], + [56, styles.icon28], + [72, styles.icon32], + [96, styles.icon48], + ] as const)('picks the glyph size for size %s', (size, expected) => { + expect(renderPopover({ size }, { indicator: 'icon' }).trigger.classList).toContain(expected); + }); + + it('takes the same group-child gap as a sibling item', () => { + const { trigger } = renderPopover({ layout: 'stack', size: 32 }); + + expect(trigger.classList).toContain(itemStyles.stackGapXs); + expect(trigger.classList).toContain(itemStyles.stackThick); + }); + }); + + describe('the open surface', () => { + it('renders the windmod PopoverSurface carrying the small look', () => { + const { surface } = renderPopover({}, { open: true }); + + expect(surface?.classList).toContain('fui-popover-surface'); + expect(surface?.getAttribute('data-size')).toBe('small'); + expect(surface?.classList).toContain(styles.popoverSurface); + }); + + it('renders the windmod PopoverSurface in production mode, where assertSlots does not run', () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + try { + expect(renderPopover({}, { open: true }).surface?.classList).toContain('fui-popover-surface'); + } finally { + process.env.NODE_ENV = previous; + } + }); + + it('stamps data-selected on the trigger while open, and never under pie', () => { + expect(renderPopover({}, { open: true }).trigger.hasAttribute('data-selected')).toBe(true); + expect(renderPopover({}, { open: false }).trigger.hasAttribute('data-selected')).toBe(false); + expect(renderPopover({ layout: 'pie' }, { open: true }).trigger.hasAttribute('data-selected')).toBe(false); + }); + + it('pins the overflowed items to their own size', () => { + const { surface } = renderPopover({ size: 96 }, { open: true }); + + expect(surface?.querySelector('.fui-avatar-group-item')?.getAttribute('data-size')).toBe('24'); + }); + + it('carries the content class on the list', () => { + const { surface } = renderPopover({}, { open: true }); + + expect(surface?.querySelector('ul')?.classList).toContain(styles.content); + }); + }); + + describe('the tooltip slot', () => { + // Compile-time guard: the props type is derived from windmod's own slots, so the tooltip + // shorthand carries the look props the headless Tooltip omits. Aliasing the headless props + // instead fails to compile at the `satisfies` below (TS2353), which no runtime assertion can + // reach. + it('accepts an appearance the headless tooltip slot type omits', () => { + const popoverProps = { + tooltip: { visible: true, content: 'View more people.', relationship: 'label', appearance: 'inverted' }, + } satisfies Partial; + + const { container } = renderPopover({}, popoverProps); + + expect(container.querySelector('.fui-tooltip')?.classList).toContain(tooltipStyles.inverted); + }); + + it('renders the windmod Tooltip', () => { + const { container } = renderPopover( + {}, + { tooltip: { visible: true, content: 'View more people.', relationship: 'label' } }, + ); + + expect(container.querySelector('.fui-tooltip')).not.toBeNull(); + }); + + it('renders the windmod Tooltip in production mode, where assertSlots does not run', () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + try { + const { container } = renderPopover( + {}, + { tooltip: { visible: true, content: 'View more people.', relationship: 'label' } }, + ); + + expect(container.querySelector('.fui-tooltip')).not.toBeNull(); + } finally { + process.env.NODE_ENV = previous; + } + }); + }); + + it('warns nothing in development, so the components map and the element types agree', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + renderPopover({}, { open: true }); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('passes everything else through to the headless hook untouched', () => { + const onOpenChange = jest.fn(); + + renderPopover({}, { count: 7, openOnHover: true, onOpenChange }); + + expect(handedOver()).toMatchObject({ count: 7, openOnHover: true, onOpenChange }); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.tsx b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.tsx new file mode 100644 index 0000000000000..b3b2b253b7eb6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.tsx @@ -0,0 +1,83 @@ +'use client'; + +import * as React from 'react'; +import { slot } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import { + renderAvatarGroupPopover, + useAvatarGroupContext, + useAvatarGroupPopover, +} from '@fluentui/react-headless-components-preview/avatar-group'; +import { MoreHorizontalRegular } from '@fluentui/react-icons/headless/svg/more-horizontal'; + +import { PopoverLookProvider } from '../Popover/PopoverContext'; +import { resolvePopoverArrow } from '../Popover/popoverArrow'; +import { PopoverSurface } from '../PopoverSurface'; +import { Tooltip } from '../Tooltip'; +import type { AvatarGroupPopoverProps } from './AvatarGroupPopover.types'; +import { useAvatarGroupPopoverStyles } from './useAvatarGroupPopoverStyles'; + +/** Griffel pins the overflowed items to a single size regardless of the group's own. */ +const OVERFLOW_ITEM_SIZE = 24; + +/** + * An AvatarGroupPopover holds the members of an AvatarGroup that do not fit inline, behind a + * counting trigger button. Windmod AvatarGroupPopover: the headless popover decorated with the + * Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const AvatarGroupPopover = (props: AvatarGroupPopoverProps): JSXElement => { + // Not wrapped in forwardRef, so the parameter list is part of the emitted public signature — + // see Popover.tsx. + const size = useAvatarGroupContext(ctx => ctx.size) ?? 32; + const layout = useAvatarGroupContext(ctx => ctx.layout); + + // The indicator default is size-keyed and the headless base defaults to `count` unconditionally, + // so the resolved value is passed in rather than corrected afterwards. + const indicator = props.indicator ?? (size < 24 ? 'icon' : 'count'); + + // The headless hook ASSIGNS the string '...' to the trigger's children when it finds them + // nullish, so the default glyph has to be materialised before the hook rather than restored + // after it. Gated on the state actually wanting a glyph: an unconditional materialisation would + // put an empty slot in front of the count. + const supplied = slot.resolveShorthand(props.triggerButton); + const triggerButton = + layout !== 'pie' && indicator === 'icon' + ? { ...supplied, children: supplied?.children ?? } + : props.triggerButton; + + const base = useAvatarGroupPopover({ ...props, indicator, triggerButton }); + + // renderAvatarGroupPopover hardcodes the headless Popover, which is not a slot and cannot be + // swapped; the two things the windmod Popover adds are recovered instead. The look context is + // the only channel by which the surface learns its size, and the arrow geometry is the shared + // helper the windmod Popover itself calls. + const look = React.useMemo(() => ({ size: 'small' as const }), []); + + // The headless popover context publishes `isOverflow` alone. Griffel also pins the overflowed + // items' size, and without it every overflow row would fall back to the group default. + const avatarGroup = React.useMemo(() => ({ isOverflow: true, size: OVERFLOW_ITEM_SIZE }) as const, []); + + return ( + + {renderAvatarGroupPopover( + useAvatarGroupPopoverStyles({ + ...base, + // eslint-disable-next-line @typescript-eslint/no-deprecated -- reading base.components to keep every other slot's element type + components: { ...base.components, popoverSurface: PopoverSurface, tooltip: Tooltip }, + // Both slots are re-created with the windmod element types rather than swapped in + // `components` alone — see Combobox.tsx. + popoverSurface: slot.always({ ...base.popoverSurface }, { elementType: PopoverSurface }), + tooltip: slot.always({ ...base.tooltip }, { elementType: Tooltip }), + popover: { + ...base.popover, + ...resolvePopoverArrow(base.popover.positioning, base.popover.withArrow ?? false, 'small'), + }, + size, + }), + { avatarGroup }, + )} + + ); +}; + +AvatarGroupPopover.displayName = 'AvatarGroupPopover'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.types.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.types.ts new file mode 100644 index 0000000000000..bb6610eacb521 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/AvatarGroupPopover.types.ts @@ -0,0 +1,39 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { + AvatarGroupPopoverPopoverProps, + AvatarGroupPopoverProps as AvatarGroupPopoverHeadlessProps, + AvatarGroupPopoverSlots as AvatarGroupPopoverHeadlessSlots, + AvatarGroupPopoverState as AvatarGroupPopoverHeadlessState, +} from '@fluentui/react-headless-components-preview/avatar-group'; + +import type { AvatarSize } from '../Avatar'; +import type { PopoverSurface } from '../PopoverSurface'; +import type { Tooltip } from '../Tooltip'; + +export type { AvatarGroupPopoverPopoverProps }; + +/** + * The headless slots type the surface and the tooltip against the headless components, which + * carry none of the look props windmod's add — see PersonaSlots. + */ +export type AvatarGroupPopoverSlots = Omit & { + popoverSurface: NonNullable>; + tooltip: NonNullable>; +}; + +/** + * Windmod AvatarGroupPopover props: the slot props re-derived from the swapped slots, plus the + * headless behaviour props unchanged. The overflow trigger's look is selected entirely by the + * group's size, which arrives on the context, so no new look prop is declared here. Re-exporting + * the headless props instead would type the tooltip shorthand against the headless Tooltip, which + * omits `appearance` — see PersonaProps. + */ +export type AvatarGroupPopoverProps = Omit>, 'children'> & + AvatarGroupPopoverPopoverProps & + Pick; + +/** Windmod AvatarGroupPopover state: headless state plus the size read off the group context. */ +export type AvatarGroupPopoverState = ComponentState & + Pick & { + size: AvatarSize; + }; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/index.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/index.ts new file mode 100644 index 0000000000000..cb5cdbe0915c5 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/index.ts @@ -0,0 +1,8 @@ +export { AvatarGroupPopover } from './AvatarGroupPopover'; +export { avatarGroupPopoverClassNames, useAvatarGroupPopoverStyles } from './useAvatarGroupPopoverStyles'; +export type { + AvatarGroupPopoverPopoverProps, + AvatarGroupPopoverProps, + AvatarGroupPopoverSlots, + AvatarGroupPopoverState, +} from './AvatarGroupPopover.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/useAvatarGroupPopoverStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/useAvatarGroupPopoverStyles.ts new file mode 100644 index 0000000000000..38e697dc3bc0b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/AvatarGroupPopover/useAvatarGroupPopoverStyles.ts @@ -0,0 +1,86 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { AvatarSize } from '../Avatar'; +import { groupChildClasses } from '../AvatarGroupItem/groupChildClasses'; +import type { AvatarGroupPopoverState } from './AvatarGroupPopover.types'; + +import styles from './AvatarGroupPopover.module.css'; + +/** + * The only public classes — see componentMarkers. AvatarGroupPopover renders no root element of + * its own, so the pair sits on the trigger button. + */ +export const avatarGroupPopoverClassNames: { triggerButton: string } = { + triggerButton: componentMarkers('avatar-group-popover'), +}; + +type TriggerButtonDataAttributes = { + 'data-size'?: AvatarGroupPopoverState['size']; + 'data-selected'?: true; +}; + +// Exclusive upper boundaries, unlike the two content ladders below. Every range is written in full +// so the keys partition AvatarSize with no implied else; `+()` coerces a condition to 1 or 0 +// because TS rejects a bare boolean computed key (TS2464). +const borderClass = (size: AvatarSize) => + ({ + [+(size < 36)]: styles.borderThin, + [+(size >= 36 && size < 56)]: styles.borderThick, + [+(size >= 56 && size < 72)]: styles.borderThicker, + [+(size >= 72)]: styles.borderThickest, + })[1]; + +// Typography lives here and never on `.trigger-button`: Griffel's base bucket sets no font, and +// under `indicator="icon"` no typography class is merged at all. +const countClass = (size: AvatarSize) => + ({ + [+(size <= 24)]: styles.textCaption2Strong, + [+(size > 24 && size <= 28)]: styles.textCaption1Strong, + [+(size > 28 && size <= 40)]: styles.textBody1Strong, + [+(size > 40 && size <= 56)]: styles.textSubtitle2, + [+(size > 56 && size <= 96)]: styles.textSubtitle1, + [+(size > 96)]: styles.textTitle3, + })[1]; + +const iconClass = (size: AvatarSize) => + ({ + [+(size <= 16)]: styles.icon12, + [+(size > 16 && size <= 24)]: styles.icon16, + [+(size > 24 && size <= 40)]: styles.icon20, + [+(size > 40 && size <= 48)]: styles.icon24, + [+(size > 48 && size <= 56)]: styles.icon28, + [+(size > 56 && size <= 72)]: styles.icon32, + [+(size > 72)]: styles.icon48, + })[1]; + +/** + * Applies the visual contract, returning new state. `data-selected` mirrors the JS gate Griffel + * uses to merge its selected bucket, and the pie layout suppresses it there too. + */ +export const useAvatarGroupPopoverStyles = (state: AvatarGroupPopoverState): AvatarGroupPopoverState => { + const { indicator, layout, popoverOpen, size } = state; + + const triggerButton: AvatarGroupPopoverState['triggerButton'] & TriggerButtonDataAttributes = { + ...state.triggerButton, + 'data-size': size, + 'data-selected': layout !== 'pie' && popoverOpen ? true : undefined, + className: clsx( + avatarGroupPopoverClassNames.triggerButton, + groupChildClasses(layout, size), + styles.triggerButton, + layout === 'pie' && styles.triggerButtonPie, + borderClass(size), + indicator === 'count' ? countClass(size) : iconClass(size), + state.triggerButton.className, + ), + }; + + return { + ...state, + triggerButton, + content: slotClasses(state.content, styles.content), + popoverSurface: slotClasses(state.popoverSurface, styles.popoverSurface), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.module.css b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.module.css new file mode 100644 index 0000000000000..6736ec5ac7abe --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.module.css @@ -0,0 +1,230 @@ +@reference '#theme'; + +/* The border rides the root's ::after so it never affects layout or padding. It carries no + module class, so it is reached with Tailwind's built-in after variant, which also supplies + `content: var(--tw-content)` — registered with initial-value "". `border-radius: inherit` + and `border-color: inherit` are how the shape and colour classes reach it. */ + +@layer fui.components.l1 { + .root { + @apply relative box-border inline-flex h-20 min-w-20 items-center justify-center rounded-circular border-transparent-stroke px-horizontal-s-nudge py-0 font-base text-base-200 leading-base-200 font-semibold; + + @variant after { + @apply absolute inset-0 rounded-[inherit] border-thin border-solid border-inherit; + } + + @variant size-small, size-extra-small, size-tiny { + @apply text-base-100 leading-base-100; + } + + @variant size-tiny { + @apply size-6 min-w-auto p-0 text-4 leading-[1]; + } + + @variant size-extra-small { + @apply size-10 min-w-auto p-0 text-6 leading-[1]; + } + + @variant size-small { + @apply h-16 min-w-16 px-horizontal-xs; + } + + @variant size-large { + @apply h-24 min-w-24; + } + + @variant size-extra-large { + @apply h-32 min-w-32 px-horizontal-s; + } + } + + .rounded { + @apply rounded-medium; + + @variant size-small, size-extra-small, size-tiny { + @apply rounded-small; + } + } + + .square { + @apply rounded-none; + } + + .ghost { + @variant after { + @apply hidden; + } + } + + .outline { + @apply border-current; + } + + .filled-brand { + @apply bg-brand-background text-neutral-foreground-on-brand; + } + + .filled-danger { + @apply bg-palette-red-background-3 text-neutral-foreground-on-brand; + } + + .filled-important { + @apply bg-neutral-foreground-1 text-neutral-background-1; + } + + .filled-informative { + @apply bg-neutral-background-5 text-neutral-foreground-3; + } + + .filled-severe { + @apply bg-palette-dark-orange-background-3 text-neutral-foreground-on-brand; + } + + .filled-subtle { + @apply bg-neutral-background-1 text-neutral-foreground-1; + } + + .filled-success { + @apply bg-palette-green-background-3 text-neutral-foreground-on-brand; + } + + .filled-warning { + @apply bg-palette-yellow-background-3 text-neutral-foreground-1-static; + } + + .ghost-brand { + @apply text-brand-foreground-1; + } + + .ghost-danger { + @apply text-palette-red-foreground-3; + } + + .ghost-important { + @apply text-neutral-foreground-1; + } + + .ghost-informative { + @apply text-neutral-foreground-3; + } + + .ghost-severe { + @apply text-palette-dark-orange-foreground-3; + } + + .ghost-subtle { + @apply text-neutral-foreground-static-inverted; + } + + .ghost-success { + @apply text-palette-green-foreground-3; + } + + .ghost-warning { + @apply text-palette-yellow-foreground-2; + } + + .outline-brand { + @apply text-brand-foreground-1; + } + + .outline-danger { + @apply border-palette-red-border-2 text-palette-red-foreground-3; + } + + .outline-important { + @apply border-neutral-stroke-accessible text-neutral-foreground-3; + } + + .outline-informative { + @apply border-neutral-stroke-2 text-neutral-foreground-3; + } + + .outline-severe { + @apply text-palette-dark-orange-foreground-3; + } + + .outline-subtle { + @apply text-neutral-foreground-static-inverted; + } + + .outline-success { + @apply border-palette-green-border-2 text-palette-green-foreground-3; + } + + .outline-warning { + @apply text-palette-yellow-foreground-2; + } + + .tint-brand { + @apply border-brand-stroke-2 bg-brand-background-2 text-brand-foreground-2; + } + + .tint-danger { + @apply border-palette-red-border-1 bg-palette-red-background-1 text-palette-red-foreground-1; + } + + .tint-important { + @apply border-transparent-stroke bg-neutral-foreground-3 text-neutral-background-1; + } + + .tint-informative { + @apply border-neutral-stroke-2 bg-neutral-background-4 text-neutral-foreground-3; + } + + .tint-severe { + @apply border-palette-dark-orange-border-1 bg-palette-dark-orange-background-1 text-palette-dark-orange-foreground-1; + } + + .tint-subtle { + @apply border-neutral-stroke-2 bg-neutral-background-1 text-neutral-foreground-3; + } + + .tint-success { + @apply border-palette-green-border-1 bg-palette-green-background-1 text-palette-green-foreground-1; + } + + .tint-warning { + @apply border-palette-yellow-border-1 bg-palette-yellow-background-1 text-palette-yellow-foreground-1; + } + + .icon { + @apply -mx-horizontal-xxs my-0 flex text-icon-12 leading-none; + + @variant group-not-empty/fui-badge { + @variant group-icon-before/fui-badge { + @apply me-horizontal-xs; + } + + @variant group-icon-after/fui-badge { + @apply ms-horizontal-xs; + } + + @variant group-size-extra-large/fui-badge { + @variant group-icon-before/fui-badge { + @apply me-horizontal-s-nudge; + } + + @variant group-icon-after/fui-badge { + @apply ms-horizontal-s-nudge; + } + } + } + + @variant group-size-tiny/fui-badge { + @apply text-6; + } + + @variant group-size-extra-small/fui-badge { + @apply text-base-100; + } + + @variant group-size-large/fui-badge { + @apply text-icon-16; + } + + @variant group-size-extra-large/fui-badge { + @apply text-icon-20; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.test.tsx new file mode 100644 index 0000000000000..1df4eb05aafdb --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.test.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { Badge } from './Badge'; +import { isConformant } from '../../testing/isConformant'; +import type { BadgeState } from './Badge.types'; +import { badgeClassNames, useBadgeStyles } from './useBadgeStyles'; + +import styles from './Badge.module.css'; + +describe('Badge', () => { + isConformant({ + Component: Badge, + displayName: 'Badge', + requiredProps: { children: 'Badge' }, + }); + + it('stamps data-size and the marker class', () => { + const { getByTestId } = render( + <> + Default + + Large + + , + ); + + expect(getByTestId('default').getAttribute('data-size')).toBe('medium'); + expect(getByTestId('large').getAttribute('data-size')).toBe('large'); + expect(getByTestId('default').className).toContain(badgeClassNames.root); + expect(getByTestId('default')).toHaveClass('fui-badge'); + expect(getByTestId('default')).toHaveClass('group/fui-badge'); + expect(getByTestId('default').classList[0]).toBe('fui-badge'); + }); + + it('stamps data-empty by counting rendered children, not by truthiness', () => { + const { getByTestId } = render( + <> + Content + {0} + {''} + + {null} + {false} + , + ); + + // 0 and '' render text and keep the icon spacing; null/false/absent do not. + expect(getByTestId('text').hasAttribute('data-empty')).toBe(false); + expect(getByTestId('zero').hasAttribute('data-empty')).toBe(false); + expect(getByTestId('emptyString').hasAttribute('data-empty')).toBe(false); + expect(getByTestId('none').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('null').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('false').hasAttribute('data-empty')).toBe(true); + }); + + it('leaves data-icon-position to the headless hook', () => { + const { getByTestId } = render( + <> + Badge + }> + Badge + + } iconPosition="after"> + Badge + + , + ); + + expect(getByTestId('noIcon').hasAttribute('data-icon-position')).toBe(false); + expect(getByTestId('before').getAttribute('data-icon-position')).toBe('before'); + expect(getByTestId('after').getAttribute('data-icon-position')).toBe('after'); + }); + + it('gives every appearance and color pair its own module class', () => { + const colors = ['brand', 'danger', 'important', 'informative', 'severe', 'subtle', 'success', 'warning'] as const; + const { getByTestId } = render( + <> + {colors.map(color => ( + + 8 + + ))} + , + ); + + // Appearance is held fixed so the one class that moves is the appearance-color pair. + const classLists = colors.map(color => getByTestId(color).className.split(' ')); + + expect(new Set(classLists.map(list => list.join(' '))).size).toBe(colors.length); + for (const list of classLists.slice(1)) { + expect(list).toHaveLength(classLists[0].length); + expect(list.filter(name => !classLists[0].includes(name))).toHaveLength(1); + } + }); + + it('looks up the root and icon classes by the keys the stylesheet authors', () => { + const { getByTestId } = render( + }> + 8 + , + ); + + // The css-module proxy answers every key — see testing/classOccurrences.ts. + const root = getByTestId('root'); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass(styles.rounded); + expect(root).toHaveClass(styles.outline); + expect(root).toHaveClass(styles.outlineDanger); + expect(getByTestId('glyph').parentElement).toHaveClass(styles.icon); + }); + + it('does not mutate the state it is given', () => { + const state = { + appearance: 'filled', + color: 'brand', + components: { root: 'div', icon: 'span' }, + icon: { as: 'span', className: 'consumer-icon' }, + iconPosition: 'before', + root: { as: 'div', children: 'Badge', className: 'consumer' }, + shape: 'circular', + size: 'medium', + } as unknown as BadgeState; + + const styled = useBadgeStyles(state); + + expect(styled).not.toBe(state); + expect(state.root).not.toHaveProperty('data-size'); + expect(state.root.className).toBe('consumer'); + expect(state.icon?.className).toBe('consumer-icon'); + expect(styled.root.className).toContain('consumer'); + expect(styled.icon?.className).toContain('consumer-icon'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.tsx b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.tsx new file mode 100644 index 0000000000000..6e895dbc0a7e8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderBadge, useBadge } from '@fluentui/react-headless-components-preview/badge'; + +import type { BadgeProps } from './Badge.types'; +import { useBadgeStyles } from './useBadgeStyles'; + +/** + * A Badge is a visual decoration for UI elements. Windmod Badge: the headless badge + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Badge: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-badge's styled useBadge. + ({ appearance = 'filled', color = 'brand', shape = 'circular', size = 'medium', ...rest }, ref) => { + return renderBadge( + useBadgeStyles({ + ...useBadge(rest, ref), + appearance, + color, + shape, + size, + }), + ); + // Casting is required due to lack of distributive union to support union on @types/react + }, +) as ForwardRefComponent; + +Badge.displayName = 'Badge'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.types.ts new file mode 100644 index 0000000000000..7b0cf7927345e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/Badge.types.ts @@ -0,0 +1,36 @@ +import type { + BadgeProps as BadgeHeadlessProps, + BadgeState as BadgeHeadlessState, +} from '@fluentui/react-headless-components-preview/badge'; + +export type { BadgeSlots } from '@fluentui/react-headless-components-preview/badge'; + +/** Visual style of the Badge. `'filled'` is the base look. */ +export type BadgeAppearance = 'filled' | 'ghost' | 'outline' | 'tint'; + +/** Preset color of the Badge. */ +export type BadgeColor = 'brand' | 'danger' | 'important' | 'informative' | 'severe' | 'subtle' | 'success' | 'warning'; + +/** Corner treatment of the Badge. `'circular'` is the base look. */ +export type BadgeShape = 'circular' | 'rounded' | 'square'; + +/** Size of the Badge. */ +export type BadgeSize = 'tiny' | 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'; + +/** + * Windmod Badge props: the headless badge plus the look props the headless surface + * deliberately omits (they exist purely to select styles). + */ +export type BadgeProps = BadgeHeadlessProps & { + /** @default 'filled' */ + appearance?: BadgeAppearance; + /** @default 'brand' */ + color?: BadgeColor; + /** @default 'circular' */ + shape?: BadgeShape; + /** @default 'medium' */ + size?: BadgeSize; +}; + +/** Windmod Badge state: headless state plus the resolved look props. */ +export type BadgeState = BadgeHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Badge/index.ts new file mode 100644 index 0000000000000..51e01578c2d02 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/index.ts @@ -0,0 +1,11 @@ +export { Badge } from './Badge'; +export { badgeClassNames, useBadgeStyles } from './useBadgeStyles'; +export type { + BadgeAppearance, + BadgeColor, + BadgeProps, + BadgeShape, + BadgeSize, + BadgeSlots, + BadgeState, +} from './Badge.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Badge/useBadgeStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Badge/useBadgeStyles.ts new file mode 100644 index 0000000000000..397bf70c382b4 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Badge/useBadgeStyles.ts @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { BadgeState } from './Badge.types'; + +import styles from './Badge.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const badgeClassNames: { root: string } = { + root: componentMarkers('badge'), +}; + +type BadgeRootDataAttributes = { + 'data-size'?: BadgeState['size']; + 'data-empty'?: true; +}; + +/** Applies the visual contract, returning new state. The headless hook already stamps + * data-icon-position, and only when an icon slot is present. `data-empty` must count with + * React.Children.toArray: `0` and `''` are rendered content that keeps the icon spacing, + * while `[]` is not. `circular`, `filled` and `tint` are the base looks and carry no class. */ +export const useBadgeStyles = (state: BadgeState): BadgeState => { + const { appearance, color, shape, size } = state; + + const root: BadgeState['root'] & BadgeRootDataAttributes = { + ...state.root, + 'data-size': size, + 'data-empty': React.Children.toArray(state.root.children).length === 0 || undefined, + className: clsx( + badgeClassNames.root, + styles.root, + styles[shape], + styles[appearance], + styles[`${appearance}-${color}`], + state.root.className, + ), + }; + + return { + ...state, + root, + icon: slotClasses(state.icon, styles.icon), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.module.css b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.module.css new file mode 100644 index 0000000000000..a8698c54ef09f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.module.css @@ -0,0 +1,7 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .list { + @apply m-0 flex list-none items-center p-0; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx new file mode 100644 index 0000000000000..a8fb13932c8b8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx @@ -0,0 +1,191 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { stampsOf } from '../../testing/stampsOf'; +import { Breadcrumb } from './Breadcrumb'; +import { BreadcrumbButton } from '../BreadcrumbButton/BreadcrumbButton'; +import { BreadcrumbDivider } from '../BreadcrumbDivider/BreadcrumbDivider'; +import { BreadcrumbItem } from '../BreadcrumbItem/BreadcrumbItem'; +import type { BreadcrumbState } from './Breadcrumb.types'; +import { breadcrumbClassNames, useBreadcrumbStyles } from './useBreadcrumbStyles'; + +import styles from './Breadcrumb.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/breadcrumb', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/breadcrumb'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useBreadcrumb: (...args: Parameters) => deepFreezeState(actual.useBreadcrumb(...args)), + }; +}); + +const sizes = ['small', 'medium', 'large'] as const; + +describe('Breadcrumb', () => { + isConformant({ + Component: Breadcrumb, + displayName: 'Breadcrumb', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-breadcrumb'); + expect(root).toHaveClass('group/fui-breadcrumb'); + expect(root.classList[0]).toBe('fui-breadcrumb'); + expect(breadcrumbClassNames.root).toBe('fui-breadcrumb group/fui-breadcrumb'); + }); + + it('decorates the list slot and honours list={null}', () => { + const { getByTestId } = render( + <> + + Home + + + Home + + , + ); + + const list = getByTestId('root').querySelector('ol'); + + expect(list).not.toBeNull(); + expect(list!.getAttribute('role')).toBe('list'); + expect(list).toHaveClass(styles.list); + expect(list!.querySelector('li')).not.toBeNull(); + + const suppressed = getByTestId('suppressed'); + + expect(suppressed.querySelector('ol')).toBeNull(); + expect(suppressed.querySelector('li')).toBeNull(); + }); + + it('stamps its own resolved size, defaulting to medium', () => { + const { getByTestId } = render( + <> + + {sizes.map(size => ( + + ))} + , + ); + + expect(getByTestId('default').getAttribute('data-size')).toBe('medium'); + + for (const size of sizes) { + expect(getByTestId(size).getAttribute('data-size')).toBe(size); + } + }); + + it('publishes its size to the children through the breadcrumb context', () => { + const { getByTestId } = render( + <> + + + Go + + {sizes.map(size => ( + + + Go + + ))} + , + ); + + expect(getByTestId('divider-default').getAttribute('data-size')).toBe('medium'); + expect(getByTestId('button-default').getAttribute('data-size')).toBe('medium'); + + for (const size of sizes) { + expect(getByTestId(`divider-${size}`).getAttribute('data-size')).toBe(size); + expect(getByTestId(`button-${size}`).getAttribute('data-size')).toBe(size); + } + }); + + it('gives a nested breadcrumb its own size', () => { + const { getByTestId } = render( + + + + + + , + ); + + expect(getByTestId('outer').getAttribute('data-size')).toBe('large'); + expect(getByTestId('inner').getAttribute('data-size')).toBe('small'); + }); + + it('leaves the headless accessibility contract intact', () => { + const { getByTestId } = render( + + Home + , + ); + + const root = getByTestId('root'); + + expect(root.tagName).toBe('NAV'); + expect(root.getAttribute('aria-label')).toBe('breadcrumb'); + expect(root.querySelector('ol')!.getAttribute('role')).toBe('list'); + }); + + it('passes consumer props through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(ref.current).toBe(root); + expect(root.id).toBe('bc'); + expect(root.getAttribute('aria-label')).toBe('trail'); + expect(root).toHaveClass('consumer'); + expect(root.style.margin).toBe('2px'); + }); + + it('keeps the consumer class on the list slot', () => { + const { getByTestId } = render(); + + const list = getByTestId('root').querySelector('ol')!; + + expect(list).toHaveClass('consumer-list'); + expect(list).toHaveClass(styles.list); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'nav', list: 'ol' }, + list: { className: 'consumer-list' }, + root: { className: 'consumer' }, + size: 'large', + } as unknown as BreadcrumbState; + + const styled = useBreadcrumbStyles(state); + + expect(styled).not.toBe(state); + expect(state.root.className).toBe('consumer'); + expect(state.root).not.toHaveProperty('data-size'); + expect(state.list!.className).toBe('consumer-list'); + expect(stampsOf(styled.root)['data-size']).toBe('large'); + expect(styled.list!.className).toContain('consumer-list'); + }); + + it('renders no list slot when the consumer suppresses it', () => { + const state = { + components: { root: 'nav', list: 'ol' }, + root: {}, + size: 'medium', + } as unknown as BreadcrumbState; + + expect(useBreadcrumbStyles(state).list).toBeUndefined(); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.tsx b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000000..d9dbd051576e6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.tsx @@ -0,0 +1,30 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderBreadcrumb, + useBreadcrumb, + useBreadcrumbContextValues, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +import type { BreadcrumbProps } from './Breadcrumb.types'; +import { useBreadcrumbStyles } from './useBreadcrumbStyles'; + +/** + * A Breadcrumb shows where a page sits in a navigation hierarchy. Windmod Breadcrumb: the + * headless breadcrumb decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Breadcrumb: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-breadcrumb's styled useBreadcrumb. + ({ size = 'medium', ...rest }, ref) => { + // The headless state omits `size`, so the context values must be built from the state that + // carries it — otherwise the children read `undefined` instead of the breadcrumb's size. + const styled = useBreadcrumbStyles({ ...useBreadcrumb(rest, ref), size }); + + return renderBreadcrumb(styled, useBreadcrumbContextValues(styled)); + }, +); + +Breadcrumb.displayName = 'Breadcrumb'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts new file mode 100644 index 0000000000000..9b7ecc54a613c --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts @@ -0,0 +1,21 @@ +import type { + BreadcrumbProps as BreadcrumbHeadlessProps, + BreadcrumbState as BreadcrumbHeadlessState, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +export type { BreadcrumbContextValues, BreadcrumbSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; + +/** Size of the Breadcrumb. It reaches BreadcrumbItem, BreadcrumbDivider and BreadcrumbButton through the breadcrumb context. */ +export type BreadcrumbSize = 'small' | 'medium' | 'large'; + +/** + * Windmod Breadcrumb props: the headless breadcrumb plus the look prop the headless surface + * deliberately omits (it exists purely to select styles). + */ +export type BreadcrumbProps = BreadcrumbHeadlessProps & { + /** @default 'medium' */ + size?: BreadcrumbSize; +}; + +/** Windmod Breadcrumb state: headless state plus the resolved look prop. */ +export type BreadcrumbState = BreadcrumbHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000000000..969a625166fd1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/index.ts @@ -0,0 +1,9 @@ +export { Breadcrumb } from './Breadcrumb'; +export { breadcrumbClassNames, useBreadcrumbStyles } from './useBreadcrumbStyles'; +export type { + BreadcrumbContextValues, + BreadcrumbProps, + BreadcrumbSize, + BreadcrumbSlots, + BreadcrumbState, +} from './Breadcrumb.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/useBreadcrumbStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/useBreadcrumbStyles.ts new file mode 100644 index 0000000000000..a4f3af8c90b72 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Breadcrumb/useBreadcrumbStyles.ts @@ -0,0 +1,37 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { BreadcrumbState } from './Breadcrumb.types'; + +import styles from './Breadcrumb.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const breadcrumbClassNames: { root: string } = { + root: componentMarkers('breadcrumb'), +}; + +type BreadcrumbRootDataAttributes = { + 'data-size'?: BreadcrumbState['size']; +}; + +/** + * Applies the visual contract, returning new state. No rule of this family selects the root's + * own data-size; it is stamped only so a consumer can compose group variants against it. + * + * The root authors no typography or colour: the descendants that paint set their own, and the + * provider supplies the inherited base. + */ +export const useBreadcrumbStyles = (state: BreadcrumbState): BreadcrumbState => { + const root: BreadcrumbState['root'] & BreadcrumbRootDataAttributes = { + ...state.root, + 'data-size': state.size, + className: clsx(breadcrumbClassNames.root, state.root.className), + }; + + return { + ...state, + root, + list: slotClasses(state.list, styles.list), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.module.css b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.module.css new file mode 100644 index 0000000000000..4be761ddaa1ae --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.module.css @@ -0,0 +1,55 @@ +@reference '#theme'; + +@layer fui.components.l2 { + .root { + @apply h-32 min-w-[unset] p-horizontal-s-nudge text-base-300 leading-base-300 font-regular text-nowrap; + + @variant size-small { + @apply h-24 text-base-200 leading-base-200; + } + + @variant size-large { + @apply h-40 p-horizontal-s text-base-400 leading-base-400; + } + + /* Griffel keys this branch off the native `:disabled` pseudo-class, so it must not reach the + anchor form of this button — hence disabled-native rather than disabled. */ + @variant current { + @apply font-semibold; + + @variant hover, hover-active, disabled-native { + @apply cursor-auto bg-transparent-background text-neutral-foreground-2; + } + } + } + + .icon { + @apply me-horizontal-xs size-16 text-icon-16 leading-base-400; + + @variant group-size-small/fui-breadcrumb-button { + @apply size-12 text-icon-12 leading-base-200; + } + + @variant group-size-large/fui-breadcrumb-button { + @apply size-20 text-icon-20 leading-[calc(32/20)]; + } + + /* A current entry is not an affordance, so it undoes Button's interactive glyph swap as + well as its icon recolour. Both reach Button's l1 rules by layer order alone. */ + @variant group-current/fui-breadcrumb-button { + @variant group-hover/fui-breadcrumb-button, + group-hover-active/fui-breadcrumb-button, + group-disabled-native/fui-breadcrumb-button { + @apply text-inherit; + + @variant icon-filled { + @apply hidden; + } + + @variant icon-regular { + @apply inline; + } + } + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx new file mode 100644 index 0000000000000..9114647b2bb26 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx @@ -0,0 +1,256 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { stampsOf } from '../../testing/stampsOf'; +import { Breadcrumb } from '../Breadcrumb/Breadcrumb'; +import { BreadcrumbButton } from './BreadcrumbButton'; +import type { BreadcrumbButtonState } from './BreadcrumbButton.types'; +import { breadcrumbButtonClassNames, useBreadcrumbButtonStyles } from './useBreadcrumbButtonStyles'; + +import buttonStyles from '../Button/Button.module.css'; +import styles from './BreadcrumbButton.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/breadcrumb', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/breadcrumb'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useBreadcrumbButton: (...args: Parameters) => + deepFreezeState(actual.useBreadcrumbButton(...args)), + }; +}); + +const sizes = ['small', 'medium', 'large'] as const; + +const iconOf = (root: HTMLElement): HTMLElement => { + const icon = root.querySelector('span'); + + if (!icon) { + throw new Error('BreadcrumbButton rendered no icon slot'); + } + + return icon; +}; + +const Glyph = (): React.ReactElement => ; + +describe('BreadcrumbButton', () => { + isConformant({ + Component: BreadcrumbButton, + displayName: 'BreadcrumbButton', + }); + + it('stamps its own marker pair and the composed Button pair', () => { + const { getByTestId } = render(Go); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-breadcrumb-button'); + expect(root).toHaveClass('group/fui-breadcrumb-button'); + expect(root).toHaveClass('fui-button'); + expect(root).toHaveClass('group/fui-button'); + expect(root.classList[0]).toBe('fui-breadcrumb-button'); + expect(breadcrumbButtonClassNames.root).toBe('fui-breadcrumb-button group/fui-breadcrumb-button'); + }); + + it('carries the root class of both stylesheets', () => { + const { getByTestId } = render(Go); + + expect(classOccurrences(getByTestId('root'), styles.root)).toBe(2); + }); + + it('carries the icon class of both stylesheets', () => { + const { getByTestId } = render( + }> + Go + , + ); + + expect(classOccurrences(iconOf(getByTestId('root')), styles.icon)).toBe(2); + }); + + it('pins appearance and shape', () => { + const { getByTestId } = render(Go); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-appearance')).toBe('subtle'); + // `rounded` is Button's base look and owns no class, so the pin reads as the absence of the + // other two shapes. + expect(root).not.toHaveClass(buttonStyles.circular); + expect(root).not.toHaveClass(buttonStyles.square); + }); + + it('takes its size from the breadcrumb context, defaulting to medium', () => { + const { getByTestId } = render( + <> + Go + {sizes.map(size => ( + + Go + + ))} + , + ); + + expect(getByTestId('loose').getAttribute('data-size')).toBe('medium'); + + for (const size of sizes) { + expect(getByTestId(size).getAttribute('data-size')).toBe(size); + } + }); + + it('stamps the four Button attributes the headless breadcrumb hook omits', () => { + const { getByTestId } = render( + <> + Go + + Go + + + Go + + } aria-label="Go" /> + , + ); + + const plain = getByTestId('plain'); + + expect(plain.hasAttribute('data-disabled')).toBe(false); + expect(plain.hasAttribute('data-disabled-focusable')).toBe(false); + expect(plain.hasAttribute('data-icon-only')).toBe(false); + expect(plain.hasAttribute('data-icon-position')).toBe(false); + + expect(getByTestId('disabled').getAttribute('data-disabled')).toBe('true'); + expect(getByTestId('disabled-focusable').getAttribute('data-disabled-focusable')).toBe('true'); + + const iconOnly = getByTestId('icon-only'); + + expect(iconOnly.getAttribute('data-icon-only')).toBe('true'); + expect(iconOnly.getAttribute('data-icon-position')).toBe('before'); + expect(iconOnly.hasAttribute('data-empty')).toBe(true); + }); + + // `iconPosition` is honoured by the base hook but absent from both libraries' props types, so + // the trailing-glyph stamp is only reachable at the hook seam. + it('stamps the icon position it is given, and nothing without an icon', () => { + const withIcon = { + appearance: 'subtle', + components: { root: 'button', icon: 'span' }, + icon: {}, + iconPosition: 'after', + root: { as: 'button' }, + shape: 'rounded', + size: 'medium', + } as unknown as BreadcrumbButtonState; + + const withoutIcon = { ...withIcon, icon: undefined } as unknown as BreadcrumbButtonState; + + expect(stampsOf(useBreadcrumbButtonStyles(withIcon).root)['data-icon-position']).toBe('after'); + expect(stampsOf(useBreadcrumbButtonStyles(withoutIcon).root)['data-icon-position']).toBeUndefined(); + }); + + it('leaves the headless current contract intact through the composition', () => { + const { getByTestId } = render( + <> + Go + + Go + + , + ); + + const current = getByTestId('current'); + + expect(current.getAttribute('data-current')).toBe(''); + expect(current.getAttribute('aria-current')).toBe('page'); + expect(current.getAttribute('aria-disabled')).toBe('true'); + // A current entry is inert but still reachable: aria-disabled, never the disabled attribute. + expect(current.hasAttribute('disabled')).toBe(false); + expect(getByTestId('plain').hasAttribute('data-current')).toBe(false); + }); + + it('renders an anchor when the consumer supplies href', () => { + const { getByTestId } = render( + <> + + Go + + + Go + + , + ); + + const link = getByTestId('link'); + + expect(link.tagName).toBe('A'); + expect(link.getAttribute('href')).toBe('#a'); + + // The ARIA button behaviour strips href from a disabled anchor, so the current-plus-disabled + // anchor is the one cell where the disabled look has to come from data-disabled alone. + const inert = getByTestId('inert'); + + expect(inert.tagName).toBe('A'); + expect(inert.hasAttribute('href')).toBe(false); + expect(inert.getAttribute('data-current')).toBe(''); + expect(inert.getAttribute('data-disabled')).toBe('true'); + expect(inert.getAttribute('aria-disabled')).toBe('true'); + }); + + it('passes consumer props through to the root', () => { + const { getByTestId } = render( + + Go + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('bc-btn'); + expect(root).toHaveClass('consumer'); + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(root.style.margin).toBe('2px'); + }); + + it('does not mutate the state it is given', () => { + const state = { + appearance: 'subtle', + components: { root: 'button', icon: 'span' }, + disabled: true, + icon: { className: 'consumer-icon' }, + iconPosition: 'before', + root: { as: 'button', className: 'consumer' }, + shape: 'rounded', + size: 'large', + } as unknown as BreadcrumbButtonState; + + const styled = useBreadcrumbButtonStyles(state); + + expect(styled).not.toBe(state); + expect(state.root.className).toBe('consumer'); + expect(state.root).not.toHaveProperty('data-disabled'); + expect(state.icon!.className).toBe('consumer-icon'); + expect(stampsOf(styled.root)['data-disabled']).toBe(true); + expect(stampsOf(styled.root)['data-icon-position']).toBe('before'); + expect(stampsOf(styled.root)['data-size']).toBe('large'); + expect(styled.root.className).toContain('consumer'); + expect(classOccurrences(styled.icon!.className!, styles.icon)).toBe(2); + }); + + it('renders no icon slot when the consumer supplies none', () => { + const state = { + appearance: 'subtle', + components: { root: 'button', icon: 'span' }, + root: { as: 'button' }, + shape: 'rounded', + size: 'medium', + } as unknown as BreadcrumbButtonState; + + expect(useBreadcrumbButtonStyles(state).icon).toBeUndefined(); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.tsx new file mode 100644 index 0000000000000..0349ac9f57a90 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.tsx @@ -0,0 +1,35 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderBreadcrumbButton, + useBreadcrumbButton, + useBreadcrumbContext, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +import type { BreadcrumbButtonProps } from './BreadcrumbButton.types'; +import { useBreadcrumbButtonStyles } from './useBreadcrumbButtonStyles'; + +/** + * A BreadcrumbButton is the interactive entry of a Breadcrumb trail. Windmod BreadcrumbButton: + * the headless breadcrumb button decorated with the Fluent visual contract (Tailwind v4 + CSS + * Modules), composed over Button's. + */ +export const BreadcrumbButton: ForwardRefComponent = React.forwardRef((props, ref) => { + // Look props belong to windmod. Griffel's styled useBreadcrumbButton pins appearance and + // shape and takes size from the breadcrumb, so a size prop on the button is not accepted. + const { size } = useBreadcrumbContext(); + + return renderBreadcrumbButton( + useBreadcrumbButtonStyles({ + ...useBreadcrumbButton(props, ref), + appearance: 'subtle', + shape: 'rounded', + size, + }), + ); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +BreadcrumbButton.displayName = 'BreadcrumbButton'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts new file mode 100644 index 0000000000000..7c6c9f92a0f1a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts @@ -0,0 +1,17 @@ +import type { + BreadcrumbButtonProps as BreadcrumbButtonHeadlessProps, + BreadcrumbButtonState as BreadcrumbButtonHeadlessState, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +import type { BreadcrumbProps } from '../Breadcrumb/Breadcrumb.types'; +import type { ButtonProps } from '../Button/Button.types'; + +export type { BreadcrumbButtonSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; + +/** Windmod BreadcrumbButton props. The breadcrumb fixes appearance and shape, and size comes from its context. */ +export type BreadcrumbButtonProps = BreadcrumbButtonHeadlessProps; + +/** Windmod BreadcrumbButton state: headless state plus the look props the breadcrumb pins. */ +export type BreadcrumbButtonState = BreadcrumbButtonHeadlessState & + Required> & + Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/index.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/index.ts new file mode 100644 index 0000000000000..ffe5c8a5a8ad8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/index.ts @@ -0,0 +1,3 @@ +export { BreadcrumbButton } from './BreadcrumbButton'; +export { breadcrumbButtonClassNames, useBreadcrumbButtonStyles } from './useBreadcrumbButtonStyles'; +export type { BreadcrumbButtonProps, BreadcrumbButtonSlots, BreadcrumbButtonState } from './BreadcrumbButton.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/useBreadcrumbButtonStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/useBreadcrumbButtonStyles.ts new file mode 100644 index 0000000000000..47a2045b4fc96 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbButton/useBreadcrumbButtonStyles.ts @@ -0,0 +1,40 @@ +'use client'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { restackOver } from '../../utils/restackOver'; +import { useButtonStyles } from '../Button/useButtonStyles'; +import type { BreadcrumbButtonState } from './BreadcrumbButton.types'; + +import styles from './BreadcrumbButton.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const breadcrumbButtonClassNames: { root: string } = { + root: componentMarkers('breadcrumb-button'), +}; + +type BreadcrumbButtonRootDataAttributes = { + 'data-disabled'?: true; + 'data-disabled-focusable'?: true; + 'data-icon-only'?: true; + 'data-icon-position'?: BreadcrumbButtonState['iconPosition']; +}; + +/** + * Applies the visual contract on top of Button's, returning new state. The headless breadcrumb + * button routes through Griffel's base hook rather than the headless one, so none of the four + * Button stamps below reach the root on their own. + * + * The root keeps Button's marker pair alongside its own — see `restackOver`. + */ +export const useBreadcrumbButtonStyles = (state: BreadcrumbButtonState): BreadcrumbButtonState => + restackOver(state, useButtonStyles(state), { + marker: breadcrumbButtonClassNames.root, + root: styles.root, + icon: styles.icon, + rootAttributes: { + 'data-disabled': state.disabled || undefined, + 'data-disabled-focusable': state.disabledFocusable || undefined, + 'data-icon-only': state.iconOnly || undefined, + 'data-icon-position': state.icon ? state.iconPosition : undefined, + } satisfies BreadcrumbButtonRootDataAttributes, + }); diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.module.css b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.module.css new file mode 100644 index 0000000000000..24a36e36c5923 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.module.css @@ -0,0 +1,15 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply flex text-icon-16 leading-[calc(20/16)]; + + @variant size-small { + @apply text-icon-12 leading-[calc(20/12)]; + } + + @variant size-large { + @apply text-icon-20 leading-[calc(20/20)]; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.test.tsx new file mode 100644 index 0000000000000..905ec612d98fe --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.test.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { ChevronLeftRegular } from '@fluentui/react-icons/headless/svg/chevron-left'; +import { ChevronRightRegular } from '@fluentui/react-icons/headless/svg/chevron-right'; + +import { isConformant } from '../../testing/isConformant'; +import { stampsOf } from '../../testing/stampsOf'; +import { Breadcrumb } from '../Breadcrumb/Breadcrumb'; +import { BreadcrumbDivider } from './BreadcrumbDivider'; +import type { BreadcrumbDividerState } from './BreadcrumbDivider.types'; +import { FluentProvider } from '../FluentProvider'; +import { breadcrumbDividerClassNames, useBreadcrumbDividerStyles } from './useBreadcrumbDividerStyles'; + +import styles from './BreadcrumbDivider.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/breadcrumb', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/breadcrumb'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useBreadcrumbDivider: (...args: Parameters) => + deepFreezeState(actual.useBreadcrumbDivider(...args)), + }; +}); + +const sizes = ['small', 'medium', 'large'] as const; + +const pathOf = (element: HTMLElement): string | null => element.querySelector('svg path')?.getAttribute('d') ?? null; + +const glyphPath = (glyph: React.ReactElement): string | null => { + const { container } = render(glyph); + + return container.querySelector('path')?.getAttribute('d') ?? null; +}; + +describe('BreadcrumbDivider', () => { + isConformant({ + Component: BreadcrumbDivider, + displayName: 'BreadcrumbDivider', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root.tagName).toBe('LI'); + expect(root).toHaveClass('fui-breadcrumb-divider'); + expect(root).toHaveClass('group/fui-breadcrumb-divider'); + expect(root).toHaveClass(styles.root); + expect(root.classList[0]).toBe('fui-breadcrumb-divider'); + expect(breadcrumbDividerClassNames.root).toBe('fui-breadcrumb-divider group/fui-breadcrumb-divider'); + expect(root.getAttribute('aria-hidden')).toBe('true'); + }); + + it('renders exactly one chevron and discards consumer children', () => { + const { getByTestId } = render( + <> + + X + + B + + , + ); + + const chevron = glyphPath(); + + for (const id of ['bare', 'text', 'element']) { + const root = getByTestId(id); + + expect(root.querySelectorAll('svg')).toHaveLength(1); + expect(pathOf(root)).toBe(chevron); + expect(root.textContent).toBe(''); + expect(root.querySelector('b')).toBeNull(); + } + }); + + it('chooses the chevron from the provider direction', () => { + const right = glyphPath(); + const left = glyphPath(); + + expect(right).not.toBe(left); + + const { getByTestId } = render( + <> + + + + + + + + , + ); + + expect(pathOf(getByTestId('no-provider'))).toBe(right); + expect(pathOf(getByTestId('ltr'))).toBe(right); + expect(pathOf(getByTestId('rtl'))).toBe(left); + }); + + it('takes its size from the breadcrumb context, defaulting to medium', () => { + const { getByTestId } = render( + <> + + {sizes.map(size => ( + + + + ))} + , + ); + + expect(getByTestId('loose').getAttribute('data-size')).toBe('medium'); + + for (const size of sizes) { + expect(getByTestId(size).getAttribute('data-size')).toBe(size); + } + }); + + it('passes consumer props through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(ref.current).toBe(root); + expect(root.id).toBe('divider'); + expect(root).toHaveClass('consumer'); + expect(root.style.margin).toBe('2px'); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'li' }, + root: { className: 'consumer' }, + size: 'large', + } as unknown as BreadcrumbDividerState; + + const styled = useBreadcrumbDividerStyles(state); + + expect(styled).not.toBe(state); + expect(state.root.className).toBe('consumer'); + expect(state.root).not.toHaveProperty('data-size'); + expect(stampsOf(styled.root)['data-size']).toBe('large'); + expect(styled.root.className).toContain('consumer'); + }); + + it('falls back to medium when no breadcrumb supplied a size', () => { + const state = { + components: { root: 'li' }, + root: {}, + } as unknown as BreadcrumbDividerState; + + expect(stampsOf(useBreadcrumbDividerStyles(state).root)['data-size']).toBe('medium'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.tsx new file mode 100644 index 0000000000000..6aa805f253d9d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.tsx @@ -0,0 +1,39 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderBreadcrumbDivider, + useBreadcrumbContext, + useBreadcrumbDivider, +} from '@fluentui/react-headless-components-preview/breadcrumb'; +import { useProviderContext } from '@fluentui/react-headless-components-preview/provider'; +import { ChevronLeftRegular } from '@fluentui/react-icons/headless/svg/chevron-left'; +import { ChevronRightRegular } from '@fluentui/react-icons/headless/svg/chevron-right'; + +import type { BreadcrumbDividerProps, BreadcrumbDividerState } from './BreadcrumbDivider.types'; +import { useBreadcrumbDividerStyles } from './useBreadcrumbDividerStyles'; + +/** + * A BreadcrumbDivider separates two Breadcrumb entries. Windmod BreadcrumbDivider: the headless + * breadcrumb divider decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + * + * The divider is aria-hidden scenery whose only content is the direction-correct chevron, so + * the children override is unconditional — consumer children are replaced, not defaulted. + */ +export const BreadcrumbDivider: ForwardRefComponent = React.forwardRef((props, ref) => { + const { size } = useBreadcrumbContext(); + const { dir } = useProviderContext(); + + const state = useBreadcrumbDivider(props, ref); + + const styled: BreadcrumbDividerState = useBreadcrumbDividerStyles({ + ...state, + root: { ...state.root, children: dir === 'rtl' ? : }, + size, + }); + + return renderBreadcrumbDivider(styled); +}); + +BreadcrumbDivider.displayName = 'BreadcrumbDivider'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.types.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.types.ts new file mode 100644 index 0000000000000..17aac26b4e6db --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/BreadcrumbDivider.types.ts @@ -0,0 +1,14 @@ +import type { + BreadcrumbDividerProps as BreadcrumbDividerHeadlessProps, + BreadcrumbDividerState as BreadcrumbDividerHeadlessState, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +import type { BreadcrumbProps } from '../Breadcrumb/Breadcrumb.types'; + +export type { BreadcrumbDividerSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; + +/** Windmod BreadcrumbDivider props. `size` is not among them — it comes from the breadcrumb context. */ +export type BreadcrumbDividerProps = BreadcrumbDividerHeadlessProps; + +/** Windmod BreadcrumbDivider state: headless state plus the size read off the breadcrumb context. */ +export type BreadcrumbDividerState = BreadcrumbDividerHeadlessState & Pick; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/index.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/index.ts new file mode 100644 index 0000000000000..cb7e1648f2125 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/index.ts @@ -0,0 +1,3 @@ +export { BreadcrumbDivider } from './BreadcrumbDivider'; +export { breadcrumbDividerClassNames, useBreadcrumbDividerStyles } from './useBreadcrumbDividerStyles'; +export type { BreadcrumbDividerProps, BreadcrumbDividerSlots, BreadcrumbDividerState } from './BreadcrumbDivider.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/useBreadcrumbDividerStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/useBreadcrumbDividerStyles.ts new file mode 100644 index 0000000000000..4adb90921f267 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbDivider/useBreadcrumbDividerStyles.ts @@ -0,0 +1,37 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { BreadcrumbDividerState } from './BreadcrumbDivider.types'; + +import styles from './BreadcrumbDivider.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const breadcrumbDividerClassNames: { root: string } = { + root: componentMarkers('breadcrumb-divider'), +}; + +type BreadcrumbDividerRootDataAttributes = { + 'data-size'?: BreadcrumbDividerState['size']; +}; + +/** + * Applies the visual contract, returning new state. The size default is reachable only under a + * headless Breadcrumb, which publishes no size to its children. + * + * The divider authors no colour: it inherits the provider's, which is what makes its chevron + * match Griffel's. + */ +export const useBreadcrumbDividerStyles = (state: BreadcrumbDividerState): BreadcrumbDividerState => { + const { size = 'medium' } = state; + + const root: BreadcrumbDividerState['root'] & BreadcrumbDividerRootDataAttributes = { + ...state.root, + 'data-size': size, + className: clsx(breadcrumbDividerClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.module.css b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.module.css new file mode 100644 index 0000000000000..651c4de2f46f3 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.module.css @@ -0,0 +1,7 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply box-border flex items-center text-nowrap text-neutral-foreground-2; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.test.tsx new file mode 100644 index 0000000000000..2d366a08f80fe --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.test.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { isConformant } from '../../testing/isConformant'; +import { Breadcrumb } from '../Breadcrumb/Breadcrumb'; +import { BreadcrumbItem } from './BreadcrumbItem'; +import type { BreadcrumbItemState } from './BreadcrumbItem.types'; +import { breadcrumbItemClassNames, useBreadcrumbItemStyles } from './useBreadcrumbItemStyles'; + +import styles from './BreadcrumbItem.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/breadcrumb', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/breadcrumb'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useBreadcrumbItem: (...args: Parameters) => + deepFreezeState(actual.useBreadcrumbItem(...args)), + }; +}); + +describe('BreadcrumbItem', () => { + isConformant({ + Component: BreadcrumbItem, + displayName: 'BreadcrumbItem', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { getByTestId } = render(Home); + + const root = getByTestId('root'); + + expect(root.tagName).toBe('LI'); + expect(root).toHaveClass('fui-breadcrumb-item'); + expect(root).toHaveClass('group/fui-breadcrumb-item'); + expect(root).toHaveClass(styles.root); + expect(root.classList[0]).toBe('fui-breadcrumb-item'); + expect(breadcrumbItemClassNames.root).toBe('fui-breadcrumb-item group/fui-breadcrumb-item'); + }); + + it('takes no size of its own at any breadcrumb size', () => { + const { getByTestId } = render( + + Home + , + ); + + expect(getByTestId('item').hasAttribute('data-size')).toBe(false); + }); + + it('passes consumer props through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Home + , + ); + + const root = getByTestId('root'); + + expect(ref.current).toBe(root); + expect(root.id).toBe('item'); + expect(root).toHaveClass('consumer'); + expect(root.style.margin).toBe('2px'); + expect(root.textContent).toBe('Home'); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'li' }, + root: { className: 'consumer' }, + } as unknown as BreadcrumbItemState; + + const styled = useBreadcrumbItemStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(state.root.className).toBe('consumer'); + expect(styled.root.className).toContain('consumer'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.tsx b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.tsx new file mode 100644 index 0000000000000..bb0194a42a4cb --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderBreadcrumbItem, useBreadcrumbItem } from '@fluentui/react-headless-components-preview/breadcrumb'; + +import type { BreadcrumbItemProps } from './BreadcrumbItem.types'; +import { useBreadcrumbItemStyles } from './useBreadcrumbItemStyles'; + +/** + * A BreadcrumbItem is one entry of a Breadcrumb trail. Windmod BreadcrumbItem: the headless + * breadcrumb item decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const BreadcrumbItem: ForwardRefComponent = React.forwardRef((props, ref) => { + return renderBreadcrumbItem(useBreadcrumbItemStyles(useBreadcrumbItem(props, ref))); +}); + +BreadcrumbItem.displayName = 'BreadcrumbItem'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.types.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.types.ts new file mode 100644 index 0000000000000..98628a470eb10 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/BreadcrumbItem.types.ts @@ -0,0 +1,12 @@ +import type { + BreadcrumbItemProps as BreadcrumbItemHeadlessProps, + BreadcrumbItemState as BreadcrumbItemHeadlessState, +} from '@fluentui/react-headless-components-preview/breadcrumb'; + +export type { BreadcrumbItemSlots } from '@fluentui/react-headless-components-preview/breadcrumb'; + +/** Windmod BreadcrumbItem props. The headless surface carries every prop; windmod adds no look props. */ +export type BreadcrumbItemProps = BreadcrumbItemHeadlessProps; + +/** Windmod BreadcrumbItem state. The item has no size branch — only the divider and the button do. */ +export type BreadcrumbItemState = BreadcrumbItemHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/index.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/index.ts new file mode 100644 index 0000000000000..64cace7107ee3 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/index.ts @@ -0,0 +1,3 @@ +export { BreadcrumbItem } from './BreadcrumbItem'; +export { breadcrumbItemClassNames, useBreadcrumbItemStyles } from './useBreadcrumbItemStyles'; +export type { BreadcrumbItemProps, BreadcrumbItemSlots, BreadcrumbItemState } from './BreadcrumbItem.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/useBreadcrumbItemStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/useBreadcrumbItemStyles.ts new file mode 100644 index 0000000000000..3167228a4a391 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/BreadcrumbItem/useBreadcrumbItemStyles.ts @@ -0,0 +1,20 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { BreadcrumbItemState } from './BreadcrumbItem.types'; + +import styles from './BreadcrumbItem.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const breadcrumbItemClassNames: { root: string } = { + root: componentMarkers('breadcrumb-item'), +}; + +/** Applies the visual contract, returning new state. The item's look is size-independent, so it stamps nothing. */ +export const useBreadcrumbItemStyles = (state: BreadcrumbItemState): BreadcrumbItemState => ({ + ...state, + root: { + ...state.root, + className: clsx(breadcrumbItemClassNames.root, styles.root, state.root.className), + }, +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/Button.module.css b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.module.css new file mode 100644 index 0000000000000..e7e32d9fadf52 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.module.css @@ -0,0 +1,363 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply m-horizontal-none box-border inline-flex min-w-96 items-center justify-center overflow-hidden rounded-medium border-thin border-solid border-neutral-stroke-1 bg-neutral-background-1 px-horizontal-m py-5 align-middle font-base text-base-300 leading-base-300 font-semibold text-neutral-foreground-1 no-underline duration-faster ease-easy-ease outline-none; + + transition-property: background, border, color; + + @variant hover { + @apply cursor-pointer border-neutral-stroke-1-hover bg-neutral-background-1-hover text-neutral-foreground-1-hover; + } + + @variant hover-active { + @apply border-neutral-stroke-1-pressed bg-neutral-background-1-pressed text-neutral-foreground-1-pressed outline-none; + } + + @variant size-small { + @apply min-w-64 rounded-medium px-horizontal-s py-3 text-base-200 leading-base-200 font-regular; + } + + @variant size-large { + @apply min-w-96 rounded-medium px-horizontal-l py-vertical-s text-base-400 leading-base-400 font-semibold; + } + + /* The appearance blocks all follow this one at equal specificity, so each of them + re-asserts the disabled background and foreground it would otherwise win back. */ + @variant disabled, disabled-focusable { + @apply cursor-not-allowed border-neutral-stroke-disabled bg-neutral-background-disabled text-neutral-foreground-disabled; + } + + /* Griffel's focus indicator carries an attribute on top of the class, so it outranks every + border recolour by specificity. Here position carries that: the ring trails the disabled + sub-block, and every appearance class that recolours the border repeats it after its own. */ + @variant focus-visible { + @apply fui-focus-ring; + + @variant size-small { + @apply rounded-small; + } + + @variant size-large { + @apply rounded-large; + } + } + + /* Mozilla BugID 1857642 */ + @supports (-moz-appearance: button) { + @variant focus-visible { + box-shadow: 0 0 0 calc(var(--spacing-thin) + 0.25px) var(--color-stroke-focus-2) inset; + } + } + + @variant with-icon { + @variant size-small { + @apply py-1; + } + + @variant size-large { + @apply py-7; + } + } + + @variant icon-only { + @variant size-small { + @apply max-w-24 min-w-24 p-1; + } + + @variant size-medium { + @apply max-w-32 min-w-32 p-5; + } + + @variant size-large { + @apply max-w-40 min-w-40 p-7; + } + } + + /* Griffel emits every forced-colors rule into its media style bucket, which its sheet writes + after every other bucket, so those rules outrank the resting, hover, disabled and focus + rules whatever their source position. Source order is the only equivalent here: the + forced-colors look therefore trails every class block, and each appearance class below + repeats whatever it would otherwise win back from this one. */ + @variant forced-colors { + @variant focus { + @apply border-[ButtonText]; + } + + @variant hover, hover-active { + @apply border-[Highlight] bg-[HighlightText] text-[Highlight] forced-color-adjust-none; + } + + @variant disabled, disabled-focusable { + @apply border-[GrayText] bg-[ButtonFace] text-[GrayText]; + } + } + } + + .icon { + @apply inline-flex size-20 items-center justify-center text-icon-20 leading-[calc(20/20)]; + + --fui-button-icon-spacing: calc(6px * var(--base-scale)); + + @variant group-not-empty/fui-button { + @variant group-icon-before/fui-button { + @apply me-(--fui-button-icon-spacing); + } + + @variant group-icon-after/fui-button { + @apply ms-(--fui-button-icon-spacing); + } + } + + @variant group-size-small/fui-button { + @apply size-20 text-icon-20 leading-[calc(16/20)]; + + --fui-button-icon-spacing: calc(4px * var(--base-scale)); + } + + @variant group-size-large/fui-button { + @apply size-24 text-icon-24 leading-[calc(22/24)]; + + --fui-button-icon-spacing: calc(6px * var(--base-scale)); + } + + @variant group-appearance-subtle/fui-button { + @variant group-hover/fui-button { + @apply text-neutral-foreground-2-brand-hover; + } + + @variant group-hover-active/fui-button { + @apply text-neutral-foreground-2-brand-pressed; + } + + @variant forced-colors { + @variant group-hover/fui-button, group-hover-active/fui-button { + @apply text-[Highlight]; + } + } + } + + /* Glyph swap on hover — the icon-filled/icon-regular variants select the glyphs as this + slot's direct children. Every icon rule shares equal specificity, so in-block order + carries the cascade: appearance-hover swaps before the disabled swap-back, which must + win last. */ + @variant group-appearance-subtle/fui-button, group-appearance-transparent/fui-button { + @variant group-hover/fui-button, group-hover-active/fui-button { + @variant icon-filled { + @apply inline; + } + + @variant icon-regular { + @apply hidden; + } + } + } + + /* Group-variant form; equal specificity to the appearance-hover icon-color and glyph-swap + rules above, so source order alone must put the disabled swap-back after them. */ + @variant group-disabled/fui-button, group-disabled-focusable/fui-button { + @apply text-neutral-foreground-disabled; + + @variant group-hover/fui-button, group-hover-active/fui-button { + @apply text-neutral-foreground-disabled; + } + + @variant forced-colors { + @apply text-[GrayText]; + + @variant group-hover/fui-button, group-hover-active/fui-button { + @apply text-[GrayText]; + } + } + + @variant group-hover/fui-button, group-hover-active/fui-button { + @variant icon-filled { + @apply hidden; + } + + @variant icon-regular { + @apply inline; + } + } + } + } + + .outline { + @apply bg-transparent-background; + + @variant hover { + @apply bg-transparent-background-hover; + } + + @variant hover-active { + @apply bg-transparent-background-pressed; + } + + @variant disabled, disabled-focusable { + @apply bg-transparent-background; + } + + /* Background only: this class never touches the border or the text, so .root's trailing + forced-colors block still supplies both. */ + @variant forced-colors { + @variant hover, hover-active { + @apply bg-[HighlightText]; + } + + @variant disabled, disabled-focusable { + @apply bg-[ButtonFace]; + } + } + } + + .primary { + @apply border-transparent bg-brand-background text-neutral-foreground-on-brand; + + @variant hover { + @apply border-transparent bg-brand-background-hover text-neutral-foreground-on-brand; + } + + @variant hover-active { + @apply border-transparent bg-brand-background-pressed text-neutral-foreground-on-brand; + } + + @variant disabled, disabled-focusable { + @apply border-transparent bg-neutral-background-disabled text-neutral-foreground-disabled; + } + + @variant focus-visible { + @apply border-stroke-focus-2; + + box-shadow: + var(--shadow-2), + 0 0 0 var(--spacing-thin) var(--color-stroke-focus-2) inset, + 0 0 0 var(--spacing-thick) var(--color-neutral-foreground-on-brand) inset; + + @variant hover { + @apply border-stroke-focus-2; + + box-shadow: + var(--shadow-2), + 0 0 0 var(--spacing-thin) var(--color-stroke-focus-2) inset; + } + } + + /* Mozilla BugID 1857642 */ + @supports (-moz-appearance: button) { + @variant focus-visible { + box-shadow: + var(--shadow-2), + 0 0 0 calc(var(--spacing-thin) + 0.25px) var(--color-stroke-focus-2) inset, + 0 0 0 var(--spacing-thick) var(--color-neutral-foreground-on-brand) inset; + + @variant hover { + box-shadow: + var(--shadow-2), + 0 0 0 calc(var(--spacing-thin) + 0.25px) var(--color-stroke-focus-2) inset; + } + } + } + + @variant forced-colors { + @apply border-[HighlightText] bg-[Highlight] text-[HighlightText] forced-color-adjust-none; + + @variant hover, hover-active { + @apply border-[Highlight] bg-[HighlightText] text-[Highlight]; + } + + /* Griffel reaches this from the base rule, where the focus pseudo-class outranks this + appearance by specificity inside the shared media bucket. */ + @variant focus { + @apply border-[ButtonText]; + } + + @variant disabled, disabled-focusable { + @apply border-[GrayText] bg-[ButtonFace] text-[GrayText]; + } + } + } + + .subtle { + @apply border-transparent bg-subtle-background text-neutral-foreground-2; + + @variant hover { + @apply border-transparent bg-subtle-background-hover text-neutral-foreground-2-hover; + } + + @variant hover-active { + @apply border-transparent bg-subtle-background-pressed text-neutral-foreground-2-pressed; + } + + @variant disabled, disabled-focusable { + @apply border-transparent bg-transparent-background text-neutral-foreground-disabled; + } + + @variant focus-visible { + border-color: var(--fui-focus-ring-color, var(--color-stroke-focus-2)); + } + + @variant forced-colors { + @variant hover, hover-active { + @apply border-[Highlight] bg-[HighlightText] text-[Highlight]; + } + + @variant focus { + @apply border-[ButtonText]; + } + + @variant disabled, disabled-focusable { + @apply border-[GrayText] bg-[ButtonFace] text-[GrayText]; + } + } + } + + .transparent { + @apply border-transparent bg-transparent-background text-neutral-foreground-2; + + @variant hover { + @apply border-transparent bg-transparent-background-hover text-neutral-foreground-2-brand-hover; + } + + @variant hover-active { + @apply border-transparent bg-transparent-background-pressed text-neutral-foreground-2-brand-pressed; + } + + @variant disabled, disabled-focusable { + @apply border-transparent bg-transparent-background text-neutral-foreground-disabled; + } + + @variant focus-visible { + border-color: var(--fui-focus-ring-color, var(--color-stroke-focus-2)); + } + + @variant forced-colors { + @variant hover, hover-active { + @apply border-[Highlight] bg-transparent-background text-[Highlight]; + } + + @variant focus { + @apply border-[ButtonText]; + } + + @variant disabled, disabled-focusable { + @apply border-[GrayText] bg-[ButtonFace] text-[GrayText]; + } + } + } + + .circular { + @apply rounded-circular; + + @variant focus-visible { + @apply rounded-circular; + } + } + + .square { + @apply rounded-none; + + @variant focus-visible { + @apply rounded-none; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/Button.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.test.tsx new file mode 100644 index 0000000000000..008854aa8b043 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.test.tsx @@ -0,0 +1,240 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { stampsOf } from '../../testing/stampsOf'; +import { Button } from './Button'; +import type { ButtonAppearance, ButtonShape, ButtonSize, ButtonState } from './Button.types'; +import { buttonClassNames, useButtonStyles } from './useButtonStyles'; + +import styles from './Button.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/button', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/button'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useButton: (...args: Parameters) => deepFreezeState(actual.useButton(...args)), + }; +}); + +const appearances: ButtonAppearance[] = ['secondary', 'primary', 'outline', 'subtle', 'transparent']; +const shapes: ButtonShape[] = ['rounded', 'circular', 'square']; +const sizes: ButtonSize[] = ['small', 'medium', 'large']; + +const Glyph = (): React.ReactElement => ; + +describe('Button', () => { + isConformant({ + Component: Button, + displayName: 'Button', + }); + + it('stamps its marker pair, slash-free class first', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-button'); + expect(root).toHaveClass('group/fui-button'); + expect(root.classList[0]).toBe('fui-button'); + expect(buttonClassNames.root).toBe('fui-button group/fui-button'); + }); + + it('carries the root module class exactly once', () => { + const { getByTestId } = render(); + + expect(classOccurrences(getByTestId('root'), styles.root)).toBe(1); + }); + + it('keeps a consumer className on the root exactly once', () => { + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(root).toHaveClass('consumer'); + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(classOccurrences(root, styles.root)).toBe(1); + }); + + it('resolves the base look when no look prop is given', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-appearance')).toBe('secondary'); + expect(root.getAttribute('data-size')).toBe('medium'); + expect(root).toHaveClass(styles.secondary); + expect(root).toHaveClass(styles.rounded); + }); + + it.each(appearances)('stamps data-appearance and the appearance class for %s', appearance => { + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-appearance')).toBe(appearance); + expect(root).toHaveClass(styles[appearance]); + }); + + it.each(shapes)('carries the shape class for %s', shape => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('root')).toHaveClass(styles[shape]); + }); + + it.each(sizes)('stamps data-size for %s', size => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('root').getAttribute('data-size')).toBe(size); + }); + + it('stamps data-empty only when the root has no children', () => { + const { getByTestId: getEmpty } = render(); + + expect(getEmpty('root').getAttribute('data-empty')).toBe('true'); + expect(getLabelled('labelled').hasAttribute('data-empty')).toBe(false); + }); + + it('leaves the headless stamps to the headless hook, exactly once each', () => { + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-disabled')).toBe(''); + expect(root.getAttribute('data-icon-position')).toBe('after'); + expect(root.getAttributeNames().filter(name => name === 'data-disabled')).toHaveLength(1); + expect(root.getAttributeNames().filter(name => name === 'data-icon-position')).toHaveLength(1); + }); + + it('decorates the icon slot', () => { + const { getByTestId } = render( + , + ); + + const icon = getByTestId('root').querySelector('span'); + + expect(icon).not.toBeNull(); + expect(icon).toHaveClass(styles.icon); + expect(classOccurrences(icon!, styles.icon)).toBe(1); + }); + + it('does not mutate the state it is given', () => { + const state = { + appearance: 'primary', + components: { root: 'button', icon: 'span' }, + icon: { className: 'consumer-icon' }, + root: { as: 'button', children: 'Send', className: 'consumer' }, + shape: 'circular', + size: 'large', + } as unknown as ButtonState; + + const styled = useButtonStyles(state); + + expect(styled).not.toBe(state); + expect(state.root.className).toBe('consumer'); + expect(state.root).not.toHaveProperty('data-appearance'); + expect(state.icon!.className).toBe('consumer-icon'); + expect(styled.root.className).toContain('consumer'); + expect(styled.root.className).toContain(buttonClassNames.root); + expect(stampsOf(styled.root)['data-appearance']).toBe('primary'); + expect(stampsOf(styled.root)['data-size']).toBe('large'); + expect(styled.root.className).toContain(styles.circular); + expect(styled.icon!.className).toContain('consumer-icon'); + expect(classOccurrences(styled.icon!.className!, styles.icon)).toBe(1); + }); + + it('renders no icon slot when the consumer supplies none', () => { + const state = { + appearance: 'secondary', + components: { root: 'button', icon: 'span' }, + root: { as: 'button', children: 'Send' }, + shape: 'rounded', + size: 'medium', + } as unknown as ButtonState; + + expect(useButtonStyles(state).icon).toBeUndefined(); + }); + + // Griffel gates the icon spacing on `!!state.root.children` + // (react-button/src/components/Button/useButtonStyles.styles.ts:601). `data-empty` is the + // windmod mirror of that one expression, so the two must agree case for case — including the + // falsy-but-rendered values (`0`, `''`) that Badge deliberately counts instead. + it('stamps data-empty on exactly the children Griffel treats as absent', () => { + const { getByTestId } = render( + <> + + + + + + + , + ); + + expect(getByTestId('text').hasAttribute('data-empty')).toBe(false); + expect(getByTestId('zero').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('emptyString').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('none').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('null').hasAttribute('data-empty')).toBe(true); + expect(getByTestId('false').hasAttribute('data-empty')).toBe(true); + // An empty array is truthy, so Griffel keeps the spacing here — a child-counting predicate + // would not. + expect(getByTestId('emptyArray').hasAttribute('data-empty')).toBe(false); + }); + + // The falsy children never reach the DOM: `iconOnly` is + // `Boolean(iconShorthand?.children && !props.children)` (useButton.ts:48) and renderButton + // emits `{!iconOnly && state.root.children}`, so dropping the gap is what keeps the icon + // centred rather than what strands it. + it('renders no text for a falsy child beside an icon, so the dropped gap is correct', () => { + const { getByTestId } = render( + <> + + + + , + ); + + expect(getByTestId('zero').textContent).toBe(''); + expect(getByTestId('zero').getAttribute('data-icon-only')).toBe(''); + expect(getByTestId('emptyString').textContent).toBe(''); + expect(getByTestId('emptyString').getAttribute('data-icon-only')).toBe(''); + + expect(getByTestId('text').textContent).toBe('Label'); + expect(getByTestId('text').hasAttribute('data-icon-only')).toBe(false); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/Button.tsx b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.tsx new file mode 100644 index 0000000000000..b73b4e9ed3a92 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.tsx @@ -0,0 +1,38 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderButton, useButton, useButtonContext } from '@fluentui/react-headless-components-preview/button'; + +import { mergeContextProps } from '../../utils/mergeContextProps'; +import type { ButtonProps } from './Button.types'; +import { useButtonStyles } from './useButtonStyles'; + +/** + * Buttons give people a way to trigger an action. Windmod Button: the headless button + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Button: ForwardRefComponent = React.forwardRef((props, ref) => { + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // The context is merged before destructuring, so a container that publishes `size` supplies the + // default while an explicit prop still wins. The context instance is Griffel's own, re-exported + // by headless, because the provider is Griffel's too. + const { + appearance = 'secondary', + shape = 'rounded', + size = 'medium', + ...rest + } = mergeContextProps(useButtonContext(), props); + + return renderButton( + useButtonStyles({ + ...useButton(rest, ref), + appearance, + shape, + size, + }), + ); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +Button.displayName = 'Button'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/Button.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.types.ts new file mode 100644 index 0000000000000..cce3c022d6e3e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/Button.types.ts @@ -0,0 +1,31 @@ +import type { + ButtonProps as ButtonHeadlessProps, + ButtonState as ButtonHeadlessState, +} from '@fluentui/react-headless-components-preview/button'; + +export type { ButtonSlots } from '@fluentui/react-headless-components-preview/button'; + +/** Visual style of the Button. `'secondary'` is the base look. */ +export type ButtonAppearance = 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent'; + +/** Corner treatment of the Button. `'rounded'` is the base look. */ +export type ButtonShape = 'rounded' | 'circular' | 'square'; + +/** Size of the Button. */ +export type ButtonSize = 'small' | 'medium' | 'large'; + +/** + * Windmod Button props: the headless button plus the look props the headless surface + * deliberately omits (they exist purely to select styles). + */ +export type ButtonProps = ButtonHeadlessProps & { + /** @default 'secondary' */ + appearance?: ButtonAppearance; + /** @default 'rounded' */ + shape?: ButtonShape; + /** @default 'medium' */ + size?: ButtonSize; +}; + +/** Windmod Button state: headless state plus the resolved look props. */ +export type ButtonState = ButtonHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Button/index.ts new file mode 100644 index 0000000000000..44d3852627b06 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/index.ts @@ -0,0 +1,3 @@ +export { Button } from './Button'; +export { buttonClassNames, useButtonStyles } from './useButtonStyles'; +export type { ButtonAppearance, ButtonProps, ButtonShape, ButtonSize, ButtonSlots, ButtonState } from './Button.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Button/useButtonStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Button/useButtonStyles.ts new file mode 100644 index 0000000000000..5f7ed3199e807 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Button/useButtonStyles.ts @@ -0,0 +1,40 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { ButtonState } from './Button.types'; + +import styles from './Button.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const buttonClassNames: { root: string } = { + root: componentMarkers('button'), +}; + +type ButtonRootDataAttributes = { + 'data-appearance'?: ButtonState['appearance']; + 'data-size'?: ButtonState['size']; + 'data-empty'?: true; +}; + +/** Applies the visual contract, returning new state. The headless hook already stamps + * data-disabled/-disabled-focusable/-icon-only/-icon-position; these three are style-only. + * The icon rules select through `group/fui-button`, so a hook composing this one must keep + * Button's marker pair on its root alongside its own. */ +export const useButtonStyles = (state: ButtonState): ButtonState => { + const { appearance, shape, size } = state; + + const root: ButtonState['root'] & ButtonRootDataAttributes = { + ...state.root, + 'data-appearance': appearance, + 'data-size': size, + 'data-empty': !state.root.children || undefined, + className: clsx(buttonClassNames.root, styles.root, styles[appearance], styles[shape], state.root.className), + }; + + return { + ...state, + root, + icon: slotClasses(state.icon, styles.icon), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/Card.module.css b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.module.css new file mode 100644 index 0000000000000..775aac693c72d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.module.css @@ -0,0 +1,216 @@ +@reference '#theme'; + +/* Block order is the cascade, mirroring Griffel's own root merge order: size, orientation, + appearance, interactive, selected, disabled. Every selector is :where()-flat, so source + order alone arbitrates. The forced-colors block is authored last because Griffel emits + those rules into the `m` bucket, which sorts after `d`, `h` and `a`. + + `--fui-card-size` and `--fui-card-border-radius` are this file's own custom properties; + CardPreview.module.css reads --fui-card-size for its flush margins. */ + +@layer fui.components.l1 { + .root { + @apply relative box-border flex gap-(--fui-card-size) overflow-hidden rounded-(--fui-card-border-radius) bg-neutral-background-1 p-(--fui-card-size) text-neutral-foreground-1 shadow-4; + + /* The border lives on ::after so a CardPreview can render behind it. */ + @variant after { + @apply pointer-events-none absolute inset-0 rounded-(--fui-card-border-radius) border-thin border-solid border-transparent-stroke; + } + + @variant size-small { + --fui-card-size: calc(8px * var(--base-scale)); + --fui-card-border-radius: var(--radius-small); + } + + @variant size-medium { + --fui-card-size: calc(12px * var(--base-scale)); + --fui-card-border-radius: var(--radius-medium); + } + + @variant size-large { + --fui-card-size: calc(16px * var(--base-scale)); + --fui-card-border-radius: var(--radius-large); + } + + @variant vertical { + @apply flex-col; + } + + @variant horizontal { + @apply flex-row items-center; + } + + @variant appearance-filled-alternative { + @apply bg-neutral-background-2; + } + + @variant appearance-outline { + @apply bg-transparent-background shadow-none; + + @variant after { + @apply border-neutral-stroke-1; + } + } + + @variant appearance-subtle { + @apply bg-subtle-background shadow-none; + } + + @variant interactive { + @apply cursor-pointer; + + @variant hover { + @apply bg-neutral-background-1-hover text-neutral-foreground-1-hover shadow-8; + } + + @variant active { + @apply bg-neutral-background-1-pressed; + } + + @variant appearance-filled-alternative { + @variant hover { + @apply bg-neutral-background-2-hover text-neutral-foreground-2-hover; + } + + @variant active { + @apply bg-neutral-background-2-pressed; + } + } + + @variant appearance-outline { + @variant hover { + @apply bg-transparent-background-hover text-neutral-foreground-1-hover shadow-none; + + @variant after { + @apply border-neutral-stroke-1-hover; + } + } + + @variant active { + @apply bg-transparent-background-pressed; + + @variant after { + @apply border-neutral-stroke-1-pressed; + } + } + } + + @variant appearance-subtle { + @variant hover { + @apply bg-subtle-background-hover shadow-none; + } + + @variant active { + @apply bg-subtle-background-pressed; + } + } + } + + @variant selected { + @apply bg-neutral-background-1-selected; + + @variant after { + @apply border-neutral-stroke-1-selected; + } + + @variant hover { + @apply bg-neutral-background-1-selected text-neutral-foreground-1-selected; + } + + @variant appearance-filled-alternative { + @apply bg-neutral-background-2-selected; + + @variant hover { + @apply bg-neutral-background-2-selected text-neutral-foreground-2-selected; + } + } + + @variant appearance-outline { + @apply bg-transparent-background-selected; + + @variant hover { + @apply bg-transparent-background-selected text-neutral-foreground-1-selected; + } + } + + @variant appearance-subtle { + @apply bg-subtle-background-selected; + + @variant hover { + @apply bg-subtle-background-selected text-neutral-foreground-1-selected; + } + } + } + + @variant disabled { + @apply cursor-not-allowed bg-neutral-background-disabled text-neutral-foreground-disabled shadow-2 select-none; + + /* Overlay that swallows pointer events on the card's own content. */ + @variant before { + @apply absolute inset-0; + + z-index: calc(var(--z-index-content, 1) + 1); + } + + @variant after { + @apply border-neutral-stroke-disabled; + } + + @variant appearance-outline { + @apply bg-transparent-background shadow-none; + } + } + + @variant forced-colors { + @variant selected { + @apply bg-[Highlight] text-[HighlightText] forced-color-adjust-none; + + @variant after { + @apply border-[Highlight]; + } + } + + @variant interactive { + @variant after { + @apply border-[Highlight]; + } + + @variant hover, active { + @apply bg-[Highlight] text-[HighlightText] forced-color-adjust-none; + } + } + } + } + + /* The two focus rings decorate the same ::after the resting border uses; both blocks follow + .root so their equal-specificity ::after rules win. Which one is applied is decided in JS, + exactly as Griffel decides it. */ + .focused { + @variant focus-visible { + @apply fui-focus-outline; + + --fui-focus-outline-radius: var(--fui-card-border-radius); + --fui-focus-outline-offset: calc(var(--stroke-width-thick) * -1); + } + } + + .selectable-focused { + @variant focus-within-visible { + @apply fui-focus-outline; + + --fui-focus-outline-radius: var(--fui-card-border-radius); + --fui-focus-outline-offset: calc(var(--stroke-width-thick) * -1); + } + } + + .floating-action { + @apply absolute end-4 top-4 z-content; + } + + .checkbox { + @apply absolute size-px overflow-hidden whitespace-nowrap; + + clip: rect(0 0 0 0); + clip-path: inset(50%); + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/Card.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.test.tsx new file mode 100644 index 0000000000000..94d1f74650e8d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.test.tsx @@ -0,0 +1,347 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { Card } from './Card'; +import { CardHeader } from '../CardHeader/CardHeader'; +import { CardPreview } from '../CardPreview/CardPreview'; +import { isConformant } from '../../testing/isConformant'; +import { cardClassNames, useCardStyles } from './useCardStyles'; + +import styles from './Card.module.css'; +import previewStyles from '../CardPreview/CardPreview.module.css'; + +const mockContextValue = jest.fn(); + +// Frozen-state guard — see testing/freezeState.ts. +// The context-value spy records the state the component actually feeds to renderCard. +jest.mock('@fluentui/react-headless-components-preview/card', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/card'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useCard: (...args: Parameters) => deepFreezeState(actual.useCard(...args)), + useCardContextValue: (...args: Parameters) => { + mockContextValue(...args); + + return actual.useCardContextValue(...args); + }, + }; +}); + +// `fuicm-root` is the same jest ident for all four modules in this family, so every root is +// identified by its marker class and every cross-component assertion goes through the DOM tree. +const cardRoot = (container: HTMLElement) => container.querySelector('.fui-card') as HTMLElement; + +// The return annotation is load-bearing: an unannotated inline arrow inside an it.each table +// makes the table's own type circular (TS7023). +const noop = (): void => undefined; + +describe('Card', () => { + beforeEach(() => mockContextValue.mockClear()); + + isConformant({ + Component: Card, + displayName: 'Card', + requiredProps: { children: 'Card' }, + }); + + it('stamps the marker class', () => { + const { getByTestId } = render(Card); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-card'); + expect(root).toHaveClass('group/fui-card'); + expect(root.classList[0]).toBe('fui-card'); + expect(cardClassNames.root).toBe('fui-card group/fui-card'); + }); + + it('carries the root style class and the consumer className', () => { + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass('consumer'); + expect(root).not.toHaveClass(previewStyles.logo); + }); + + it('defaults the three look props', () => { + const { getByTestId } = render(Card); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-appearance')).toBe('filled'); + expect(root.getAttribute('data-orientation')).toBe('vertical'); + expect(root.getAttribute('data-size')).toBe('medium'); + expect(root.hasAttribute('data-interactive')).toBe(false); + }); + + it('stamps the three look props from props', () => { + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + expect(root.getAttribute('data-appearance')).toBe('outline'); + expect(root.getAttribute('data-orientation')).toBe('horizontal'); + expect(root.getAttribute('data-size')).toBe('large'); + }); + + it.each([ + ['onClick', { onClick: noop }, true], + ['onDoubleClick', { onDoubleClick: noop }, true], + ['onDragStart', { onDragStart: noop }, true], + ['onFocus', { onFocus: noop }, false], + ['selectable', { selected: false, onSelectionChange: noop }, true], + ['onClick + disabled', { onClick: noop, disabled: true }, false], + ['selected + disabled', { selected: true, onSelectionChange: noop, disabled: true }, false], + ])('gates data-interactive on %s', (_label, props, expected) => { + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + expect(root.hasAttribute('data-interactive')).toBe(expected); + // `|| undefined` keeps the attribute off entirely rather than stamping the string "false". + expect(root.getAttribute('data-interactive')).toBe(expected ? 'true' : null); + }); + + it('does not stamp a floating-action attribute — CardPreview selects the adjacency by the peer marker class', () => { + const { getByTestId } = render( + A }}> + Card + , + ); + + expect(getByTestId('root').hasAttribute('data-floating-action')).toBe(false); + }); + + it.each([ + ['default', {}, 'focused'], + ['selectable at rest', { selected: false, onSelectionChange: noop }, 'none'], + ['disabled', { disabled: true }, 'none'], + ['disabled + selected', { selected: true, onSelectionChange: noop, disabled: true }, 'none'], + ])('picks the focus class for %s', (_label, props, expected) => { + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + expect(root.classList.contains(styles.focused)).toBe(expected === 'focused'); + expect(root.classList.contains(styles.selectableFocused)).toBe(false); + }); + + it('swaps to the selectable focus class while the hidden checkbox holds focus', () => { + const { container, getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + + expect(root.classList.contains(styles.selectableFocused)).toBe(false); + + fireEvent.focus(checkbox); + expect(root.classList.contains(styles.selectableFocused)).toBe(true); + expect(root.classList.contains(styles.focused)).toBe(false); + + fireEvent.blur(checkbox); + expect(root.classList.contains(styles.selectableFocused)).toBe(false); + }); + + it('decorates the floatingAction slot and keeps its consumer className', () => { + const { container } = render( + Action, className: 'consumer-action', id: 'slot-action' }}> + Card + , + ); + + const action = cardRoot(container).firstElementChild as HTMLElement; + + expect(action).toHaveClass(styles.floatingAction); + // The sibling marker CardPreview's after-floating-action variant selects on. + expect(action).toHaveClass('peer/fui-card-floating-action'); + expect(action).toHaveClass('consumer-action'); + // Content and the slot-level id separate a decorated slot from one rebuilt as a bare + // className holder: dropping the slot's state spread keeps the class and loses the rest. + expect(action).toHaveTextContent('Action'); + expect(action).toHaveAttribute('id', 'slot-action'); + }); + + it('decorates the checkbox slot and keeps its consumer className', () => { + const { container } = render( + + Card + , + ); + + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + + expect(checkbox).toHaveClass(styles.checkbox); + expect(checkbox).toHaveClass('consumer-checkbox'); + }); + + it.each([ + ['selectable', { selected: false, onSelectionChange: noop }, true], + [ + 'selectable + floatingAction', + { selected: false, onSelectionChange: noop, floatingAction: { children: } }, + false, + ], + ['selectable + checkbox null', { selected: false, onSelectionChange: noop, checkbox: null }, false], + ['not selectable', {}, false], + ])('passes the headless checkbox gating through for %s', (_label, props, expected) => { + const { container } = render(Card); + + expect(container.querySelector('input[type="checkbox"]') !== null).toBe(expected); + }); + + it('passes consumer props straight through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('my-card'); + expect(root.getAttribute('role')).toBe('article'); + expect(root.style.zIndex).toBe('7'); + expect(ref.current).toBe(root); + }); + + it('keeps the headless selection behaviour intact', () => { + const onSelectionChange = jest.fn(); + const onClick = jest.fn(); + const { getByTestId } = render( + + Card + , + ); + + const root = getByTestId('root'); + + fireEvent.click(root); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange.mock.calls[0][1]).toEqual({ selected: true }); + + fireEvent.keyDown(root, { key: 'Enter' }); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + }); + + it('fires no selection handler on a disabled selectable card', () => { + const onSelectionChange = jest.fn(); + const { getByTestId } = render( + + Card + , + ); + + fireEvent.click(getByTestId('root')); + fireEvent.keyDown(getByTestId('root'), { key: 'Enter' }); + + expect(onSelectionChange).not.toHaveBeenCalled(); + }); + + it('builds the card context from the STYLED state', () => { + render(Card); + + expect(mockContextValue).toHaveBeenCalled(); + + const state = mockContextValue.mock.calls[0][0]; + + expect(state.root.className).toContain(styles.root); + expect(state.root.className).toContain('fui-card'); + }); + + it('wires a CardHeader id into the checkbox aria-labelledby through the context', () => { + const { container } = render( + + + , + ); + + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + const header = container.querySelector('.fui-card-header') as HTMLElement; + const labelledBy = checkbox.getAttribute('aria-labelledby'); + + expect(labelledBy).toBeTruthy(); + expect(header.querySelector(`#${labelledBy}`)).not.toBeNull(); + }); + + it('gives a CardHeader outside a Card no card-driven labelling', () => { + const { container } = render(); + + // Control for the wiring test above. The header's id is supplied by the card context, so + // outside a Card there is no id at all — asserting on the id is what makes this a control; + // asserting that a CardHeader renders no checkbox would hold no matter what the wiring did. + const header = container.querySelector('.fui-card-header')?.firstElementChild as HTMLElement; + + expect(header).toHaveTextContent('Header'); + expect(header.hasAttribute('id')).toBe(false); + }); + + it('keeps each component’s own root class inside one tree', () => { + const { container } = render( + + + media + + , + ); + + const root = cardRoot(container); + const preview = container.querySelector('.fui-card-preview') as HTMLElement; + + expect(root).toHaveClass(styles.root); + expect(preview).toHaveClass(previewStyles.root); + expect(preview).not.toBe(root); + expect(preview.parentElement).toBe(root); + }); + + it('returns a new state and mutates nothing', () => { + const state = { + appearance: 'filled', + orientation: 'vertical', + size: 'medium', + root: { className: 'kept' }, + floatingAction: { className: 'kept-action' }, + checkbox: { className: 'kept-checkbox' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + const snapshot = { + root: state.root, + rootClassName: state.root.className, + actionClassName: state.floatingAction.className, + checkboxClassName: state.checkbox.className, + }; + + const styled = useCardStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(snapshot.root); + expect(state.root.className).toBe(snapshot.rootClassName); + expect(state.floatingAction.className).toBe(snapshot.actionClassName); + expect(state.checkbox.className).toBe(snapshot.checkboxClassName); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/Card.tsx b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.tsx new file mode 100644 index 0000000000000..bca910b01fbad --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCard, useCard, useCardContextValue } from '@fluentui/react-headless-components-preview/card'; + +import type { CardProps } from './Card.types'; +import { useCardStyles } from './useCardStyles'; + +/** + * Cards are used to group similar concepts and tasks. Windmod Card: the headless card + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Card: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-card's styled useCard. + ({ appearance = 'filled', orientation = 'vertical', size = 'medium', ...rest }, ref) => { + // The context value is built from the styled state, and renderCard requires it. + const styled = useCardStyles({ + ...useCard(rest, ref as React.Ref), + appearance, + orientation, + size, + }); + + return renderCard(styled, useCardContextValue(styled)); + // Casting is required due to lack of distributive union to support union on @types/react + }, +) as ForwardRefComponent; + +Card.displayName = 'Card'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/Card.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.types.ts new file mode 100644 index 0000000000000..f2de7463e276b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/Card.types.ts @@ -0,0 +1,30 @@ +import type { + CardProps as CardHeadlessProps, + CardState as CardHeadlessState, +} from '@fluentui/react-headless-components-preview/card'; + +export type { + CardContextValue, + CardOnSelectionChangeEvent, + CardSlots, +} from '@fluentui/react-headless-components-preview/card'; + +/** Visual style of the Card. `'filled'` is the base look. */ +export type CardAppearance = 'filled' | 'filled-alternative' | 'outline' | 'subtle'; + +/** Layout direction of the Card's children. */ +export type CardOrientation = 'horizontal' | 'vertical'; + +/** Padding, gap and corner radius scale of the Card. */ +export type CardSize = 'small' | 'medium' | 'large'; + +export type CardProps = CardHeadlessProps & { + /** @default 'filled' */ + appearance?: CardAppearance; + /** @default 'vertical' */ + orientation?: CardOrientation; + /** @default 'medium' */ + size?: CardSize; +}; + +export type CardState = CardHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Card/index.ts new file mode 100644 index 0000000000000..a1239b7991456 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/index.ts @@ -0,0 +1,12 @@ +export { Card } from './Card'; +export { cardClassNames, useCardStyles } from './useCardStyles'; +export type { + CardAppearance, + CardContextValue, + CardOnSelectionChangeEvent, + CardOrientation, + CardProps, + CardSize, + CardSlots, + CardState, +} from './Card.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Card/useCardStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Card/useCardStyles.ts new file mode 100644 index 0000000000000..7c0a873e38039 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Card/useCardStyles.ts @@ -0,0 +1,56 @@ +import { clsx } from 'clsx'; + +import { componentMarkers, peerMarker } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { CardState } from './Card.types'; + +import styles from './Card.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const cardClassNames: { root: string } = { + root: componentMarkers('card'), +}; + +type CardRootDataAttributes = { + 'data-appearance'?: CardState['appearance']; + 'data-orientation'?: CardState['orientation']; + 'data-size'?: CardState['size']; + 'data-interactive'?: true; +}; + +const focusedClass = ({ disabled, selectable, selectFocused }: CardState) => { + if (disabled) { + return undefined; + } + if (!selectable) { + return styles.focused; + } + return selectFocused && styles.selectableFocused; +}; + +/** Applies the visual contract, returning new state. `filled` is the base look and carries no + * class. `data-interactive` is read by CardFooter and CardPreview, so it must be an attribute; + * the two focus classes are chosen in JS because no CSS rule can select on `selectFocused`. */ +export const useCardStyles = (state: CardState): CardState => { + const { appearance, disabled, interactive, orientation, selectable, size } = state; + + // Griffel's own gate: the interactive look is suppressed on a disabled card. + const isSelectableOrInteractive = !disabled && (interactive || selectable); + + const root: CardState['root'] & CardRootDataAttributes = { + ...state.root, + 'data-appearance': appearance, + 'data-orientation': orientation, + 'data-size': size, + 'data-interactive': isSelectableOrInteractive || undefined, + className: clsx(cardClassNames.root, styles.root, focusedClass(state), state.root.className), + }; + + return { + ...state, + root, + // The peer marker lets CardPreview's own module see the adjacency (after-floating-action). + floatingAction: slotClasses(state.floatingAction, styles.floatingAction, peerMarker('card-floating-action')), + checkbox: slotClasses(state.checkbox, styles.checkbox), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.module.css b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.module.css new file mode 100644 index 0000000000000..8ae2270991a0e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.module.css @@ -0,0 +1,36 @@ +@reference '#theme'; + +/* `shrink-0` and the horizontal `grow` are Card's rules on `> .fui-CardFooter`; they are + scoped back to "inside a Card" by the orientation the Card always stamps. */ + +@layer fui.components.l1 { + .root { + @apply flex flex-row gap-12; + + @variant group-horizontal/fui-card, group-vertical/fui-card { + @apply shrink-0; + } + + @variant group-horizontal/fui-card { + @variant last-of-type { + @apply grow; + } + } + + @variant forced-colors { + @variant group-selected/fui-card { + @apply forced-color-adjust-auto; + } + + @variant group-interactive/fui-card { + @variant group-hover/fui-card, group-active/fui-card { + @apply forced-color-adjust-auto; + } + } + } + } + + .action { + @apply ms-auto; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.test.tsx new file mode 100644 index 0000000000000..bf315c9f33526 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.test.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { CardFooter } from './CardFooter'; +import { isConformant } from '../../testing/isConformant'; +import { cardFooterClassNames, useCardFooterStyles } from './useCardFooterStyles'; + +import styles from './CardFooter.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/card', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/card'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useCardFooter: (...args: Parameters) => deepFreezeState(actual.useCardFooter(...args)), + }; +}); + +describe('CardFooter', () => { + isConformant({ + Component: CardFooter, + displayName: 'CardFooter', + requiredProps: { children: 'Footer' }, + }); + + it('stamps the marker class', () => { + const { getByTestId } = render(Footer); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-card-footer'); + expect(root).toHaveClass('group/fui-card-footer'); + expect(root.classList[0]).toBe('fui-card-footer'); + expect(cardFooterClassNames.root).toBe('fui-card-footer group/fui-card-footer'); + }); + + it('carries the root style class and the consumer className', () => { + const { getByTestId } = render( + + Footer + , + ); + + const root = getByTestId('root'); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass('consumer'); + }); + + it('decorates the action slot and keeps its consumer className', () => { + const { getByTestId } = render( + Action, className: 'consumer-action', id: 'slot-action' }} + > + Footer + , + ); + + const action = getByTestId('root').querySelector('.consumer-action'); + + expect(action).toHaveClass(styles.action); + // Content and the slot-level id separate a decorated slot from one rebuilt as a bare + // className holder: dropping the slot's state spread keeps the class and loses the rest. + expect(action).toHaveTextContent('Action'); + expect(action).toHaveAttribute('id', 'slot-action'); + }); + + it('renders no action slot when none is given', () => { + const { getByTestId } = render(Footer); + + expect(getByTestId('root').querySelector(`.${styles.action}`)).toBeNull(); + }); + + it('passes consumer props straight through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + Footer + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('my-footer'); + expect(root.style.zIndex).toBe('7'); + expect(ref.current).toBe(root); + }); + + it('returns a new state and mutates nothing', () => { + const state = { + root: { className: 'kept' }, + action: { className: 'kept-action' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + const rootBefore = state.root; + const classNamesBefore = [state.root.className, state.action.className]; + + const styled = useCardFooterStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(rootBefore); + expect([state.root.className, state.action.className]).toEqual(classNamesBefore); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.tsx new file mode 100644 index 0000000000000..95a2c499e51bd --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCardFooter, useCardFooter } from '@fluentui/react-headless-components-preview/card'; + +import type { CardFooterProps } from './CardFooter.types'; +import { useCardFooterStyles } from './useCardFooterStyles'; + +/** + * CardFooter is used to display actions for a Card. Windmod CardFooter: the headless card + * footer decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const CardFooter: ForwardRefComponent = React.forwardRef((props, ref) => { + return renderCardFooter(useCardFooterStyles(useCardFooter(props, ref))); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +CardFooter.displayName = 'CardFooter'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.types.ts b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.types.ts new file mode 100644 index 0000000000000..405c614fad54e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/CardFooter.types.ts @@ -0,0 +1,11 @@ +import type { + CardFooterProps as CardFooterHeadlessProps, + CardFooterState as CardFooterHeadlessState, +} from '@fluentui/react-headless-components-preview/card'; + +export type { CardFooterSlots } from '@fluentui/react-headless-components-preview/card'; + +/** CardFooter has no look props on either layer. */ +export type CardFooterProps = CardFooterHeadlessProps; + +export type CardFooterState = CardFooterHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/index.ts b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/index.ts new file mode 100644 index 0000000000000..e0bcd8086ce76 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/index.ts @@ -0,0 +1,3 @@ +export { CardFooter } from './CardFooter'; +export { cardFooterClassNames, useCardFooterStyles } from './useCardFooterStyles'; +export type { CardFooterProps, CardFooterSlots, CardFooterState } from './CardFooter.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardFooter/useCardFooterStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/useCardFooterStyles.ts new file mode 100644 index 0000000000000..59f6ca895be77 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardFooter/useCardFooterStyles.ts @@ -0,0 +1,19 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { CardFooterState } from './CardFooter.types'; + +import styles from './CardFooter.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const cardFooterClassNames: { root: string } = { + root: componentMarkers('card-footer'), +}; + +/** Applies the visual contract, returning new state. */ +export const useCardFooterStyles = (state: CardFooterState): CardFooterState => ({ + ...state, + root: { ...state.root, className: clsx(cardFooterClassNames.root, styles.root, state.root.className) }, + action: slotClasses(state.action, styles.action), +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.module.css b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.module.css new file mode 100644 index 0000000000000..a81b697ed84e9 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.module.css @@ -0,0 +1,62 @@ +@reference '#theme'; + +/* Two box models: flex without a description, a three-column grid with one. Griffel picks + between two style sets in JS; here the description's presence is stamped on the root and + the slots read it through the header's own group variants. + + `shrink-0` and the horizontal `grow` are Card's rules on `> .fui-CardHeader`; they are + scoped back to "inside a Card" by the orientation the Card always stamps. */ + +@layer fui.components.l1 { + .root { + @apply flex items-center; + + --fui-card-header-gap: calc(12px * var(--base-scale)); + + @variant with-description { + @apply grid auto-cols-[min-content_1fr_min-content]; + } + + @variant group-horizontal/fui-card, group-vertical/fui-card { + @apply shrink-0; + } + + @variant group-horizontal/fui-card { + @variant last-of-type { + @apply grow; + } + } + } + + .image { + @apply me-(--fui-card-header-gap) inline-flex; + + @variant group-with-description/fui-card-header { + @apply col-start-1 row-start-[span_2]; + } + } + + .header { + @apply flex; + + @variant group-with-description/fui-card-header { + @apply col-start-2 row-start-1; + } + + @variant group-not-with-description/fui-card-header { + @apply grow; + } + } + + .description { + @apply col-start-2 row-start-2 flex; + } + + .action { + @apply ms-(--fui-card-header-gap); + + @variant group-with-description/fui-card-header { + @apply col-start-3 row-start-[span_2]; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.test.tsx new file mode 100644 index 0000000000000..1df4bc2bfc44d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.test.tsx @@ -0,0 +1,156 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { CardHeader } from './CardHeader'; +import { isConformant } from '../../testing/isConformant'; +import { cardHeaderClassNames, useCardHeaderStyles } from './useCardHeaderStyles'; + +import styles from './CardHeader.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/card', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/card'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useCardHeader: (...args: Parameters) => deepFreezeState(actual.useCardHeader(...args)), + }; +}); + +const slots = (root: HTMLElement) => ({ + image: root.querySelector(`.${styles.image}`), + header: root.querySelector(`.${styles.header}`), + description: root.querySelector(`.${styles.description}`), + action: root.querySelector(`.${styles.action}`), +}); + +describe('CardHeader', () => { + isConformant({ + Component: CardHeader, + displayName: 'CardHeader', + requiredProps: { header: 'Header' }, + }); + + it('stamps the marker class', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-card-header'); + expect(root).toHaveClass('group/fui-card-header'); + expect(root.classList[0]).toBe('fui-card-header'); + expect(cardHeaderClassNames.root).toBe('fui-card-header group/fui-card-header'); + }); + + it('carries the root style class and the consumer className', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass('consumer'); + }); + + it('stamps data-description only when the slot exists', () => { + const { getByTestId } = render( + <> + + + , + ); + + expect(getByTestId('plain').hasAttribute('data-description')).toBe(false); + expect(getByTestId('described').getAttribute('data-description')).toBe('true'); + }); + + it('decorates every slot it renders and carries its content through', () => { + const { getByTestId } = render( + Image }} + header="Header" + description="Description" + action={{ children: }} + />, + ); + + const found = slots(getByTestId('root')); + + // The content assertions are what separate a decorated slot from one rebuilt as a bare + // className holder: dropping a slot's state spread keeps the class and loses the children. + expect(found.image).toHaveTextContent('Image'); + expect(found.header).toHaveTextContent('Header'); + expect(found.description).toHaveTextContent('Description'); + expect(found.action).toHaveTextContent('Action'); + }); + + it('keeps each slot’s consumer className and slot-level props', () => { + const { getByTestId } = render( + , className: 'consumer-image', id: 'slot-image' }} + header={{ children: 'Header', className: 'consumer-header', id: 'slot-header' }} + description={{ children: 'Description', className: 'consumer-description', id: 'slot-description' }} + action={{ children: , className: 'consumer-action', id: 'slot-action' }} + />, + ); + + const root = getByTestId('root'); + + expect(root.querySelector('.consumer-image')).toHaveClass(styles.image); + expect(root.querySelector('.consumer-header')).toHaveClass(styles.header); + expect(root.querySelector('.consumer-description')).toHaveClass(styles.description); + expect(root.querySelector('.consumer-action')).toHaveClass(styles.action); + + // A slot carries more than its className; the id stands in for every other slot-level prop. + expect(root.querySelector('.consumer-image')).toHaveAttribute('id', 'slot-image'); + expect(root.querySelector('.consumer-header')).toHaveAttribute('id', 'slot-header'); + expect(root.querySelector('.consumer-description')).toHaveAttribute('id', 'slot-description'); + expect(root.querySelector('.consumer-action')).toHaveAttribute('id', 'slot-action'); + }); + + it('passes consumer props straight through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('my-header'); + expect(root.style.zIndex).toBe('7'); + expect(ref.current).toBe(root); + }); + + it('returns a new state and mutates nothing', () => { + const state = { + root: { className: 'kept' }, + image: { className: 'kept-image' }, + header: { className: 'kept-header' }, + description: { className: 'kept-description' }, + action: { className: 'kept-action' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + const rootBefore = state.root; + const classNamesBefore = [ + state.root.className, + state.image.className, + state.header.className, + state.description.className, + state.action.className, + ]; + + const styled = useCardHeaderStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(rootBefore); + expect([ + state.root.className, + state.image.className, + state.header.className, + state.description.className, + state.action.className, + ]).toEqual(classNamesBefore); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.tsx new file mode 100644 index 0000000000000..7c0e2a0a7cad2 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCardHeader, useCardHeader } from '@fluentui/react-headless-components-preview/card'; + +import type { CardHeaderProps } from './CardHeader.types'; +import { useCardHeaderStyles } from './useCardHeaderStyles'; + +/** + * CardHeader is used to display a header and description for a Card. Windmod CardHeader: the + * headless card header decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const CardHeader: ForwardRefComponent = React.forwardRef((props, ref) => { + return renderCardHeader(useCardHeaderStyles(useCardHeader(props, ref))); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +CardHeader.displayName = 'CardHeader'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.types.ts b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.types.ts new file mode 100644 index 0000000000000..12464e76fd54f --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/CardHeader.types.ts @@ -0,0 +1,11 @@ +import type { + CardHeaderProps as CardHeaderHeadlessProps, + CardHeaderState as CardHeaderHeadlessState, +} from '@fluentui/react-headless-components-preview/card'; + +export type { CardHeaderSlots } from '@fluentui/react-headless-components-preview/card'; + +/** CardHeader has no look props on either layer. */ +export type CardHeaderProps = CardHeaderHeadlessProps; + +export type CardHeaderState = CardHeaderHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/index.ts b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/index.ts new file mode 100644 index 0000000000000..ca4217275cd20 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/index.ts @@ -0,0 +1,3 @@ +export { CardHeader } from './CardHeader'; +export { cardHeaderClassNames, useCardHeaderStyles } from './useCardHeaderStyles'; +export type { CardHeaderProps, CardHeaderSlots, CardHeaderState } from './CardHeader.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardHeader/useCardHeaderStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/useCardHeaderStyles.ts new file mode 100644 index 0000000000000..9009bdcda3eb6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardHeader/useCardHeaderStyles.ts @@ -0,0 +1,36 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { CardHeaderState } from './CardHeader.types'; + +import styles from './CardHeader.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const cardHeaderClassNames: { root: string } = { + root: componentMarkers('card-header'), +}; + +type CardHeaderRootDataAttributes = { + 'data-description'?: true; +}; + +/** Applies the visual contract, returning new state. The description's presence switches the + * root between a flex and a three-column grid box model, and the three placed slots read it + * back through the header's own group marker. */ +export const useCardHeaderStyles = (state: CardHeaderState): CardHeaderState => { + const root: CardHeaderState['root'] & CardHeaderRootDataAttributes = { + ...state.root, + 'data-description': state.description ? true : undefined, + className: clsx(cardHeaderClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + image: slotClasses(state.image, styles.image), + header: slotClasses(state.header, styles.header), + description: slotClasses(state.description, styles.description), + action: slotClasses(state.action, styles.action), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.module.css b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.module.css new file mode 100644 index 0000000000000..334d1a141938d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.module.css @@ -0,0 +1,65 @@ +@reference '#theme'; + +/* The flush margins are Card's, not CardPreview's: Griffel writes them as `> .fui-CardPreview` + rules on the Card root. Reaching them through the card's group variants keeps every declaration on + the element it paints, at the cost of matching any descendant rather than a direct child. + `--fui-card-size` is declared by Card.module.css. */ + +@layer fui.components.l1 { + .root { + @apply relative; + + /* The media is consumer children with no slot to carry a class, so the fill is reachable only as + an exclusion. Excluding the logo — the one child that IS a slot — keeps the two `size` + declarations from meeting, so nothing here depends on block order to settle them. */ + & > :not(.logo) { + @apply block size-full; + } + + @variant group-horizontal/fui-card { + @apply -my-(--fui-card-size); + + @variant first-of-type { + @apply -ms-(--fui-card-size); + } + + @variant last-of-type { + @apply -me-(--fui-card-size); + } + } + + @variant group-vertical/fui-card { + @apply -mx-(--fui-card-size); + + @variant first-of-type { + @apply -mt-(--fui-card-size); + } + + /* A preview behind the floating action still sits flush with the card's top edge: + the action is the first painted child, so :first-of-type misses the preview. */ + @variant after-floating-action { + @apply -mt-(--fui-card-size); + } + + @variant last-of-type { + @apply -mb-(--fui-card-size); + } + } + + @variant forced-colors { + @variant group-selected/fui-card { + @apply forced-color-adjust-auto; + } + + @variant group-interactive/fui-card { + @variant group-hover/fui-card, group-active/fui-card { + @apply forced-color-adjust-auto; + } + } + } + } + + .logo { + @apply absolute start-12 bottom-12 size-32; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.test.tsx new file mode 100644 index 0000000000000..bd47db2d428e1 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.test.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { CardPreview } from './CardPreview'; +import { isConformant } from '../../testing/isConformant'; +import { cardPreviewClassNames, useCardPreviewStyles } from './useCardPreviewStyles'; + +import styles from './CardPreview.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/card', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/card'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useCardPreview: (...args: Parameters) => + deepFreezeState(actual.useCardPreview(...args)), + }; +}); + +describe('CardPreview', () => { + isConformant({ + Component: CardPreview, + displayName: 'CardPreview', + requiredProps: { children: media }, + }); + + it('stamps the marker class', () => { + const { getByTestId } = render( + + media + , + ); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-card-preview'); + expect(root).toHaveClass('group/fui-card-preview'); + expect(root.classList[0]).toBe('fui-card-preview'); + expect(cardPreviewClassNames.root).toBe('fui-card-preview group/fui-card-preview'); + }); + + it('carries the root style class and the consumer className', () => { + const { getByTestId } = render( + + media + , + ); + + const root = getByTestId('root'); + + expect(root).toHaveClass(styles.root); + expect(root).toHaveClass('consumer'); + }); + + it('decorates the logo slot and keeps its consumer className', () => { + const { getByTestId } = render( + Logo, className: 'consumer-logo', id: 'slot-logo' }}> + media + , + ); + + const logo = getByTestId('root').querySelector('.consumer-logo'); + + expect(logo).toHaveClass(styles.logo); + // Content and the slot-level id separate a decorated slot from one rebuilt as a bare + // className holder: dropping the slot's state spread keeps the class and loses the rest. + expect(logo).toHaveTextContent('Logo'); + expect(logo).toHaveAttribute('id', 'slot-logo'); + }); + + it('renders no logo slot when none is given', () => { + const { getByTestId } = render( + + media + , + ); + + expect(getByTestId('root').querySelector(`.${styles.logo}`)).toBeNull(); + }); + + it('passes consumer props straight through to the root', () => { + const ref = React.createRef(); + const { getByTestId } = render( + + media + , + ); + + const root = getByTestId('root'); + + expect(root.id).toBe('my-preview'); + expect(root.style.zIndex).toBe('7'); + expect(ref.current).toBe(root); + }); + + it('returns a new state and mutates nothing', () => { + const state = { + root: { className: 'kept' }, + logo: { className: 'kept-logo' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + const rootBefore = state.root; + const classNamesBefore = [state.root.className, state.logo.className]; + + const styled = useCardPreviewStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(rootBefore); + expect([state.root.className, state.logo.className]).toEqual(classNamesBefore); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.tsx b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.tsx new file mode 100644 index 0000000000000..d49dde47f5e1a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCardPreview, useCardPreview } from '@fluentui/react-headless-components-preview/card'; + +import type { CardPreviewProps } from './CardPreview.types'; +import { useCardPreviewStyles } from './useCardPreviewStyles'; + +/** + * CardPreview is used to display a preview of the content of a Card. Windmod CardPreview: the + * headless card preview decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const CardPreview: ForwardRefComponent = React.forwardRef((props, ref) => { + return renderCardPreview(useCardPreviewStyles(useCardPreview(props, ref))); + // Casting is required due to lack of distributive union to support union on @types/react +}) as ForwardRefComponent; + +CardPreview.displayName = 'CardPreview'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.types.ts b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.types.ts new file mode 100644 index 0000000000000..24a705c0913fa --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/CardPreview.types.ts @@ -0,0 +1,11 @@ +import type { + CardPreviewProps as CardPreviewHeadlessProps, + CardPreviewState as CardPreviewHeadlessState, +} from '@fluentui/react-headless-components-preview/card'; + +export type { CardPreviewSlots } from '@fluentui/react-headless-components-preview/card'; + +/** CardPreview has no look props on either layer. */ +export type CardPreviewProps = CardPreviewHeadlessProps; + +export type CardPreviewState = CardPreviewHeadlessState; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/index.ts b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/index.ts new file mode 100644 index 0000000000000..191bc594139af --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/index.ts @@ -0,0 +1,3 @@ +export { CardPreview } from './CardPreview'; +export { cardPreviewClassNames, useCardPreviewStyles } from './useCardPreviewStyles'; +export type { CardPreviewProps, CardPreviewSlots, CardPreviewState } from './CardPreview.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/CardPreview/useCardPreviewStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/useCardPreviewStyles.ts new file mode 100644 index 0000000000000..5b364af859aaa --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/CardPreview/useCardPreviewStyles.ts @@ -0,0 +1,19 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { CardPreviewState } from './CardPreview.types'; + +import styles from './CardPreview.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const cardPreviewClassNames: { root: string } = { + root: componentMarkers('card-preview'), +}; + +/** Applies the visual contract, returning new state. */ +export const useCardPreviewStyles = (state: CardPreviewState): CardPreviewState => ({ + ...state, + root: { ...state.root, className: clsx(cardPreviewClassNames.root, styles.root, state.root.className) }, + logo: slotClasses(state.logo, styles.logo), +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.module.css b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.module.css new file mode 100644 index 0000000000000..e6fc23dcdb851 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.module.css @@ -0,0 +1,133 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply relative inline-flex max-w-fit cursor-pointer align-middle text-neutral-foreground-3; + + @variant focus { + @apply outline-none; + } + + @variant focus-within-visible { + @apply fui-focus-outline; + } + + @variant enabled { + @variant not-checked { + @variant hover { + @apply text-neutral-foreground-2; + } + + @variant active { + @apply text-neutral-foreground-1; + } + } + + @variant checked { + @apply text-neutral-foreground-1; + } + } + + @variant disabled { + @apply cursor-default text-neutral-foreground-disabled; + + @variant forced-colors { + @apply text-[GrayText]; + } + } + } + + .input { + @apply absolute top-0 m-0 box-border h-full w-32 cursor-[inherit] opacity-0; + + @variant group-label-before/fui-checkbox { + @apply end-0; + } + + @variant group-label-after/fui-checkbox { + @apply start-0; + } + + @variant group-size-large/fui-checkbox { + @apply w-36; + } + } + + .indicator { + @apply pointer-events-none mx-horizontal-s my-vertical-s box-border flex size-16 shrink-0 items-center justify-center self-start overflow-hidden rounded-small border-thin border-solid border-neutral-stroke-accessible fill-current text-icon-12 leading-[calc(20/12)]; + + @variant group-size-large/fui-checkbox { + @apply size-20 text-icon-16 leading-[calc(20/16)]; + } + + @variant group-enabled/fui-checkbox { + @variant group-not-checked/fui-checkbox { + @variant group-hover/fui-checkbox { + @apply border-neutral-stroke-accessible-hover; + } + + @variant group-active/fui-checkbox { + @apply border-neutral-stroke-accessible-pressed; + } + } + + @variant group-checked/fui-checkbox { + @variant group-not-checked-mixed/fui-checkbox { + @apply border-compound-brand-background bg-compound-brand-background text-neutral-foreground-inverted; + + @variant group-hover/fui-checkbox { + @apply border-compound-brand-background-hover bg-compound-brand-background-hover; + } + + @variant group-active/fui-checkbox { + @apply border-compound-brand-background-pressed bg-compound-brand-background-pressed; + } + } + } + + @variant group-checked-mixed/fui-checkbox { + @apply border-compound-brand-stroke text-compound-brand-foreground-1; + + @variant group-hover/fui-checkbox { + @apply border-compound-brand-stroke-hover text-compound-brand-foreground-1-hover; + } + + @variant group-active/fui-checkbox { + @apply border-compound-brand-stroke-pressed text-compound-brand-foreground-1-pressed; + } + } + } + + @variant group-disabled/fui-checkbox { + @apply border-neutral-stroke-disabled text-neutral-foreground-disabled; + + @variant forced-colors { + @apply text-[GrayText]; + } + } + } + + .circular { + @apply rounded-circular; + } + + .label { + --fui-checkbox-label-offset: calc((16px * var(--base-scale) - var(--text-base-300) * var(--leading-base-300)) / 2); + + @apply my-(--fui-checkbox-label-offset) cursor-[inherit] self-center px-horizontal-s py-vertical-s font-base text-base-300 leading-base-300; + + @variant group-size-large/fui-checkbox { + --fui-checkbox-label-offset: calc( + (20px * var(--base-scale) - var(--text-base-300) * var(--leading-base-300)) / 2 + ); + } + + @variant group-label-before/fui-checkbox { + @apply pe-horizontal-xs; + } + + @variant group-label-after/fui-checkbox { + @apply ps-horizontal-xs; + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.test.tsx new file mode 100644 index 0000000000000..1b290180f5e64 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.test.tsx @@ -0,0 +1,360 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { Checkbox } from './Checkbox'; +import type { CheckboxState } from './Checkbox.types'; +import { checkboxClassNames, useCheckboxStyles } from './useCheckboxStyles'; + +import styles from './Checkbox.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/checkbox', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/checkbox'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useCheckbox: (...args: Parameters) => deepFreezeState(actual.useCheckbox(...args)), + }; +}); + +const sizes = ['medium', 'large'] as const; + +// `input` is the primary slot, so every native prop — data-testid included — lands on the ; +// the root is only reachable through the container. The indicator is the root's only
, read +// structurally so a dropped module class cannot hide it. +const parts = (root: HTMLElement) => ({ + root, + input: root.querySelector('input')!, + indicator: root.querySelector('div'), + label: root.querySelector('label'), +}); + +const renderCheckbox = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return parts(container.firstElementChild as HTMLElement); +}; + +const glyph = (indicator: HTMLElement | null) => { + const svg = indicator?.querySelector('svg'); + + return svg && { width: svg.getAttribute('width'), d: svg.querySelector('path')!.getAttribute('d') }; +}; + +describe('Checkbox', () => { + isConformant({ + Component: Checkbox, + displayName: 'Checkbox', + primarySlot: 'input', + }); + + it('stamps the marker pair on the root', () => { + const { root } = renderCheckbox(); + + expect(root).toHaveClass('fui-checkbox'); + expect(root).toHaveClass('group/fui-checkbox'); + expect(root.classList[0]).toBe('fui-checkbox'); + expect(checkboxClassNames.root).toBe('fui-checkbox group/fui-checkbox'); + }); + + it('applies one module class per slot, and never a class belonging to another slot', () => { + const { root, input, indicator, label } = renderCheckbox({ label: 'Hello', shape: 'circular' }); + const owners = [ + [root, styles.root], + [input, styles.input], + [indicator, styles.indicator], + [label, styles.label], + ] as const; + + owners.forEach(([element, own]) => { + expect(element).toHaveClass(own); + + owners.forEach(([, other]) => { + if (other !== own) { + expect(element).not.toHaveClass(other); + } + }); + + if (element !== indicator) { + expect(element).not.toHaveClass(styles.circular); + } + }); + }); + + it('adds the circular class to the indicator only for shape="circular"', () => { + expect(renderCheckbox({ shape: 'circular' }).indicator).toHaveClass(styles.circular); + expect(renderCheckbox().indicator).not.toHaveClass(styles.circular); + expect(renderCheckbox({ shape: 'square' }).indicator).not.toHaveClass(styles.circular); + }); + + it('stamps data-size on the root alone', () => { + expect(renderCheckbox().root.getAttribute('data-size')).toBe('medium'); + + sizes.forEach(size => { + const { root, input, indicator, label } = renderCheckbox({ size, label: 'Hello' }); + + expect(root.getAttribute('data-size')).toBe(size); + expect(input.hasAttribute('data-size')).toBe(false); + expect(indicator!.hasAttribute('data-size')).toBe(false); + expect(label!.hasAttribute('data-size')).toBe(false); + }); + }); + + it('restores the checkmark glyph, sized by the size prop', () => { + expect(glyph(renderCheckbox({ defaultChecked: true }).indicator)).toEqual({ + width: '12', + d: expect.stringMatching(/^M9\.76 3\.2c/), + }); + expect(glyph(renderCheckbox({ defaultChecked: true, size: 'large' }).indicator)).toEqual({ + width: '16', + d: expect.stringMatching(/^M14\.05 3\.49c/), + }); + }); + + it('restores the mixed glyph, branching on shape before size', () => { + expect(glyph(renderCheckbox({ checked: 'mixed' }).indicator)).toEqual({ + width: '12', + d: expect.stringMatching(/^M2 4c0-1\.1/), + }); + expect(glyph(renderCheckbox({ checked: 'mixed', size: 'large' }).indicator)).toEqual({ + width: '16', + d: expect.stringMatching(/^M2 4\.5A2\.5/), + }); + + sizes.forEach(size => { + expect(glyph(renderCheckbox({ checked: 'mixed', shape: 'circular', size }).indicator)).toEqual({ + width: '1em', + d: expect.stringMatching(/^M10 2a8 8/), + }); + }); + }); + + it('renders the indicator without a glyph while unchecked', () => { + const { indicator } = renderCheckbox(); + + expect(indicator).not.toBeNull(); + expect(indicator!.querySelector('svg')).toBeNull(); + }); + + it('lets a consumer glyph win over the restored one', () => { + const { indicator } = renderCheckbox({ defaultChecked: true, indicator: }); + + expect(indicator!.querySelector('[data-custom]')).not.toBeNull(); + expect(indicator!.querySelector('svg')).toBeNull(); + }); + + it('keeps both the resolved slot props and the glyph for a childless indicator object', () => { + const { indicator } = renderCheckbox({ defaultChecked: true, indicator: { className: 'x', id: 'box' } }); + + expect(indicator).toHaveClass(styles.indicator); + expect(indicator).toHaveClass('x'); + expect(indicator!.id).toBe('box'); + expect(indicator!.querySelector('svg')).not.toBeNull(); + }); + + it('keeps the glyph for indicator={{ children: undefined }} and for children: null', () => { + expect( + renderCheckbox({ defaultChecked: true, indicator: { children: undefined } }).indicator!.querySelector('svg'), + ).not.toBeNull(); + expect( + renderCheckbox({ defaultChecked: true, indicator: { children: null } }).indicator!.querySelector('svg'), + ).not.toBeNull(); + }); + + it('treats falsy-but-present indicator children as consumer content', () => { + // The fallback is nullish-coalescing, matching Griffel's `children ??= glyph`: 0, '' and false + // are content a consumer supplied, not an absent glyph. + ([0, '', false] as const).forEach(children => { + const { indicator } = renderCheckbox({ defaultChecked: true, indicator: { children } }); + + expect(indicator!.querySelector('svg')).toBeNull(); + }); + + expect(renderCheckbox({ defaultChecked: true, indicator: { children: 0 } }).indicator!.textContent).toBe('0'); + }); + + it('removes the indicator slot for indicator={null}', () => { + const { root, indicator } = renderCheckbox({ defaultChecked: true, indicator: null }); + + expect(indicator).toBeNull(); + expect(root.querySelector('svg')).toBeNull(); + expect(root.children).toHaveLength(1); + }); + + it('keeps the headless aria-hidden default on the indicator, glyph or not', () => { + expect(renderCheckbox().indicator!.getAttribute('aria-hidden')).toBe('true'); + expect(renderCheckbox({ defaultChecked: true }).indicator!.getAttribute('aria-hidden')).toBe('true'); + }); + + it('keeps the circular class at both sizes', () => { + sizes.forEach(size => { + expect(renderCheckbox({ shape: 'circular', size }).indicator).toHaveClass(styles.circular); + }); + }); + + it('renders no label slot without a label prop', () => { + const { root, label } = renderCheckbox(); + + expect(label).toBeNull(); + expect(root.children).toHaveLength(2); + }); + + it('keeps a consumer className on the label slot', () => { + const { label } = renderCheckbox({ label: { children: 'Hello', className: 'x' } }); + + expect(label).toHaveClass(styles.label); + expect(label).toHaveClass('x'); + expect(label!.textContent).toBe('Hello'); + }); + + it('orders the slots input, then label around the indicator', () => { + const after = renderCheckbox({ label: 'Hello' }); + + expect(after.root.children[0]).toBe(after.input); + expect(after.root.children[1]).toBe(after.indicator); + expect(after.root.children[2]).toBe(after.label); + + const before = renderCheckbox({ label: 'Hello', labelPosition: 'before' }); + + expect(before.root.children[0]).toBe(before.input); + expect(before.root.children[1]).toBe(before.label); + expect(before.root.children[2]).toBe(before.indicator); + }); + + it('leaves the headless data attributes alone', () => { + const unchecked = renderCheckbox(); + + expect(unchecked.root.hasAttribute('data-checked')).toBe(false); + expect(unchecked.root.hasAttribute('data-disabled')).toBe(false); + expect(unchecked.root.getAttribute('data-label-position')).toBe('after'); + + expect(renderCheckbox({ checked: true }).root.getAttribute('data-checked')).toBe(''); + expect(renderCheckbox({ checked: 'mixed' }).root.getAttribute('data-checked')).toBe('mixed'); + expect(renderCheckbox({ disabled: true }).root.getAttribute('data-disabled')).toBe(''); + expect(renderCheckbox({ labelPosition: 'before' }).root.getAttribute('data-label-position')).toBe('before'); + }); + + it('carries both data-disabled and data-checked when disabled and checked', () => { + const { root } = renderCheckbox({ defaultChecked: true, disabled: true }); + + expect(root.getAttribute('data-disabled')).toBe(''); + expect(root.getAttribute('data-checked')).toBe(''); + }); + + it('carries data-disabled alongside data-checked="mixed", glyph included', () => { + const { root, indicator } = renderCheckbox({ checked: 'mixed', disabled: true }); + + expect(root.getAttribute('data-disabled')).toBe(''); + expect(root.getAttribute('data-checked')).toBe('mixed'); + expect(glyph(indicator)).toEqual({ width: '12', d: expect.stringMatching(/^M2 4c0-1\.1/) }); + }); + + it('recomputes the glyph as a controlled checked value transitions mixed → true → false', () => { + const onChange = jest.fn(); + const { rerender, container } = render(); + const { root, indicator } = parts(container.firstElementChild as HTMLElement); + + expect(root.getAttribute('data-checked')).toBe('mixed'); + expect(glyph(indicator)).toEqual({ width: '12', d: expect.stringMatching(/^M2 4c0-1\.1/) }); + + rerender(); + expect(root.getAttribute('data-checked')).toBe(''); + expect(glyph(indicator)).toEqual({ width: '12', d: expect.stringMatching(/^M9\.76 3\.2c/) }); + + rerender(); + expect(root.hasAttribute('data-checked')).toBe(false); + expect(indicator!.querySelector('svg')).toBeNull(); + }); + + it('passes the uncontrolled tri-state machinery through untouched', () => { + const onChange = jest.fn(); + const { root, input } = renderCheckbox({ defaultChecked: 'mixed', onChange }); + + expect(root.getAttribute('data-checked')).toBe('mixed'); + + fireEvent.click(input); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][1]).toEqual({ checked: true }); + expect(root.getAttribute('data-checked')).toBe(''); + }); + + it('passes the controlled tri-state machinery through untouched', () => { + const onChange = jest.fn(); + const { root, input } = renderCheckbox({ checked: false, onChange }); + + fireEvent.click(input); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(root.hasAttribute('data-checked')).toBe(false); + }); + + it('splits native props onto the input and root props onto the root', () => { + const { root, input } = renderCheckbox({ + className: 'consumer', + 'data-testid': 't', + id: 'my-id', + required: true, + } as React.ComponentProps); + + expect(input.id).toBe('my-id'); + expect(input.required).toBe(true); + expect(input.getAttribute('data-testid')).toBe('t'); + expect(root.hasAttribute('data-testid')).toBe(false); + expect(root).toHaveClass('consumer'); + expect(input).not.toHaveClass('consumer'); + }); + + it('keeps a consumer className on the root exactly once', () => { + const { root } = renderCheckbox({ className: 'consumer' }); + + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(classOccurrences(root, styles.root)).toBe(1); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'span', input: 'input', indicator: 'div', label: 'label' }, + checked: true, + indicator: { className: 'consumer-indicator' }, + input: { className: 'native' }, + label: { className: 'consumer-label' }, + root: { className: 'consumer' }, + shape: 'circular', + size: 'large', + } as unknown as CheckboxState; + + const styled = useCheckboxStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.input).not.toBe(state.input); + expect(styled.indicator).not.toBe(state.indicator); + expect(styled.label).not.toBe(state.label); + + expect(state.root.className).toBe('consumer'); + expect(state.input.className).toBe('native'); + expect(state.indicator!.className).toBe('consumer-indicator'); + expect(state.label!.className).toBe('consumer-label'); + expect('data-size' in state.root).toBe(false); + + expect(styled.root.className).toContain('consumer'); + expect(styled.root.className).toContain(checkboxClassNames.root); + expect(styled.input.className).toContain('native'); + expect(styled.indicator!.className).toContain('consumer-indicator'); + expect(styled.indicator!.className).toContain(styles.circular); + expect(styled.label!.className).toContain('consumer-label'); + }); + + it('renders the full pipeline against a frozen headless state without throwing', () => { + expect(() => render()).not.toThrow(); + const onChange = jest.fn(); + + expect(() => render()).not.toThrow(); + expect(() => render()).not.toThrow(); + expect(() => render()).not.toThrow(); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.tsx b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000000..4e6f23bc8794d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,65 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { renderCheckbox, useCheckbox } from '@fluentui/react-headless-components-preview/checkbox'; +import { Checkmark12Filled, Checkmark16Filled } from '@fluentui/react-icons/headless/svg/checkmark'; +import { CircleFilled } from '@fluentui/react-icons/headless/svg/circle'; +import { Square12Filled, Square16Filled } from '@fluentui/react-icons/headless/svg/square'; + +import type { CheckboxProps, CheckboxShape, CheckboxSize, CheckboxState } from './Checkbox.types'; +import { useCheckboxStyles } from './useCheckboxStyles'; + +// Not a hook: it is called on the right of `??`, where a `use` prefix would read as a +// conditionally-called hook. +const getIndicatorGlyph = (checked: 'mixed' | boolean, shape: CheckboxShape, size: CheckboxSize) => { + if (checked === 'mixed') { + if (shape === 'circular') { + return ; + } + + return size === 'large' ? : ; + } + + if (checked) { + return size === 'large' ? : ; + } + + return undefined; +}; + +/** + * A Checkbox is a tri-state control for a single choice. Windmod Checkbox: the headless checkbox + * decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const Checkbox: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // Defaults mirror @fluentui/react-checkbox's styled useCheckbox. + ({ shape = 'square', size = 'medium', ...rest }, ref) => { + const state: CheckboxState = { + ...useCheckbox(rest, ref), + shape, + size, + }; + + // The indicator slot renders by default, so no pre-hook materialisation is needed (see + // MenuButton.tsx for the case that does need it). Consumer-supplied children always win; null or + // undefined children fall back to the resolved glyph; `indicator={null}` still removes the slot. + const styled = useCheckboxStyles( + state.indicator + ? { + ...state, + indicator: { + ...state.indicator, + children: state.indicator.children ?? getIndicatorGlyph(state.checked, shape, size), + }, + } + : state, + ); + + return renderCheckbox(styled); + // Casting is required due to lack of distributive union to support union on @types/react + }, +) as ForwardRefComponent; + +Checkbox.displayName = 'Checkbox'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.types.ts b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.types.ts new file mode 100644 index 0000000000000..5d2ed605aa43a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/Checkbox.types.ts @@ -0,0 +1,26 @@ +import type { + CheckboxProps as CheckboxHeadlessProps, + CheckboxState as CheckboxHeadlessState, +} from '@fluentui/react-headless-components-preview/checkbox'; + +export type { CheckboxSlots } from '@fluentui/react-headless-components-preview/checkbox'; + +/** Shape of the Checkbox indicator. `'circular'` suits checklists, where it cannot be read as a radio. */ +export type CheckboxShape = 'square' | 'circular'; + +/** Size of the Checkbox indicator. */ +export type CheckboxSize = 'medium' | 'large'; + +/** + * Windmod Checkbox props: the headless checkbox plus the look props the headless surface + * deliberately omits (they exist purely to select styles). + */ +export type CheckboxProps = CheckboxHeadlessProps & { + /** @default 'square' */ + shape?: CheckboxShape; + /** @default 'medium' */ + size?: CheckboxSize; +}; + +/** Windmod Checkbox state: headless state plus the resolved look props. */ +export type CheckboxState = CheckboxHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/index.ts b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/index.ts new file mode 100644 index 0000000000000..af9305001b861 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/index.ts @@ -0,0 +1,3 @@ +export { Checkbox } from './Checkbox'; +export { checkboxClassNames, useCheckboxStyles } from './useCheckboxStyles'; +export type { CheckboxProps, CheckboxShape, CheckboxSize, CheckboxSlots, CheckboxState } from './Checkbox.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Checkbox/useCheckboxStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/useCheckboxStyles.ts new file mode 100644 index 0000000000000..e6b4769a43187 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Checkbox/useCheckboxStyles.ts @@ -0,0 +1,38 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { CheckboxState } from './Checkbox.types'; + +import styles from './Checkbox.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const checkboxClassNames: { root: string } = { + root: componentMarkers('checkbox'), +}; + +type CheckboxRootDataAttributes = { + 'data-size'?: CheckboxState['size']; +}; + +/** Applies the visual contract, returning new state. The headless hook already stamps + * data-checked/-disabled/-label-position — data-label-position on every Checkbox whether or not a + * label slot is present, so it is never a label-presence test. `data-size` is style-only, and + * `shape` selects a class because only the indicator changes with it. */ +export const useCheckboxStyles = (state: CheckboxState): CheckboxState => { + const { shape, size } = state; + + const root: CheckboxState['root'] & CheckboxRootDataAttributes = { + ...state.root, + 'data-size': size, + className: clsx(checkboxClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + input: slotClasses(state.input, styles.input), + indicator: slotClasses(state.indicator, styles.indicator, shape === 'circular' && styles.circular), + label: slotClasses(state.label, styles.label), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.module.css b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.module.css new file mode 100644 index 0000000000000..f855c37a29b31 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.module.css @@ -0,0 +1,48 @@ +@reference '#theme'; + +@layer fui.components.l1 { + /* The `--fui-Area*` custom properties are written by the headless hook into the root's inline + style and are read only here. */ + .root { + @apply relative mb-vertical-s-nudge box-border inline-grid min-h-300 min-w-300 touch-none items-start justify-items-start rounded-medium border-thin border-solid border-neutral-stroke-1 forced-color-adjust-none; + + --fui-color-area-thumb-size: calc(20px * var(--base-scale)); + + background: + linear-gradient(to bottom, transparent, #000), linear-gradient(to right, #fff, transparent), + var(--fui-Area--main-color); + + @variant rtl { + background: + linear-gradient(to bottom, transparent, #000), linear-gradient(to left, #fff, transparent), + var(--fui-Area--main-color); + } + + @variant shape-square { + @apply rounded-none; + } + } + + .thumb { + @apply pointer-events-none absolute start-(--fui-AreaX--progress) bottom-(--fui-AreaY--progress) size-(--fui-color-area-thumb-size) -translate-x-1/2 translate-y-1/2 rounded-circular border-thin border-solid border-neutral-foreground-4 bg-(--fui-Area__thumb--color) shadow-4 forced-color-adjust-none outline-none; + + @variant before { + @apply absolute inset-0 box-border rounded-circular border-thick border-solid border-neutral-background-1; + } + + @variant rtl { + @apply translate-x-1/2; + } + + @variant focus-within-visible { + @apply fui-focus-outline; + + --fui-focus-outline-width: var(--spacing-thick); + --fui-focus-outline-radius: var(--radius-circular); + } + } + + .input { + @apply pointer-events-none absolute start-0 top-0 m-0 size-full overflow-hidden p-0 opacity-0; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.test.tsx new file mode 100644 index 0000000000000..91a5ac0ee01ad --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.test.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; +import { ColorArea } from './ColorArea'; +import type { ColorAreaState } from './ColorArea.types'; +import { colorAreaClassNames, useColorAreaStyles } from './useColorAreaStyles'; + +import styles from './ColorArea.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/color-picker', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/color-picker'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useColorArea: (...args: Parameters) => deepFreezeState(actual.useColorArea(...args)), + }; +}); + +const teal = { h: 180, s: 0.5, v: 0.6 }; + +// The inputs live inside the thumb, which is the root's only child. +const slotsOf = (root: HTMLElement) => { + const thumb = root.firstElementChild as HTMLElement; + const [inputX, inputY] = Array.from(thumb.children) as HTMLInputElement[]; + + return { inputX, inputY, root, thumb }; +}; + +const renderColorArea = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return slotsOf(container.firstElementChild as HTMLElement); +}; + +describe('ColorArea', () => { + isConformant({ + Component: ColorArea, + displayName: 'ColorArea', + }); + + it('stamps the marker pair on the root', () => { + const { root } = renderColorArea(); + + expect(root).toHaveClass('fui-color-area'); + expect(root).toHaveClass('group/fui-color-area'); + expect(root.classList[0]).toBe('fui-color-area'); + expect(colorAreaClassNames.root).toBe('fui-color-area group/fui-color-area'); + }); + + it('gives both range inputs the same module class and wraps them in the thumb', () => { + const { inputX, inputY, root, thumb } = renderColorArea(); + + expect(root.children).toHaveLength(1); + expect(thumb.children).toHaveLength(2); + expect(thumb).toHaveClass(styles.thumb); + expect(inputX).toHaveClass(styles.input); + expect(inputY).toHaveClass(styles.input); + expect(inputX.type).toBe('range'); + expect(inputY.type).toBe('range'); + expect(root).toHaveClass(styles.root); + }); + + it('stamps data-shape on the root alone, defaulting to rounded', () => { + expect(renderColorArea().root.getAttribute('data-shape')).toBe('rounded'); + + (['rounded', 'square'] as const).forEach(shape => { + const { inputX, inputY, root, thumb } = renderColorArea({ shape }); + + expect(root.getAttribute('data-shape')).toBe(shape); + [inputX, inputY, thumb].forEach(slot => expect(slot.hasAttribute('data-shape')).toBe(false)); + }); + }); + + it('takes the shape from an enclosing ColorPicker, its own prop winning', () => { + const rootOf = (element: React.ReactElement) => + (render(element).container.firstElementChild as HTMLElement).firstElementChild as HTMLElement; + + expect( + rootOf( + + + , + ).getAttribute('data-shape'), + ).toBe('square'); + + expect( + rootOf( + + + , + ).getAttribute('data-shape'), + ).toBe('rounded'); + + expect( + rootOf( + + + , + ).getAttribute('data-shape'), + ).toBe('rounded'); + }); + + it('adds no stamp the headless hook did not, and keeps the look prop off the DOM', () => { + const { root } = renderColorArea({ shape: 'square' }); + + expect(root.hasAttribute('data-channel')).toBe(false); + expect(root.hasAttribute('data-orientation')).toBe(false); + expect(root.hasAttribute('shape')).toBe(false); + }); + + it('leaves the root inline custom properties exactly as the headless hook wrote them', () => { + const { root } = renderColorArea({ color: teal, style: { marginTop: 3 } }); + const style = root.getAttribute('style')!; + + expect(root.style.getPropertyValue('--fui-AreaX--progress')).toBe('50%'); + expect(root.style.getPropertyValue('--fui-AreaY--progress')).toBe('60%'); + expect(root.style.getPropertyValue('--fui-Area__thumb--color')).not.toBe(''); + expect(root.style.getPropertyValue('--fui-Area--main-color')).not.toBe(''); + expect(root.style.marginTop).toBe('3px'); + + // ColorArea's hook spreads {...state.root.style, ...rootVariables} — the opposite order from + // the two sliders — so the consumer style lands FIRST and the hook's values win. + expect(style.indexOf('margin-top')).toBeLessThan(style.indexOf('--fui-AreaX--progress')); + }); + + it('reports a colour change from either input', () => { + const onChange = jest.fn(); + const { inputX, inputY } = renderColorArea({ color: teal, onChange }); + + fireEvent.change(inputX, { target: { value: '80' } }); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][1].color).toEqual(expect.objectContaining({ s: 0.8 })); + + fireEvent.change(inputY, { target: { value: '20' } }); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange.mock.calls[1][1].color).toEqual(expect.objectContaining({ v: 0.2 })); + }); + + it('keeps a consumer className on every slot exactly once', () => { + const { inputX, inputY, root, thumb } = renderColorArea({ + className: 'consumer', + inputX: { className: 'x' }, + inputY: { className: 'y' }, + thumb: { className: 't' }, + }); + + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(thumb).toHaveClass('t'); + expect(inputX).toHaveClass('x'); + expect(inputY).toHaveClass('y'); + }); + + it('lands native props on the root', () => { + const ref = React.createRef(); + const { root } = renderColorArea({ + 'data-testid': 'probe', + id: 'area', + ref, + } as React.ComponentProps); + + expect(root.id).toBe('area'); + expect(root.getAttribute('data-testid')).toBe('probe'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { inputX: 'input', inputY: 'input', root: 'div', thumb: 'div' }, + inputX: { className: 'x' }, + inputY: { className: 'y' }, + root: { className: 'consumer', style: { '--fui-AreaX--progress': '50%' } }, + shape: 'square', + thumb: { className: 'consumer-thumb' }, + } as unknown as ColorAreaState; + + const styled = useColorAreaStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.thumb).not.toBe(state.thumb); + expect(styled.inputX).not.toBe(state.inputX); + expect(styled.inputY).not.toBe(state.inputY); + + expect(state.root.className).toBe('consumer'); + expect(state.thumb.className).toBe('consumer-thumb'); + expect(state.inputX.className).toBe('x'); + expect(state.inputY.className).toBe('y'); + expect('data-shape' in state.root).toBe(false); + + expect(styled.root.style).toBe(state.root.style); + expect(styled.root.className).toContain(colorAreaClassNames.root); + expect(styled.inputX.className).toContain('x'); + expect(styled.inputY.className).toContain('y'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.tsx new file mode 100644 index 0000000000000..42e683af71327 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.tsx @@ -0,0 +1,35 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderColorArea, + useColorArea, + useColorPickerContextValue, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorAreaProps } from './ColorArea.types'; +import { useColorAreaStyles } from './useColorAreaStyles'; + +/** + * A ColorArea picks saturation and value together. Windmod ColorArea: the headless area decorated + * with the Fluent visual contract (Tailwind v4 + CSS Modules). + * + * The per-instance colour and thumb position are inline custom properties the headless base hook + * writes; the windmod layer never writes to, re-orders or merges into that style — the module + * reads them. + */ +export const ColorArea: ForwardRefComponent = React.forwardRef(({ shape: shapeProp, ...rest }, ref) => { + const shapeFromContext = useColorPickerContextValue(ctx => ctx.shape); + + return renderColorArea( + useColorAreaStyles({ + ...useColorArea(rest, ref), + // The context's own default value applies only with no picker at all, so a picker that leaves + // `shape` unset publishes undefined and the trailing fallback is what resolves it. + shape: shapeProp ?? shapeFromContext ?? 'rounded', + }), + ); +}); + +ColorArea.displayName = 'ColorArea'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.types.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.types.ts new file mode 100644 index 0000000000000..1974e3f2ea91e --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/ColorArea.types.ts @@ -0,0 +1,20 @@ +import type { + ColorAreaProps as ColorAreaHeadlessProps, + ColorAreaState as ColorAreaHeadlessState, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorPickerShape } from '../ColorPicker/ColorPicker.types'; + +export type { ColorAreaSlots } from '@fluentui/react-headless-components-preview/color-picker'; + +/** + * Windmod ColorArea props: the headless area plus the look prop the headless surface deliberately + * omits. + */ +export type ColorAreaProps = ColorAreaHeadlessProps & { + /** @default the enclosing ColorPicker's shape, then 'rounded' */ + shape?: ColorPickerShape; +}; + +/** Windmod ColorArea state: headless state plus the resolved look prop. */ +export type ColorAreaState = ColorAreaHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/index.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/index.ts new file mode 100644 index 0000000000000..2f61db6e92802 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/index.ts @@ -0,0 +1,3 @@ +export { ColorArea } from './ColorArea'; +export { colorAreaClassNames, useColorAreaStyles } from './useColorAreaStyles'; +export type { ColorAreaProps, ColorAreaSlots, ColorAreaState } from './ColorArea.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorArea/useColorAreaStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/useColorAreaStyles.ts new file mode 100644 index 0000000000000..0fb878f7db8f9 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorArea/useColorAreaStyles.ts @@ -0,0 +1,39 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { ColorAreaState } from './ColorArea.types'; + +import styles from './ColorArea.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const colorAreaClassNames: { root: string } = { + root: componentMarkers('color-area'), +}; + +type ColorAreaRootDataAttributes = { + 'data-shape'?: ColorAreaState['shape']; +}; + +/** + * Applies the visual contract, returning new state. The root's inline custom properties + * (--fui-AreaX--progress / --fui-AreaY--progress and the two colours) are computed and written by + * the headless hook; they pass through by spread and are never read or rewritten here. The headless + * hook stamps nothing, so `data-shape` is the only attribute, and it is style-only. Both range + * inputs take the same module class, matching @fluentui/react-color-picker. + */ +export const useColorAreaStyles = (state: ColorAreaState): ColorAreaState => { + const root: ColorAreaState['root'] & ColorAreaRootDataAttributes = { + ...state.root, + 'data-shape': state.shape, + className: clsx(colorAreaClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + thumb: slotClasses(state.thumb, styles.thumb), + inputX: slotClasses(state.inputX, styles.input), + inputY: slotClasses(state.inputY, styles.input), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.module.css b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.module.css new file mode 100644 index 0000000000000..6746e00e8fad4 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.module.css @@ -0,0 +1,7 @@ +@reference '#theme'; + +@layer fui.components.l1 { + .root { + @apply flex flex-col gap-vertical-xs; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.test.tsx new file mode 100644 index 0000000000000..67b79af5da484 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.test.tsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { useColorPickerContextValue } from '@fluentui/react-headless-components-preview/color-picker'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { ColorArea } from '../ColorArea/ColorArea'; +import { ColorSlider } from '../ColorSlider/ColorSlider'; +import { ColorPicker } from './ColorPicker'; +import type { ColorPickerState } from './ColorPicker.types'; +import { colorPickerClassNames, useColorPickerStyles } from './useColorPickerStyles'; + +import styles from './ColorPicker.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/color-picker', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/color-picker'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useColorPicker: (...args: Parameters) => + deepFreezeState(actual.useColorPicker(...args)), + }; +}); + +const teal = { h: 180, s: 0.5, v: 0.6 }; + +const renderColorPicker = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return { root: container.firstElementChild as HTMLElement }; +}; + +/** Reports what a child actually reads off the picker context, which has no DOM consequence. */ +const ShapeProbe = ({ onRead }: { onRead: (shape: unknown) => void }): null => { + onRead(useColorPickerContextValue(ctx => ctx.shape)); + + return null; +}; + +describe('ColorPicker', () => { + isConformant({ + Component: ColorPicker, + displayName: 'ColorPicker', + }); + + it('stamps the marker pair on the root', () => { + const { root } = renderColorPicker(); + + expect(root).toHaveClass('fui-color-picker'); + expect(root).toHaveClass('group/fui-color-picker'); + expect(root.classList[0]).toBe('fui-color-picker'); + expect(colorPickerClassNames.root).toBe('fui-color-picker group/fui-color-picker'); + expect(root).toHaveClass(styles.root); + }); + + it('stamps nothing on the root', () => { + const { root } = renderColorPicker({ shape: 'square' }); + + expect(root.hasAttribute('data-shape')).toBe(false); + expect(root.hasAttribute('shape')).toBe(false); + }); + + it('publishes its shape to the context', () => { + const reads: unknown[] = []; + + render( + + reads.push(shape)} /> + , + ); + + expect(reads).toContain('square'); + }); + + it('publishes undefined when no shape is set, leaving the fallback to the controls', () => { + const reads: unknown[] = []; + + render( + + reads.push(shape)} /> + , + ); + + expect(reads.length).toBeGreaterThan(0); + reads.forEach(shape => expect(shape).toBeUndefined()); + }); + + it('reaches every kind of control with its shape', () => { + const { container } = render( + + + + , + ); + const [slider, area] = Array.from((container.firstElementChild as HTMLElement).children) as HTMLElement[]; + + expect(slider.getAttribute('data-shape')).toBe('square'); + expect(area.getAttribute('data-shape')).toBe('square'); + }); + + it('reaches its controls with its colour', () => { + const { container } = render( + + + , + ); + const inherited = (container.firstElementChild as HTMLElement).firstElementChild as HTMLElement; + + const direct = render().container.firstElementChild as HTMLElement; + + expect(inherited.style.getPropertyValue('--fui-Slider__rail--color')).toBe( + direct.style.getPropertyValue('--fui-Slider__rail--color'), + ); + expect(inherited.style.getPropertyValue('--fui-Slider--progress')).toBe( + direct.style.getPropertyValue('--fui-Slider--progress'), + ); + expect(inherited.style.getPropertyValue('--fui-Slider--progress')).toBe('50%'); + }); + + it('carries a control change back out through onColorChange', () => { + const onColorChange = jest.fn(); + const { container } = render( + + + , + ); + const input = (container.firstElementChild as HTMLElement).firstElementChild!.firstElementChild as HTMLInputElement; + + fireEvent.change(input, { target: { value: '240' } }); + + expect(onColorChange).toHaveBeenCalledTimes(1); + expect(onColorChange.mock.calls[0][1].color).toEqual(expect.objectContaining({ h: 240 })); + }); + + it('renders its children inside the root', () => { + const { root } = renderColorPicker({ children: }); + + expect(root.querySelector('[data-testid="child"]')).not.toBeNull(); + }); + + it('keeps a consumer className on the root exactly once', () => { + const { root } = renderColorPicker({ className: 'consumer' }); + + expect(classOccurrences(root, 'consumer')).toBe(1); + }); + + it('lands native props on the root', () => { + const ref = React.createRef(); + const { root } = renderColorPicker({ + 'data-testid': 'probe', + id: 'picker', + ref, + } as React.ComponentProps); + + expect(root.id).toBe('picker'); + expect(root.getAttribute('data-testid')).toBe('probe'); + expect(ref.current).toBe(root); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'div' }, + root: { className: 'consumer', style: { marginTop: 3 } }, + shape: 'square', + } as unknown as ColorPickerState; + + const styled = useColorPickerStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(state.root.className).toBe('consumer'); + expect(styled.root.style).toBe(state.root.style); + expect(styled.root.className).toContain('consumer'); + expect(styled.root.className).toContain(colorPickerClassNames.root); + expect(styled.shape).toBe('square'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.tsx new file mode 100644 index 0000000000000..8afc0952d6b5a --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.tsx @@ -0,0 +1,35 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderColorPicker, + useColorPicker, + useColorPickerContextValues, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorPickerProps, ColorPickerState } from './ColorPicker.types'; +import { useColorPickerStyles } from './useColorPickerStyles'; + +/** + * A ColorPicker coordinates colour controls and reports their changes. Windmod ColorPicker: the + * headless picker decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + */ +export const ColorPicker: ForwardRefComponent = React.forwardRef( + // Look props belong to windmod — the headless hook neither accepts nor resolves them. + // `shape` keeps no default: an unset one must reach the context as `undefined` so each control + // resolves 'rounded' for itself, matching @fluentui/react-color-picker's styled picker. + ({ shape, ...rest }, ref) => { + // The headless state omits the look prop, so the context values must be built from the state + // that carries it — otherwise the controls read `undefined` even when a shape was set. + const state: ColorPickerState = { + ...useColorPicker(rest, ref), + shape, + }; + const contextValues = useColorPickerContextValues(state); + + return renderColorPicker(useColorPickerStyles(state), contextValues); + }, +); + +ColorPicker.displayName = 'ColorPicker'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.types.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.types.ts new file mode 100644 index 0000000000000..c3fa480846085 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/ColorPicker.types.ts @@ -0,0 +1,24 @@ +import type { + ColorPickerProps as ColorPickerHeadlessProps, + ColorPickerState as ColorPickerHeadlessState, +} from '@fluentui/react-headless-components-preview/color-picker'; + +export type { ColorPickerSlots } from '@fluentui/react-headless-components-preview/color-picker'; + +/** Shape of the controls a ColorPicker coordinates — it reaches them through the context. */ +export type ColorPickerShape = 'rounded' | 'square'; + +/** + * Windmod ColorPicker props: the headless picker plus the look prop the headless surface + * deliberately omits (it exists purely to select styles, on the controls rather than here). + */ +export type ColorPickerProps = ColorPickerHeadlessProps & { + /** @default undefined — each control resolves 'rounded' for itself. */ + shape?: ColorPickerShape; +}; + +/** + * Windmod ColorPicker state: headless state plus the look prop. It is deliberately not + * `Required` — an unset `shape` must reach the context as `undefined`. + */ +export type ColorPickerState = ColorPickerHeadlessState & Pick; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/index.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/index.ts new file mode 100644 index 0000000000000..5c722abbe67a6 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/index.ts @@ -0,0 +1,3 @@ +export { ColorPicker } from './ColorPicker'; +export { colorPickerClassNames, useColorPickerStyles } from './useColorPickerStyles'; +export type { ColorPickerProps, ColorPickerShape, ColorPickerSlots, ColorPickerState } from './ColorPicker.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/useColorPickerStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/useColorPickerStyles.ts new file mode 100644 index 0000000000000..25c5eaf565e45 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorPicker/useColorPickerStyles.ts @@ -0,0 +1,23 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import type { ColorPickerState } from './ColorPicker.types'; + +import styles from './ColorPicker.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const colorPickerClassNames: { root: string } = { + root: componentMarkers('color-picker'), +}; + +/** + * Applies the visual contract, returning new state. The picker stamps nothing: its module has no + * state-dependent rule, and `shape` reaches the controls through the colorPicker context. + */ +export const useColorPickerStyles = (state: ColorPickerState): ColorPickerState => ({ + ...state, + root: { + ...state.root, + className: clsx(colorPickerClassNames.root, styles.root, state.root.className), + }, +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.module.css b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.module.css new file mode 100644 index 0000000000000..b3118debd8133 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.module.css @@ -0,0 +1,90 @@ +@reference '#theme'; + +@layer fui.components.l1 { + /* The `--fui-Slider--*` and `--fui-Slider__*` custom properties are written by the headless + hook into the root's inline style and are read only here. The kebab-case ones are this + file's own. The resting look is the horizontal one, so every block that sets a main-axis + property carries a `vertical` counterpart resetting it. */ + .root { + @apply relative inline-grid min-h-32 min-w-200 touch-none grid-cols-[1fr_100%_1fr] grid-rows-[1fr_var(--fui-color-slider-thumb-size)_1fr] items-center justify-items-center; + + --fui-color-slider-thumb-size: calc(20px * var(--base-scale)); + --fui-color-slider-rail-size: calc(20px * var(--base-scale)); + + @variant vertical { + @apply min-h-280 min-w-auto grid-cols-[1fr_var(--fui-color-slider-thumb-size)_1fr] grid-rows-[1fr_100%_1fr]; + } + } + + /* The channel gradients stay raw declarations: a Tailwind gradient utility emits the + --tw-gradient-* machinery and its own interpolation space instead of the literal + linear-gradient() Griffel compiles. The two literal channel gradients name a physical + axis, so they need an explicit rtl counterpart; the hue gradient reads the hook's + direction custom property, which is already direction-aware. */ + .rail { + @apply pointer-events-none relative col-start-2 col-end-2 row-start-2 row-end-2 h-(--fui-color-slider-rail-size) w-full rounded-medium outline-thin outline-transparent-stroke forced-color-adjust-none outline-solid; + + background-image: linear-gradient(var(--fui-Slider--direction), red, fuchsia, blue, aqua, lime, yellow, red); + + @variant group-channel-saturation/fui-color-slider { + background-image: linear-gradient(to right, #808080, var(--fui-Slider__rail--color)); + + @variant rtl { + background-image: linear-gradient(to left, #808080, var(--fui-Slider__rail--color)); + } + } + + @variant group-channel-value/fui-color-slider { + background-image: linear-gradient(to right, #000, var(--fui-Slider__rail--color)); + + @variant rtl { + background-image: linear-gradient(to left, #000, var(--fui-Slider__rail--color)); + } + } + + @variant group-shape-square/fui-color-slider { + @apply rounded-none; + } + + @variant group-vertical/fui-color-slider { + @apply h-full w-(--fui-color-slider-rail-size); + } + } + + .thumb { + @apply pointer-events-none absolute start-(--fui-Slider--progress) col-start-2 col-end-2 row-start-2 row-end-2 size-(--fui-color-slider-thumb-size) -translate-x-1/2 rounded-circular border-thin border-solid border-neutral-foreground-4 bg-(--fui-Slider__thumb--color) shadow-4 forced-color-adjust-none outline-none; + + @variant before { + @apply absolute inset-0 box-border rounded-circular border-thick border-solid border-neutral-background-1; + } + + @variant rtl { + @apply translate-x-1/2; + } + + @variant group-vertical/fui-color-slider { + @apply start-auto bottom-(--fui-Slider--progress) translate-x-0 translate-y-1/2; + } + + /* Keyed off the input's own focus, matching Griffel's sibling selector: the range input is + the only focusable element in the tree and it always precedes the thumb. */ + @variant peer-focus-visible/fui-color-slider { + @apply rounded-circular border-thick border-solid border-stroke-focus-2 outline-thick outline-transparent-stroke outline-solid; + } + } + + .input { + @apply col-start-1 col-end-[-1] row-start-1 row-end-[-1] m-0 h-(--fui-color-slider-thumb-size) w-full cursor-pointer p-0 opacity-0; + + @variant group-vertical/fui-color-slider { + @apply h-full w-(--fui-color-slider-thumb-size); + writing-mode: vertical-lr; + + direction: rtl; + + @variant group-rtl/fui-color-slider { + direction: ltr; + } + } + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.test.tsx new file mode 100644 index 0000000000000..577547314350b --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.test.tsx @@ -0,0 +1,248 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { classOccurrences } from '../../testing/classOccurrences'; +import { isConformant } from '../../testing/isConformant'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; +import { ColorSlider } from './ColorSlider'; +import type { ColorSliderState } from './ColorSlider.types'; +import { colorSliderClassNames, useColorSliderStyles } from './useColorSliderStyles'; + +import styles from './ColorSlider.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/color-picker', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/color-picker'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useColorSlider: (...args: Parameters) => + deepFreezeState(actual.useColorSlider(...args)), + }; +}); + +const teal = { h: 180, s: 0.5, v: 0.6 }; + +// `input` is the primary slot, so every native prop — data-testid included — lands on the ; +// the root is only reachable through the container. Render order is input, rail, thumb. +const slotsOf = (root: HTMLElement) => { + const [input, rail, thumb] = Array.from(root.children) as HTMLElement[]; + + return { input: input as HTMLInputElement, rail, root, thumb }; +}; + +const renderColorSlider = (props: React.ComponentProps = {}) => { + const { container } = render(); + + return slotsOf(container.firstElementChild as HTMLElement); +}; + +// A slider inside a picker is found by position, never by class — see testing/classOccurrences.ts. +const sliderInPicker = (picker: HTMLElement) => slotsOf(picker.firstElementChild as HTMLElement); + +describe('ColorSlider', () => { + isConformant({ + Component: ColorSlider, + displayName: 'ColorSlider', + primarySlot: 'input', + }); + + it('stamps the marker pair on the root', () => { + const { root } = renderColorSlider(); + + expect(root).toHaveClass('fui-color-slider'); + expect(root).toHaveClass('group/fui-color-slider'); + expect(root.classList[0]).toBe('fui-color-slider'); + expect(colorSliderClassNames.root).toBe('fui-color-slider group/fui-color-slider'); + }); + + it('applies one module class per slot', () => { + const { input, rail, root, thumb } = renderColorSlider(); + + expect(root.children).toHaveLength(3); + expect(input.tagName).toBe('INPUT'); + expect(input.type).toBe('range'); + + const slots = { input, rail, root, thumb }; + const classes = { input: styles.input, rail: styles.rail, root: styles.root, thumb: styles.thumb }; + + (Object.keys(slots) as Array).forEach(slot => { + expect(slots[slot]).toHaveClass(classes[slot]); + (Object.keys(classes) as Array) + .filter(other => other !== slot) + .forEach(other => expect(slots[slot]).not.toHaveClass(classes[other])); + }); + }); + + it('carries the peer marker on the input alone', () => { + const { input, rail, root, thumb } = renderColorSlider(); + + expect(input).toHaveClass('peer/fui-color-slider'); + expect(input.classList[0]).toBe(styles.input); + + [rail, root, thumb].forEach(slot => expect(slot).not.toHaveClass('peer/fui-color-slider')); + }); + + it('stamps data-shape on the root alone, defaulting to rounded', () => { + expect(renderColorSlider().root.getAttribute('data-shape')).toBe('rounded'); + + (['rounded', 'square'] as const).forEach(shape => { + const { input, rail, root, thumb } = renderColorSlider({ shape }); + + expect(root.getAttribute('data-shape')).toBe(shape); + [input, rail, thumb].forEach(slot => expect(slot.hasAttribute('data-shape')).toBe(false)); + }); + }); + + it('takes the shape from an enclosing ColorPicker', () => { + const { container } = render( + + + , + ); + + expect(sliderInPicker(container.firstElementChild as HTMLElement).root.getAttribute('data-shape')).toBe('square'); + }); + + it('lets its own shape prop win over the picker context', () => { + const { container } = render( + + + , + ); + + expect(sliderInPicker(container.firstElementChild as HTMLElement).root.getAttribute('data-shape')).toBe('rounded'); + }); + + it('falls back to rounded inside a ColorPicker that sets no shape', () => { + const { container } = render( + + + , + ); + + expect(sliderInPicker(container.firstElementChild as HTMLElement).root.getAttribute('data-shape')).toBe('rounded'); + }); + + it('keeps the look prop off the range input', () => { + (['rounded', 'square'] as const).forEach(shape => { + expect(renderColorSlider({ shape }).input.hasAttribute('shape')).toBe(false); + }); + }); + + it('leaves the headless stamps and the native range contract alone', () => { + const { input, root } = renderColorSlider(); + + expect(root.getAttribute('data-channel')).toBe('hue'); + expect(root.getAttribute('data-orientation')).toBe('horizontal'); + expect(root.getAttribute('role')).toBe('group'); + expect(input.getAttribute('min')).toBe('0'); + expect(input.getAttribute('max')).toBe('360'); + expect(input.getAttribute('tabindex')).toBe('0'); + expect(input.getAttribute('aria-orientation')).toBe('horizontal'); + + const vertical = renderColorSlider({ vertical: true }); + + expect(vertical.root.getAttribute('data-orientation')).toBe('vertical'); + expect(vertical.input.getAttribute('aria-orientation')).toBe('vertical'); + + const saturation = renderColorSlider({ channel: 'saturation' }); + + expect(saturation.root.getAttribute('data-channel')).toBe('saturation'); + expect(saturation.input.getAttribute('max')).toBe('100'); + }); + + it('leaves the root inline custom properties exactly as the headless hook wrote them', () => { + const { root } = renderColorSlider({ channel: 'saturation', color: teal, style: { marginTop: 3 } }); + const style = root.getAttribute('style')!; + + expect(root.style.getPropertyValue('--fui-Slider--direction')).toBe('-90deg'); + expect(root.style.getPropertyValue('--fui-Slider--progress')).toBe('50%'); + expect(root.style.getPropertyValue('--fui-Slider__thumb--color')).not.toBe(''); + expect(root.style.getPropertyValue('--fui-Slider__rail--color')).not.toBe(''); + expect(root.style.marginTop).toBe('3px'); + + // The hook spreads {...rootVariables, ...state.root.style}, so a consumer style wins and + // lands last. Order is only visible in the raw attribute. + expect(style.indexOf('--fui-Slider--direction')).toBeLessThan(style.indexOf('margin-top')); + }); + + it('reports a colour change from the input', () => { + const onChange = jest.fn(); + const { input } = renderColorSlider({ color: teal, onChange }); + + fireEvent.change(input, { target: { value: '240' } }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][1].color).toEqual(expect.objectContaining({ h: 240 })); + }); + + it('leaves a controlled colour where it was put', () => { + const { input } = renderColorSlider({ color: teal }); + + expect(input.value).toBe('180'); + fireEvent.change(input, { target: { value: '240' } }); + expect(input.value).toBe('180'); + }); + + it('keeps a consumer className on every slot exactly once', () => { + const { input, rail, root, thumb } = renderColorSlider({ + className: 'consumer', + input: { className: 'i' }, + rail: { className: 'r' }, + thumb: { className: 't' }, + }); + + expect(classOccurrences(root, 'consumer')).toBe(1); + expect(rail).toHaveClass('r'); + expect(thumb).toHaveClass('t'); + expect(input).toHaveClass('i'); + }); + + it('lands native props on the primary slot', () => { + const ref = React.createRef(); + const { input, root } = renderColorSlider({ + 'aria-label': 'hue', + 'data-testid': 'probe', + id: 'field', + ref, + } as React.ComponentProps); + + expect(input.id).toBe('field'); + expect(input.getAttribute('data-testid')).toBe('probe'); + expect(input.getAttribute('aria-label')).toBe('hue'); + expect(ref.current).toBe(input); + expect(root.hasAttribute('data-testid')).toBe(false); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { input: 'input', rail: 'div', root: 'div', thumb: 'div' }, + input: { className: 'native' }, + rail: { className: 'consumer-rail' }, + root: { className: 'consumer', style: { '--fui-Slider--progress': '50%' } }, + shape: 'square', + thumb: { className: 'consumer-thumb' }, + } as unknown as ColorSliderState; + + const styled = useColorSliderStyles(state); + + expect(styled).not.toBe(state); + expect(styled.root).not.toBe(state.root); + expect(styled.rail).not.toBe(state.rail); + expect(styled.thumb).not.toBe(state.thumb); + expect(styled.input).not.toBe(state.input); + + expect(state.root.className).toBe('consumer'); + expect(state.rail.className).toBe('consumer-rail'); + expect(state.thumb.className).toBe('consumer-thumb'); + expect(state.input.className).toBe('native'); + expect('data-shape' in state.root).toBe(false); + + expect(styled.root.style).toBe(state.root.style); + expect(styled.root.className).toContain('consumer'); + expect(styled.root.className).toContain(colorSliderClassNames.root); + expect(styled.input.className).toContain('native'); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.tsx new file mode 100644 index 0000000000000..ab68ee2b87f32 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.tsx @@ -0,0 +1,35 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderColorSlider, + useColorPickerContextValue, + useColorSlider, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorSliderProps } from './ColorSlider.types'; +import { useColorSliderStyles } from './useColorSliderStyles'; + +/** + * A ColorSlider picks one channel of a colour. Windmod ColorSlider: the headless slider decorated + * with the Fluent visual contract (Tailwind v4 + CSS Modules). + * + * The per-instance colour and geometry are inline custom properties the headless base hook writes — + * see `ColorArea` for the rule the windmod layer follows around them. + */ +export const ColorSlider: ForwardRefComponent = React.forwardRef( + ({ shape: shapeProp, ...rest }, ref) => { + const shapeFromContext = useColorPickerContextValue(ctx => ctx.shape); + + return renderColorSlider( + useColorSliderStyles({ + ...useColorSlider(rest, ref), + // The trailing fallback, not the context default, is what resolves `shape` — see `ColorArea`. + shape: shapeProp ?? shapeFromContext ?? 'rounded', + }), + ); + }, +); + +ColorSlider.displayName = 'ColorSlider'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.types.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.types.ts new file mode 100644 index 0000000000000..c8844bb33cd63 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/ColorSlider.types.ts @@ -0,0 +1,20 @@ +import type { + ColorSliderProps as ColorSliderHeadlessProps, + ColorSliderState as ColorSliderHeadlessState, +} from '@fluentui/react-headless-components-preview/color-picker'; + +import type { ColorPickerShape } from '../ColorPicker/ColorPicker.types'; + +export type { ColorSliderSlots } from '@fluentui/react-headless-components-preview/color-picker'; + +/** + * Windmod ColorSlider props: the headless slider plus the look prop the headless surface + * deliberately omits. + */ +export type ColorSliderProps = ColorSliderHeadlessProps & { + /** @default the enclosing ColorPicker's shape, then 'rounded' */ + shape?: ColorPickerShape; +}; + +/** Windmod ColorSlider state: headless state plus the resolved look prop. */ +export type ColorSliderState = ColorSliderHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/index.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/index.ts new file mode 100644 index 0000000000000..8ceb9099b2824 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/index.ts @@ -0,0 +1,3 @@ +export { ColorSlider } from './ColorSlider'; +export { colorSliderClassNames, useColorSliderStyles } from './useColorSliderStyles'; +export type { ColorSliderProps, ColorSliderSlots, ColorSliderState } from './ColorSlider.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/useColorSliderStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/useColorSliderStyles.ts new file mode 100644 index 0000000000000..e418a75ccf7ab --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSlider/useColorSliderStyles.ts @@ -0,0 +1,40 @@ +import { clsx } from 'clsx'; + +import { componentMarkers, peerMarker } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { ColorSliderState } from './ColorSlider.types'; + +import styles from './ColorSlider.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const colorSliderClassNames: { root: string } = { + root: componentMarkers('color-slider'), +}; + +type ColorSliderRootDataAttributes = { + 'data-shape'?: ColorSliderState['shape']; +}; + +/** + * Applies the visual contract, returning new state. The root's inline custom properties + * (--fui-Slider--direction / --progress and the two colours) are computed and written by the + * headless hook; they pass through by spread and are never read or rewritten here. The headless + * hook also stamps data-channel and data-orientation, which the module selects on; `data-shape` + * is style-only. The thumb's focus ring keys off the input's own :focus-visible, so the input + * carries the peer marker. + */ +export const useColorSliderStyles = (state: ColorSliderState): ColorSliderState => { + const root: ColorSliderState['root'] & ColorSliderRootDataAttributes = { + ...state.root, + 'data-shape': state.shape, + className: clsx(colorSliderClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + rail: slotClasses(state.rail, styles.rail), + thumb: slotClasses(state.thumb, styles.thumb), + input: slotClasses(state.input, styles.input, peerMarker('color-slider')), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.module.css b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.module.css new file mode 100644 index 0000000000000..06d48a0a5d4e7 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.module.css @@ -0,0 +1,178 @@ +@reference '#theme'; + +@layer fui.components.l1 { + /* Block order inside .root is load-bearing: it reproduces the resolution order Griffel gets + from its mergeClasses argument order (reset, size, shape, selected, selectedSmall, disabled) + for same-key conflicts and from styleBucketOrdering for the rest — rest declarations before + hover ones, forced-colors last. Every catalog variant is :where()-flat, so source order is + the only thing deciding between these blocks. */ + .root { + @apply box-border inline-flex size-28 shrink-0 items-center justify-center overflow-hidden rounded-none bg-(--fui-SwatchPicker--color) p-0 outline-none; + + /* --fui-SwatchPicker--color and --fui-SwatchPicker--borderColor are per-instance custom + properties the headless base hook writes as an inline style. */ + border: var(--stroke-width-thin) solid var(--fui-SwatchPicker--borderColor, var(--color-transparent-stroke)); + + @variant hover { + @apply cursor-pointer border-none border-current; + + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-brand-stroke-1), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + + @variant hover-active { + @apply border-none border-current; + + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-compound-brand-stroke-pressed), + inset 0 0 0 var(--stroke-width-thickest) var(--color-stroke-focus-1); + } + + @variant focus-visible { + @apply border-none border-current; + + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-stroke-focus-2), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + + @variant size-extra-small { + @apply size-20; + + @variant hover { + box-shadow: + inset 0 0 0 var(--stroke-width-thin) var(--color-brand-stroke-1), + inset 0 0 0 var(--stroke-width-thick) var(--color-stroke-focus-1); + } + + @variant hover-active { + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-compound-brand-stroke-pressed), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + } + + @variant size-small { + @apply size-24; + + @variant hover-active { + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-compound-brand-stroke-pressed), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + } + + @variant size-large { + @apply size-32; + } + + @variant shape-rounded { + @apply rounded-medium; + } + + @variant shape-circular { + @apply rounded-circular; + } + + @variant selected { + @apply border-none border-current; + + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-brand-stroke-1), + inset 0 0 0 5px var(--color-stroke-focus-1); + + @variant size-extra-small, size-small { + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-brand-stroke-1), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + + /* The selected focus ring is size-independent: Griffel gives its small selected branch no + focus indicator, so this block must outrank the size branch above at every size. */ + @variant focus-visible { + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-2), + inset 0 0 0 5px var(--color-stroke-focus-1); + } + + @variant hover { + box-shadow: + inset 0 0 0 var(--stroke-width-thickest) var(--color-compound-brand-stroke-hover), + inset 0 0 0 6px var(--color-stroke-focus-1); + } + + @variant hover-active { + box-shadow: + inset 0 0 0 var(--stroke-width-thickest) var(--color-compound-brand-stroke-pressed), + inset 0 0 0 7px var(--color-stroke-focus-1); + } + + @variant size-extra-small, size-small { + @variant hover { + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-compound-brand-stroke-hover), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + + @variant hover-active { + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-compound-brand-stroke-pressed), + inset 0 0 0 var(--stroke-width-thickest) var(--color-stroke-focus-1); + } + } + } + + @variant disabled { + @variant hover { + @apply cursor-not-allowed; + + box-shadow: none; + } + } + + @variant forced-colors { + @apply forced-color-adjust-none; + + @variant selected { + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-brand-stroke-2-pressed), + inset 0 0 0 5px var(--color-stroke-focus-1); + } + + @variant hover { + box-shadow: + inset 0 0 0 var(--stroke-width-thick) var(--color-brand-stroke-2-hover), + inset 0 0 0 var(--stroke-width-thicker) var(--color-stroke-focus-1); + } + + @variant disabled { + @variant hover { + box-shadow: none; + } + } + + @variant hover-active { + box-shadow: + inset 0 0 0 var(--stroke-width-thicker) var(--color-brand-stroke-2-pressed), + inset 0 0 0 var(--stroke-width-thickest) var(--color-stroke-focus-1); + } + } + } + + .icon { + @apply absolute flex self-center text-icon-20 leading-[calc(20/20)]; + + @variant group-size-extra-small/fui-color-swatch, group-size-small/fui-color-swatch { + @apply text-icon-16 leading-[calc(20/16)]; + } + + @variant group-size-large/fui-color-swatch { + @apply text-icon-24 leading-[calc(20/24)]; + } + } + + .disabled-icon { + @apply text-neutral-foreground-inverted drop-shadow-[0_1px_1px_rgb(0_0_0)]; + } +} diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.test.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.test.tsx new file mode 100644 index 0000000000000..8067cdd2f886d --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.test.tsx @@ -0,0 +1,297 @@ +import * as React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { SwatchPicker as HeadlessSwatchPicker } from '@fluentui/react-headless-components-preview/swatch-picker'; + +import { isConformant } from '../../testing/isConformant'; +import { SwatchPicker } from '../SwatchPicker/SwatchPicker'; +import { ColorSwatch } from './ColorSwatch'; +import type { ColorSwatchState } from './ColorSwatch.types'; +import { colorSwatchClassNames, useColorSwatchStyles } from './useColorSwatchStyles'; + +import styles from './ColorSwatch.module.css'; + +// Frozen-state guard — see testing/freezeState.ts. +jest.mock('@fluentui/react-headless-components-preview/swatch-picker', () => { + const actual = jest.requireActual('@fluentui/react-headless-components-preview/swatch-picker'); + const { deepFreezeState } = require('../../testing/freezeState'); + + return { + ...actual, + useColorSwatch: (...args: Parameters) => + deepFreezeState(actual.useColorSwatch(...args)), + }; +}); + +const sizes = ['extra-small', 'small', 'medium', 'large'] as const; +const shapes = ['rounded', 'square', 'circular'] as const; + +const stampsOf = (root: object): Record => root as Record; + +describe('ColorSwatch', () => { + isConformant({ + Component: ColorSwatch, + displayName: 'ColorSwatch', + requiredProps: { value: 'a', color: '#f00' }, + }); + + it('stamps its marker pair, slash-free class first', () => { + const { getByTestId } = render(); + + const root = getByTestId('root'); + + expect(root).toHaveClass('fui-color-swatch'); + expect(root).toHaveClass('group/fui-color-swatch'); + expect(root).toHaveClass(styles.root); + expect(root.classList[0]).toBe('fui-color-swatch'); + expect(colorSwatchClassNames.root).toBe('fui-color-swatch group/fui-color-swatch'); + }); + + it('stamps its own resolved size and shape, defaulting to medium and square', () => { + const { getByTestId } = render( + <> + + {sizes.map(size => ( + + ))} + {shapes.map(shape => ( + + ))} + , + ); + + expect(getByTestId('default').getAttribute('data-size')).toBe('medium'); + expect(getByTestId('default').getAttribute('data-shape')).toBe('square'); + + for (const size of sizes) { + expect(getByTestId(`size-${size}`).getAttribute('data-size')).toBe(size); + } + + for (const shape of shapes) { + expect(getByTestId(`shape-${shape}`).getAttribute('data-shape')).toBe(shape); + } + }); + + it('lets its own size and shape beat the picker', () => { + const { getByTestId } = render( + + + + , + ); + + expect(getByTestId('inherit').getAttribute('data-size')).toBe('large'); + expect(getByTestId('inherit').getAttribute('data-shape')).toBe('circular'); + expect(getByTestId('override').getAttribute('data-size')).toBe('small'); + expect(getByTestId('override').getAttribute('data-shape')).toBe('rounded'); + }); + + it('falls back to medium and square inside a headless picker, which publishes neither', () => { + // Standalone, the context's own default value already supplies both; the `?? 'medium'` and + // `?? 'square'` guards are reachable only here, where a provider IS present but publishes + // undefined for all three look props. + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('root').getAttribute('data-size')).toBe('medium'); + expect(getByTestId('root').getAttribute('data-shape')).toBe('square'); + }); + + it('restores the default glyph on a disabled swatch and on nothing else', () => { + const { getByTestId } = render( + <> + + + , + ); + + const disabled = getByTestId('disabled'); + const span = disabled.querySelector('span'); + + expect(span).not.toBeNull(); + expect(span!.querySelectorAll('svg')).toHaveLength(1); + expect(getByTestId('enabled').querySelector('span')).toBeNull(); + expect(getByTestId('enabled').querySelector('svg')).toBeNull(); + }); + + it('resolves the glyph seam edge inputs', () => { + const { getByTestId } = render( + <> + {/* Griffel blanks this one; the uniform post-hook rule restores the glyph instead. */} + + + }} + /> + + , + ); + + expect(getByTestId('children-null').querySelectorAll('svg')).toHaveLength(1); + expect(getByTestId('slot-null').querySelector('span')).toBeNull(); + expect(getByTestId('consumer').querySelector('svg')).toBeNull(); + expect(getByTestId('consumer').querySelector('b')).not.toBeNull(); + expect(getByTestId('empty-obj').querySelectorAll('svg')).toHaveLength(1); + }); + + it('builds the disabledIcon slot with the element type the render function expects', () => { + // The slot is materialised before the headless hook runs, so its elementType has to agree + // with the hook's own components.disabledIcon. A disagreement is DOM-invisible — the render + // function reads components — and assertSlots only warns, so nothing else would catch it. + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + try { + render(); + + expect(warn.mock.calls.flat().join('\n')).not.toContain('assertSlots'); + } finally { + warn.mockRestore(); + } + }); + + it('decorates the icon and disabledIcon slots', () => { + const { getByTestId } = render( + <> + + + , + ); + + // fuicm-icon is shared by four shipped components under jest, so the disabledIcon slot is + // identified by the unique fuicm-disabled-icon ident and its element position. + const disabledIcon = getByTestId('disabled').querySelector('span')!; + + expect(disabledIcon).toHaveClass(styles.disabledIcon); + expect(disabledIcon).toHaveClass(styles.icon); + expect(disabledIcon).toHaveClass('consumer'); + + const icon = getByTestId('icon').querySelector('span')!; + + expect(icon).toHaveClass(styles.icon); + expect(icon).not.toHaveClass(styles.disabledIcon); + expect(icon).toHaveClass('consumer-icon'); + }); + + it('leaves the per-instance inline style exactly as the headless hook wrote it', () => { + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + // Declaration ORDER is the assertion: the headless custom properties come first and the + // consumer's own style last. Anything the windmod layer wrote, or a re-ordered spread, + // changes this string. Whitespace is jsdom's serialization, not the component's. + expect(root.getAttribute('style')!.replace(/\s+/g, '').replace(/;$/, '')).toBe( + '--fui-SwatchPicker--color:#f00;--fui-SwatchPicker--borderColor:#0f0;margin:2px', + ); + }); + + it('keeps the headless selection stamps and button contract', () => { + const { getByTestId } = render( + + + + , + ); + + const selected = getByTestId('a'); + + expect(selected.tagName).toBe('BUTTON'); + expect(selected.getAttribute('type')).toBe('button'); + expect(selected.getAttribute('role')).toBe('radio'); + // The headless package spells its own stamps as presence attributes; windmod adds none of + // its own here and duplicates none of these. + expect(selected.getAttribute('data-selected')).toBe(''); + + const disabled = getByTestId('b'); + + expect(disabled.getAttribute('data-disabled')).toBe(''); + expect(disabled).toBeDisabled(); + }); + + it('does not fire a selection change from a disabled swatch', () => { + const onSelectionChange = jest.fn(); + const { getByTestId } = render( + + + , + ); + + fireEvent.click(getByTestId('a')); + + expect(onSelectionChange).not.toHaveBeenCalled(); + }); + + it('passes consumer props through to the root', () => { + const ref = React.createRef(); + const onClick = jest.fn(); + const { getByTestId } = render( + , + ); + + const root = getByTestId('root'); + + expect(ref.current).toBe(root); + expect(root.id).toBe('cs'); + expect(root.getAttribute('aria-label')).toBe('red'); + expect(root).toHaveClass('consumer'); + + fireEvent.click(root); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not mutate the state it is given', () => { + const state = { + components: { root: 'button', icon: 'span', disabledIcon: 'span' }, + root: { className: 'consumer' }, + icon: { className: 'consumer-icon' }, + disabledIcon: { className: 'consumer-disabled' }, + size: 'large', + shape: 'circular', + } as unknown as ColorSwatchState; + + const styled = useColorSwatchStyles(state); + + expect(styled).not.toBe(state); + expect(state.root.className).toBe('consumer'); + expect(state.root).not.toHaveProperty('data-size'); + expect(state.icon!.className).toBe('consumer-icon'); + expect(state.disabledIcon!.className).toBe('consumer-disabled'); + expect(stampsOf(styled.root)['data-size']).toBe('large'); + expect(stampsOf(styled.root)['data-shape']).toBe('circular'); + expect(styled.icon!.className).toContain('consumer-icon'); + expect(styled.disabledIcon!.className).toContain('consumer-disabled'); + }); + + it('leaves suppressed slots undefined', () => { + const state = { + components: { root: 'button', icon: 'span', disabledIcon: 'span' }, + root: {}, + size: 'medium', + shape: 'square', + } as unknown as ColorSwatchState; + + const styled = useColorSwatchStyles(state); + + expect(styled.icon).toBeUndefined(); + expect(styled.disabledIcon).toBeUndefined(); + }); +}); diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.tsx b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.tsx new file mode 100644 index 0000000000000..d0c88f1ba5e07 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.tsx @@ -0,0 +1,46 @@ +'use client'; + +import * as React from 'react'; +import { slot } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { + renderColorSwatch, + useColorSwatch, + useSwatchPickerContextValue, +} from '@fluentui/react-headless-components-preview/swatch-picker'; +import { ProhibitedFilled } from '@fluentui/react-icons/headless/svg/prohibited'; + +import type { ColorSwatchProps } from './ColorSwatch.types'; +import { useColorSwatchStyles } from './useColorSwatchStyles'; + +/** + * A ColorSwatch is one selectable colour inside a SwatchPicker. Windmod ColorSwatch: the headless + * colour swatch decorated with the Fluent visual contract (Tailwind v4 + CSS Modules). + * + * The per-instance colour is an inline style the headless base hook writes; the windmod layer + * never writes to, re-orders or merges into it — the module reads the custom properties instead. + */ +export const ColorSwatch: ForwardRefComponent = React.forwardRef((props, ref) => { + const { size: sizeProp, shape: shapeProp, ...rest } = props; + const sizeFromContext = useSwatchPickerContextValue(ctx => ctx.size); + const shapeFromContext = useSwatchPickerContextValue(ctx => ctx.shape); + + // The headless hook builds the disabledIcon slot bare, so the default glyph is restored here. + // renderByDefault materialises the slot when the prop is absent; slot.optional's own null check + // still removes it for disabledIcon={null}. Only a disabled swatch renders the slot at all. + const disabledIconSlot = slot.optional(props.disabledIcon, { renderByDefault: true, elementType: 'span' }); + + return renderColorSwatch( + useColorSwatchStyles({ + ...useColorSwatch(rest, ref), + size: sizeProp ?? sizeFromContext ?? 'medium', + shape: shapeProp ?? shapeFromContext ?? 'square', + disabledIcon: disabledIconSlot && { + ...disabledIconSlot, + children: disabledIconSlot.children ?? , + }, + }), + ); +}); + +ColorSwatch.displayName = 'ColorSwatch'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.types.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.types.ts new file mode 100644 index 0000000000000..8e7a09df1ef08 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/ColorSwatch.types.ts @@ -0,0 +1,22 @@ +import type { + ColorSwatchProps as ColorSwatchHeadlessProps, + ColorSwatchState as ColorSwatchHeadlessState, +} from '@fluentui/react-headless-components-preview/swatch-picker'; + +import type { SwatchPickerShape, SwatchPickerSize } from '../SwatchPicker/SwatchPicker.types'; + +export type { ColorSwatchSlots } from '@fluentui/react-headless-components-preview/swatch-picker'; + +/** + * Windmod ColorSwatch props: the headless colour swatch plus the look props the headless surface + * deliberately omits. Both default to the picker context before falling back. + */ +export type ColorSwatchProps = ColorSwatchHeadlessProps & { + /** @default the picker's size, then 'medium' */ + size?: SwatchPickerSize; + /** @default the picker's shape, then 'square' */ + shape?: SwatchPickerShape; +}; + +/** Windmod ColorSwatch state: headless state plus the resolved look props. */ +export type ColorSwatchState = ColorSwatchHeadlessState & Required>; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/index.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/index.ts new file mode 100644 index 0000000000000..a74f8fc804d74 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/index.ts @@ -0,0 +1,3 @@ +export { ColorSwatch } from './ColorSwatch'; +export { colorSwatchClassNames, useColorSwatchStyles } from './useColorSwatchStyles'; +export type { ColorSwatchProps, ColorSwatchSlots, ColorSwatchState } from './ColorSwatch.types'; diff --git a/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/useColorSwatchStyles.ts b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/useColorSwatchStyles.ts new file mode 100644 index 0000000000000..66543055d8ee8 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/ColorSwatch/useColorSwatchStyles.ts @@ -0,0 +1,38 @@ +import { clsx } from 'clsx'; + +import { componentMarkers } from '../../utils/groupMarker'; +import { slotClasses } from '../../utils/slotClasses'; +import type { ColorSwatchState } from './ColorSwatch.types'; + +import styles from './ColorSwatch.module.css'; + +/** The only public classes — see componentMarkers; internals are hashed idents. */ +export const colorSwatchClassNames: { root: string } = { + root: componentMarkers('color-swatch'), +}; + +type ColorSwatchRootDataAttributes = { + 'data-size'?: ColorSwatchState['size']; + 'data-shape'?: ColorSwatchState['shape']; +}; + +/** + * Applies the visual contract, returning new state. The disabledIcon slot carries the icon class + * as well as its own, matching the size scale every icon in this component follows; the render + * function draws it only on a disabled swatch, so decorating it unconditionally is safe. + */ +export const useColorSwatchStyles = (state: ColorSwatchState): ColorSwatchState => { + const root: ColorSwatchState['root'] & ColorSwatchRootDataAttributes = { + ...state.root, + 'data-size': state.size, + 'data-shape': state.shape, + className: clsx(colorSwatchClassNames.root, styles.root, state.root.className), + }; + + return { + ...state, + root, + icon: slotClasses(state.icon, styles.icon), + disabledIcon: slotClasses(state.disabledIcon, styles.icon, styles.disabledIcon), + }; +}; diff --git a/packages/react-components/react-windmod-preview/library/src/components/Combobox/Combobox.module.css b/packages/react-components/react-windmod-preview/library/src/components/Combobox/Combobox.module.css new file mode 100644 index 0000000000000..8fdc1397cb886 --- /dev/null +++ b/packages/react-components/react-windmod-preview/library/src/components/Combobox/Combobox.module.css @@ -0,0 +1,208 @@ +@reference '#theme'; + +/* Root-modifier block order is the cascade: every selector below is :where()-flat, so source order + alone arbitrates. Appearance blocks, then .outline-interactive, then .disabled, then the two + invalid forms — Griffel's mergeClasses order with the last pair swapped, for the reason stated on + .disabled below. The size blocks are nested in .root and therefore precede every appearance + block, which Griffel orders the other way; the two sets share no declaration (sizes author + height, padding-inline-end and column-gap; appearances author background-color, the border + longhands and border-radius), so the reorder cannot change a winner — the same zero-overlap check + that licenses this file's layer. */ + +@layer fui.components.l1 { + .root { + @apply relative box-border inline-grid min-w-250 grid-cols-[1fr_auto] items-center justify-between gap-x-horizontal-xxs rounded-medium align-middle; + + /* The bottom focus border, shared with Input, Select and SpinButton. No transition-delay and no + timing function: Griffel writes a curve token into transition-delay, which is not a