Skip to content

Commit db22700

Browse files
theme designer: suggestions from Paul (#23836)
* add new reducer to handle overrides * fix dom failing to update correctly * have colortokenslist update to reflect change in theme * alphabetize tokens list * add reset button * indicator of change * clean up code + fluentblocks color update * Update yarn lock * revert to old contrast * remove dispatch within reducer * Update packages/react-components/theme-designer/package.json Co-authored-by: Tristan Watanabe <[email protected]> * fluent-blocks/color update * rename handleButtonClick to handleResetClick * remove dispatchState from useColorOverrideReducer * rename state to appState * create type ColorOverrideBrands * removing dispatchState and dispatchColorOverride from ColorTokensList's props in favor on an onChange event handler prop * hoist useColorOverrideReducer * move edittab form to usetab, changed theme reducer to no longer be hard coded * continue editing code * lint errors * micah's suggestions * fix custom overrides not showing up Co-authored-by: Tristan Watanabe <[email protected]>
1 parent 06add44 commit db22700

File tree

15 files changed

+436
-480
lines changed

15 files changed

+436
-480
lines changed

packages/react-components/theme-designer/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"tslib": "^2.1.0",
3535
"@fluentui/react-components": "^9.0.1",
3636
"@fluentui/react-icons": "^2.0.175",
37-
"@fluent-blocks/colors": "9.1.0",
37+
"@fluent-blocks/colors": "9.2.0",
3838
"codesandbox-import-utils": "2.2.3",
3939
"@types/dedent": "0.7.0",
4040
"@fluentui/react-alert": "9.0.0-beta.5"

packages/react-components/theme-designer/src/ThemeDesigner.tsx

+4-10
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,16 @@ import { Content } from './components/Content/Content';
1414
export const ThemeDesigner: React.FC<ThemeDesignerProps> = props => {
1515
const styles = useStyles();
1616

17-
const [state, dispatchState] = useThemeDesignerReducer();
17+
const [appState, dispatchAppState] = useThemeDesignerReducer();
1818

19-
const { brand, theme, isDark, overrides } = state;
19+
const { brand, isDark, overrides } = appState;
2020

2121
return (
2222
<FluentProvider theme={teamsLightTheme}>
2323
<div className={styles.root}>
2424
<Nav className={styles.nav} brand={brand} isDark={isDark} overrides={overrides} />
25-
<Sidebar className={styles.sidebar} dispatchState={dispatchState} />
26-
<Content
27-
className={styles.content}
28-
brand={brand}
29-
theme={{ ...theme, ...overrides }}
30-
isDark={state.isDark}
31-
dispatchState={dispatchState}
32-
/>
25+
<Sidebar className={styles.sidebar} dispatchAppState={dispatchAppState} />
26+
<Content className={styles.content} appState={appState} dispatchAppState={dispatchAppState} />
3327
</div>
3428
</FluentProvider>
3529
);

packages/react-components/theme-designer/src/components/AccessibilityChecker/AccessibilityChecker.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react';
22
import { AccessibilityList } from './AccessibilityList';
3-
import { Vec3, hex_to_sRGB } from '@fluent-blocks/colors';
4-
import { contrast } from '../../utils/csswg';
3+
import { contrast, hex_to_sRGB, Vec3 } from '@fluent-blocks/colors';
54
import { Caption1, Theme } from '@fluentui/react-components';
65

76
export interface AccessibilityCheckerProps {

packages/react-components/theme-designer/src/components/ColorTokens/ColorTokens.tsx

+60-20
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,102 @@
1+
/* eslint-disable react/jsx-no-bind */
12
import * as React from 'react';
23
import { makeStyles } from '@griffel/react';
34
import { ColorTokensList } from './ColorTokensList';
4-
import { Caption1 } from '@fluentui/react-components';
5-
import { Brands, BrandVariants, teamsLightTheme, Theme } from '@fluentui/react-theme';
5+
import { Button, Caption1 } from '@fluentui/react-components';
6+
import { Brands, BrandVariants, teamsLightTheme } from '@fluentui/react-theme';
67
import { OverridableTokenBrandColors } from './OverridableTokenBrandColors';
78
import { brandTeams } from '../../utils/brandColors';
8-
99
import type { DispatchTheme } from '../../useThemeDesignerReducer';
10+
import { themeNames } from '../../utils/themeList';
1011

1112
export interface ColorTokensProps {
1213
className?: string;
13-
isDark: boolean;
1414
brand: BrandVariants;
15-
dispatchState: React.Dispatch<DispatchTheme>;
15+
themeLabel: string;
16+
dispatchAppState: React.Dispatch<DispatchTheme>;
1617
}
1718

19+
export type ColorOverrideBrands = Record<string, Brands>;
20+
21+
export type ColorOverrides = Record<string, ColorOverrideBrands>;
22+
23+
export type DispatchColorOverrides = {
24+
type: string;
25+
colorToken?: string;
26+
newValue?: Brands;
27+
};
28+
29+
const getCurrentOverride = (themeLabel: string, colorOverride: ColorOverrides) => {
30+
return colorOverride[themeLabel];
31+
};
32+
33+
const initialColorOverride: ColorOverrides = Object.fromEntries(themeNames.map(currTheme => [currTheme, {}]));
34+
1835
const useStyles = makeStyles({
1936
root: {},
2037
row: {
2138
display: 'grid',
22-
gridTemplateColumns: '1fr 1fr 1fr',
39+
gridTemplateColumns: '1fr 1fr 1fr .5fr',
2340
alignItems: 'center',
2441
},
2542
});
2643

27-
const brandColors: Record<string, Brands> = OverridableTokenBrandColors(teamsLightTheme, brandTeams);
44+
const brandColors: ColorOverrideBrands = OverridableTokenBrandColors(teamsLightTheme, brandTeams);
2845

2946
export const ColorTokens: React.FunctionComponent<ColorTokensProps> = props => {
3047
const styles = useStyles();
3148

32-
const { brand, dispatchState } = props;
33-
34-
const [overrideList, setOverrideList] = React.useState<Partial<Theme>>({});
49+
const { brand, themeLabel, dispatchAppState } = props;
3550

3651
const colorOverrideReducer: (
37-
state: Record<string, Brands>,
38-
action: { colorToken: string; newValue: Brands },
39-
) => Record<string, Brands> = (state, action) => {
40-
const overrides = { ...state, [action.colorToken]: action.newValue };
41-
setOverrideList({ ...overrideList, [action.colorToken]: brand[action.newValue] });
42-
dispatchState({ type: 'Overrides', overrides: overrideList });
43-
return overrides;
52+
state: ColorOverrides,
53+
action: { type: string; colorToken?: string; newValue?: Brands },
54+
) => ColorOverrides = (state, action) => {
55+
switch (action.type) {
56+
case 'Add Override':
57+
if (!action.colorToken || !action.newValue) {
58+
return state;
59+
}
60+
return {
61+
...state,
62+
[themeLabel]: { ...state[themeLabel], [action.colorToken]: action.newValue },
63+
};
64+
case 'Reset Overrides':
65+
return { ...state, [themeLabel]: {} };
66+
case 'Reset Custom Overrides':
67+
return { ...state, customLight: {}, customDark: {} };
68+
default:
69+
return state;
70+
}
71+
};
72+
73+
const [colorOverride, dispatchColorOverride] = React.useReducer(colorOverrideReducer, initialColorOverride);
74+
75+
const onNewOverride = (color: string, newColor: Brands) => {
76+
dispatchAppState({ type: 'Override', overrides: { [color]: brand[newColor] } });
77+
dispatchColorOverride({ type: 'Add Override', colorToken: color, newValue: newColor });
4478
};
4579

46-
const [colorOverrides, dispatchColorOverrides] = React.useReducer(colorOverrideReducer, {});
80+
const handleResetClick = () => {
81+
dispatchAppState({ type: 'Override' });
82+
dispatchColorOverride({ type: 'Reset Overrides' });
83+
};
4784

4885
return (
4986
<div className={props.className}>
5087
<div className={styles.row}>
5188
<Caption1>Color tokens</Caption1>
5289
<Caption1>Assigned values</Caption1>
5390
<Caption1>Usage examples</Caption1>
91+
<Button size="small" onClick={handleResetClick}>
92+
Reset Customizations
93+
</Button>
5494
</div>
5595
<ColorTokensList
5696
brand={brand}
5797
brandColors={brandColors}
58-
colorOverrides={colorOverrides}
59-
dispatchColorOverrides={dispatchColorOverrides}
98+
colorOverride={getCurrentOverride(themeLabel, colorOverride)}
99+
onNewOverride={onNewOverride}
60100
/>
61101
</div>
62102
);

packages/react-components/theme-designer/src/components/ColorTokens/ColorTokensList.tsx

+16-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import * as React from 'react';
33
import { makeStyles } from '@griffel/react';
44
import {
5+
Badge,
56
Divider,
67
Menu,
78
MenuButton,
@@ -18,12 +19,13 @@ import { Brands, BrandVariants } from '@fluentui/react-theme';
1819
import { CircleFilled } from '@fluentui/react-icons';
1920

2021
import { usageList } from './UsageList';
22+
import { ColorOverrideBrands } from './ColorTokens';
2123

2224
export interface ColorTokensListProps {
2325
brand: BrandVariants;
24-
brandColors: Record<string, Brands>;
25-
colorOverrides: Record<string, Brands>;
26-
dispatchColorOverrides: React.Dispatch<{ colorToken: string; newValue: Brands }>;
26+
brandColors: ColorOverrideBrands;
27+
colorOverride: ColorOverrideBrands;
28+
onNewOverride: (color: string, newColor: Brands) => void;
2729
}
2830

2931
export interface ColorTokenRowProps {
@@ -46,10 +48,10 @@ const useStyles = makeStyles({
4648
paddingLeft: '5px',
4749
paddingRight: '5px',
4850
display: 'grid',
49-
gridTemplateColumns: '1fr 1fr 1fr',
51+
gridTemplateColumns: '15px 1fr 1fr 1.5fr',
5052
alignItems: 'center',
51-
paddingTop: tokens.spacingVerticalXXXL,
52-
paddingBottom: tokens.spacingVerticalXXXL,
53+
paddingTop: tokens.spacingVerticalXL,
54+
paddingBottom: tokens.spacingVerticalXL,
5355
},
5456
});
5557

@@ -68,9 +70,9 @@ const ColorTokenRow: React.FunctionComponent<ColorTokenRowProps> = props => {
6870

6971
export const ColorTokensList: React.FunctionComponent<ColorTokensListProps> = props => {
7072
const styles = useStyles();
71-
const { brand, brandColors, colorOverrides, dispatchColorOverrides } = props;
7273

73-
const newColors = { ...brandColors, ...colorOverrides };
74+
const { brand, brandColors, colorOverride, onNewOverride } = props;
75+
const newColors = { ...brandColors, ...colorOverride };
7476

7577
return (
7678
<div>
@@ -80,12 +82,17 @@ export const ColorTokensList: React.FunctionComponent<ColorTokensListProps> = pr
8082

8183
const handleColorChange: MenuProps['onCheckedValueChange'] = (e, data) => {
8284
const newColor = parseInt(data.checkedItems[0] as string, 10) as Brands;
83-
dispatchColorOverrides({ colorToken: color, newValue: newColor });
85+
onNewOverride?.(color, newColor);
8486
};
8587

88+
const overridenTokens = Object.keys(colorOverride);
89+
8690
return (
8791
<div key={color.toString()}>
8892
<div className={styles.row}>
93+
<div className={styles.col}>
94+
{overridenTokens.includes(color) ? <Badge appearance="filled" color="success" size="tiny" /> : <> </>}
95+
</div>
8996
<div className={styles.col}>
9097
<Subtitle2 className={styles.colorLabel}>{color}</Subtitle2>
9198
<Subtitle2>Global.Color.Brand.{colorValue}</Subtitle2>

packages/react-components/theme-designer/src/components/ColorTokens/OverridableTokenBrandColors.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Brands, BrandVariants, Theme } from '@fluentui/react-theme';
2+
import { ColorOverrideBrands } from './ColorTokens';
23

34
export const brandRamp: Brands[] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160];
45

@@ -12,7 +13,7 @@ export const brandRamp: Brands[] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110
1213
* @param brand The brand that the theme uses
1314
* @returns A list of color tokens whos values are overridable by the user
1415
*/
15-
export const OverridableTokenBrandColors = (theme: Theme, brand: BrandVariants): Record<string, Brands> => {
16+
export const OverridableTokenBrandColors = (theme: Theme, brand: BrandVariants): ColorOverrideBrands => {
1617
const addList: string[] = ['colorNeutralStrokeAccessibleSelected'];
1718
const removeList: string[] = ['colorBrandBackgroundInverted', 'colorNeutralForegroundOnBrand'];
1819

@@ -32,16 +33,28 @@ export const OverridableTokenBrandColors = (theme: Theme, brand: BrandVariants):
3233
);
3334
});
3435

36+
const sortedOverrideableColorTokens = overridableColorTokens.sort((a, b) => {
37+
if (a.includes('Inverted') && b.includes('Inverted')) {
38+
return a.localeCompare(b);
39+
} else if (a.includes('Inverted')) {
40+
return 1;
41+
} else if (b.includes('Inverted')) {
42+
return -1;
43+
} else {
44+
return a.localeCompare(b);
45+
}
46+
});
47+
3548
// Flips the brand ramp to use the hex values as keys and the brand ramp colors as values for O(1) indexing
36-
const hexColorToBrand: Record<string, Brands> = brandRamp.reduce((a: Record<string, Brands>, c, i) => {
49+
const hexColorToBrand: ColorOverrideBrands = brandRamp.reduce((a: ColorOverrideBrands, c, i) => {
3750
a[brand[c]] = c;
3851
return a;
3952
}, {});
4053

4154
// Create an assignment of color tokens to brand ramp colors given the hex value
42-
const brandColors: Record<string, Brands> = {};
43-
for (let i = 0; i < overridableColorTokens.length; i++) {
44-
const key = overridableColorTokens[i];
55+
const brandColors: ColorOverrideBrands = {};
56+
for (let i = 0; i < sortedOverrideableColorTokens.length; i++) {
57+
const key = sortedOverrideableColorTokens[i];
4558
const themeColor = ((theme as unknown) as Record<string, string>)[key];
4659
brandColors[key] = hexColorToBrand[themeColor];
4760
}

packages/react-components/theme-designer/src/components/Content/Content.tsx

+7-10
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,17 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
33
import { Divider, FluentProvider, tokens } from '@fluentui/react-components';
44
import { Alert } from '@fluentui/react-alert';
55

6-
import type { DispatchTheme } from '../../useThemeDesignerReducer';
6+
import type { AppState, DispatchTheme } from '../../useThemeDesignerReducer';
77

88
import { Demo } from '../Demo/Demo';
99
import { AccessibilityChecker } from '../AccessibilityChecker/AccessibilityChecker';
1010
import { Palette } from '../Palette/Palette';
1111
import { ColorTokens } from '../ColorTokens/ColorTokens';
1212

13-
import { Theme, BrandVariants } from '@fluentui/react-theme';
14-
1513
export interface ContentProps {
1614
className?: string;
17-
brand: BrandVariants;
18-
theme: Theme;
19-
isDark: boolean;
20-
dispatchState: React.Dispatch<DispatchTheme>;
15+
appState: AppState;
16+
dispatchAppState: React.Dispatch<DispatchTheme>;
2117
}
2218

2319
const useStyles = makeStyles({
@@ -33,20 +29,21 @@ const useStyles = makeStyles({
3329

3430
export const Content: React.FC<ContentProps> = props => {
3531
const styles = useStyles();
36-
const { className, brand, theme, isDark, dispatchState } = props;
32+
const { className, appState, dispatchAppState } = props;
33+
const theme = { ...appState.theme, ...appState.overrides };
3734

3835
return (
3936
<FluentProvider theme={theme}>
4037
<Alert intent="warning" action={{ appearance: 'transparent' }}>
4138
This tool is still a work in progress - colors are still subject to adjustment.
4239
</Alert>
4340
<div className={mergeClasses(styles.root, className)}>
44-
<Palette brandColors={brand} />
41+
<Palette brandColors={appState.brand} />
4542
<Demo theme={theme} />
4643
<Divider />
4744
<AccessibilityChecker theme={theme} />
4845
<Divider />
49-
<ColorTokens isDark={isDark} brand={brand} dispatchState={dispatchState} />
46+
<ColorTokens brand={appState.brand} themeLabel={appState.themeLabel} dispatchAppState={dispatchAppState} />
5047
</div>
5148
</FluentProvider>
5249
);

0 commit comments

Comments
 (0)