From 23882bd12cbf9556afd75e69a5c8bd56ab804b87 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 9 Oct 2025 14:19:55 -0400 Subject: [PATCH 01/17] feat(Button): add VisualRefresh story with button shape variations --- .../src/Button/VisualRefresh.stories.tsx | 30 +++++++++++++++++++ .../stories/src/Button/index.stories.tsx | 15 +++++----- 2 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx diff --git a/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx b/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx new file mode 100644 index 00000000000000..5dfc9f548e12f4 --- /dev/null +++ b/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import type { JSXElement } from '@fluentui/react-components'; +import { makeStyles, Button } from '@fluentui/react-components'; + +const useStyles = makeStyles({ + wrapper: { + columnGap: '15px', + display: 'flex', + minWidth: 'min-content', + }, +}); + +export const VisualRefresh = (): JSXElement => { + const styles = useStyles(); + return ( +
+ + + +
+ ); +}; + +VisualRefresh.parameters = { + docs: { + description: { + story: 'A button can be rounded, circular, or square.', + }, + }, +}; diff --git a/packages/react-components/react-button/stories/src/Button/index.stories.tsx b/packages/react-components/react-button/stories/src/Button/index.stories.tsx index bf0635afed6ee6..705c6ae80652bf 100644 --- a/packages/react-components/react-button/stories/src/Button/index.stories.tsx +++ b/packages/react-components/react-button/stories/src/Button/index.stories.tsx @@ -4,13 +4,14 @@ import descriptionMd from './ButtonDescription.md'; import bestPracticesMd from './ButtonBestPractices.md'; export { Default } from './ButtonDefault.stories'; -export { Shape } from './ButtonShape.stories'; -export { Appearance } from './ButtonAppearance.stories'; -export { Icon } from './ButtonIcon.stories'; -export { Size } from './ButtonSize.stories'; -export { Disabled } from './ButtonDisabled.stories'; -export { Loading } from './ButtonLoading.stories'; -export { WithLongText } from './ButtonWithLongText.stories'; +export { VisualRefresh } from './VisualRefresh.stories'; +// export { Shape } from './ButtonShape.stories'; +// export { Appearance } from './ButtonAppearance.stories'; +// export { Icon } from './ButtonIcon.stories'; +// export { Size } from './ButtonSize.stories'; +// export { Disabled } from './ButtonDisabled.stories'; +// export { Loading } from './ButtonLoading.stories'; +// export { WithLongText } from './ButtonWithLongText.stories'; export default { title: 'Components/Button/Button', From a969de8ed355a4ec2a5c290f720d284d72498d4a Mon Sep 17 00:00:00 2001 From: David Zukowski Date: Thu, 9 Oct 2025 11:51:33 -0700 Subject: [PATCH 02/17] setup visual refresh storybook --- .github/CODEOWNERS | 2 + .../Button/useButtonStyles.styles.ts | 13 +++- .../src/Button/VisualRefresh.stories.tsx | 1 + .../library/.babelrc.json | 4 ++ .../library/.eslintrc.json | 4 ++ .../visual-refresh-preview/library/.swcrc | 30 +++++++++ .../visual-refresh-preview/library/LICENSE | 15 +++++ .../visual-refresh-preview/library/README.md | 5 ++ .../library/config/api-extractor.json | 5 ++ .../library/config/tests.js | 1 + .../library/docs/Spec.md | 63 +++++++++++++++++++ .../library/etc/visual-refresh-preview.api.md | 0 .../library/jest.config.js | 34 ++++++++++ .../library/package.json | 56 +++++++++++++++++ .../library/project.json | 8 +++ .../library/src/index.ts | 26 ++++++++ .../library/src/testing/isConformant.ts | 15 +++++ .../library/tsconfig.json | 22 +++++++ .../library/tsconfig.lib.json | 22 +++++++ .../library/tsconfig.spec.json | 17 +++++ .../stories/.eslintrc.json | 12 ++++ .../stories/.storybook/main.js | 14 +++++ .../stories/.storybook/preview.js | 7 +++ .../stories/.storybook/tsconfig.json | 10 +++ .../visual-refresh-preview/stories/README.md | 17 +++++ .../stories/package.json | 11 ++++ .../stories/project.json | 8 +++ .../stories/src/.gitkeep | 0 .../VisualRefresh/VisualRefresh.stories.tsx | 63 +++++++++++++++++++ .../src/VisualRefresh/index.stories.tsx | 12 ++++ .../stories/src/index.ts | 1 + .../stories/tsconfig.json | 22 +++++++ .../stories/tsconfig.lib.json | 10 +++ tsconfig.base.all.json | 6 +- tsconfig.base.json | 4 ++ 35 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 packages/react-components/visual-refresh-preview/library/.babelrc.json create mode 100644 packages/react-components/visual-refresh-preview/library/.eslintrc.json create mode 100644 packages/react-components/visual-refresh-preview/library/.swcrc create mode 100644 packages/react-components/visual-refresh-preview/library/LICENSE create mode 100644 packages/react-components/visual-refresh-preview/library/README.md create mode 100644 packages/react-components/visual-refresh-preview/library/config/api-extractor.json create mode 100644 packages/react-components/visual-refresh-preview/library/config/tests.js create mode 100644 packages/react-components/visual-refresh-preview/library/docs/Spec.md create mode 100644 packages/react-components/visual-refresh-preview/library/etc/visual-refresh-preview.api.md create mode 100644 packages/react-components/visual-refresh-preview/library/jest.config.js create mode 100644 packages/react-components/visual-refresh-preview/library/package.json create mode 100644 packages/react-components/visual-refresh-preview/library/project.json create mode 100644 packages/react-components/visual-refresh-preview/library/src/index.ts create mode 100644 packages/react-components/visual-refresh-preview/library/src/testing/isConformant.ts create mode 100644 packages/react-components/visual-refresh-preview/library/tsconfig.json create mode 100644 packages/react-components/visual-refresh-preview/library/tsconfig.lib.json create mode 100644 packages/react-components/visual-refresh-preview/library/tsconfig.spec.json create mode 100644 packages/react-components/visual-refresh-preview/stories/.eslintrc.json create mode 100644 packages/react-components/visual-refresh-preview/stories/.storybook/main.js create mode 100644 packages/react-components/visual-refresh-preview/stories/.storybook/preview.js create mode 100644 packages/react-components/visual-refresh-preview/stories/.storybook/tsconfig.json create mode 100644 packages/react-components/visual-refresh-preview/stories/README.md create mode 100644 packages/react-components/visual-refresh-preview/stories/package.json create mode 100644 packages/react-components/visual-refresh-preview/stories/project.json create mode 100644 packages/react-components/visual-refresh-preview/stories/src/.gitkeep create mode 100644 packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx create mode 100644 packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx create mode 100644 packages/react-components/visual-refresh-preview/stories/src/index.ts create mode 100644 packages/react-components/visual-refresh-preview/stories/tsconfig.json create mode 100644 packages/react-components/visual-refresh-preview/stories/tsconfig.lib.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9288891ec7e324..57f8579394b223 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -331,6 +331,8 @@ packages/react-components/component-selector-preview/library @microsoft/teams-pr packages/react-components/component-selector-preview/stories @microsoft/teams-prg 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/visual-refresh-preview/library @microsoft/teams-prg +packages/react-components/visual-refresh-preview/stories @microsoft/teams-prg # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 186d0f7a86d58e..706d6835d810f8 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -4,6 +4,7 @@ import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; import { tokens } from '@fluentui/react-theme'; import { shorthands, makeStyles, makeResetStyles, mergeClasses } from '@griffel/react'; +import { useIsVisualRefreshEnabled, VISUAL_REFRESH_TOKENS } from '@fluentui/visual-refresh-preview'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { ButtonSlots, ButtonState } from './Button.types'; @@ -550,9 +551,18 @@ const useIconStyles = makeStyles({ }, }); +const useVisualRefreshStyles = makeStyles({ + root: { + borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, + }, +}); + export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { 'use no memo'; + const isVisualRefreshEnabled = useIsVisualRefreshEnabled(); + const visualRefreshStyles = useVisualRefreshStyles(); + const rootBaseClassName = useRootBaseClassName(); const iconBaseClassName = useIconBaseClassName(); @@ -590,6 +600,8 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { // User provided class name state.root.className, + + isVisualRefreshEnabled && visualRefreshStyles.root, ); if (state.icon) { @@ -601,6 +613,5 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { state.icon.className, ); } - return state; }; diff --git a/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx b/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx index 5dfc9f548e12f4..3a08e3778a04d1 100644 --- a/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx +++ b/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx @@ -1,3 +1,4 @@ +// yarn start > button import * as React from 'react'; import type { JSXElement } from '@fluentui/react-components'; import { makeStyles, Button } from '@fluentui/react-components'; diff --git a/packages/react-components/visual-refresh-preview/library/.babelrc.json b/packages/react-components/visual-refresh-preview/library/.babelrc.json new file mode 100644 index 00000000000000..630deaf765c49f --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/.babelrc.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../.babelrc-v9.json", + "plugins": ["annotate-pure-calls", "@babel/transform-react-pure-annotations"] +} diff --git a/packages/react-components/visual-refresh-preview/library/.eslintrc.json b/packages/react-components/visual-refresh-preview/library/.eslintrc.json new file mode 100644 index 00000000000000..ceea884c70dccc --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["plugin:@fluentui/eslint-plugin/react"], + "root": true +} diff --git a/packages/react-components/visual-refresh-preview/library/.swcrc b/packages/react-components/visual-refresh-preview/library/.swcrc new file mode 100644 index 00000000000000..b4ffa86dee3067 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/.swcrc @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "exclude": [ + "/testing", + "/**/*.cy.ts", + "/**/*.cy.tsx", + "/**/*.spec.ts", + "/**/*.spec.tsx", + "/**/*.test.ts", + "/**/*.test.tsx" + ], + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": false, + "dynamicImport": false + }, + "externalHelpers": true, + "transform": { + "react": { + "runtime": "classic", + "useSpread": true + } + }, + "target": "es2019" + }, + "minify": false, + "sourceMaps": true +} diff --git a/packages/react-components/visual-refresh-preview/library/LICENSE b/packages/react-components/visual-refresh-preview/library/LICENSE new file mode 100644 index 00000000000000..19c1105d93e9fb --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/LICENSE @@ -0,0 +1,15 @@ +@fluentui/visual-refresh-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/visual-refresh-preview/library/README.md b/packages/react-components/visual-refresh-preview/library/README.md new file mode 100644 index 00000000000000..7108e5f3b67a46 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/README.md @@ -0,0 +1,5 @@ +# @fluentui/visual-refresh-preview + +**Visual Refresh components for [Fluent UI React](https://react.fluentui.dev/)** + +These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. diff --git a/packages/react-components/visual-refresh-preview/library/config/api-extractor.json b/packages/react-components/visual-refresh-preview/library/config/api-extractor.json new file mode 100644 index 00000000000000..8d482156d10d53 --- /dev/null +++ b/packages/react-components/visual-refresh-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/visual-refresh-preview/library/config/tests.js b/packages/react-components/visual-refresh-preview/library/config/tests.js new file mode 100644 index 00000000000000..2e211ae9e21420 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/config/tests.js @@ -0,0 +1 @@ +/** Jest test setup file. */ diff --git a/packages/react-components/visual-refresh-preview/library/docs/Spec.md b/packages/react-components/visual-refresh-preview/library/docs/Spec.md new file mode 100644 index 00000000000000..2f87c2d6b8de3d --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/docs/Spec.md @@ -0,0 +1,63 @@ +# @fluentui/visual-refresh-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 folow **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 Oder 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/visual-refresh-preview/library/etc/visual-refresh-preview.api.md b/packages/react-components/visual-refresh-preview/library/etc/visual-refresh-preview.api.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/visual-refresh-preview/library/jest.config.js b/packages/react-components/visual-refresh-preview/library/jest.config.js new file mode 100644 index 00000000000000..2ce82741b48f77 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/jest.config.js @@ -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: 'visual-refresh-preview', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/visual-refresh-preview/library/package.json b/packages/react-components/visual-refresh-preview/library/package.json new file mode 100644 index 00000000000000..fbb0c07eca8584 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/package.json @@ -0,0 +1,56 @@ +{ + "name": "@fluentui/visual-refresh-preview", + "version": "0.0.0", + "private": true, + "description": "New fluentui react package", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "devDependencies": { + "@fluentui/eslint-plugin": "*", + "@fluentui/react-conformance": "*", + "@fluentui/react-conformance-griffel": "*", + "@fluentui/scripts-api-extractor": "*" + }, + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.2.2", + "@fluentui/react-shared-contexts": "^9.25.2", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.1", + "@griffel/react": "^1.5.22", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "import": "./lib/index.js", + "require": "./lib-commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/visual-refresh-preview/library/project.json b/packages/react-components/visual-refresh-preview/library/project.json new file mode 100644 index 00000000000000..c6fc76d313d139 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "visual-refresh-preview", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/visual-refresh-preview/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts new file mode 100644 index 00000000000000..d058614c5ee6e0 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; + +export const VisualRefreshContext = React.createContext(false); + +export function useIsVisualRefreshEnabled() { + return React.useContext(VisualRefreshContext); +} + +function createToken(name: string) { + return `var(--visual-refresh-${name})`; +} + +export const VISUAL_REFRESH_TOKENS = { + buttonBorderRadius: createToken('buttonBorderRadius'), +} as const; + +type VisualRefreshToken = keyof typeof VISUAL_REFRESH_TOKENS; +type VisualRefreshTheme = Record; + +const GLOBAL_TOKENS = { + cornerRadiusMedium: 8, +}; + +export const TEAMS_VISUAL_REFRESH_TOKENS = { + buttonBorderRadius: '100px', +} satisfies VisualRefreshTheme; diff --git a/packages/react-components/visual-refresh-preview/library/src/testing/isConformant.ts b/packages/react-components/visual-refresh-preview/library/src/testing/isConformant.ts new file mode 100644 index 00000000000000..8ed2da0f925135 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/src/testing/isConformant.ts @@ -0,0 +1,15 @@ +import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; +import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; +import griffelTests from '@fluentui/react-conformance-griffel'; + +export function isConformant( + testInfo: Omit, 'componentPath'> & { componentPath?: string }, +): void { + const defaultOptions: Partial> = { + tsConfig: { configName: 'tsconfig.spec.json' }, + componentPath: require.main?.filename.replace('.test', ''), + extraTests: griffelTests as TestObject, + }; + + baseIsConformant(defaultOptions, testInfo); +} diff --git a/packages/react-components/visual-refresh-preview/library/tsconfig.json b/packages/react-components/visual-refresh-preview/library/tsconfig.json new file mode 100644 index 00000000000000..32bdbdf1ac26f0 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/react-components/visual-refresh-preview/library/tsconfig.lib.json b/packages/react-components/visual-refresh-preview/library/tsconfig.lib.json new file mode 100644 index 00000000000000..53066fdd11fff0 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../../dist/out-tsc/types", + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/visual-refresh-preview/library/tsconfig.spec.json b/packages/react-components/visual-refresh-preview/library/tsconfig.spec.json new file mode 100644 index 00000000000000..911456fe4b4d91 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/library/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/packages/react-components/visual-refresh-preview/stories/.eslintrc.json b/packages/react-components/visual-refresh-preview/stories/.eslintrc.json new file mode 100644 index 00000000000000..a41120835dcc92 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "extends": ["plugin:@fluentui/eslint-plugin/react"], + "root": true, + "rules": { + "import/no-extraneous-dependencies": [ + "error", + { + "packageDir": [".", "../../../../"] + } + ] + } +} diff --git a/packages/react-components/visual-refresh-preview/stories/.storybook/main.js b/packages/react-components/visual-refresh-preview/stories/.storybook/main.js new file mode 100644 index 00000000000000..b380cd896aea19 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/visual-refresh-preview/stories/.storybook/preview.js b/packages/react-components/visual-refresh-preview/stories/.storybook/preview.js new file mode 100644 index 00000000000000..94455f782364e4 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/.storybook/preview.js @@ -0,0 +1,7 @@ +import * as rootPreview from '../../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; diff --git a/packages/react-components/visual-refresh-preview/stories/.storybook/tsconfig.json b/packages/react-components/visual-refresh-preview/stories/.storybook/tsconfig.json new file mode 100644 index 00000000000000..4cdd1ce9d006f1 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment"] + }, + "include": ["*.js"] +} diff --git a/packages/react-components/visual-refresh-preview/stories/README.md b/packages/react-components/visual-refresh-preview/stories/README.md new file mode 100644 index 00000000000000..2fda219a947114 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/README.md @@ -0,0 +1,17 @@ +# @fluentui/visual-refresh-preview-stories + +Storybook stories for packages/react-components/visual-refresh-preview + +## Usage + +To include within storybook specify stories globs: + +\`\`\`js +module.exports = { +stories: ['../packages/react-components/visual-refresh-preview/stories/src/**/*.stories.mdx', '../packages/react-components/visual-refresh-preview/stories/src/**/index.stories.@(ts|tsx)'], +} +\`\`\` + +## API + +no public API available diff --git a/packages/react-components/visual-refresh-preview/stories/package.json b/packages/react-components/visual-refresh-preview/stories/package.json new file mode 100644 index 00000000000000..e0fe00f72b8014 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/package.json @@ -0,0 +1,11 @@ +{ + "name": "@fluentui/visual-refresh-preview-stories", + "version": "0.0.0", + "private": true, + "devDependencies": { + "@fluentui/react-storybook-addon": "*", + "@fluentui/react-storybook-addon-export-to-sandbox": "*", + "@fluentui/scripts-storybook": "*", + "@fluentui/eslint-plugin": "*" + } +} diff --git a/packages/react-components/visual-refresh-preview/stories/project.json b/packages/react-components/visual-refresh-preview/stories/project.json new file mode 100644 index 00000000000000..a32425de19419f --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/project.json @@ -0,0 +1,8 @@ +{ + "name": "visual-refresh-preview-stories", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/visual-refresh-preview/stories/src", + "tags": ["vNext", "platform:web", "type:stories"], + "implicitDependencies": [] +} diff --git a/packages/react-components/visual-refresh-preview/stories/src/.gitkeep b/packages/react-components/visual-refresh-preview/stories/src/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx new file mode 100644 index 00000000000000..a873a335464640 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import type { JSXElement } from '@fluentui/react-components'; +import { makeStyles, Button } from '@fluentui/react-components'; +import { TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext } from '@fluentui/visual-refresh-preview'; + +const useStyles = makeStyles({ + wrapper: { + columnGap: '15px', + display: 'flex', + minWidth: 'min-content', + }, +}); + +const VisualRefreshProvider = ({ children }) => { + const theme = TEAMS_VISUAL_REFRESH_TOKENS; + const style: React.CSSProperties = {}; + for (const key of Object.keys(theme)) { + style[`--visual-refresh-${key}`] = theme[key]; + } + return ( + +
{children}
+
+ ); +}; + +const VisualRefreshPreview = ({ children }) => { + return ( +
+
{children}
+
+ {children} +
+
+ ); +}; + +const ButtonExamples = () => { + const styles = useStyles(); + return ( +
+ + + +
+ ); +}; + +export const VisualRefresh = (): JSXElement => { + return ( + + + + ); +}; + +VisualRefresh.parameters = { + docs: { + description: { + story: 'A button can be rounded, circular, or square.', + }, + }, +}; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx new file mode 100644 index 00000000000000..0b80d786d5fa2b --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { Meta } from '@storybook/react'; + +export { VisualRefresh } from './VisualRefresh.stories'; + +const Component = () =>
; + +export default { + title: 'Components/VisualRefresh', + component: Component, + parameters: {}, +} as Meta; diff --git a/packages/react-components/visual-refresh-preview/stories/src/index.ts b/packages/react-components/visual-refresh-preview/stories/src/index.ts new file mode 100644 index 00000000000000..cb0ff5c3b541f6 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-components/visual-refresh-preview/stories/tsconfig.json b/packages/react-components/visual-refresh-preview/stories/tsconfig.json new file mode 100644 index 00000000000000..efc50169d1df18 --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/visual-refresh-preview/stories/tsconfig.lib.json b/packages/react-components/visual-refresh-preview/stories/tsconfig.lib.json new file mode 100644 index 00000000000000..9486b224643d9f --- /dev/null +++ b/packages/react-components/visual-refresh-preview/stories/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2019", "dom"], + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index 9b9756df7a1a05..0161cbeac676a0 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -252,9 +252,13 @@ "@fluentui/storybook-llms-extractor": ["tools/storybook-llms-extractor/src/index.ts"], "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], "@fluentui/tokens": ["packages/tokens/src/index.ts"], + "@fluentui/visual-refresh-preview": ["packages/react-components/visual-refresh-preview/library/src/index.ts"], "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] + "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], + "@fluentui/visual-refresh-preview-stories": [ + "packages/react-components/visual-refresh-preview/stories/src/index.ts" + ] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index ded6aaaa1f2528..776ecd04f2076d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -208,6 +208,10 @@ "@fluentui/storybook-llms-extractor": ["tools/storybook-llms-extractor/src/index.ts"], "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], "@fluentui/tokens": ["packages/tokens/src/index.ts"], + "@fluentui/visual-refresh-preview": ["packages/react-components/visual-refresh-preview/library/src/index.ts"], + "@fluentui/visual-refresh-preview-stories": [ + "packages/react-components/visual-refresh-preview/stories/src/index.ts" + ], "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] From ec90326e6d5c543574d13a440a87a792b0ac5c9c Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 9 Oct 2025 15:29:03 -0400 Subject: [PATCH 03/17] refactor(react-button): auto-apply Visual Refresh classes per slot --- .../Button/useButtonStyles.styles.ts | 10 ++++-- .../src/Button/VisualRefresh.stories.tsx | 31 ------------------- .../stories/src/Button/index.stories.tsx | 15 +++++---- .../library/src/index.ts | 6 ++-- .../VisualRefresh/VisualRefresh.stories.tsx | 12 +++---- 5 files changed, 23 insertions(+), 51 deletions(-) delete mode 100644 packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 706d6835d810f8..633824f5ee1c63 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -552,7 +552,7 @@ const useIconStyles = makeStyles({ }); const useVisualRefreshStyles = makeStyles({ - root: { + rounded: { borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, }, }); @@ -574,6 +574,12 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { const { appearance, disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state; + const visualRefreshStylesRecord = visualRefreshStyles as unknown as Record; + const visualRefreshSlots: Array = ['root', appearance, shape, size]; + const visualRefreshOverrides = isVisualRefreshEnabled + ? mergeClasses(...visualRefreshSlots.map(slot => (slot ? visualRefreshStylesRecord[slot] : undefined))) + : undefined; + state.root.className = mergeClasses( buttonClassNames.root, rootBaseClassName, @@ -601,7 +607,7 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { // User provided class name state.root.className, - isVisualRefreshEnabled && visualRefreshStyles.root, + visualRefreshOverrides, ); if (state.icon) { diff --git a/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx b/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx deleted file mode 100644 index 3a08e3778a04d1..00000000000000 --- a/packages/react-components/react-button/stories/src/Button/VisualRefresh.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// yarn start > button -import * as React from 'react'; -import type { JSXElement } from '@fluentui/react-components'; -import { makeStyles, Button } from '@fluentui/react-components'; - -const useStyles = makeStyles({ - wrapper: { - columnGap: '15px', - display: 'flex', - minWidth: 'min-content', - }, -}); - -export const VisualRefresh = (): JSXElement => { - const styles = useStyles(); - return ( -
- - - -
- ); -}; - -VisualRefresh.parameters = { - docs: { - description: { - story: 'A button can be rounded, circular, or square.', - }, - }, -}; diff --git a/packages/react-components/react-button/stories/src/Button/index.stories.tsx b/packages/react-components/react-button/stories/src/Button/index.stories.tsx index 705c6ae80652bf..bf0635afed6ee6 100644 --- a/packages/react-components/react-button/stories/src/Button/index.stories.tsx +++ b/packages/react-components/react-button/stories/src/Button/index.stories.tsx @@ -4,14 +4,13 @@ import descriptionMd from './ButtonDescription.md'; import bestPracticesMd from './ButtonBestPractices.md'; export { Default } from './ButtonDefault.stories'; -export { VisualRefresh } from './VisualRefresh.stories'; -// export { Shape } from './ButtonShape.stories'; -// export { Appearance } from './ButtonAppearance.stories'; -// export { Icon } from './ButtonIcon.stories'; -// export { Size } from './ButtonSize.stories'; -// export { Disabled } from './ButtonDisabled.stories'; -// export { Loading } from './ButtonLoading.stories'; -// export { WithLongText } from './ButtonWithLongText.stories'; +export { Shape } from './ButtonShape.stories'; +export { Appearance } from './ButtonAppearance.stories'; +export { Icon } from './ButtonIcon.stories'; +export { Size } from './ButtonSize.stories'; +export { Disabled } from './ButtonDisabled.stories'; +export { Loading } from './ButtonLoading.stories'; +export { WithLongText } from './ButtonWithLongText.stories'; export default { title: 'Components/Button/Button', diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index d058614c5ee6e0..e79c963cb30cba 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -17,10 +17,8 @@ export const VISUAL_REFRESH_TOKENS = { type VisualRefreshToken = keyof typeof VISUAL_REFRESH_TOKENS; type VisualRefreshTheme = Record; -const GLOBAL_TOKENS = { - cornerRadiusMedium: 8, -}; +export const GLOBAL_TOKENS = {}; export const TEAMS_VISUAL_REFRESH_TOKENS = { - buttonBorderRadius: '100px', + buttonBorderRadius: '14px', } satisfies VisualRefreshTheme; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index a873a335464640..f746565a301da4 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -11,20 +11,20 @@ const useStyles = makeStyles({ }, }); -const VisualRefreshProvider = ({ children }) => { +const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { const theme = TEAMS_VISUAL_REFRESH_TOKENS; - const style: React.CSSProperties = {}; - for (const key of Object.keys(theme)) { - style[`--visual-refresh-${key}`] = theme[key]; + const customProperties: Record = {}; + for (const key of Object.keys(theme) as Array) { + customProperties[`--visual-refresh-${key}`] = theme[key]; } return ( -
{children}
+
{children}
); }; -const VisualRefreshPreview = ({ children }) => { +const VisualRefreshPreview = ({ children }: { children: React.ReactNode }) => { return (
{children}
From 31102483d2f712dd726d673dee71c6d68147c015 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 9 Oct 2025 15:47:43 -0400 Subject: [PATCH 04/17] feat(Button): enhance visual refresh styles with new token properties and button examples --- .../Button/useButtonStyles.styles.ts | 12 ++++++++++ .../library/src/index.ts | 23 ++++++++++++++++++- .../VisualRefresh/VisualRefresh.stories.tsx | 20 +++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 633824f5ee1c63..c2f64db0cf891d 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -552,8 +552,20 @@ const useIconStyles = makeStyles({ }); const useVisualRefreshStyles = makeStyles({ + root: { + fontWeight: VISUAL_REFRESH_TOKENS.buttonRootFontWeight, + padding: VISUAL_REFRESH_TOKENS.buttonRootPadding, + ...shorthands.borderWidth(VISUAL_REFRESH_TOKENS.buttonRootBorderWidth), + }, rounded: { borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, + fontFamily: VISUAL_REFRESH_TOKENS.buttonRoundedFontFamily, + }, + square: { + fontWeight: VISUAL_REFRESH_TOKENS.buttonSquareFontWeight, + }, + primary: { + backgroundColor: VISUAL_REFRESH_TOKENS.buttonPrimaryBackgroundColor, }, }); diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index e79c963cb30cba..3623cb6847a4a3 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -12,6 +12,12 @@ function createToken(name: string) { export const VISUAL_REFRESH_TOKENS = { buttonBorderRadius: createToken('buttonBorderRadius'), + buttonRootFontWeight: createToken('buttonRootFontWeight'), + buttonRootPadding: createToken('buttonRootPadding'), + buttonRootBorderWidth: createToken('buttonRootBorderWidth'), + buttonRoundedFontFamily: createToken('buttonRoundedFontFamily'), + buttonSquareFontWeight: createToken('buttonSquareFontWeight'), + buttonPrimaryBackgroundColor: createToken('buttonPrimaryBackgroundColor'), } as const; type VisualRefreshToken = keyof typeof VISUAL_REFRESH_TOKENS; @@ -19,6 +25,21 @@ type VisualRefreshTheme = Record; export const GLOBAL_TOKENS = {}; +// Note: `VisualRefreshTheme` expects a flat key/value map, so the exported theme must stay flat. +// Question to David: If grouping is needed, create intermediate objects +// (e.g. `const buttonShape = { ... }`) and spread them here. export const TEAMS_VISUAL_REFRESH_TOKENS = { - buttonBorderRadius: '14px', + // Button + // Shape tokens + buttonBorderRadius: '16px', + buttonRoundedFontFamily: '"Comic Sans MS", "Comic Sans", cursive', + buttonSquareFontWeight: '800', + + // Root tokens + buttonRootFontWeight: '400', + buttonRootPadding: '8px 16px', + buttonRootBorderWidth: '2px', + + // Appearance tokens + buttonPrimaryBackgroundColor: 'purple', } satisfies VisualRefreshTheme; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index f746565a301da4..83b74a799ef2d2 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -2,6 +2,9 @@ import * as React from 'react'; import type { JSXElement } from '@fluentui/react-components'; import { makeStyles, Button } from '@fluentui/react-components'; import { TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext } from '@fluentui/visual-refresh-preview'; +import { bundleIcon, CalendarMonthFilled, CalendarMonthRegular } from '@fluentui/react-icons'; + +const CalendarMonth = bundleIcon(CalendarMonthFilled, CalendarMonthRegular); const useStyles = makeStyles({ wrapper: { @@ -26,9 +29,11 @@ const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { const VisualRefreshPreview = ({ children }: { children: React.ReactNode }) => { return ( -
+
{children}
+
+

Visual Refresh Theme

{children}
@@ -42,6 +47,19 @@ const ButtonExamples = () => { + + + + +
); }; From c9fd69199da274401b77f5f8b64199558bf8b8d5 Mon Sep 17 00:00:00 2001 From: David Zukowski Date: Wed, 15 Oct 2025 11:41:50 -0700 Subject: [PATCH 05/17] riffing on button --- .../Button/useButtonStyles.styles.ts | 32 ++- .../library/src/index.ts | 229 ++++++++++++++++-- .../VisualRefresh/VisualRefresh.stories.tsx | 28 ++- 3 files changed, 259 insertions(+), 30 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index c2f64db0cf891d..a801ff7f9c431e 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -4,7 +4,7 @@ import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; import { tokens } from '@fluentui/react-theme'; import { shorthands, makeStyles, makeResetStyles, mergeClasses } from '@griffel/react'; -import { useIsVisualRefreshEnabled, VISUAL_REFRESH_TOKENS } from '@fluentui/visual-refresh-preview'; +import { semanticTokenVar, useIsVisualRefreshEnabled } from '@fluentui/visual-refresh-preview'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { ButtonSlots, ButtonState } from './Button.types'; @@ -553,19 +553,32 @@ const useIconStyles = makeStyles({ const useVisualRefreshStyles = makeStyles({ root: { - fontWeight: VISUAL_REFRESH_TOKENS.buttonRootFontWeight, - padding: VISUAL_REFRESH_TOKENS.buttonRootPadding, - ...shorthands.borderWidth(VISUAL_REFRESH_TOKENS.buttonRootBorderWidth), + background: SEMANTIC_TOKENS.groupButtonBackground, + // fontWeight: VISUAL_REFRESH_TOKENS.buttonRootFontWeight, + // padding: VISUAL_REFRESH_TOKENS.buttonRootPadding, + // ...shorthands.borderWidth(VISUAL_REFRESH_TOKENS.buttonRootBorderWidth), }, rounded: { - borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, - fontFamily: VISUAL_REFRESH_TOKENS.buttonRoundedFontFamily, + // borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, + // fontFamily: VISUAL_REFRESH_TOKENS.buttonRoundedFontFamily, }, square: { - fontWeight: VISUAL_REFRESH_TOKENS.buttonSquareFontWeight, + // fontWeight: VISUAL_REFRESH_TOKENS.buttonSquareFontWeight, }, primary: { - backgroundColor: VISUAL_REFRESH_TOKENS.buttonPrimaryBackgroundColor, + // backgroundColor: VISUAL_REFRESH_TOKENS.buttonPrimaryBackgroundColor, + }, + small: { + height: semanticTokenVar('size/ctrl/sm'), + minHeight: semanticTokenVar('size/ctrl/sm'), + }, + medium: { + height: semanticTokenVar('size/ctrl/default'), + minHeight: semanticTokenVar('size/ctrl/default'), + }, + large: { + height: semanticTokenVar('size/ctrl/lg'), + minHeight: semanticTokenVar('size/ctrl/lg'), }, }); @@ -619,7 +632,8 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { // User provided class name state.root.className, - visualRefreshOverrides, + isVisualRefreshEnabled && visualRefreshOverrides, + isVisualRefreshEnabled && visualRefreshStyles[size], ); if (state.icon) { diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 3623cb6847a4a3..58f452eb6e92e5 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { tokens } from '@fluentui/react-theme'; export const VisualRefreshContext = React.createContext(false); @@ -6,24 +7,221 @@ export function useIsVisualRefreshEnabled() { return React.useContext(VisualRefreshContext); } -function createToken(name: string) { - return `var(--visual-refresh-${name})`; -} +export const MAI_SEMANTIC_TOKENS = { + 'stv1_size/ctrl/default': {}, + 'stv1_size/ctrl/sm': {}, + 'stv1_size/ctrl/lg': {}, + // Padding + 'stv1_padding/ctrl/textTop': {}, + 'stv1_padding/ctrl/textBottom': {}, + 'stv1_padding/ctrl/textSide': {}, + 'stv1_padding/ctrl/horizontal-default': {}, + 'stv1_padding/ctrl/horizontal-iconOnly': {}, + // Gap + 'stv1_gap/inside/ctrl/default': {}, + 'stv1_gap/inside/ctrl/toSecondaryIcon': {}, + // Corner + 'stv1_corner/ctrl/default': {}, + 'stv1_corner/ctrl/sm': {}, + 'stv1_corner/ctrl/lg': {}, + // Background + 'stv1_background/ctrl/neutral/rest': {}, + 'stv1_background/ctrl/neutral/hover': {}, + 'stv1_background/ctrl/neutral/pressed': {}, + 'stv1_background/ctrl/neutral/disabled': {}, + 'stv1_background/ctrl/neutral/selected/rest': {}, + 'stv1_background/ctrl/neutral/selected/hover': {}, + 'stv1_background/ctrl/neutral/selected/pressed': {}, + 'stv1_background/ctrl/neutral/selected/disabled': {}, + 'stv1_background/ctrl/brand/rest': {}, + 'stv1_background/ctrl/brand/hover': {}, + 'stv1_background/ctrl/brand/pressed': {}, + 'stv1_background/ctrl/brand/disabled': {}, + 'stv1_background/ctrl/brand/selected/rest': {}, + 'stv1_background/ctrl/brand/selected/hover': {}, + 'stv1_background/ctrl/brand/selected/pressed': {}, + 'stv1_background/ctrl/brand/selected/disabled': {}, + 'stv1_background/ctrl/outline/rest': {}, + 'stv1_background/ctrl/outline/hover': {}, + 'stv1_background/ctrl/outline/pressed': {}, + 'stv1_background/ctrl/outline/disabled': {}, + 'stv1_background/ctrl/outline/selected/rest': {}, + 'stv1_background/ctrl/outline/selected/hover': {}, + 'stv1_background/ctrl/outline/selected/pressed': {}, + 'stv1_background/ctrl/outline/selected/disabled': {}, + 'stv1_background/ctrl/subtle/rest': {}, + 'stv1_background/ctrl/subtle/hover': {}, + 'stv1_background/ctrl/subtle/pressed': {}, + 'stv1_background/ctrl/subtle/disabled': {}, + 'stv1_background/ctrl/subtle/selected/rest': {}, + 'stv1_background/ctrl/subtle/selected/hover': {}, + 'stv1_background/ctrl/subtle/selected/pressed': {}, + 'stv1_background/ctrl/subtle/selected/disabled': {}, + 'stv1_background/ctrl/transparent/rest': {}, + 'stv1_background/ctrl/transparent/hover': {}, + 'stv1_background/ctrl/transparent/pressed': {}, + 'stv1_background/ctrl/transparent/disabled': {}, + 'stv1_background/ctrl/transparent/selected/rest': {}, + 'stv1_background/ctrl/transparent/selected/hover': {}, + 'stv1_background/ctrl/transparent/selected/pressed': {}, + 'stv1_background/ctrl/transparent/selected/disabled': {}, + // Foreground" + 'stv1_foreground/ctrl/neutral/rest': {}, + 'stv1_foreground/ctrl/neutral/hover': {}, + 'stv1_foreground/ctrl/neutral/pressed': {}, + 'stv1_foreground/ctrl/neutral/disabled': {}, + 'stv1_foreground/ctrl/neutral/selected/rest': {}, + 'stv1_foreground/ctrl/neutral/selected/hover': {}, + 'stv1_foreground/ctrl/neutral/selected/pressed': {}, + 'stv1_foreground/ctrl/neutral/selected/disabled': {}, + 'stv1_foreground/ctrl/brand/rest': {}, + 'stv1_foreground/ctrl/brand/hover': {}, + 'stv1_foreground/ctrl/brand/pressed': {}, + 'stv1_foreground/ctrl/brand/disabled': {}, + 'stv1_foreground/ctrl/brand/selected/rest': {}, + 'stv1_foreground/ctrl/brand/selected/hover': {}, + 'stv1_foreground/ctrl/brand/selected/pressed': {}, + 'stv1_foreground/ctrl/brand/selected/disabled': {}, + 'stv1_foreground/ctrl/outline/rest': {}, + 'stv1_foreground/ctrl/outline/hover': {}, + 'stv1_foreground/ctrl/outline/pressed': {}, + 'stv1_foreground/ctrl/outline/disabled': {}, + 'stv1_foreground/ctrl/outline/selected/rest': {}, + 'stv1_foreground/ctrl/outline/selected/hover': {}, + 'stv1_foreground/ctrl/outline/selected/pressed': {}, + 'stv1_foreground/ctrl/outline/selected/disabled': {}, + 'stv1_foreground/ctrl/subtle/rest': {}, + 'stv1_foreground/ctrl/subtle/hover': {}, + 'stv1_foreground/ctrl/subtle/pressed': {}, + 'stv1_foreground/ctrl/subtle/disabled': {}, + 'stv1_foreground/ctrl/subtle/selected/rest': {}, + 'stv1_foreground/ctrl/subtle/selected/hover': {}, + 'stv1_foreground/ctrl/subtle/selected/pressed': {}, + 'stv1_foreground/ctrl/subtle/selected/disabled': {}, + 'stv1_foreground/ctrl/transparent/rest': {}, + 'stv1_foreground/ctrl/transparent/hover': {}, + 'stv1_foreground/ctrl/transparent/pressed': {}, + 'stv1_foreground/ctrl/transparent/disabled': {}, + 'stv1_foreground/ctrl/transparent/selected/rest': {}, + 'stv1_foreground/ctrl/transparent/selected/hover': {}, + 'stv1_foreground/ctrl/transparent/selected/pressed': {}, + 'stv1_foreground/ctrl/transparent/selected/disabled': {}, + // Stroke" + 'stv1_stroke/ctrl/neutral/rest': {}, + 'stv1_stroke/ctrl/neutral/hover': {}, + 'stv1_stroke/ctrl/neutral/pressed': {}, + 'stv1_stroke/ctrl/neutral/disabled': {}, + 'stv1_stroke/ctrl/neutral/selected/rest': {}, + 'stv1_stroke/ctrl/neutral/selected/hover': {}, + 'stv1_stroke/ctrl/neutral/selected/pressed': {}, + 'stv1_stroke/ctrl/neutral/selected/disabled': {}, + 'stv1_stroke/ctrl/brand/rest': {}, + 'stv1_stroke/ctrl/brand/hover': {}, + 'stv1_stroke/ctrl/brand/pressed': {}, + 'stv1_stroke/ctrl/brand/disabled': {}, + 'stv1_stroke/ctrl/brand/selected/rest': {}, + 'stv1_stroke/ctrl/brand/selected/hover': {}, + 'stv1_stroke/ctrl/brand/selected/pressed': {}, + 'stv1_stroke/ctrl/brand/selected/disabled': {}, + 'stv1_stroke/ctrl/outline/rest': {}, + 'stv1_stroke/ctrl/outline/hover': {}, + 'stv1_stroke/ctrl/outline/pressed': {}, + 'stv1_stroke/ctrl/outline/disabled': {}, + 'stv1_stroke/ctrl/outline/selected/rest': {}, + 'stv1_stroke/ctrl/outline/selected/hover': {}, + 'stv1_stroke/ctrl/outline/selected/pressed': {}, + 'stv1_stroke/ctrl/outline/selected/disabled': {}, + 'stv1_stroke/control/on/outline/rest': {}, + 'stv1_stroke/control/on/outline/hover': {}, + 'stv1_stroke/control/on/outline/pressed': {}, + 'stv1_stroke/control/on/outline/disabled': {}, + 'stv1_stroke/control/on/outline/selected/rest': {}, + 'stv1_stroke/control/on/outline/selected/hover': {}, + 'stv1_stroke/control/on/outline/selected/pressed': {}, + 'stv1_stroke/control/on/outline/selected/disabled': {}, + // Stroke Width" + 'stv1_strokeWidth/ctrl/outline/rest': {}, + 'stv1_strokeWidth/ctrl/outline/hover': {}, + 'stv1_strokeWidth/ctrl/outline/pressed': {}, + 'stv1_strokeWidth/ctrl/outline/disabled': {}, + 'stv1_strokeWidth/ctrl/outline/selected/rest': {}, + 'stv1_strokeWidth/ctrl/outline/selected/hover': {}, + 'stv1_strokeWidth/ctrl/outline/selected/pressed': {}, + 'stv1_strokeWidth/ctrl/outline/selected/disabled': {}, + 'stv1_strokeWidth/default': {}, + // Shadow" + 'stv1_shadow/ctrl/default/rest': {}, + 'stv1_shadow/ctrl/default/hover': {}, + 'stv1_shadow/ctrl/default/pressed': {}, + 'stv1_shadow/ctrl/default/disabled': {}, + 'stv1_shadow/ctrl/default/selected/rest': {}, + 'stv1_shadow/ctrl/default/selected/hover': {}, + 'stv1_shadow/ctrl/default/selected/pressed': {}, + 'stv1_shadow/ctrl/default/selected/disabled': {}, + 'stv1_shadow/ctrl/brand/rest': {}, + 'stv1_shadow/ctrl/brand/hover': {}, + 'stv1_shadow/ctrl/brand/pressed': {}, + 'stv1_shadow/ctrl/brand/disabled': {}, + 'stv1_shadow/ctrl/brand/selected/rest': {}, + 'stv1_shadow/ctrl/brand/selected/hover': {}, + 'stv1_shadow/ctrl/brand/selected/pressed': {}, + 'stv1_shadow/ctrl/brand/selected/disabled': {}, + 'stv1_shadow/ctrl/outline/rest': {}, + 'stv1_shadow/ctrl/outline/hover': {}, + 'stv1_shadow/ctrl/outline/pressed': {}, + 'stv1_shadow/ctrl/outline/disabled': {}, + 'stv1_shadow/ctrl/outline/selected/rest': {}, + 'stv1_shadow/ctrl/outline/selected/hover': {}, + 'stv1_shadow/ctrl/outline/selected/pressed': {}, + 'stv1_shadow/ctrl/outline/selected/disabled': {}, + // Icon Theme" + 'stv1_iconTheme/ctrl/default/rest': {}, + 'stv1_iconTheme/ctrl/default/hover': {}, + 'stv1_iconTheme/ctrl/default/pressed': {}, + 'stv1_iconTheme/ctrl/default/selected': {}, + 'stv1_iconTheme/ctrl/subtle/rest': {}, + 'stv1_iconTheme/ctrl/subtle/hover': {}, + 'stv1_iconTheme/ctrl/subtle/pressed': {}, + 'stv1_iconTheme/ctrl/subtle/selected': {}, + 'stv1_iconTheme/ctrl/chevron/default': {}, + 'stv1_iconTheme/ctrl/chevron/selected': {}, + // Icon Color" + 'stv1_iconColor/ctrl/default/rest': {}, + 'stv1_iconColor/ctrl/default/hover': {}, + 'stv1_iconColor/ctrl/default/pressed': {}, + 'stv1_iconColor/ctrl/default/selected': {}, + 'stv1_iconColor/ctrl/chevron/default': {}, + 'stv1_iconColor/ctrl/chevron/selected': {}, + // Typography" + 'stv1_fontSize/ctrl/default': {}, + 'stv1_fontWeight/ctrl/default': {}, + 'stv1_fontFamily/ctrl/default': {}, + 'stv1_lineHeight/ctrl/default': {}, + 'stv1_letterSpacing/ctrl/default': {}, + 'stv1_textStyle/ctrl/body': {}, + 'stv1_textStyle/ctrl/header': {}, +} as const satisfies { + [key: string]: any; +}; + +export const TEAMS_VISUAL_REFRESH_THEME = { + 'size/ctrl/default': '36px', + 'size/ctrl/sm': '28px', + 'size/ctrl/lg': '40px', +}; -export const VISUAL_REFRESH_TOKENS = { - buttonBorderRadius: createToken('buttonBorderRadius'), - buttonRootFontWeight: createToken('buttonRootFontWeight'), - buttonRootPadding: createToken('buttonRootPadding'), - buttonRootBorderWidth: createToken('buttonRootBorderWidth'), - buttonRoundedFontFamily: createToken('buttonRoundedFontFamily'), - buttonSquareFontWeight: createToken('buttonSquareFontWeight'), - buttonPrimaryBackgroundColor: createToken('buttonPrimaryBackgroundColor'), -} as const; +export const EXPECTED_SEMANTIC_V2_TOKENS = { + groupButtonBackground: 'background/ctrl/neutral/rest', // -> colorNeutralBackground1 +}; -type VisualRefreshToken = keyof typeof VISUAL_REFRESH_TOKENS; -type VisualRefreshTheme = Record; +export function sanitizeTokenName(token: string) { + return token.replace(/\//g, '_'); +} -export const GLOBAL_TOKENS = {}; +type TokenName = keyof typeof MAI_SEMANTIC_TOKENS; +export function semanticTokenVar(token: TokenName) { + return `var(--${sanitizeTokenName(token)})`; +} // Note: `VisualRefreshTheme` expects a flat key/value map, so the exported theme must stay flat. // Question to David: If grouping is needed, create intermediate objects @@ -42,4 +240,5 @@ export const TEAMS_VISUAL_REFRESH_TOKENS = { // Appearance tokens buttonPrimaryBackgroundColor: 'purple', + buttonHorizontalPadding: '12px', } satisfies VisualRefreshTheme; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 83b74a799ef2d2..674a2fd3f16fff 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -1,7 +1,12 @@ import * as React from 'react'; import type { JSXElement } from '@fluentui/react-components'; import { makeStyles, Button } from '@fluentui/react-components'; -import { TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext } from '@fluentui/visual-refresh-preview'; +import { + sanitizeTokenName, + TEAMS_VISUAL_REFRESH_THEME, + TEAMS_VISUAL_REFRESH_TOKENS, + VisualRefreshContext, +} from '@fluentui/visual-refresh-preview'; import { bundleIcon, CalendarMonthFilled, CalendarMonthRegular } from '@fluentui/react-icons'; const CalendarMonth = bundleIcon(CalendarMonthFilled, CalendarMonthRegular); @@ -10,6 +15,7 @@ const useStyles = makeStyles({ wrapper: { columnGap: '15px', display: 'flex', + flexDirection: 'column', minWidth: 'min-content', }, }); @@ -20,6 +26,9 @@ const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { for (const key of Object.keys(theme) as Array) { customProperties[`--visual-refresh-${key}`] = theme[key]; } + for (const key of Object.keys(TEAMS_VISUAL_REFRESH_THEME) as Array) { + customProperties[`--${sanitizeTokenName(key)}`] = TEAMS_VISUAL_REFRESH_THEME[key]; + } return (
{children}
@@ -29,12 +38,16 @@ const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { const VisualRefreshPreview = ({ children }: { children: React.ReactNode }) => { return ( -
-
{children}
-
+
+
+

V9 Theme

+
{children}
+

Visual Refresh Theme

- {children} + +
{children}
+
); @@ -44,7 +57,10 @@ const ButtonExamples = () => { const styles = useStyles(); return (
- + + + + From 90d535b22edc0bb6f86c96a1abd1b3262d97d357 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 13:31:22 -0400 Subject: [PATCH 06/17] feat(Button): enhance visual refresh styles with new token properties and button examples --- .../Button/useButtonStyles.styles.ts | 2 +- .../library/src/index.ts | 2 +- .../VisualRefresh/VisualRefresh.stories.tsx | 337 +++++++++++++++--- 3 files changed, 291 insertions(+), 50 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index a801ff7f9c431e..24c106490bca3f 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -553,7 +553,7 @@ const useIconStyles = makeStyles({ const useVisualRefreshStyles = makeStyles({ root: { - background: SEMANTIC_TOKENS.groupButtonBackground, + // background: SEMANTIC_TOKENS.groupButtonBackground, // fontWeight: VISUAL_REFRESH_TOKENS.buttonRootFontWeight, // padding: VISUAL_REFRESH_TOKENS.buttonRootPadding, // ...shorthands.borderWidth(VISUAL_REFRESH_TOKENS.buttonRootBorderWidth), diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 58f452eb6e92e5..317a1507f9ac30 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -241,4 +241,4 @@ export const TEAMS_VISUAL_REFRESH_TOKENS = { // Appearance tokens buttonPrimaryBackgroundColor: 'purple', buttonHorizontalPadding: '12px', -} satisfies VisualRefreshTheme; +}; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 674a2fd3f16fff..5534ceaa8da82a 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -1,22 +1,140 @@ import * as React from 'react'; import type { JSXElement } from '@fluentui/react-components'; -import { makeStyles, Button } from '@fluentui/react-components'; +import { Button, Input, Label, Select, makeStyles, useId } from '@fluentui/react-components'; +import type { ButtonProps, InputProps } from '@fluentui/react-components'; +import { tokens } from '@fluentui/react-theme'; import { sanitizeTokenName, TEAMS_VISUAL_REFRESH_THEME, TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext, } from '@fluentui/visual-refresh-preview'; -import { bundleIcon, CalendarMonthFilled, CalendarMonthRegular } from '@fluentui/react-icons'; +import { mergeClasses } from '@griffel/react'; -const CalendarMonth = bundleIcon(CalendarMonthFilled, CalendarMonthRegular); +type ComponentState = 'rest' | 'hover' | 'focus' | 'disabled'; -const useStyles = makeStyles({ - wrapper: { - columnGap: '15px', +const buttonStateOrder: ComponentState[] = ['rest', 'hover', 'focus', 'disabled']; +const buttonStateLabels: Record = { + rest: 'Rest', + hover: 'Hover', + focus: 'Focus', + disabled: 'Disabled', +}; + +const useStoryStyles = makeStyles({ + container: { display: 'flex', flexDirection: 'column', - minWidth: 'min-content', + gap: '1.5rem', + maxWidth: 'max-content', + }, + controls: { + display: 'flex', + alignItems: 'center', + gap: '0.75rem', + }, + select: { + width: '200px', + }, + table: { + borderCollapse: 'collapse', + minWidth: '720px', + }, + headerCell: { + borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, + color: tokens.colorNeutralForeground2, + fontWeight: tokens.fontWeightRegular, + padding: '0.75rem', + textAlign: 'left', + }, + componentCell: { + borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, + fontWeight: tokens.fontWeightSemibold, + padding: '0.75rem', + verticalAlign: 'top', + width: '160px', + }, + variantCell: { + borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, + padding: '0.75rem', + verticalAlign: 'top', + width: '180px', + }, + stateCell: { + borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, + padding: '0.75rem', + textAlign: 'center', + width: '140px', + }, + stateContent: { + display: 'flex', + justifyContent: 'center', + }, +}); + +const useButtonStateStyles = makeStyles({ + base: {}, + hoverSecondary: { + backgroundColor: tokens.colorNeutralBackground1Hover, + borderColor: tokens.colorNeutralStroke1Hover, + color: tokens.colorNeutralForeground1Hover, + cursor: 'pointer', + }, + hoverPrimary: { + backgroundColor: tokens.colorBrandBackgroundHover, + borderColor: 'transparent', + color: tokens.colorNeutralForegroundOnBrand, + cursor: 'pointer', + }, + hoverSubtle: { + backgroundColor: tokens.colorSubtleBackgroundHover, + borderColor: 'transparent', + color: tokens.colorNeutralForeground2Hover, + cursor: 'pointer', + }, + hoverTransparent: { + backgroundColor: tokens.colorTransparentBackgroundHover, + borderColor: 'transparent', + color: tokens.colorNeutralForeground2BrandHover, + cursor: 'pointer', + }, + focus: { + borderColor: tokens.colorStrokeFocus2, + boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorStrokeFocus2} inset`, + outline: `${tokens.strokeWidthThick} solid ${tokens.colorTransparentStroke}`, + outlineOffset: '2px', + }, +}); + +const useInputStateStyles = makeStyles({ + base: { + width: '200px', + }, + hoverOutline: { + borderColor: tokens.colorNeutralStroke1Hover, + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + cursor: 'text', + }, + hoverUnderline: { + borderBottomColor: tokens.colorNeutralStrokeAccessibleHover, + cursor: 'text', + }, + focusOutline: { + borderColor: tokens.colorNeutralStroke1Pressed, + borderBottomColor: tokens.colorNeutralStrokeAccessiblePressed, + cursor: 'text', + '::after': { + borderBottomColor: tokens.colorCompoundBrandStroke, + transform: 'scaleX(1)', + }, + }, + focusUnderline: { + borderBottomColor: tokens.colorCompoundBrandStroke, + cursor: 'text', + '::after': { + borderBottomColor: tokens.colorCompoundBrandStroke, + transform: 'scaleX(1)', + }, }, }); @@ -36,62 +154,185 @@ const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { ); }; -const VisualRefreshPreview = ({ children }: { children: React.ReactNode }) => { +const ButtonStateCell = ({ + appearance, + state, + children, +}: { + appearance?: ButtonProps['appearance']; + state: ComponentState; + children: React.ReactNode; +}) => { + const buttonStateClasses = useButtonStateStyles(); + const hoverClass = + state === 'hover' + ? appearance === 'primary' + ? buttonStateClasses.hoverPrimary + : appearance === 'subtle' + ? buttonStateClasses.hoverSubtle + : appearance === 'transparent' + ? buttonStateClasses.hoverTransparent + : buttonStateClasses.hoverSecondary + : undefined; + const focusClass = state === 'focus' ? buttonStateClasses.focus : undefined; + const className = mergeClasses(buttonStateClasses.base, hoverClass, focusClass); + return ( -
-
-

V9 Theme

-
{children}
-
-
-

Visual Refresh Theme

- -
{children}
-
-
-
+ ); }; -const ButtonExamples = () => { - const styles = useStyles(); +const InputStateCell = ({ + appearance, + state, + defaultValue, + disabledValue, +}: { + appearance: NonNullable; + state: ComponentState; + defaultValue: string; + disabledValue: string; +}) => { + const inputStateClasses = useInputStateStyles(); + const hoverClass = + state === 'hover' + ? appearance === 'underline' + ? inputStateClasses.hoverUnderline + : inputStateClasses.hoverOutline + : undefined; + const focusClass = + state === 'focus' + ? appearance === 'underline' + ? inputStateClasses.focusUnderline + : inputStateClasses.focusOutline + : undefined; + const className = mergeClasses(inputStateClasses.base, hoverClass, focusClass); + return ( -
- - - - - - - - - - - -
+ ); }; -export const VisualRefresh = (): JSXElement => { +const ComponentStatesTable = () => { + const styles = useStoryStyles(); + + const buttonVariants: Array<{ label: string; appearance?: ButtonProps['appearance']; content: string }> = [ + { label: 'Secondary', appearance: 'secondary', content: 'Secondary' }, + { label: 'Primary', appearance: 'primary', content: 'Primary' }, + { label: 'Subtle', appearance: 'subtle', content: 'Subtle' }, + ]; + + const inputVariants: Array<{ + label: string; + appearance: NonNullable; + defaultValue: string; + disabledValue: string; + }> = [ + { label: 'Outline', appearance: 'outline', defaultValue: 'Outline input', disabledValue: 'Outline disabled' }, + { label: 'Underline', appearance: 'underline', defaultValue: 'Underline input', disabledValue: 'Underline disabled' }, + ]; + return ( - - - + + + + + + {buttonStateOrder.map(state => ( + + ))} + + + + + {buttonVariants.map((variant, index) => ( + + {index === 0 && ( + + )} + + {buttonStateOrder.map(state => ( + + ))} + + ))} + + + {inputVariants.map((variant, index) => ( + + {index === 0 && ( + + )} + + {buttonStateOrder.map(state => ( + + ))} + + ))} + + +
ComponentVariant + {buttonStateLabels[state]} +
+ Button + {variant.label} +
+ + {state === 'disabled' ? 'Disabled' : variant.content} + +
+
+ Input + {variant.label} +
+ +
+
); }; +export const VisualRefresh = (): JSXElement => { + const styles = useStoryStyles(); + const selectId = useId('visual-refresh-toggle'); + const [isVisualRefreshEnabled, setIsVisualRefreshEnabled] = React.useState<'off' | 'on'>('off'); + + const handleChange = (event: React.ChangeEvent) => { + setIsVisualRefreshEnabled(event.target.value as 'off' | 'on'); + }; + + const content = ( +
+
+ + +
+ +
+ ); + + return isVisualRefreshEnabled === 'on' ? {content} : content; +}; + VisualRefresh.parameters = { docs: { description: { - story: 'A button can be rounded, circular, or square.', + story: 'Compare Button and Input variants across interaction states with and without the visual refresh theme.', }, }, }; From b9f1db9f9bbdf10b8c114c8e7ab643e4b9e99ce1 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 13:44:17 -0400 Subject: [PATCH 07/17] feat(Button): update button styles for visual refresh and add control size selection in stories --- .../Button/useButtonStyles.styles.ts | 3 +- .../VisualRefresh/VisualRefresh.stories.tsx | 51 +++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 24c106490bca3f..6888af7c070662 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -569,8 +569,9 @@ const useVisualRefreshStyles = makeStyles({ // backgroundColor: VISUAL_REFRESH_TOKENS.buttonPrimaryBackgroundColor, }, small: { - height: semanticTokenVar('size/ctrl/sm'), minHeight: semanticTokenVar('size/ctrl/sm'), + height: semanticTokenVar('size/ctrl/sm'), + maxHeight: semanticTokenVar('size/ctrl/sm'), }, medium: { height: semanticTokenVar('size/ctrl/default'), diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 5534ceaa8da82a..d58da6dc42f643 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -32,10 +32,16 @@ const useStoryStyles = makeStyles({ display: 'flex', alignItems: 'center', gap: '0.75rem', + flexWrap: 'wrap', }, select: { width: '200px', }, + controlItem: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + }, table: { borderCollapse: 'collapse', minWidth: '720px', @@ -73,7 +79,9 @@ const useStoryStyles = makeStyles({ }); const useButtonStateStyles = makeStyles({ - base: {}, + base: { + pointerEvents: 'none', + }, hoverSecondary: { backgroundColor: tokens.colorNeutralBackground1Hover, borderColor: tokens.colorNeutralStroke1Hover, @@ -109,6 +117,7 @@ const useButtonStateStyles = makeStyles({ const useInputStateStyles = makeStyles({ base: { width: '200px', + pointerEvents: 'none', }, hoverOutline: { borderColor: tokens.colorNeutralStroke1Hover, @@ -158,10 +167,12 @@ const ButtonStateCell = ({ appearance, state, children, + size, }: { appearance?: ButtonProps['appearance']; state: ComponentState; children: React.ReactNode; + size: ButtonProps['size']; }) => { const buttonStateClasses = useButtonStateStyles(); const hoverClass = @@ -178,7 +189,7 @@ const ButtonStateCell = ({ const className = mergeClasses(buttonStateClasses.base, hoverClass, focusClass); return ( - ); @@ -189,11 +200,13 @@ const InputStateCell = ({ state, defaultValue, disabledValue, + size, }: { appearance: NonNullable; state: ComponentState; defaultValue: string; disabledValue: string; + size: NonNullable; }) => { const inputStateClasses = useInputStateStyles(); const hoverClass = @@ -216,12 +229,13 @@ const InputStateCell = ({ className={className} defaultValue={state === 'disabled' ? disabledValue : defaultValue} disabled={state === 'disabled'} + size={size} readOnly={state !== 'disabled'} /> ); }; -const ComponentStatesTable = () => { +const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size'] }) => { const styles = useStoryStyles(); const buttonVariants: Array<{ label: string; appearance?: ButtonProps['appearance']; content: string }> = [ @@ -266,7 +280,7 @@ const ComponentStatesTable = () => { {buttonStateOrder.map(state => (
- + {state === 'disabled' ? 'Disabled' : variant.content}
@@ -292,6 +306,7 @@ const ComponentStatesTable = () => { state={state} defaultValue={variant.defaultValue} disabledValue={variant.disabledValue} + size={controlSize} />
@@ -307,22 +322,38 @@ const ComponentStatesTable = () => { export const VisualRefresh = (): JSXElement => { const styles = useStoryStyles(); const selectId = useId('visual-refresh-toggle'); + const sizeSelectId = useId('visual-refresh-size'); const [isVisualRefreshEnabled, setIsVisualRefreshEnabled] = React.useState<'off' | 'on'>('off'); + const [controlSize, setControlSize] = React.useState('medium'); const handleChange = (event: React.ChangeEvent) => { setIsVisualRefreshEnabled(event.target.value as 'off' | 'on'); }; + const handleSizeChange = (event: React.ChangeEvent) => { + setControlSize(event.target.value as ButtonProps['size']); + }; + const content = (
- - +
+ + +
+
+ + +
- +
); From f8637cd9f5c2107ec97548b961be6582d88233ed Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 16:18:02 -0400 Subject: [PATCH 08/17] feat(Button): update button styles for visual refresh with new padding, border radius, and font properties feat(VisualRefresh): enhance theme tokens for improved control sizes and add switch for visual refresh toggle --- .../Button/useButtonStyles.styles.ts | 186 ++++++++++++++++-- .../library/src/index.ts | 121 +++++++++--- .../VisualRefresh/VisualRefresh.stories.tsx | 49 +++-- 3 files changed, 298 insertions(+), 58 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 6888af7c070662..04ab7faba474f2 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -552,34 +552,188 @@ const useIconStyles = makeStyles({ }); const useVisualRefreshStyles = makeStyles({ - root: { - // background: SEMANTIC_TOKENS.groupButtonBackground, - // fontWeight: VISUAL_REFRESH_TOKENS.buttonRootFontWeight, - // padding: VISUAL_REFRESH_TOKENS.buttonRootPadding, - // ...shorthands.borderWidth(VISUAL_REFRESH_TOKENS.buttonRootBorderWidth), - }, - rounded: { - // borderRadius: VISUAL_REFRESH_TOKENS.buttonBorderRadius, - // fontFamily: VISUAL_REFRESH_TOKENS.buttonRoundedFontFamily, + base: { + backgroundColor: semanticTokenVar('background/ctrl/neutral/rest'), + ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/rest')), + ':hover': { + backgroundColor: semanticTokenVar('background/ctrl/neutral/hover'), + ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/hover')), + }, + ':hover:active': { + backgroundColor: semanticTokenVar('background/ctrl/neutral/pressed'), + ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/pressed')), + }, + ':disabled': { + backgroundColor: semanticTokenVar('background/ctrl/neutral/disabled'), + ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/disabled')), + }, }, - square: { - // fontWeight: VISUAL_REFRESH_TOKENS.buttonSquareFontWeight, + + // Appearance variations + outline: { + backgroundColor: tokens.colorTransparentBackground, + + ':hover': { + backgroundColor: tokens.colorTransparentBackgroundHover, + }, + + ':hover:active': { + backgroundColor: tokens.colorTransparentBackgroundPressed, + }, }, primary: { - // backgroundColor: VISUAL_REFRESH_TOKENS.buttonPrimaryBackgroundColor, + backgroundColor: tokens.colorBrandBackground, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForegroundOnBrand, + + ':hover': { + backgroundColor: tokens.colorBrandBackgroundHover, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForegroundOnBrand, + }, + + ':hover:active': { + backgroundColor: tokens.colorBrandBackgroundPressed, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForegroundOnBrand, + }, + + '@media (forced-colors: active)': { + backgroundColor: 'Highlight', + ...shorthands.borderColor('HighlightText'), + color: 'HighlightText', + forcedColorAdjust: 'none', + + ':hover': { + backgroundColor: 'HighlightText', + ...shorthands.borderColor('Highlight'), + color: 'Highlight', + }, + + ':hover:active': { + backgroundColor: 'HighlightText', + ...shorthands.borderColor('Highlight'), + color: 'Highlight', + }, + }, }, + secondary: { + /* The secondary styles are exactly the same as the base styles. */ + }, + subtle: { + backgroundColor: tokens.colorSubtleBackground, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2, + + ':hover': { + backgroundColor: tokens.colorSubtleBackgroundHover, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2Hover, + [`& .${iconFilledClassName}`]: { + display: 'inline', + }, + [`& .${iconRegularClassName}`]: { + display: 'none', + }, + [`& .${buttonClassNames.icon}`]: { + color: tokens.colorNeutralForeground2BrandHover, + }, + }, + + ':hover:active': { + backgroundColor: tokens.colorSubtleBackgroundPressed, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2Pressed, + [`& .${iconFilledClassName}`]: { + display: 'inline', + }, + [`& .${iconRegularClassName}`]: { + display: 'none', + }, + [`& .${buttonClassNames.icon}`]: { + color: tokens.colorNeutralForeground2BrandPressed, + }, + }, + + '@media (forced-colors: active)': { + ':hover': { + color: 'Highlight', + + [`& .${buttonClassNames.icon}`]: { + color: 'Highlight', + }, + }, + ':hover:active': { + color: 'Highlight', + + [`& .${buttonClassNames.icon}`]: { + color: 'Highlight', + }, + }, + }, + }, + transparent: { + backgroundColor: tokens.colorTransparentBackground, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2, + + ':hover': { + backgroundColor: tokens.colorTransparentBackgroundHover, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2BrandHover, + [`& .${iconFilledClassName}`]: { + display: 'inline', + }, + [`& .${iconRegularClassName}`]: { + display: 'none', + }, + }, + + ':hover:active': { + backgroundColor: tokens.colorTransparentBackgroundPressed, + ...shorthands.borderColor('transparent'), + color: tokens.colorNeutralForeground2BrandPressed, + [`& .${iconFilledClassName}`]: { + display: 'inline', + }, + [`& .${iconRegularClassName}`]: { + display: 'none', + }, + }, + }, + small: { minHeight: semanticTokenVar('size/ctrl/sm'), height: semanticTokenVar('size/ctrl/sm'), - maxHeight: semanticTokenVar('size/ctrl/sm'), + + padding: `${semanticTokenVar('padding/ctrl/vertical/sm')} ${semanticTokenVar('padding/ctrl/horizontal/sm')}`, + borderRadius: semanticTokenVar('corner/ctrl/sm'), + + fontSize: semanticTokenVar('fontSize/ctrl/sm'), + fontWeight: semanticTokenVar('lineHeight/ctrl/sm'), + lineHeight: semanticTokenVar('fontWeight/ctrl/sm'), }, medium: { - height: semanticTokenVar('size/ctrl/default'), - minHeight: semanticTokenVar('size/ctrl/default'), + minHeight: semanticTokenVar('size/ctrl/md'), + height: semanticTokenVar('size/ctrl/md'), + + padding: `${semanticTokenVar('padding/ctrl/vertical/md')} ${semanticTokenVar('padding/ctrl/horizontal/md')}`, + borderRadius: semanticTokenVar('corner/ctrl/md'), + + fontSize: semanticTokenVar('fontSize/ctrl/md'), + fontWeight: semanticTokenVar('lineHeight/ctrl/md'), + lineHeight: semanticTokenVar('fontWeight/ctrl/md'), }, large: { - height: semanticTokenVar('size/ctrl/lg'), minHeight: semanticTokenVar('size/ctrl/lg'), + height: semanticTokenVar('size/ctrl/lg'), + + padding: `${semanticTokenVar('padding/ctrl/vertical/lg')} ${semanticTokenVar('padding/ctrl/horizontal/lg')}`, + borderRadius: semanticTokenVar('corner/ctrl/lg'), + + fontSize: semanticTokenVar('fontSize/ctrl/lg'), + fontWeight: semanticTokenVar('lineHeight/ctrl/lg'), + lineHeight: semanticTokenVar('fontWeight/ctrl/lg'), }, }); diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 317a1507f9ac30..68de9430273fe5 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -204,12 +204,109 @@ export const MAI_SEMANTIC_TOKENS = { [key: string]: any; }; +/** + * Notes + * 1. sm, default, lg -> sm, md, lg. + * 2. padding/ctrl/horizontal-default -> padding/ctrl/horizontal/md + * 3. gap/inside/ctrl/default -> gap/ctrl/md + * 4. fontWeight, lineHeight - ? + * 5. @media (forced-colors: active) ? + * 6. background/ctrl/neutral/selected - Selected? + */ + export const TEAMS_VISUAL_REFRESH_THEME = { - 'size/ctrl/default': '36px', + 'size/ctrl/md': '36px', 'size/ctrl/sm': '28px', 'size/ctrl/lg': '40px', + // Button + // Padding + 'padding/ctrl/horizontal/sm': '8px', + 'padding/ctrl/horizontal/md': '12px', + 'padding/ctrl/horizontal/lg': '12px', + + 'padding/ctrl/vertical/sm': '4px', + 'padding/ctrl/vertical/md': '8px', + 'padding/ctrl/vertical/lg': '10px', + // Gap + 'gap/ctrl/sm': '4px', + 'gap/ctrl/md': '6px', + 'gap/ctrl/lg': '6px', + + // Border radius + 'corner/ctrl/sm': '12px', + 'corner/ctrl/md': '12px', + 'corner/ctrl/lg': '12px', + + // Font + 'fontSize/ctrl/sm': '12px', + 'fontSize/ctrl/md': '14px', + 'fontSize/ctrl/lg': '14px', + + 'lineHeight/ctrl/sm': '16px', + 'lineHeight/ctrl/md': '20px', + 'lineHeight/ctrl/lg': '20px', + + 'fontWeight/ctrl/sm': tokens.fontWeightRegular, + 'fontWeight/ctrl/md': tokens.fontWeightRegular, + 'fontWeight/ctrl/lg': tokens.fontWeightRegular, + + // Background + 'background/ctrl/neutral/rest': 'hsl(0, 0%, 98%)', + 'background/ctrl/neutral/hover': 'hsl(0, 0%, 94%)', + 'background/ctrl/neutral/pressed': 'hsl(0, 0%, 86%)', + 'background/ctrl/neutral/disabled': 'hsl(0, 0%, 94%)', + + 'background/ctrl/brand/rest': 'hsl(182, 95%, 25%)', + 'background/ctrl/brand/hover': 'hsl(183, 100%, 21%)', + 'background/ctrl/brand/pressed': 'hsl(184, 100%, 12%)', + 'background/ctrl/brand/disabled': 'hsl(0, 0%, 94%)', + + 'background/ctrl/outline/rest': {}, + 'background/ctrl/outline/hover': {}, + 'background/ctrl/outline/pressed': {}, + 'background/ctrl/outline/disabled': {}, + + 'background/ctrl/subtle/rest': {}, + 'background/ctrl/subtle/hover': {}, + 'background/ctrl/subtle/pressed': {}, + 'background/ctrl/subtle/disabled': {}, + + 'background/ctrl/transparent/rest': {}, + 'background/ctrl/transparent/hover': {}, + 'background/ctrl/transparent/pressed': {}, + 'background/ctrl/transparent/disabled': {}, + + // Border + 'borderColor/ctrl/neutral/rest': '#D1D1D1', + 'borderColor/ctrl/neutral/hover': '#C7C7C7', + 'borderColor/ctrl/neutral/pressed': '#B3B3B3', + 'borderColor/ctrl/neutral/disabled': '#E0E0E0', + + 'borderColor/ctrl/brand/rest': 'transparent', // Acturally should be the same as background, if not sizes would be different + 'borderColor/ctrl/brand/hover': 'transparent', + 'borderColor/ctrl/brand/pressed': 'transparent', + 'borderColor/ctrl/brand/disabled': 'transparent', + + 'borderColor/ctrl/outline/rest': {}, + 'borderColor/ctrl/outline/hover': {}, + 'borderColor/ctrl/outline/pressed': {}, + 'borderColor/ctrl/outline/disabled': {}, + + 'borderColor/ctrl/subtle/rest': {}, + 'borderColor/ctrl/subtle/hover': {}, + 'borderColor/ctrl/subtle/pressed': {}, + 'borderColor/ctrl/subtle/disabled': {}, + + 'borderColor/ctrl/transparent/rest': {}, + 'borderColor/ctrl/transparent/hover': {}, + 'borderColor/ctrl/transparent/pressed': {}, + 'borderColor/ctrl/transparent/disabled': {}, }; +export const TEAMS_VISUAL_REFRESH_TOKENS = Object.fromEntries( + Object.entries(TEAMS_VISUAL_REFRESH_THEME).map(([key, value]) => [sanitizeTokenName(key), String(value)]), +) as Record; + export const EXPECTED_SEMANTIC_V2_TOKENS = { groupButtonBackground: 'background/ctrl/neutral/rest', // -> colorNeutralBackground1 }; @@ -218,27 +315,7 @@ export function sanitizeTokenName(token: string) { return token.replace(/\//g, '_'); } -type TokenName = keyof typeof MAI_SEMANTIC_TOKENS; +type TokenName = keyof typeof MAI_SEMANTIC_TOKENS | keyof typeof TEAMS_VISUAL_REFRESH_THEME; export function semanticTokenVar(token: TokenName) { return `var(--${sanitizeTokenName(token)})`; } - -// Note: `VisualRefreshTheme` expects a flat key/value map, so the exported theme must stay flat. -// Question to David: If grouping is needed, create intermediate objects -// (e.g. `const buttonShape = { ... }`) and spread them here. -export const TEAMS_VISUAL_REFRESH_TOKENS = { - // Button - // Shape tokens - buttonBorderRadius: '16px', - buttonRoundedFontFamily: '"Comic Sans MS", "Comic Sans", cursive', - buttonSquareFontWeight: '800', - - // Root tokens - buttonRootFontWeight: '400', - buttonRootPadding: '8px 16px', - buttonRootBorderWidth: '2px', - - // Appearance tokens - buttonPrimaryBackgroundColor: 'purple', - buttonHorizontalPadding: '12px', -}; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index d58da6dc42f643..6eb8d3f551de95 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { JSXElement } from '@fluentui/react-components'; -import { Button, Input, Label, Select, makeStyles, useId } from '@fluentui/react-components'; -import type { ButtonProps, InputProps } from '@fluentui/react-components'; +import { Button, Input, Label, Select, Switch, makeStyles, useId } from '@fluentui/react-components'; +import type { ButtonProps, InputProps, SwitchOnChangeData } from '@fluentui/react-components'; import { tokens } from '@fluentui/react-theme'; import { sanitizeTokenName, @@ -50,6 +50,7 @@ const useStoryStyles = makeStyles({ borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, color: tokens.colorNeutralForeground2, fontWeight: tokens.fontWeightRegular, + opacity: '0.8', padding: '0.75rem', textAlign: 'left', }, @@ -148,13 +149,12 @@ const useInputStateStyles = makeStyles({ }); const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { - const theme = TEAMS_VISUAL_REFRESH_TOKENS; const customProperties: Record = {}; - for (const key of Object.keys(theme) as Array) { - customProperties[`--visual-refresh-${key}`] = theme[key]; + for (const [key, value] of Object.entries(TEAMS_VISUAL_REFRESH_TOKENS ?? {})) { + customProperties[`--visual-refresh-${key}`] = value; } - for (const key of Object.keys(TEAMS_VISUAL_REFRESH_THEME) as Array) { - customProperties[`--${sanitizeTokenName(key)}`] = TEAMS_VISUAL_REFRESH_THEME[key]; + for (const [key, value] of Object.entries(TEAMS_VISUAL_REFRESH_THEME)) { + customProperties[`--${sanitizeTokenName(key)}`] = String(value); } return ( @@ -239,9 +239,12 @@ const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size' const styles = useStoryStyles(); const buttonVariants: Array<{ label: string; appearance?: ButtonProps['appearance']; content: string }> = [ - { label: 'Secondary', appearance: 'secondary', content: 'Secondary' }, + { label: 'Outline', appearance: 'outline', content: 'Outline' }, { label: 'Primary', appearance: 'primary', content: 'Primary' }, + { label: 'Secondary', appearance: 'secondary', content: 'Secondary' }, { label: 'Subtle', appearance: 'subtle', content: 'Subtle' }, + { label: 'Transparent', appearance: 'transparent', content: 'Transparent' }, + { label: 'Tint', appearance: 'secondary', content: 'Tint' }, ]; const inputVariants: Array<{ @@ -251,7 +254,12 @@ const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size' disabledValue: string; }> = [ { label: 'Outline', appearance: 'outline', defaultValue: 'Outline input', disabledValue: 'Outline disabled' }, - { label: 'Underline', appearance: 'underline', defaultValue: 'Underline input', disabledValue: 'Underline disabled' }, + { + label: 'Underline', + appearance: 'underline', + defaultValue: 'Underline input', + disabledValue: 'Underline disabled', + }, ]; return ( @@ -321,13 +329,13 @@ const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size' export const VisualRefresh = (): JSXElement => { const styles = useStoryStyles(); - const selectId = useId('visual-refresh-toggle'); + const switchId = useId('visual-refresh-toggle'); const sizeSelectId = useId('visual-refresh-size'); - const [isVisualRefreshEnabled, setIsVisualRefreshEnabled] = React.useState<'off' | 'on'>('off'); + const [isVisualRefreshEnabled, setIsVisualRefreshEnabled] = React.useState(false); const [controlSize, setControlSize] = React.useState('medium'); - const handleChange = (event: React.ChangeEvent) => { - setIsVisualRefreshEnabled(event.target.value as 'off' | 'on'); + const handleThemeChange = (_event: React.ChangeEvent, data: SwitchOnChangeData) => { + setIsVisualRefreshEnabled(Boolean(data.checked)); }; const handleSizeChange = (event: React.ChangeEvent) => { @@ -338,17 +346,18 @@ export const VisualRefresh = (): JSXElement => {
- - +
@@ -357,7 +366,7 @@ export const VisualRefresh = (): JSXElement => {
); - return isVisualRefreshEnabled === 'on' ? {content} : content; + return isVisualRefreshEnabled ? {content} : content; }; VisualRefresh.parameters = { From e3a8d3357cee9824f7277a140841dbb508e3dfc1 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 16:28:28 -0400 Subject: [PATCH 09/17] feat(Button): refactor visual refresh styles to use semantic tokens and improve appearance variants --- .../Button/useButtonStyles.styles.ts | 183 +++++------------- 1 file changed, 52 insertions(+), 131 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 04ab7faba474f2..defae0c8dd700d 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -4,6 +4,7 @@ import { iconFilledClassName, iconRegularClassName } from '@fluentui/react-icons import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; import { tokens } from '@fluentui/react-theme'; import { shorthands, makeStyles, makeResetStyles, mergeClasses } from '@griffel/react'; +import type { GriffelStyle } from '@griffel/react'; import { semanticTokenVar, useIsVisualRefreshEnabled } from '@fluentui/visual-refresh-preview'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { ButtonSlots, ButtonState } from './Button.types'; @@ -551,84 +552,44 @@ const useIconStyles = makeStyles({ }, }); -const useVisualRefreshStyles = makeStyles({ - base: { - backgroundColor: semanticTokenVar('background/ctrl/neutral/rest'), - ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/rest')), - ':hover': { - backgroundColor: semanticTokenVar('background/ctrl/neutral/hover'), - ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/hover')), - }, - ':hover:active': { - backgroundColor: semanticTokenVar('background/ctrl/neutral/pressed'), - ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/pressed')), - }, - ':disabled': { - backgroundColor: semanticTokenVar('background/ctrl/neutral/disabled'), - ...shorthands.borderColor(semanticTokenVar('borderColor/ctrl/neutral/disabled')), - }, - }, - - // Appearance variations - outline: { - backgroundColor: tokens.colorTransparentBackground, - - ':hover': { - backgroundColor: tokens.colorTransparentBackgroundHover, - }, - - ':hover:active': { - backgroundColor: tokens.colorTransparentBackgroundPressed, - }, - }, - primary: { - backgroundColor: tokens.colorBrandBackground, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForegroundOnBrand, - - ':hover': { - backgroundColor: tokens.colorBrandBackgroundHover, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForegroundOnBrand, - }, - - ':hover:active': { - backgroundColor: tokens.colorBrandBackgroundPressed, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForegroundOnBrand, - }, - - '@media (forced-colors: active)': { - backgroundColor: 'Highlight', - ...shorthands.borderColor('HighlightText'), - color: 'HighlightText', - forcedColorAdjust: 'none', +type VisualRefreshAppearanceVariant = + | 'base' + | 'neutral' + | 'secondary' + | 'brand' + | 'primary' + | 'outline' + | 'subtle' + | 'transparent'; + +const visualRefreshAppearanceAlias: Record< + VisualRefreshAppearanceVariant, + 'neutral' | 'brand' | 'outline' | 'subtle' | 'transparent' +> = { + base: 'neutral', + neutral: 'neutral', + secondary: 'neutral', + brand: 'brand', + primary: 'brand', + outline: 'outline', + subtle: 'subtle', + transparent: 'transparent', +}; - ':hover': { - backgroundColor: 'HighlightText', - ...shorthands.borderColor('Highlight'), - color: 'Highlight', - }, +type SemanticTokenName = Parameters[0]; - ':hover:active': { - backgroundColor: 'HighlightText', - ...shorthands.borderColor('Highlight'), - color: 'Highlight', - }, - }, - }, - secondary: { - /* The secondary styles are exactly the same as the base styles. */ - }, - subtle: { - backgroundColor: tokens.colorSubtleBackground, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2, +const getSemanticTokenValue = (token: string) => semanticTokenVar(token as SemanticTokenName); +const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearanceVariant): GriffelStyle => { + const tokenGroup = visualRefreshAppearanceAlias[appearance] ?? 'neutral'; + const backgroundTokenBase = `background/ctrl/${tokenGroup}`; + const borderTokenBase = `borderColor/ctrl/${tokenGroup}`; + return { + backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/rest`), + ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/rest`)), ':hover': { - backgroundColor: tokens.colorSubtleBackgroundHover, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2Hover, + backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/hover`), + ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/hover`)), [`& .${iconFilledClassName}`]: { display: 'inline', }, @@ -636,71 +597,31 @@ const useVisualRefreshStyles = makeStyles({ display: 'none', }, [`& .${buttonClassNames.icon}`]: { - color: tokens.colorNeutralForeground2BrandHover, + // color: tokens.colorNeutralForeground2BrandHover, }, }, - ':hover:active': { - backgroundColor: tokens.colorSubtleBackgroundPressed, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2Pressed, - [`& .${iconFilledClassName}`]: { - display: 'inline', - }, - [`& .${iconRegularClassName}`]: { - display: 'none', - }, - [`& .${buttonClassNames.icon}`]: { - color: tokens.colorNeutralForeground2BrandPressed, - }, + backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/pressed`), + ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/pressed`)), }, - - '@media (forced-colors: active)': { - ':hover': { - color: 'Highlight', - - [`& .${buttonClassNames.icon}`]: { - color: 'Highlight', - }, - }, - ':hover:active': { - color: 'Highlight', - - [`& .${buttonClassNames.icon}`]: { - color: 'Highlight', - }, - }, + ':disabled': { + backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/disabled`), + ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/disabled`)), }, - }, - transparent: { - backgroundColor: tokens.colorTransparentBackground, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2, + }; +}; - ':hover': { - backgroundColor: tokens.colorTransparentBackgroundHover, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2BrandHover, - [`& .${iconFilledClassName}`]: { - display: 'inline', - }, - [`& .${iconRegularClassName}`]: { - display: 'none', - }, - }, +const useVisualRefreshStyles = makeStyles({ + base: createVisualRefreshAppearanceStyles('neutral'), - ':hover:active': { - backgroundColor: tokens.colorTransparentBackgroundPressed, - ...shorthands.borderColor('transparent'), - color: tokens.colorNeutralForeground2BrandPressed, - [`& .${iconFilledClassName}`]: { - display: 'inline', - }, - [`& .${iconRegularClassName}`]: { - display: 'none', - }, - }, + // Appearance variations + outline: createVisualRefreshAppearanceStyles('outline'), + primary: createVisualRefreshAppearanceStyles('brand'), + secondary: { + // same as base }, + subtle: createVisualRefreshAppearanceStyles('subtle'), + transparent: createVisualRefreshAppearanceStyles('transparent'), small: { minHeight: semanticTokenVar('size/ctrl/sm'), From 9e1dc9fd951176f03f20fc8eaea009525fe0f5db Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 16:33:40 -0400 Subject: [PATCH 10/17] feat(VisualRefresh): update semantic tokens to use empty strings for improved consistency --- .../library/src/index.ts | 410 +++++++++--------- 1 file changed, 205 insertions(+), 205 deletions(-) diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 68de9430273fe5..949391ecc3282b 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -8,198 +8,198 @@ export function useIsVisualRefreshEnabled() { } export const MAI_SEMANTIC_TOKENS = { - 'stv1_size/ctrl/default': {}, - 'stv1_size/ctrl/sm': {}, - 'stv1_size/ctrl/lg': {}, + 'stv1_size/ctrl/default': '', + 'stv1_size/ctrl/sm': '', + 'stv1_size/ctrl/lg': '', // Padding - 'stv1_padding/ctrl/textTop': {}, - 'stv1_padding/ctrl/textBottom': {}, - 'stv1_padding/ctrl/textSide': {}, - 'stv1_padding/ctrl/horizontal-default': {}, - 'stv1_padding/ctrl/horizontal-iconOnly': {}, + 'stv1_padding/ctrl/textTop': '', + 'stv1_padding/ctrl/textBottom': '', + 'stv1_padding/ctrl/textSide': '', + 'stv1_padding/ctrl/horizontal-default': '', + 'stv1_padding/ctrl/horizontal-iconOnly': '', // Gap - 'stv1_gap/inside/ctrl/default': {}, - 'stv1_gap/inside/ctrl/toSecondaryIcon': {}, + 'stv1_gap/inside/ctrl/default': '', + 'stv1_gap/inside/ctrl/toSecondaryIcon': '', // Corner - 'stv1_corner/ctrl/default': {}, - 'stv1_corner/ctrl/sm': {}, - 'stv1_corner/ctrl/lg': {}, + 'stv1_corner/ctrl/default': '', + 'stv1_corner/ctrl/sm': '', + 'stv1_corner/ctrl/lg': '', // Background - 'stv1_background/ctrl/neutral/rest': {}, - 'stv1_background/ctrl/neutral/hover': {}, - 'stv1_background/ctrl/neutral/pressed': {}, - 'stv1_background/ctrl/neutral/disabled': {}, - 'stv1_background/ctrl/neutral/selected/rest': {}, - 'stv1_background/ctrl/neutral/selected/hover': {}, - 'stv1_background/ctrl/neutral/selected/pressed': {}, - 'stv1_background/ctrl/neutral/selected/disabled': {}, - 'stv1_background/ctrl/brand/rest': {}, - 'stv1_background/ctrl/brand/hover': {}, - 'stv1_background/ctrl/brand/pressed': {}, - 'stv1_background/ctrl/brand/disabled': {}, - 'stv1_background/ctrl/brand/selected/rest': {}, - 'stv1_background/ctrl/brand/selected/hover': {}, - 'stv1_background/ctrl/brand/selected/pressed': {}, - 'stv1_background/ctrl/brand/selected/disabled': {}, - 'stv1_background/ctrl/outline/rest': {}, - 'stv1_background/ctrl/outline/hover': {}, - 'stv1_background/ctrl/outline/pressed': {}, - 'stv1_background/ctrl/outline/disabled': {}, - 'stv1_background/ctrl/outline/selected/rest': {}, - 'stv1_background/ctrl/outline/selected/hover': {}, - 'stv1_background/ctrl/outline/selected/pressed': {}, - 'stv1_background/ctrl/outline/selected/disabled': {}, - 'stv1_background/ctrl/subtle/rest': {}, - 'stv1_background/ctrl/subtle/hover': {}, - 'stv1_background/ctrl/subtle/pressed': {}, - 'stv1_background/ctrl/subtle/disabled': {}, - 'stv1_background/ctrl/subtle/selected/rest': {}, - 'stv1_background/ctrl/subtle/selected/hover': {}, - 'stv1_background/ctrl/subtle/selected/pressed': {}, - 'stv1_background/ctrl/subtle/selected/disabled': {}, - 'stv1_background/ctrl/transparent/rest': {}, - 'stv1_background/ctrl/transparent/hover': {}, - 'stv1_background/ctrl/transparent/pressed': {}, - 'stv1_background/ctrl/transparent/disabled': {}, - 'stv1_background/ctrl/transparent/selected/rest': {}, - 'stv1_background/ctrl/transparent/selected/hover': {}, - 'stv1_background/ctrl/transparent/selected/pressed': {}, - 'stv1_background/ctrl/transparent/selected/disabled': {}, + 'stv1_background/ctrl/neutral/rest': '', + 'stv1_background/ctrl/neutral/hover': '', + 'stv1_background/ctrl/neutral/pressed': '', + 'stv1_background/ctrl/neutral/disabled': '', + 'stv1_background/ctrl/neutral/selected/rest': '', + 'stv1_background/ctrl/neutral/selected/hover': '', + 'stv1_background/ctrl/neutral/selected/pressed': '', + 'stv1_background/ctrl/neutral/selected/disabled': '', + 'stv1_background/ctrl/brand/rest': '', + 'stv1_background/ctrl/brand/hover': '', + 'stv1_background/ctrl/brand/pressed': '', + 'stv1_background/ctrl/brand/disabled': '', + 'stv1_background/ctrl/brand/selected/rest': '', + 'stv1_background/ctrl/brand/selected/hover': '', + 'stv1_background/ctrl/brand/selected/pressed': '', + 'stv1_background/ctrl/brand/selected/disabled': '', + 'stv1_background/ctrl/outline/rest': '', + 'stv1_background/ctrl/outline/hover': '', + 'stv1_background/ctrl/outline/pressed': '', + 'stv1_background/ctrl/outline/disabled': '', + 'stv1_background/ctrl/outline/selected/rest': '', + 'stv1_background/ctrl/outline/selected/hover': '', + 'stv1_background/ctrl/outline/selected/pressed': '', + 'stv1_background/ctrl/outline/selected/disabled': '', + 'stv1_background/ctrl/subtle/rest': '', + 'stv1_background/ctrl/subtle/hover': '', + 'stv1_background/ctrl/subtle/pressed': '', + 'stv1_background/ctrl/subtle/disabled': '', + 'stv1_background/ctrl/subtle/selected/rest': '', + 'stv1_background/ctrl/subtle/selected/hover': '', + 'stv1_background/ctrl/subtle/selected/pressed': '', + 'stv1_background/ctrl/subtle/selected/disabled': '', + 'stv1_background/ctrl/transparent/rest': '', + 'stv1_background/ctrl/transparent/hover': '', + 'stv1_background/ctrl/transparent/pressed': '', + 'stv1_background/ctrl/transparent/disabled': '', + 'stv1_background/ctrl/transparent/selected/rest': '', + 'stv1_background/ctrl/transparent/selected/hover': '', + 'stv1_background/ctrl/transparent/selected/pressed': '', + 'stv1_background/ctrl/transparent/selected/disabled': '', // Foreground" - 'stv1_foreground/ctrl/neutral/rest': {}, - 'stv1_foreground/ctrl/neutral/hover': {}, - 'stv1_foreground/ctrl/neutral/pressed': {}, - 'stv1_foreground/ctrl/neutral/disabled': {}, - 'stv1_foreground/ctrl/neutral/selected/rest': {}, - 'stv1_foreground/ctrl/neutral/selected/hover': {}, - 'stv1_foreground/ctrl/neutral/selected/pressed': {}, - 'stv1_foreground/ctrl/neutral/selected/disabled': {}, - 'stv1_foreground/ctrl/brand/rest': {}, - 'stv1_foreground/ctrl/brand/hover': {}, - 'stv1_foreground/ctrl/brand/pressed': {}, - 'stv1_foreground/ctrl/brand/disabled': {}, - 'stv1_foreground/ctrl/brand/selected/rest': {}, - 'stv1_foreground/ctrl/brand/selected/hover': {}, - 'stv1_foreground/ctrl/brand/selected/pressed': {}, - 'stv1_foreground/ctrl/brand/selected/disabled': {}, - 'stv1_foreground/ctrl/outline/rest': {}, - 'stv1_foreground/ctrl/outline/hover': {}, - 'stv1_foreground/ctrl/outline/pressed': {}, - 'stv1_foreground/ctrl/outline/disabled': {}, - 'stv1_foreground/ctrl/outline/selected/rest': {}, - 'stv1_foreground/ctrl/outline/selected/hover': {}, - 'stv1_foreground/ctrl/outline/selected/pressed': {}, - 'stv1_foreground/ctrl/outline/selected/disabled': {}, - 'stv1_foreground/ctrl/subtle/rest': {}, - 'stv1_foreground/ctrl/subtle/hover': {}, - 'stv1_foreground/ctrl/subtle/pressed': {}, - 'stv1_foreground/ctrl/subtle/disabled': {}, - 'stv1_foreground/ctrl/subtle/selected/rest': {}, - 'stv1_foreground/ctrl/subtle/selected/hover': {}, - 'stv1_foreground/ctrl/subtle/selected/pressed': {}, - 'stv1_foreground/ctrl/subtle/selected/disabled': {}, - 'stv1_foreground/ctrl/transparent/rest': {}, - 'stv1_foreground/ctrl/transparent/hover': {}, - 'stv1_foreground/ctrl/transparent/pressed': {}, - 'stv1_foreground/ctrl/transparent/disabled': {}, - 'stv1_foreground/ctrl/transparent/selected/rest': {}, - 'stv1_foreground/ctrl/transparent/selected/hover': {}, - 'stv1_foreground/ctrl/transparent/selected/pressed': {}, - 'stv1_foreground/ctrl/transparent/selected/disabled': {}, + 'stv1_foreground/ctrl/neutral/rest': '', + 'stv1_foreground/ctrl/neutral/hover': '', + 'stv1_foreground/ctrl/neutral/pressed': '', + 'stv1_foreground/ctrl/neutral/disabled': '', + 'stv1_foreground/ctrl/neutral/selected/rest': '', + 'stv1_foreground/ctrl/neutral/selected/hover': '', + 'stv1_foreground/ctrl/neutral/selected/pressed': '', + 'stv1_foreground/ctrl/neutral/selected/disabled': '', + 'stv1_foreground/ctrl/brand/rest': '', + 'stv1_foreground/ctrl/brand/hover': '', + 'stv1_foreground/ctrl/brand/pressed': '', + 'stv1_foreground/ctrl/brand/disabled': '', + 'stv1_foreground/ctrl/brand/selected/rest': '', + 'stv1_foreground/ctrl/brand/selected/hover': '', + 'stv1_foreground/ctrl/brand/selected/pressed': '', + 'stv1_foreground/ctrl/brand/selected/disabled': '', + 'stv1_foreground/ctrl/outline/rest': '', + 'stv1_foreground/ctrl/outline/hover': '', + 'stv1_foreground/ctrl/outline/pressed': '', + 'stv1_foreground/ctrl/outline/disabled': '', + 'stv1_foreground/ctrl/outline/selected/rest': '', + 'stv1_foreground/ctrl/outline/selected/hover': '', + 'stv1_foreground/ctrl/outline/selected/pressed': '', + 'stv1_foreground/ctrl/outline/selected/disabled': '', + 'stv1_foreground/ctrl/subtle/rest': '', + 'stv1_foreground/ctrl/subtle/hover': '', + 'stv1_foreground/ctrl/subtle/pressed': '', + 'stv1_foreground/ctrl/subtle/disabled': '', + 'stv1_foreground/ctrl/subtle/selected/rest': '', + 'stv1_foreground/ctrl/subtle/selected/hover': '', + 'stv1_foreground/ctrl/subtle/selected/pressed': '', + 'stv1_foreground/ctrl/subtle/selected/disabled': '', + 'stv1_foreground/ctrl/transparent/rest': '', + 'stv1_foreground/ctrl/transparent/hover': '', + 'stv1_foreground/ctrl/transparent/pressed': '', + 'stv1_foreground/ctrl/transparent/disabled': '', + 'stv1_foreground/ctrl/transparent/selected/rest': '', + 'stv1_foreground/ctrl/transparent/selected/hover': '', + 'stv1_foreground/ctrl/transparent/selected/pressed': '', + 'stv1_foreground/ctrl/transparent/selected/disabled': '', // Stroke" - 'stv1_stroke/ctrl/neutral/rest': {}, - 'stv1_stroke/ctrl/neutral/hover': {}, - 'stv1_stroke/ctrl/neutral/pressed': {}, - 'stv1_stroke/ctrl/neutral/disabled': {}, - 'stv1_stroke/ctrl/neutral/selected/rest': {}, - 'stv1_stroke/ctrl/neutral/selected/hover': {}, - 'stv1_stroke/ctrl/neutral/selected/pressed': {}, - 'stv1_stroke/ctrl/neutral/selected/disabled': {}, - 'stv1_stroke/ctrl/brand/rest': {}, - 'stv1_stroke/ctrl/brand/hover': {}, - 'stv1_stroke/ctrl/brand/pressed': {}, - 'stv1_stroke/ctrl/brand/disabled': {}, - 'stv1_stroke/ctrl/brand/selected/rest': {}, - 'stv1_stroke/ctrl/brand/selected/hover': {}, - 'stv1_stroke/ctrl/brand/selected/pressed': {}, - 'stv1_stroke/ctrl/brand/selected/disabled': {}, - 'stv1_stroke/ctrl/outline/rest': {}, - 'stv1_stroke/ctrl/outline/hover': {}, - 'stv1_stroke/ctrl/outline/pressed': {}, - 'stv1_stroke/ctrl/outline/disabled': {}, - 'stv1_stroke/ctrl/outline/selected/rest': {}, - 'stv1_stroke/ctrl/outline/selected/hover': {}, - 'stv1_stroke/ctrl/outline/selected/pressed': {}, - 'stv1_stroke/ctrl/outline/selected/disabled': {}, - 'stv1_stroke/control/on/outline/rest': {}, - 'stv1_stroke/control/on/outline/hover': {}, - 'stv1_stroke/control/on/outline/pressed': {}, - 'stv1_stroke/control/on/outline/disabled': {}, - 'stv1_stroke/control/on/outline/selected/rest': {}, - 'stv1_stroke/control/on/outline/selected/hover': {}, - 'stv1_stroke/control/on/outline/selected/pressed': {}, - 'stv1_stroke/control/on/outline/selected/disabled': {}, + 'stv1_stroke/ctrl/neutral/rest': '', + 'stv1_stroke/ctrl/neutral/hover': '', + 'stv1_stroke/ctrl/neutral/pressed': '', + 'stv1_stroke/ctrl/neutral/disabled': '', + 'stv1_stroke/ctrl/neutral/selected/rest': '', + 'stv1_stroke/ctrl/neutral/selected/hover': '', + 'stv1_stroke/ctrl/neutral/selected/pressed': '', + 'stv1_stroke/ctrl/neutral/selected/disabled': '', + 'stv1_stroke/ctrl/brand/rest': '', + 'stv1_stroke/ctrl/brand/hover': '', + 'stv1_stroke/ctrl/brand/pressed': '', + 'stv1_stroke/ctrl/brand/disabled': '', + 'stv1_stroke/ctrl/brand/selected/rest': '', + 'stv1_stroke/ctrl/brand/selected/hover': '', + 'stv1_stroke/ctrl/brand/selected/pressed': '', + 'stv1_stroke/ctrl/brand/selected/disabled': '', + 'stv1_stroke/ctrl/outline/rest': '', + 'stv1_stroke/ctrl/outline/hover': '', + 'stv1_stroke/ctrl/outline/pressed': '', + 'stv1_stroke/ctrl/outline/disabled': '', + 'stv1_stroke/ctrl/outline/selected/rest': '', + 'stv1_stroke/ctrl/outline/selected/hover': '', + 'stv1_stroke/ctrl/outline/selected/pressed': '', + 'stv1_stroke/ctrl/outline/selected/disabled': '', + 'stv1_stroke/control/on/outline/rest': '', + 'stv1_stroke/control/on/outline/hover': '', + 'stv1_stroke/control/on/outline/pressed': '', + 'stv1_stroke/control/on/outline/disabled': '', + 'stv1_stroke/control/on/outline/selected/rest': '', + 'stv1_stroke/control/on/outline/selected/hover': '', + 'stv1_stroke/control/on/outline/selected/pressed': '', + 'stv1_stroke/control/on/outline/selected/disabled': '', // Stroke Width" - 'stv1_strokeWidth/ctrl/outline/rest': {}, - 'stv1_strokeWidth/ctrl/outline/hover': {}, - 'stv1_strokeWidth/ctrl/outline/pressed': {}, - 'stv1_strokeWidth/ctrl/outline/disabled': {}, - 'stv1_strokeWidth/ctrl/outline/selected/rest': {}, - 'stv1_strokeWidth/ctrl/outline/selected/hover': {}, - 'stv1_strokeWidth/ctrl/outline/selected/pressed': {}, - 'stv1_strokeWidth/ctrl/outline/selected/disabled': {}, - 'stv1_strokeWidth/default': {}, + 'stv1_strokeWidth/ctrl/outline/rest': '', + 'stv1_strokeWidth/ctrl/outline/hover': '', + 'stv1_strokeWidth/ctrl/outline/pressed': '', + 'stv1_strokeWidth/ctrl/outline/disabled': '', + 'stv1_strokeWidth/ctrl/outline/selected/rest': '', + 'stv1_strokeWidth/ctrl/outline/selected/hover': '', + 'stv1_strokeWidth/ctrl/outline/selected/pressed': '', + 'stv1_strokeWidth/ctrl/outline/selected/disabled': '', + 'stv1_strokeWidth/default': '', // Shadow" - 'stv1_shadow/ctrl/default/rest': {}, - 'stv1_shadow/ctrl/default/hover': {}, - 'stv1_shadow/ctrl/default/pressed': {}, - 'stv1_shadow/ctrl/default/disabled': {}, - 'stv1_shadow/ctrl/default/selected/rest': {}, - 'stv1_shadow/ctrl/default/selected/hover': {}, - 'stv1_shadow/ctrl/default/selected/pressed': {}, - 'stv1_shadow/ctrl/default/selected/disabled': {}, - 'stv1_shadow/ctrl/brand/rest': {}, - 'stv1_shadow/ctrl/brand/hover': {}, - 'stv1_shadow/ctrl/brand/pressed': {}, - 'stv1_shadow/ctrl/brand/disabled': {}, - 'stv1_shadow/ctrl/brand/selected/rest': {}, - 'stv1_shadow/ctrl/brand/selected/hover': {}, - 'stv1_shadow/ctrl/brand/selected/pressed': {}, - 'stv1_shadow/ctrl/brand/selected/disabled': {}, - 'stv1_shadow/ctrl/outline/rest': {}, - 'stv1_shadow/ctrl/outline/hover': {}, - 'stv1_shadow/ctrl/outline/pressed': {}, - 'stv1_shadow/ctrl/outline/disabled': {}, - 'stv1_shadow/ctrl/outline/selected/rest': {}, - 'stv1_shadow/ctrl/outline/selected/hover': {}, - 'stv1_shadow/ctrl/outline/selected/pressed': {}, - 'stv1_shadow/ctrl/outline/selected/disabled': {}, + 'stv1_shadow/ctrl/default/rest': '', + 'stv1_shadow/ctrl/default/hover': '', + 'stv1_shadow/ctrl/default/pressed': '', + 'stv1_shadow/ctrl/default/disabled': '', + 'stv1_shadow/ctrl/default/selected/rest': '', + 'stv1_shadow/ctrl/default/selected/hover': '', + 'stv1_shadow/ctrl/default/selected/pressed': '', + 'stv1_shadow/ctrl/default/selected/disabled': '', + 'stv1_shadow/ctrl/brand/rest': '', + 'stv1_shadow/ctrl/brand/hover': '', + 'stv1_shadow/ctrl/brand/pressed': '', + 'stv1_shadow/ctrl/brand/disabled': '', + 'stv1_shadow/ctrl/brand/selected/rest': '', + 'stv1_shadow/ctrl/brand/selected/hover': '', + 'stv1_shadow/ctrl/brand/selected/pressed': '', + 'stv1_shadow/ctrl/brand/selected/disabled': '', + 'stv1_shadow/ctrl/outline/rest': '', + 'stv1_shadow/ctrl/outline/hover': '', + 'stv1_shadow/ctrl/outline/pressed': '', + 'stv1_shadow/ctrl/outline/disabled': '', + 'stv1_shadow/ctrl/outline/selected/rest': '', + 'stv1_shadow/ctrl/outline/selected/hover': '', + 'stv1_shadow/ctrl/outline/selected/pressed': '', + 'stv1_shadow/ctrl/outline/selected/disabled': '', // Icon Theme" - 'stv1_iconTheme/ctrl/default/rest': {}, - 'stv1_iconTheme/ctrl/default/hover': {}, - 'stv1_iconTheme/ctrl/default/pressed': {}, - 'stv1_iconTheme/ctrl/default/selected': {}, - 'stv1_iconTheme/ctrl/subtle/rest': {}, - 'stv1_iconTheme/ctrl/subtle/hover': {}, - 'stv1_iconTheme/ctrl/subtle/pressed': {}, - 'stv1_iconTheme/ctrl/subtle/selected': {}, - 'stv1_iconTheme/ctrl/chevron/default': {}, - 'stv1_iconTheme/ctrl/chevron/selected': {}, + 'stv1_iconTheme/ctrl/default/rest': '', + 'stv1_iconTheme/ctrl/default/hover': '', + 'stv1_iconTheme/ctrl/default/pressed': '', + 'stv1_iconTheme/ctrl/default/selected': '', + 'stv1_iconTheme/ctrl/subtle/rest': '', + 'stv1_iconTheme/ctrl/subtle/hover': '', + 'stv1_iconTheme/ctrl/subtle/pressed': '', + 'stv1_iconTheme/ctrl/subtle/selected': '', + 'stv1_iconTheme/ctrl/chevron/default': '', + 'stv1_iconTheme/ctrl/chevron/selected': '', // Icon Color" - 'stv1_iconColor/ctrl/default/rest': {}, - 'stv1_iconColor/ctrl/default/hover': {}, - 'stv1_iconColor/ctrl/default/pressed': {}, - 'stv1_iconColor/ctrl/default/selected': {}, - 'stv1_iconColor/ctrl/chevron/default': {}, - 'stv1_iconColor/ctrl/chevron/selected': {}, + 'stv1_iconColor/ctrl/default/rest': '', + 'stv1_iconColor/ctrl/default/hover': '', + 'stv1_iconColor/ctrl/default/pressed': '', + 'stv1_iconColor/ctrl/default/selected': '', + 'stv1_iconColor/ctrl/chevron/default': '', + 'stv1_iconColor/ctrl/chevron/selected': '', // Typography" - 'stv1_fontSize/ctrl/default': {}, - 'stv1_fontWeight/ctrl/default': {}, - 'stv1_fontFamily/ctrl/default': {}, - 'stv1_lineHeight/ctrl/default': {}, - 'stv1_letterSpacing/ctrl/default': {}, - 'stv1_textStyle/ctrl/body': {}, - 'stv1_textStyle/ctrl/header': {}, + 'stv1_fontSize/ctrl/default': '', + 'stv1_fontWeight/ctrl/default': '', + 'stv1_fontFamily/ctrl/default': '', + 'stv1_lineHeight/ctrl/default': '', + 'stv1_letterSpacing/ctrl/default': '', + 'stv1_textStyle/ctrl/body': '', + 'stv1_textStyle/ctrl/header': '', } as const satisfies { [key: string]: any; }; @@ -261,20 +261,20 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'background/ctrl/brand/pressed': 'hsl(184, 100%, 12%)', 'background/ctrl/brand/disabled': 'hsl(0, 0%, 94%)', - 'background/ctrl/outline/rest': {}, - 'background/ctrl/outline/hover': {}, - 'background/ctrl/outline/pressed': {}, - 'background/ctrl/outline/disabled': {}, + 'background/ctrl/outline/rest': '', + 'background/ctrl/outline/hover': '', + 'background/ctrl/outline/pressed': '', + 'background/ctrl/outline/disabled': '', - 'background/ctrl/subtle/rest': {}, - 'background/ctrl/subtle/hover': {}, - 'background/ctrl/subtle/pressed': {}, - 'background/ctrl/subtle/disabled': {}, + 'background/ctrl/subtle/rest': '', + 'background/ctrl/subtle/hover': '', + 'background/ctrl/subtle/pressed': '', + 'background/ctrl/subtle/disabled': '', - 'background/ctrl/transparent/rest': {}, - 'background/ctrl/transparent/hover': {}, - 'background/ctrl/transparent/pressed': {}, - 'background/ctrl/transparent/disabled': {}, + 'background/ctrl/transparent/rest': '', + 'background/ctrl/transparent/hover': '', + 'background/ctrl/transparent/pressed': '', + 'background/ctrl/transparent/disabled': '', // Border 'borderColor/ctrl/neutral/rest': '#D1D1D1', @@ -287,20 +287,20 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'borderColor/ctrl/brand/pressed': 'transparent', 'borderColor/ctrl/brand/disabled': 'transparent', - 'borderColor/ctrl/outline/rest': {}, - 'borderColor/ctrl/outline/hover': {}, - 'borderColor/ctrl/outline/pressed': {}, - 'borderColor/ctrl/outline/disabled': {}, + 'borderColor/ctrl/outline/rest': '', + 'borderColor/ctrl/outline/hover': '', + 'borderColor/ctrl/outline/pressed': '', + 'borderColor/ctrl/outline/disabled': '', - 'borderColor/ctrl/subtle/rest': {}, - 'borderColor/ctrl/subtle/hover': {}, - 'borderColor/ctrl/subtle/pressed': {}, - 'borderColor/ctrl/subtle/disabled': {}, + 'borderColor/ctrl/subtle/rest': '', + 'borderColor/ctrl/subtle/hover': '', + 'borderColor/ctrl/subtle/pressed': '', + 'borderColor/ctrl/subtle/disabled': '', - 'borderColor/ctrl/transparent/rest': {}, - 'borderColor/ctrl/transparent/hover': {}, - 'borderColor/ctrl/transparent/pressed': {}, - 'borderColor/ctrl/transparent/disabled': {}, + 'borderColor/ctrl/transparent/rest': '', + 'borderColor/ctrl/transparent/hover': '', + 'borderColor/ctrl/transparent/pressed': '', + 'borderColor/ctrl/transparent/disabled': '', }; export const TEAMS_VISUAL_REFRESH_TOKENS = Object.fromEntries( From e70974bad0ef3494f96cc10a1f4e0fbaff00dc26 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 16:39:07 -0400 Subject: [PATCH 11/17] feat(Button): enhance visual refresh styles with new appearance state tokens and integrate into stories --- .../Button/useButtonStyles.styles.ts | 64 ++++++++++++++++--- .../library/src/index.ts | 22 +++---- .../VisualRefresh/VisualRefresh.stories.tsx | 46 +++++++++++-- 3 files changed, 107 insertions(+), 25 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index defae0c8dd700d..dc57ab9c57b9d0 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -7,7 +7,7 @@ import { shorthands, makeStyles, makeResetStyles, mergeClasses } from '@griffel/ import type { GriffelStyle } from '@griffel/react'; import { semanticTokenVar, useIsVisualRefreshEnabled } from '@fluentui/visual-refresh-preview'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { ButtonSlots, ButtonState } from './Button.types'; +import type { ButtonProps, ButtonSlots, ButtonState } from './Button.types'; export const buttonClassNames: SlotClassNames = { root: 'fui-Button', @@ -552,15 +552,13 @@ const useIconStyles = makeStyles({ }, }); +export type VisualRefreshAppearanceName = 'base' | NonNullable; + type VisualRefreshAppearanceVariant = + | VisualRefreshAppearanceName | 'base' | 'neutral' - | 'secondary' - | 'brand' - | 'primary' - | 'outline' - | 'subtle' - | 'transparent'; + | 'brand'; const visualRefreshAppearanceAlias: Record< VisualRefreshAppearanceVariant, @@ -611,12 +609,60 @@ const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearance }; }; +type VisualRefreshInteractionState = 'rest' | 'hover' | 'pressed' | 'disabled'; + +export type VisualRefreshAppearanceStateTokens = { + background: Record; + border: Record; +}; + +const resolveVisualRefreshAppearanceVariant = ( + appearance?: VisualRefreshAppearanceName, +): VisualRefreshAppearanceVariant => { + if (!appearance) { + return 'secondary'; + } + return appearance; +}; + +export const getVisualRefreshAppearanceStateTokens = ( + appearance?: VisualRefreshAppearanceName, +): VisualRefreshAppearanceStateTokens => { + const variant = resolveVisualRefreshAppearanceVariant(appearance); + const tokenGroup = visualRefreshAppearanceAlias[variant] ?? 'neutral'; + const backgroundTokenBase = `background/ctrl/${tokenGroup}`; + const borderTokenBase = `borderColor/ctrl/${tokenGroup}`; + + const background = { + rest: getSemanticTokenValue(`${backgroundTokenBase}/rest`), + hover: getSemanticTokenValue(`${backgroundTokenBase}/hover`), + pressed: getSemanticTokenValue(`${backgroundTokenBase}/pressed`), + disabled: getSemanticTokenValue(`${backgroundTokenBase}/disabled`), + }; + + const border = { + rest: getSemanticTokenValue(`${borderTokenBase}/rest`), + hover: getSemanticTokenValue(`${borderTokenBase}/hover`), + pressed: getSemanticTokenValue(`${borderTokenBase}/pressed`), + disabled: getSemanticTokenValue(`${borderTokenBase}/disabled`), + }; + + if (appearance === 'primary' || appearance === 'subtle' || appearance === 'transparent') { + border.rest = 'transparent'; + border.hover = 'transparent'; + border.pressed = 'transparent'; + border.disabled = 'transparent'; + } + + return { background, border }; +}; + const useVisualRefreshStyles = makeStyles({ - base: createVisualRefreshAppearanceStyles('neutral'), + base: createVisualRefreshAppearanceStyles('base'), // Appearance variations outline: createVisualRefreshAppearanceStyles('outline'), - primary: createVisualRefreshAppearanceStyles('brand'), + primary: createVisualRefreshAppearanceStyles('primary'), secondary: { // same as base }, diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 949391ecc3282b..513fc15f053d16 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -261,20 +261,20 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'background/ctrl/brand/pressed': 'hsl(184, 100%, 12%)', 'background/ctrl/brand/disabled': 'hsl(0, 0%, 94%)', - 'background/ctrl/outline/rest': '', - 'background/ctrl/outline/hover': '', - 'background/ctrl/outline/pressed': '', - 'background/ctrl/outline/disabled': '', + 'background/ctrl/outline/rest': 'transparent', + 'background/ctrl/outline/hover': 'transparent', + 'background/ctrl/outline/pressed': 'transparent', + 'background/ctrl/outline/disabled': 'transparent', - 'background/ctrl/subtle/rest': '', - 'background/ctrl/subtle/hover': '', - 'background/ctrl/subtle/pressed': '', + 'background/ctrl/subtle/rest': 'transparent', + 'background/ctrl/subtle/hover': '#F5F5F5', + 'background/ctrl/subtle/pressed': '#E0E0E0', 'background/ctrl/subtle/disabled': '', - 'background/ctrl/transparent/rest': '', - 'background/ctrl/transparent/hover': '', - 'background/ctrl/transparent/pressed': '', - 'background/ctrl/transparent/disabled': '', + 'background/ctrl/transparent/rest': 'transparent', + 'background/ctrl/transparent/hover': 'transparent', + 'background/ctrl/transparent/pressed': 'transparent', + 'background/ctrl/transparent/disabled': 'transparent', // Border 'borderColor/ctrl/neutral/rest': '#D1D1D1', diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 6eb8d3f551de95..7e32e97cf04ee4 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -10,6 +10,7 @@ import { VisualRefreshContext, } from '@fluentui/visual-refresh-preview'; import { mergeClasses } from '@griffel/react'; +import { getVisualRefreshAppearanceStateTokens } from '../../../../react-button/library/src/components/Button/useButtonStyles.styles'; type ComponentState = 'rest' | 'hover' | 'focus' | 'disabled'; @@ -168,15 +169,17 @@ const ButtonStateCell = ({ state, children, size, + isVisualRefreshEnabled, }: { appearance?: ButtonProps['appearance']; state: ComponentState; children: React.ReactNode; size: ButtonProps['size']; + isVisualRefreshEnabled: boolean; }) => { const buttonStateClasses = useButtonStateStyles(); const hoverClass = - state === 'hover' + !isVisualRefreshEnabled && state === 'hover' ? appearance === 'primary' ? buttonStateClasses.hoverPrimary : appearance === 'subtle' @@ -187,9 +190,31 @@ const ButtonStateCell = ({ : undefined; const focusClass = state === 'focus' ? buttonStateClasses.focus : undefined; const className = mergeClasses(buttonStateClasses.base, hoverClass, focusClass); + const visualRefreshStyle = React.useMemo(() => { + if (!isVisualRefreshEnabled) { + return undefined; + } + const tokens = getVisualRefreshAppearanceStateTokens(appearance ?? 'secondary'); + const stateKey = state === 'hover' ? 'hover' : state === 'disabled' ? 'disabled' : 'rest'; + const style: React.CSSProperties = { + backgroundColor: tokens.background[stateKey], + borderColor: tokens.border[stateKey], + }; + if (state === 'hover') { + style.cursor = 'pointer'; + } + return style; + }, [appearance, isVisualRefreshEnabled, state]); return ( - ); @@ -235,7 +260,13 @@ const InputStateCell = ({ ); }; -const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size'] }) => { +const ComponentStatesTable = ({ + controlSize, + isVisualRefreshEnabled, +}: { + controlSize: ButtonProps['size']; + isVisualRefreshEnabled: boolean; +}) => { const styles = useStoryStyles(); const buttonVariants: Array<{ label: string; appearance?: ButtonProps['appearance']; content: string }> = [ @@ -288,7 +319,12 @@ const ComponentStatesTable = ({ controlSize }: { controlSize: ButtonProps['size' {buttonStateOrder.map(state => (
- + {state === 'disabled' ? 'Disabled' : variant.content}
@@ -362,7 +398,7 @@ export const VisualRefresh = (): JSXElement => {
- +
); From d91376872b08c4929f119343a49a91d9ef083ce2 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Thu, 16 Oct 2025 17:50:18 -0400 Subject: [PATCH 12/17] feat(Button): enhance visual refresh styles by refining foreground tokens and updating button state styles --- .../Button/useButtonStyles.styles.ts | 34 ++++++++---- .../library/src/index.ts | 55 ++++++++++++++----- .../VisualRefresh/VisualRefresh.stories.tsx | 45 ++------------- 3 files changed, 66 insertions(+), 68 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index dc57ab9c57b9d0..f0e1544b51967b 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -554,11 +554,7 @@ const useIconStyles = makeStyles({ export type VisualRefreshAppearanceName = 'base' | NonNullable; -type VisualRefreshAppearanceVariant = - | VisualRefreshAppearanceName - | 'base' - | 'neutral' - | 'brand'; +type VisualRefreshAppearanceVariant = VisualRefreshAppearanceName | 'base' | 'neutral' | 'brand'; const visualRefreshAppearanceAlias: Record< VisualRefreshAppearanceVariant, @@ -579,13 +575,16 @@ type SemanticTokenName = Parameters[0]; const getSemanticTokenValue = (token: string) => semanticTokenVar(token as SemanticTokenName); const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearanceVariant): GriffelStyle => { const tokenGroup = visualRefreshAppearanceAlias[appearance] ?? 'neutral'; + const foregroundTokenBase = `foreground/ctrl/${tokenGroup}`; const backgroundTokenBase = `background/ctrl/${tokenGroup}`; const borderTokenBase = `borderColor/ctrl/${tokenGroup}`; return { + color: getSemanticTokenValue(`${foregroundTokenBase}/rest`), backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/rest`), ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/rest`)), ':hover': { + color: getSemanticTokenValue(`${foregroundTokenBase}/hover`), backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/hover`), ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/hover`)), [`& .${iconFilledClassName}`]: { @@ -599,10 +598,12 @@ const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearance }, }, ':hover:active': { + color: getSemanticTokenValue(`${foregroundTokenBase}/pressed`), backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/pressed`), ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/pressed`)), }, ':disabled': { + color: getSemanticTokenValue(`${foregroundTokenBase}/disabled`), backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/disabled`), ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/disabled`)), }, @@ -612,6 +613,7 @@ const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearance type VisualRefreshInteractionState = 'rest' | 'hover' | 'pressed' | 'disabled'; export type VisualRefreshAppearanceStateTokens = { + foreground: Record; background: Record; border: Record; }; @@ -630,9 +632,17 @@ export const getVisualRefreshAppearanceStateTokens = ( ): VisualRefreshAppearanceStateTokens => { const variant = resolveVisualRefreshAppearanceVariant(appearance); const tokenGroup = visualRefreshAppearanceAlias[variant] ?? 'neutral'; + const foregroundTokenBase = `foreground/ctrl/${tokenGroup}`; const backgroundTokenBase = `background/ctrl/${tokenGroup}`; const borderTokenBase = `borderColor/ctrl/${tokenGroup}`; + const foreground = { + rest: getSemanticTokenValue(`${foregroundTokenBase}/rest`), + hover: getSemanticTokenValue(`${foregroundTokenBase}/hover`), + pressed: getSemanticTokenValue(`${foregroundTokenBase}/pressed`), + disabled: getSemanticTokenValue(`${foregroundTokenBase}/disabled`), + }; + const background = { rest: getSemanticTokenValue(`${backgroundTokenBase}/rest`), hover: getSemanticTokenValue(`${backgroundTokenBase}/hover`), @@ -654,7 +664,7 @@ export const getVisualRefreshAppearanceStateTokens = ( border.disabled = 'transparent'; } - return { background, border }; + return { foreground, background, border }; }; const useVisualRefreshStyles = makeStyles({ @@ -677,8 +687,8 @@ const useVisualRefreshStyles = makeStyles({ borderRadius: semanticTokenVar('corner/ctrl/sm'), fontSize: semanticTokenVar('fontSize/ctrl/sm'), - fontWeight: semanticTokenVar('lineHeight/ctrl/sm'), - lineHeight: semanticTokenVar('fontWeight/ctrl/sm'), + fontWeight: semanticTokenVar('fontWeight/ctrl/sm'), + lineHeight: semanticTokenVar('lineHeight/ctrl/sm'), }, medium: { minHeight: semanticTokenVar('size/ctrl/md'), @@ -688,8 +698,8 @@ const useVisualRefreshStyles = makeStyles({ borderRadius: semanticTokenVar('corner/ctrl/md'), fontSize: semanticTokenVar('fontSize/ctrl/md'), - fontWeight: semanticTokenVar('lineHeight/ctrl/md'), - lineHeight: semanticTokenVar('fontWeight/ctrl/md'), + fontWeight: semanticTokenVar('fontWeight/ctrl/md'), + lineHeight: semanticTokenVar('lineHeight/ctrl/md'), }, large: { minHeight: semanticTokenVar('size/ctrl/lg'), @@ -699,8 +709,8 @@ const useVisualRefreshStyles = makeStyles({ borderRadius: semanticTokenVar('corner/ctrl/lg'), fontSize: semanticTokenVar('fontSize/ctrl/lg'), - fontWeight: semanticTokenVar('lineHeight/ctrl/lg'), - lineHeight: semanticTokenVar('fontWeight/ctrl/lg'), + fontWeight: semanticTokenVar('fontWeight/ctrl/lg'), + lineHeight: semanticTokenVar('lineHeight/ctrl/lg'), }, }); diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 513fc15f053d16..d51f47f72b6d69 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -246,9 +246,34 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'lineHeight/ctrl/md': '20px', 'lineHeight/ctrl/lg': '20px', - 'fontWeight/ctrl/sm': tokens.fontWeightRegular, - 'fontWeight/ctrl/md': tokens.fontWeightRegular, - 'fontWeight/ctrl/lg': tokens.fontWeightRegular, + 'fontWeight/ctrl/sm': '600', + 'fontWeight/ctrl/md': '600', + 'fontWeight/ctrl/lg': '600', + + 'foreground/ctrl/neutral/rest': '#616161', + 'foreground/ctrl/neutral/hover': '#424242', + 'foreground/ctrl/neutral/pressed': '#424242', + 'foreground/ctrl/neutral/disabled': '#BDBDBD', + + 'foreground/ctrl/brand/rest': '#FFFFFF', + 'foreground/ctrl/brand/hover': '#FFFFFF', + 'foreground/ctrl/brand/pressed': '#FFFFFF', + 'foreground/ctrl/brand/disabled': '#BDBDBD', + + 'foreground/ctrl/outline/rest': '#616161', + 'foreground/ctrl/outline/hover': '#424242', + 'foreground/ctrl/outline/pressed': '#424242', + 'foreground/ctrl/outline/disabled': '#BDBDBD', + + 'foreground/ctrl/subtle/rest': '#616161', + 'foreground/ctrl/subtle/hover': '#424242', + 'foreground/ctrl/subtle/pressed': '#424242', + 'foreground/ctrl/subtle/disabled': '#BDBDBD', + + 'foreground/ctrl/transparent/rest': '#616161', + 'foreground/ctrl/transparent/hover': '#00686D', + 'foreground/ctrl/transparent/pressed': '#00595D', + 'foreground/ctrl/transparent/disabled': '#BDBDBD', // Background 'background/ctrl/neutral/rest': 'hsl(0, 0%, 98%)', @@ -287,20 +312,20 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'borderColor/ctrl/brand/pressed': 'transparent', 'borderColor/ctrl/brand/disabled': 'transparent', - 'borderColor/ctrl/outline/rest': '', - 'borderColor/ctrl/outline/hover': '', - 'borderColor/ctrl/outline/pressed': '', - 'borderColor/ctrl/outline/disabled': '', + 'borderColor/ctrl/outline/rest': '#D1D1D1', + 'borderColor/ctrl/outline/hover': '#C7C7C7', + 'borderColor/ctrl/outline/pressed': '#B3B3B3', + 'borderColor/ctrl/outline/disabled': '#E0E0E0', - 'borderColor/ctrl/subtle/rest': '', - 'borderColor/ctrl/subtle/hover': '', - 'borderColor/ctrl/subtle/pressed': '', - 'borderColor/ctrl/subtle/disabled': '', + 'borderColor/ctrl/subtle/rest': 'transparent', + 'borderColor/ctrl/subtle/hover': 'transparent', + 'borderColor/ctrl/subtle/pressed': 'transparent', + 'borderColor/ctrl/subtle/disabled': 'transparent', - 'borderColor/ctrl/transparent/rest': '', - 'borderColor/ctrl/transparent/hover': '', - 'borderColor/ctrl/transparent/pressed': '', - 'borderColor/ctrl/transparent/disabled': '', + 'borderColor/ctrl/transparent/rest': 'transparent', + 'borderColor/ctrl/transparent/hover': 'transparent', + 'borderColor/ctrl/transparent/pressed': 'transparent', + 'borderColor/ctrl/transparent/disabled': 'transparent', }; export const TEAMS_VISUAL_REFRESH_TOKENS = Object.fromEntries( diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 7e32e97cf04ee4..0129e0945f8ff2 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -9,7 +9,7 @@ import { TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext, } from '@fluentui/visual-refresh-preview'; -import { mergeClasses } from '@griffel/react'; +import { mergeClasses, shorthands } from '@griffel/react'; import { getVisualRefreshAppearanceStateTokens } from '../../../../react-button/library/src/components/Button/useButtonStyles.styles'; type ComponentState = 'rest' | 'hover' | 'focus' | 'disabled'; @@ -81,35 +81,8 @@ const useStoryStyles = makeStyles({ }); const useButtonStateStyles = makeStyles({ - base: { - pointerEvents: 'none', - }, - hoverSecondary: { - backgroundColor: tokens.colorNeutralBackground1Hover, - borderColor: tokens.colorNeutralStroke1Hover, - color: tokens.colorNeutralForeground1Hover, - cursor: 'pointer', - }, - hoverPrimary: { - backgroundColor: tokens.colorBrandBackgroundHover, - borderColor: 'transparent', - color: tokens.colorNeutralForegroundOnBrand, - cursor: 'pointer', - }, - hoverSubtle: { - backgroundColor: tokens.colorSubtleBackgroundHover, - borderColor: 'transparent', - color: tokens.colorNeutralForeground2Hover, - cursor: 'pointer', - }, - hoverTransparent: { - backgroundColor: tokens.colorTransparentBackgroundHover, - borderColor: 'transparent', - color: tokens.colorNeutralForeground2BrandHover, - cursor: 'pointer', - }, focus: { - borderColor: tokens.colorStrokeFocus2, + ...shorthands.borderColor(tokens.colorStrokeFocus2), boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorStrokeFocus2} inset`, outline: `${tokens.strokeWidthThick} solid ${tokens.colorTransparentStroke}`, outlineOffset: '2px', @@ -178,18 +151,7 @@ const ButtonStateCell = ({ isVisualRefreshEnabled: boolean; }) => { const buttonStateClasses = useButtonStateStyles(); - const hoverClass = - !isVisualRefreshEnabled && state === 'hover' - ? appearance === 'primary' - ? buttonStateClasses.hoverPrimary - : appearance === 'subtle' - ? buttonStateClasses.hoverSubtle - : appearance === 'transparent' - ? buttonStateClasses.hoverTransparent - : buttonStateClasses.hoverSecondary - : undefined; const focusClass = state === 'focus' ? buttonStateClasses.focus : undefined; - const className = mergeClasses(buttonStateClasses.base, hoverClass, focusClass); const visualRefreshStyle = React.useMemo(() => { if (!isVisualRefreshEnabled) { return undefined; @@ -197,6 +159,7 @@ const ButtonStateCell = ({ const tokens = getVisualRefreshAppearanceStateTokens(appearance ?? 'secondary'); const stateKey = state === 'hover' ? 'hover' : state === 'disabled' ? 'disabled' : 'rest'; const style: React.CSSProperties = { + color: tokens.foreground[stateKey], backgroundColor: tokens.background[stateKey], borderColor: tokens.border[stateKey], }; @@ -210,7 +173,7 @@ const ButtonStateCell = ({ + ); +}; + +const ComponentStatesTable = ({ + controlSize, + isVisualRefreshEnabled, +}: { + controlSize: ButtonProps['size']; + isVisualRefreshEnabled: boolean; +}) => { + const styles = useStoryStyles(); + + const buttonVariants: Array<{ label: string; appearance?: ButtonProps['appearance']; content: string }> = [ + { label: 'Outline', appearance: 'outline', content: 'Outline' }, + { label: 'Primary', appearance: 'primary', content: 'Primary' }, + { label: 'Secondary', appearance: 'secondary', content: 'Secondary' }, + { label: 'Subtle', appearance: 'subtle', content: 'Subtle' }, + { label: 'Transparent', appearance: 'transparent', content: 'Transparent' }, + { label: 'Tint', appearance: 'secondary', content: 'Tint' }, + ]; + + return ( + + + + + + {buttonStateOrder.map(state => ( + + ))} + + + + + {buttonVariants.map((variant, index) => ( + + {index === 0 && ( + + )} + + {buttonStateOrder.map(state => ( + + ))} + + ))} + + +
ComponentVariant + {buttonStateLabels[state]} +
+ Button + {variant.label} +
+ + {state === 'disabled' ? 'Disabled' : variant.content} + +
+
+ ); +}; + +export const ButtonVisualRefresh = (): JSXElement => { + const styles = useStoryStyles(); + const switchId = useId('visual-refresh-toggle'); + const sizeSelectId = useId('visual-refresh-size'); + const [isVisualRefreshEnabled, setIsVisualRefreshEnabled] = React.useState(false); + const [controlSize, setControlSize] = React.useState('medium'); + + const handleThemeChange = (_event: React.ChangeEvent, data: SwitchOnChangeData) => { + setIsVisualRefreshEnabled(Boolean(data.checked)); + }; + + const handleSizeChange = (event: React.ChangeEvent) => { + setControlSize(event.target.value as ButtonProps['size']); + }; + + const content = ( +
+
+
+ +
+
+ + +
+
+ +
+ ); + + return isVisualRefreshEnabled ? {content} : content; +}; + +ButtonVisualRefresh.parameters = { + docs: { + description: { + story: 'Compare Button variants across interaction states with and without the visual refresh theme.', + }, + }, +}; diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx index 0129e0945f8ff2..ac1f9dfca48991 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/VisualRefresh.stories.tsx @@ -28,6 +28,9 @@ const useStoryStyles = makeStyles({ flexDirection: 'column', gap: '1.5rem', maxWidth: 'max-content', + backgroundColor: 'white', + padding: '24px', + margin: '-48px -24px', }, controls: { display: 'flex', diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx index 0b80d786d5fa2b..f0647b9011fbb1 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/index.stories.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Meta } from '@storybook/react'; export { VisualRefresh } from './VisualRefresh.stories'; +export { ButtonVisualRefresh as Button } from './Button.stories'; const Component = () =>
; From 4ef0b2ec0d59d56142758ffc9fbde4b6908559b6 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Fri, 17 Oct 2025 09:40:36 -0400 Subject: [PATCH 14/17] feat(Button): implement visual refresh styles for button focus state and add button variant previews in stories --- .../Button/useButtonStyles.styles.ts | 49 ++++++++------- .../library/src/index.ts | 2 + .../src/VisualRefresh/Button.stories.tsx | 62 ++++++++++++++++--- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts index 8730cfe97a0f58..f95dfb754f2f55 100644 --- a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.ts @@ -612,25 +612,6 @@ const createVisualRefreshAppearanceStyles = (appearance: VisualRefreshAppearance }, }, - ':focus': { - ...shorthands.borderColor(tokens.colorStrokeFocus2), - boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorStrokeFocus2} inset`, - outline: `${tokens.strokeWidthThick} solid ${tokens.colorTransparentStroke}`, - outlineOffset: '2px', - color: getSemanticTokenValue(`${foregroundTokenBase}/hover`), - backgroundColor: getSemanticTokenValue(`${backgroundTokenBase}/hover`), - ...shorthands.borderColor(getSemanticTokenValue(`${borderTokenBase}/hover`)), - [`& .${iconFilledClassName}`]: { - display: 'inline', - }, - [`& .${iconRegularClassName}`]: { - display: 'none', - }, - [`& .${buttonClassNames.icon}`]: { - color: getSemanticTokenValue(`${iconColorTokenBase}/hover`), - }, - }, - ':disabled': { pointerEvents: 'none', color: getSemanticTokenValue(`${foregroundTokenBase}/disabled`), @@ -690,6 +671,28 @@ const useVisualRefreshStyles = makeStyles({ }, }); +const useVisualRefreshIconStyles = makeStyles({ + small: { + fontSize: '12px', + height: '12px', + width: '12px', + + [iconSpacingVar]: tokens.spacingHorizontalXS, + }, + medium: { + fontSize: '16px', + height: '16px', + width: '16px', + }, + large: { + fontSize: '16px', + height: '16px', + width: '16px', + + [iconSpacingVar]: tokens.spacingHorizontalSNudge, + }, +}); + export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { 'use no memo'; @@ -704,6 +707,7 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { const rootFocusStyles = useRootFocusStyles(); const rootIconOnlyStyles = useRootIconOnlyStyles(); const iconStyles = useIconStyles(); + const iconVisualRefreshStyles = useVisualRefreshIconStyles(); const { appearance, disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state; @@ -730,9 +734,9 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { appearance && (disabled || disabledFocusable) && rootDisabledStyles[appearance], // Focus styles - appearance === 'primary' && rootFocusStyles.primary, - rootFocusStyles[size], - rootFocusStyles[shape], + !isVisualRefreshEnabled && appearance === 'primary' && rootFocusStyles.primary, + !isVisualRefreshEnabled && rootFocusStyles[size], + !isVisualRefreshEnabled && rootFocusStyles[shape], // Icon-only styles iconOnly && rootIconOnlyStyles[size], @@ -750,6 +754,7 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { iconBaseClassName, !!state.root.children && iconStyles[iconPosition], iconStyles[size], + isVisualRefreshEnabled && iconVisualRefreshStyles[size], state.icon.className, ); } diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 033888a4b5ed0e..adc3296ee77080 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -217,12 +217,14 @@ export const MAI_SEMANTIC_TOKENS = { * - MenuButton + icon/ctrl * - Button + primaryIcon/ctrl + secondaryIcon/ctrl * 9. if border="transparent" it makes button height 2px size less depends on variant + * 10. size/ctrl/lg - what should we do with icon size? 12px/16px/16px */ export const TEAMS_VISUAL_REFRESH_THEME = { 'size/ctrl/md': '36px', 'size/ctrl/sm': '28px', 'size/ctrl/lg': '40px', + // Button // Padding 'padding/ctrl/horizontal/sm': '8px', diff --git a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/Button.stories.tsx b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/Button.stories.tsx index 1aaa6965a7f09f..1465e746680f80 100644 --- a/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/Button.stories.tsx +++ b/packages/react-components/visual-refresh-preview/stories/src/VisualRefresh/Button.stories.tsx @@ -9,6 +9,8 @@ import { TEAMS_VISUAL_REFRESH_TOKENS, VisualRefreshContext, } from '@fluentui/visual-refresh-preview'; +import { shorthands } from '@griffel/react'; + import { getVisualRefreshAppearanceStateTokens, type VisualRefreshAppearanceStateTokens, @@ -30,9 +32,16 @@ const componentStateToAppearanceStateKey: Record = [ + { label: 'Primary', appearance: 'primary', content: 'Primary' }, + { label: 'Outline', appearance: 'outline', content: 'Outline' }, + { label: 'Subtle', appearance: 'subtle', content: 'Subtle' }, + { label: 'Transparent', appearance: 'transparent', content: 'Transparent' }, + { label: 'Tint', appearance: 'secondary', content: 'Tint' }, +]; const useStoryStyles = makeStyles({ container: { @@ -61,6 +70,24 @@ const useStoryStyles = makeStyles({ table: { borderCollapse: 'collapse', minWidth: '720px', + pointerEvents: 'none', + }, + previewSection: { + display: 'flex', + flexDirection: 'column', + gap: '0.75rem', + paddingBottom: '1.5rem', + borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, + }, + previewLabel: { + color: tokens.colorNeutralForeground2, + fontWeight: tokens.fontWeightSemibold, + }, + previewContent: { + display: 'flex', + alignItems: 'center', + gap: '1rem', + flexWrap: 'wrap', }, headerCell: { borderBottom: `1px solid ${tokens.colorNeutralStroke2}`, @@ -95,6 +122,15 @@ const useStoryStyles = makeStyles({ }, }); +const useButtonStateStyles = makeStyles({ + focus: { + ...shorthands.borderColor(tokens.colorStrokeFocus2), + boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorStrokeFocus2} inset`, + outline: `${tokens.strokeWidthThick} solid ${tokens.colorTransparentStroke}`, + outlineOffset: '2px', + }, +}); + const VisualRefreshProvider = ({ children }: { children: React.ReactNode }) => { const customProperties: Record = {}; for (const [key, value] of Object.entries(TEAMS_VISUAL_REFRESH_TOKENS ?? {})) { @@ -123,6 +159,8 @@ const ButtonStateCell = ({ size: ButtonProps['size']; isVisualRefreshEnabled: boolean; }) => { + const buttonStateClasses = useButtonStateStyles(); + const focusClass = state === 'focus' ? buttonStateClasses.focus : undefined; const visualRefreshState = React.useMemo(() => { if (!isVisualRefreshEnabled) { return { @@ -161,6 +199,7 @@ const ButtonStateCell = ({
); }; @@ -302,7 +297,7 @@ export const ButtonVisualRefresh = (): JSXElement => {
- +
{buttonVariants.map(variant => (
- + {isVisualRefreshEnabled && ( + + )} ); From 460d59b7c69296eb8d73b01e53e52ceccc12ee54 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Fri, 17 Oct 2025 10:13:38 -0400 Subject: [PATCH 16/17] feat(VisualRefresh): update foreground, background, border, and icon colors to use new token values for improved consistency --- .../library/src/index.ts | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 3adba7c72550f0..990b902a91337e 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -258,56 +258,56 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'fontWeight/ctrl/md': '600', 'fontWeight/ctrl/lg': '600', - 'foreground/ctrl/neutral/rest': '#616161', - 'foreground/ctrl/neutral/hover': '#424242', - 'foreground/ctrl/neutral/pressed': '#424242', - 'foreground/ctrl/neutral/disabled': '#BDBDBD', - - 'foreground/ctrl/brand/rest': '#FFFFFF', - 'foreground/ctrl/brand/hover': '#FFFFFF', - 'foreground/ctrl/brand/pressed': '#FFFFFF', - 'foreground/ctrl/brand/disabled': '#BDBDBD', - - 'foreground/ctrl/outline/rest': '#616161', - 'foreground/ctrl/outline/hover': '#424242', - 'foreground/ctrl/outline/pressed': '#424242', - 'foreground/ctrl/outline/disabled': '#BDBDBD', - - 'foreground/ctrl/subtle/rest': '#616161', - 'foreground/ctrl/subtle/hover': '#424242', - 'foreground/ctrl/subtle/pressed': '#424242', - 'foreground/ctrl/subtle/disabled': '#BDBDBD', - - 'foreground/ctrl/transparent/rest': '#616161', + 'foreground/ctrl/neutral/rest': tokens.colorNeutralForeground3, + 'foreground/ctrl/neutral/hover': tokens.colorNeutralForeground3Hover, + 'foreground/ctrl/neutral/pressed': tokens.colorNeutralForeground3Pressed, + 'foreground/ctrl/neutral/disabled': tokens.colorNeutralForegroundDisabled, + + 'foreground/ctrl/brand/rest': tokens.colorNeutralForegroundOnBrand, + 'foreground/ctrl/brand/hover': tokens.colorNeutralForegroundOnBrand, + 'foreground/ctrl/brand/pressed': tokens.colorNeutralForegroundOnBrand, + 'foreground/ctrl/brand/disabled': tokens.colorNeutralForegroundDisabled, + + 'foreground/ctrl/outline/rest': tokens.colorNeutralForeground3, + 'foreground/ctrl/outline/hover': tokens.colorNeutralForeground3Hover, + 'foreground/ctrl/outline/pressed': tokens.colorNeutralForeground3Pressed, + 'foreground/ctrl/outline/disabled': tokens.colorNeutralForegroundDisabled, + + 'foreground/ctrl/subtle/rest': tokens.colorNeutralForeground3, + 'foreground/ctrl/subtle/hover': tokens.colorNeutralForeground3Hover, + 'foreground/ctrl/subtle/pressed': tokens.colorNeutralForeground3Pressed, + 'foreground/ctrl/subtle/disabled': tokens.colorNeutralForegroundDisabled, + + 'foreground/ctrl/transparent/rest': tokens.colorNeutralForeground3, 'foreground/ctrl/transparent/hover': '#00686D', 'foreground/ctrl/transparent/pressed': '#00595D', - 'foreground/ctrl/transparent/disabled': '#BDBDBD', + 'foreground/ctrl/transparent/disabled': tokens.colorNeutralForegroundDisabled, // Background - 'background/ctrl/neutral/rest': 'hsl(0, 0%, 98%)', - 'background/ctrl/neutral/hover': 'hsl(0, 0%, 94%)', - 'background/ctrl/neutral/pressed': 'hsl(0, 0%, 86%)', - 'background/ctrl/neutral/disabled': 'hsl(0, 0%, 94%)', - - 'background/ctrl/brand/rest': 'hsl(182, 95%, 25%)', - 'background/ctrl/brand/hover': 'hsl(183, 100%, 21%)', - 'background/ctrl/brand/pressed': 'hsl(184, 100%, 12%)', - 'background/ctrl/brand/disabled': 'hsl(0, 0%, 94%)', - - 'background/ctrl/outline/rest': 'transparent', - 'background/ctrl/outline/hover': 'transparent', - 'background/ctrl/outline/pressed': 'transparent', - 'background/ctrl/outline/disabled': 'transparent', - - 'background/ctrl/subtle/rest': 'transparent', + 'background/ctrl/neutral/rest': tokens.colorNeutralBackground2, + 'background/ctrl/neutral/hover': tokens.colorNeutralBackground2Hover, + 'background/ctrl/neutral/pressed': tokens.colorNeutralBackground2Pressed, + 'background/ctrl/neutral/disabled': tokens.colorNeutralBackgroundDisabled, + + 'background/ctrl/brand/rest': '#03787c', + 'background/ctrl/brand/hover': '#00666b', + 'background/ctrl/brand/pressed': '#00393d', + 'background/ctrl/brand/disabled': tokens.colorNeutralBackgroundDisabled, + + 'background/ctrl/outline/rest': tokens.colorTransparentBackground, + 'background/ctrl/outline/hover': tokens.colorTransparentBackgroundHover, + 'background/ctrl/outline/pressed': tokens.colorTransparentBackgroundPressed, + 'background/ctrl/outline/disabled': tokens.colorTransparentBackground, + + 'background/ctrl/subtle/rest': tokens.colorTransparentBackground, 'background/ctrl/subtle/hover': '#F5F5F5', 'background/ctrl/subtle/pressed': '#E0E0E0', - 'background/ctrl/subtle/disabled': '', + 'background/ctrl/subtle/disabled': tokens.colorTransparentBackground, - 'background/ctrl/transparent/rest': 'transparent', - 'background/ctrl/transparent/hover': 'transparent', - 'background/ctrl/transparent/pressed': 'transparent', - 'background/ctrl/transparent/disabled': 'transparent', + 'background/ctrl/transparent/rest': tokens.colorTransparentBackground, + 'background/ctrl/transparent/hover': tokens.colorTransparentBackgroundHover, + 'background/ctrl/transparent/pressed': tokens.colorTransparentBackgroundPressed, + 'background/ctrl/transparent/disabled': tokens.colorTransparentBackground, // Border 'borderColor/ctrl/neutral/rest': '#D1D1D1', @@ -315,51 +315,51 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'borderColor/ctrl/neutral/pressed': '#B3B3B3', 'borderColor/ctrl/neutral/disabled': '#E0E0E0', - 'borderColor/ctrl/brand/rest': 'transparent', // Acturally should be the same as background, if not sizes would be different - 'borderColor/ctrl/brand/hover': 'transparent', - 'borderColor/ctrl/brand/pressed': 'transparent', - 'borderColor/ctrl/brand/disabled': 'transparent', + 'borderColor/ctrl/brand/rest': tokens.colorTransparentStroke, // Acturally should be the same as background, if not sizes would be different + 'borderColor/ctrl/brand/hover': tokens.colorTransparentStroke, + 'borderColor/ctrl/brand/pressed': tokens.colorTransparentStroke, + 'borderColor/ctrl/brand/disabled': tokens.colorTransparentStroke, 'borderColor/ctrl/outline/rest': '#D1D1D1', 'borderColor/ctrl/outline/hover': '#C7C7C7', 'borderColor/ctrl/outline/pressed': '#B3B3B3', 'borderColor/ctrl/outline/disabled': '#E0E0E0', - 'borderColor/ctrl/subtle/rest': 'transparent', - 'borderColor/ctrl/subtle/hover': 'transparent', - 'borderColor/ctrl/subtle/pressed': 'transparent', - 'borderColor/ctrl/subtle/disabled': 'transparent', + 'borderColor/ctrl/subtle/rest': tokens.colorTransparentStroke, + 'borderColor/ctrl/subtle/hover': tokens.colorTransparentStroke, + 'borderColor/ctrl/subtle/pressed': tokens.colorTransparentStroke, + 'borderColor/ctrl/subtle/disabled': tokens.colorTransparentStroke, - 'borderColor/ctrl/transparent/rest': 'transparent', - 'borderColor/ctrl/transparent/hover': 'transparent', - 'borderColor/ctrl/transparent/pressed': 'transparent', - 'borderColor/ctrl/transparent/disabled': 'transparent', + 'borderColor/ctrl/transparent/rest': tokens.colorTransparentStroke, + 'borderColor/ctrl/transparent/hover': tokens.colorTransparentStroke, + 'borderColor/ctrl/transparent/pressed': tokens.colorTransparentStroke, + 'borderColor/ctrl/transparent/disabled': tokens.colorTransparentStroke, // Icon - 'iconColor/ctrl/neutral/rest': '#616161', - 'iconColor/ctrl/neutral/hover': '#424242', + 'iconColor/ctrl/neutral/rest': tokens.colorNeutralForeground3, + 'iconColor/ctrl/neutral/hover': tokens.colorNeutralForeground3Hover, 'iconColor/ctrl/neutral/pressed': '#00595D', 'iconColor/ctrl/neutral/disabled': '#BDBDBD', 'iconColor/ctrl/brand/rest': '#FFFFFF', 'iconColor/ctrl/brand/hover': '#FFFFFF', 'iconColor/ctrl/brand/pressed': '#FFFFFF', - 'iconColor/ctrl/brand/disabled': '#BDBDBD', + 'iconColor/ctrl/brand/disabled': tokens.colorNeutralForegroundDisabled, - 'iconColor/ctrl/outline/rest': '#616161', - 'iconColor/ctrl/outline/hover': '#424242', + 'iconColor/ctrl/outline/rest': tokens.colorNeutralForeground3, + 'iconColor/ctrl/outline/hover': tokens.colorNeutralForeground3Hover, 'iconColor/ctrl/outline/pressed': '#00595D', - 'iconColor/ctrl/outline/disabled': '#BDBDBD', + 'iconColor/ctrl/outline/disabled': tokens.colorNeutralForegroundDisabled, - 'iconColor/ctrl/subtle/rest': '#616161', + 'iconColor/ctrl/subtle/rest': tokens.colorNeutralForeground3, 'iconColor/ctrl/subtle/hover': '#00686D', 'iconColor/ctrl/subtle/pressed': '#00595D', - 'iconColor/ctrl/subtle/disabled': '#BDBDBD', + 'iconColor/ctrl/subtle/disabled': tokens.colorNeutralForegroundDisabled, - 'iconColor/ctrl/transparent/rest': '#616161', + 'iconColor/ctrl/transparent/rest': tokens.colorNeutralForeground3, 'iconColor/ctrl/transparent/hover': '#00686D', 'iconColor/ctrl/transparent/pressed': '#00595D', - 'iconColor/ctrl/transparent/disabled': '#BDBDBD', + 'iconColor/ctrl/transparent/disabled': tokens.colorNeutralForegroundDisabled, }; export const TEAMS_VISUAL_REFRESH_TOKENS = Object.fromEntries( From 835d429c236055016a03da7a0b09a5df27d7d2aa Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Fri, 17 Oct 2025 10:36:54 -0400 Subject: [PATCH 17/17] feat(VisualRefresh): update TEAMS_VISUAL_REFRESH_THEME with Fluent UI tokens --- .../library/src/index.ts | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/packages/react-components/visual-refresh-preview/library/src/index.ts b/packages/react-components/visual-refresh-preview/library/src/index.ts index 990b902a91337e..e11facd888f189 100644 --- a/packages/react-components/visual-refresh-preview/library/src/index.ts +++ b/packages/react-components/visual-refresh-preview/library/src/index.ts @@ -219,6 +219,18 @@ export const MAI_SEMANTIC_TOKENS = { * 9. if border="transparent" it makes button height 2px size less depends on variant * 10. size/ctrl/lg - what should we do with icon size? 12px/16px/16px * 11. Theme support? light/dark/HC? + * + * Delta + * ------------------------------------------------------ + * Colors + * '#00595D', // tokens.colorPaletteLightTealForeground2 + * '#00686D', // tokens.colorPaletteLightTealForeground2 + * '#03787c', // tokens.colorPaletteTealForeground2 + * '#00393d', // tokens.colorPaletteSteelForeground2 + * + * ------------------------------------------------------ + * Border Radius (12px) + * Sizes (all) */ export const TEAMS_VISUAL_REFRESH_THEME = { @@ -228,17 +240,17 @@ export const TEAMS_VISUAL_REFRESH_THEME = { // Button // Padding - 'padding/ctrl/horizontal/sm': '8px', - 'padding/ctrl/horizontal/md': '12px', - 'padding/ctrl/horizontal/lg': '12px', + 'padding/ctrl/horizontal/sm': tokens.spacingHorizontalS, + 'padding/ctrl/horizontal/md': tokens.spacingHorizontalM, + 'padding/ctrl/horizontal/lg': tokens.spacingHorizontalM, - 'padding/ctrl/vertical/sm': '4px', - 'padding/ctrl/vertical/md': '8px', - 'padding/ctrl/vertical/lg': '10px', + 'padding/ctrl/vertical/sm': tokens.spacingVerticalXS, + 'padding/ctrl/vertical/md': tokens.spacingVerticalS, + 'padding/ctrl/vertical/lg': tokens.spacingVerticalMNudge, // Gap - 'gap/ctrl/sm': '4px', - 'gap/ctrl/md': '6px', - 'gap/ctrl/lg': '6px', + 'gap/ctrl/sm': tokens.spacingHorizontalXS, + 'gap/ctrl/md': tokens.spacingHorizontalSNudge, + 'gap/ctrl/lg': tokens.spacingHorizontalSNudge, // Border radius 'corner/ctrl/sm': '12px', @@ -246,17 +258,17 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'corner/ctrl/lg': '12px', // Font - 'fontSize/ctrl/sm': '12px', - 'fontSize/ctrl/md': '14px', - 'fontSize/ctrl/lg': '14px', + 'fontSize/ctrl/sm': tokens.fontSizeBase200, + 'fontSize/ctrl/md': tokens.fontSizeBase300, + 'fontSize/ctrl/lg': tokens.fontSizeBase300, - 'lineHeight/ctrl/sm': '16px', - 'lineHeight/ctrl/md': '20px', - 'lineHeight/ctrl/lg': '20px', + 'lineHeight/ctrl/sm': tokens.lineHeightBase200, + 'lineHeight/ctrl/md': tokens.lineHeightBase300, + 'lineHeight/ctrl/lg': tokens.lineHeightBase300, - 'fontWeight/ctrl/sm': '600', - 'fontWeight/ctrl/md': '600', - 'fontWeight/ctrl/lg': '600', + 'fontWeight/ctrl/sm': tokens.fontWeightSemibold, + 'fontWeight/ctrl/md': tokens.fontWeightSemibold, + 'fontWeight/ctrl/lg': tokens.fontWeightSemibold, 'foreground/ctrl/neutral/rest': tokens.colorNeutralForeground3, 'foreground/ctrl/neutral/hover': tokens.colorNeutralForeground3Hover, @@ -279,8 +291,8 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'foreground/ctrl/subtle/disabled': tokens.colorNeutralForegroundDisabled, 'foreground/ctrl/transparent/rest': tokens.colorNeutralForeground3, - 'foreground/ctrl/transparent/hover': '#00686D', - 'foreground/ctrl/transparent/pressed': '#00595D', + 'foreground/ctrl/transparent/hover': '#00686D', // tokens.colorPaletteLightTealForeground2 + 'foreground/ctrl/transparent/pressed': '#00595D', // tokens.colorPaletteLightTealForeground2 'foreground/ctrl/transparent/disabled': tokens.colorNeutralForegroundDisabled, // Background @@ -289,9 +301,9 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'background/ctrl/neutral/pressed': tokens.colorNeutralBackground2Pressed, 'background/ctrl/neutral/disabled': tokens.colorNeutralBackgroundDisabled, - 'background/ctrl/brand/rest': '#03787c', - 'background/ctrl/brand/hover': '#00666b', - 'background/ctrl/brand/pressed': '#00393d', + 'background/ctrl/brand/rest': '#03787c', // tokens.colorPaletteTealForeground2 + 'background/ctrl/brand/hover': tokens.colorPaletteLightTealForeground2, + 'background/ctrl/brand/pressed': '#00393d', // tokens.colorPaletteSteelForeground2 'background/ctrl/brand/disabled': tokens.colorNeutralBackgroundDisabled, 'background/ctrl/outline/rest': tokens.colorTransparentBackground, @@ -300,8 +312,8 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'background/ctrl/outline/disabled': tokens.colorTransparentBackground, 'background/ctrl/subtle/rest': tokens.colorTransparentBackground, - 'background/ctrl/subtle/hover': '#F5F5F5', - 'background/ctrl/subtle/pressed': '#E0E0E0', + 'background/ctrl/subtle/hover': tokens.colorSubtleBackgroundHover, + 'background/ctrl/subtle/pressed': tokens.colorSubtleBackgroundPressed, 'background/ctrl/subtle/disabled': tokens.colorTransparentBackground, 'background/ctrl/transparent/rest': tokens.colorTransparentBackground, @@ -310,20 +322,20 @@ export const TEAMS_VISUAL_REFRESH_THEME = { 'background/ctrl/transparent/disabled': tokens.colorTransparentBackground, // Border - 'borderColor/ctrl/neutral/rest': '#D1D1D1', - 'borderColor/ctrl/neutral/hover': '#C7C7C7', - 'borderColor/ctrl/neutral/pressed': '#B3B3B3', - 'borderColor/ctrl/neutral/disabled': '#E0E0E0', + 'borderColor/ctrl/neutral/rest': tokens.colorNeutralStroke1, + 'borderColor/ctrl/neutral/hover': tokens.colorNeutralStroke1Hover, + 'borderColor/ctrl/neutral/pressed': tokens.colorNeutralStroke1Pressed, + 'borderColor/ctrl/neutral/disabled': tokens.colorNeutralStrokeDisabled, 'borderColor/ctrl/brand/rest': tokens.colorTransparentStroke, // Acturally should be the same as background, if not sizes would be different 'borderColor/ctrl/brand/hover': tokens.colorTransparentStroke, 'borderColor/ctrl/brand/pressed': tokens.colorTransparentStroke, 'borderColor/ctrl/brand/disabled': tokens.colorTransparentStroke, - 'borderColor/ctrl/outline/rest': '#D1D1D1', - 'borderColor/ctrl/outline/hover': '#C7C7C7', - 'borderColor/ctrl/outline/pressed': '#B3B3B3', - 'borderColor/ctrl/outline/disabled': '#E0E0E0', + 'borderColor/ctrl/outline/rest': tokens.colorNeutralStroke1, + 'borderColor/ctrl/outline/hover': tokens.colorNeutralStroke1Hover, + 'borderColor/ctrl/outline/pressed': tokens.colorNeutralStroke1Pressed, + 'borderColor/ctrl/outline/disabled': tokens.colorNeutralStrokeDisabled, 'borderColor/ctrl/subtle/rest': tokens.colorTransparentStroke, 'borderColor/ctrl/subtle/hover': tokens.colorTransparentStroke, @@ -338,27 +350,27 @@ export const TEAMS_VISUAL_REFRESH_THEME = { // Icon 'iconColor/ctrl/neutral/rest': tokens.colorNeutralForeground3, 'iconColor/ctrl/neutral/hover': tokens.colorNeutralForeground3Hover, - 'iconColor/ctrl/neutral/pressed': '#00595D', - 'iconColor/ctrl/neutral/disabled': '#BDBDBD', + 'iconColor/ctrl/neutral/pressed': '#00595D', // tokens.colorPaletteLightTealForeground2 + 'iconColor/ctrl/neutral/disabled': tokens.colorNeutralForegroundDisabled, - 'iconColor/ctrl/brand/rest': '#FFFFFF', - 'iconColor/ctrl/brand/hover': '#FFFFFF', - 'iconColor/ctrl/brand/pressed': '#FFFFFF', + 'iconColor/ctrl/brand/rest': tokens.colorNeutralForegroundOnBrand, + 'iconColor/ctrl/brand/hover': tokens.colorNeutralForegroundOnBrand, + 'iconColor/ctrl/brand/pressed': tokens.colorNeutralForegroundOnBrand, 'iconColor/ctrl/brand/disabled': tokens.colorNeutralForegroundDisabled, 'iconColor/ctrl/outline/rest': tokens.colorNeutralForeground3, 'iconColor/ctrl/outline/hover': tokens.colorNeutralForeground3Hover, - 'iconColor/ctrl/outline/pressed': '#00595D', + 'iconColor/ctrl/outline/pressed': '#00595D', // tokens.colorPaletteLightTealForeground2 'iconColor/ctrl/outline/disabled': tokens.colorNeutralForegroundDisabled, 'iconColor/ctrl/subtle/rest': tokens.colorNeutralForeground3, - 'iconColor/ctrl/subtle/hover': '#00686D', - 'iconColor/ctrl/subtle/pressed': '#00595D', + 'iconColor/ctrl/subtle/hover': '#00686D', // tokens.colorPaletteLightTealForeground2 + 'iconColor/ctrl/subtle/pressed': '#00595D', // tokens.colorPaletteLightTealForeground2 'iconColor/ctrl/subtle/disabled': tokens.colorNeutralForegroundDisabled, 'iconColor/ctrl/transparent/rest': tokens.colorNeutralForeground3, - 'iconColor/ctrl/transparent/hover': '#00686D', - 'iconColor/ctrl/transparent/pressed': '#00595D', + 'iconColor/ctrl/transparent/hover': '#00686D', // tokens.colorPaletteLightTealForeground2 + 'iconColor/ctrl/transparent/pressed': '#00595D', // tokens.colorPaletteLightTealForeground2 'iconColor/ctrl/transparent/disabled': tokens.colorNeutralForegroundDisabled, };