Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type ButtonProps = ComponentProps<ButtonSlots> & {
*
* @default 'secondary'
*/
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent';
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent' | 'tint';

/**
* When set, allows the button to be focusable even when it has been disabled. This is used in scenarios where it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,8 @@ export const useButtonStyles_unstable = (state: ButtonState): ButtonState => {
const rootIconOnlyStyles = useRootIconOnlyStyles();
const iconStyles = useIconStyles();

const { appearance, disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state;
const { disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state;
const appearance = state.appearance === 'tint' ? 'secondary' : state.appearance;

state.root.className = mergeClasses(
buttonClassNames.root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const Appearance = (): JSXElement => {
<Button appearance="transparent" icon={<CalendarMonth />}>
Transparent
</Button>
<Button appearance="tint" icon={<CalendarMonth />}>
Tint
</Button>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { tokens } from '@fluentui/react-components';

const EXPECTED_SEMANTIC_TOKENS = [
// card
'stroke/card/onPrimary/default/rest',
'cornerRadius/ctrl/card',
'padding/card/header/body/default/inside',
'padding/card/header/body/default/outside',
] as const;
type ExpectedSemanticToken = (typeof EXPECTED_SEMANTIC_TOKENS)[number];

const TOKEN_TYPES = ['color', 'dimension'] as const;

type TokenType = (typeof TOKEN_TYPES)[number];

export interface TokenSchema {
type: TokenType;
/**
* What's the name of the semantic token that we expect to exist? When semantic tokens v2
* is released, our token should be replaced by this one.
*/
semanticToken: ExpectedSemanticToken | null;
}

const BadgeTokens = {
colorBrandForegroundCompound: { type: 'color', semanticToken: null },
} as const satisfies Record<string, TokenSchema>;

const ButtonTokens = {
buttonPrimaryBackgroundColor: { type: 'color', semanticToken: null },
buttonPrimaryBackgroundColorHover: { type: 'color', semanticToken: null },
buttonSecondaryBackgroundColor: { type: 'color', semanticToken: null },
buttonSecondaryBackgroundColorHover: { type: 'color', semanticToken: null },
buttonSubtleBackgroundColor: { type: 'color', semanticToken: null },
buttonSubtleBackgroundColorHover: { type: 'color', semanticToken: null },
buttonOutlineBackgroundColor: { type: 'color', semanticToken: null },
buttonOutlineBackgroundColorHover: { type: 'color', semanticToken: null },
buttonTintBackgroundColor: { type: 'color', semanticToken: null },
buttonTintBackgroundColorHover: { type: 'color', semanticToken: null },
} as const satisfies Record<string, TokenSchema>;

const CardTokens = {
cardBackgroundColor: { type: 'color', semanticToken: null },
cardForegroundColor: { type: 'color', semanticToken: null },
cardBackgroundColorHover: { type: 'color', semanticToken: null },
cardForegroundColorHover: { type: 'color', semanticToken: null },
cardBackgroundColorPressed: { type: 'color', semanticToken: null },
cardForegroundColorPressed: { type: 'color', semanticToken: null },
cardBackgroundColorDisabled: { type: 'color', semanticToken: null },
cardForegroundColorDisabled: { type: 'color', semanticToken: null },
cardCornerRadius: { type: 'dimension', semanticToken: 'cornerRadius/ctrl/card' },
cardHeaderPaddingOutside: { type: 'dimension', semanticToken: 'padding/card/header/body/default/outside' },
cardHeaderPaddingInside: { type: 'dimension', semanticToken: 'padding/card/header/body/default/inside' },
cardFooterHorizontalGap: { type: 'dimension', semanticToken: null },
} as const satisfies Record<string, TokenSchema>;

const DialogTokens = {} as const satisfies Record<string, TokenSchema>;

const InputTokens = {} as const satisfies Record<string, TokenSchema>;

const MenuTokens = {} as const satisfies Record<string, TokenSchema>;

const TooltipTokens = {} as const satisfies Record<string, TokenSchema>;

export const CAPTokensSchema = {
...BadgeTokens,
...ButtonTokens,
...CardTokens,
...DialogTokens,
...InputTokens,
...MenuTokens,
...TooltipTokens,
} as const satisfies { [key: string]: TokenSchema };

export const CAPTokens = {
...(Object.keys(CAPTokensSchema).reduce((acc: any, key) => {
return { ...acc, [key]: `var(--${key})` };
}) as any),
} as Record<keyof typeof CAPTokensSchema, string>;

export type CAPTheme = {
[k in keyof typeof CAPTokens]: string | null;
};

export const CAP_THEME = {
// button
buttonPrimaryBackgroundColor: tokens.colorBrandBackground,
buttonPrimaryBackgroundColorHover: tokens.colorBrandBackgroundHover,
buttonSecondaryBackgroundColor: null,
buttonSecondaryBackgroundColorHover: null,
buttonSubtleBackgroundColor: tokens.colorBrandBackground,
buttonSubtleBackgroundColorHover: tokens.colorBrandBackground,
buttonOutlineBackgroundColor: tokens.colorTransparentBackground,
buttonOutlineBackgroundColorHover: tokens.colorTransparentBackground,
buttonTintBackgroundColor: 'red',
buttonTintBackgroundColorHover: null,

// card
cardCornerRadius: tokens.borderRadiusXLarge, // 8px
cardBackgroundColor: tokens.colorNeutralBackground1,
cardForegroundColor: tokens.colorNeutralBackground1,
cardBackgroundColorHover: '',
cardForegroundColorHover: '',
cardBackgroundColorPressed: '',
cardForegroundColorPressed: '',
cardBackgroundColorDisabled: '',
cardForegroundColorDisabled: '',
cardHeaderPaddingOutside: tokens.spacingVerticalM,
cardHeaderPaddingInside: tokens.spacingVerticalS,
cardFooterHorizontalGap: tokens.spacingHorizontalS,

// TODO: switch to BrandForegroundCompound when available
colorBrandForegroundCompound: tokens.colorBrandForeground1,
} as const satisfies CAPTheme;

export const CAP_THEME_TEAMS = {
...CAP_THEME,
} as const satisfies CAPTheme;

export const CAP_THEME_ONE_DRIVE = {
...CAP_THEME,
} as const satisfies CAPTheme;

export const CAP_THEME_SHAREPOINT = {
...CAP_THEME,
} as const satisfies CAPTheme;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import {
BadgeState,
ButtonState,
CardFooterState,
CardHeaderState,
CardState,
FluentProvider,
InputState,
Theme,
} from '@fluentui/react-components';
import { useCAPButtonStylesHook } from './components/CAPButton';
import { CustomStyleHooksContext_unstable } from '@fluentui/react-shared-contexts';
import { CAPTheme } from './CAPTheme';
import { useCAPBadgeStylesHook } from './components/CAPBadge';
import { useCAPInputStylesHook } from './components/CAPInput';
import { useCAPCardFooterStylesHook, useCAPCardHeaderStylesHook, useCAPCardStylesHook } from './components/CAPCard';

export const CAPThemeProvider = ({
children,
theme,
}: {
children: React.ReactElement;
theme: Partial<Theme> & Partial<CAPTheme>;
}) => {
const customStyleHooks = React.useMemo((): React.ContextType<typeof CustomStyleHooksContext_unstable> => {
return {
useBadgeStyles_unstable: state => useCAPBadgeStylesHook(state as BadgeState),
useButtonStyles_unstable: state => useCAPButtonStylesHook(state as ButtonState),
useCardStyles_unstable: state => useCAPCardStylesHook(state as CardState),
useCardHeaderStyles_unstable: state => useCAPCardHeaderStylesHook(state as CardHeaderState),
useCardFooterStyles_unstable: state => useCAPCardFooterStylesHook(state as CardFooterState),
useInputStyles_unstable: state => useCAPInputStylesHook(state as InputState),
};
}, []);
return (
<FluentProvider theme={theme} customStyleHooks_unstable={customStyleHooks}>
{children}
</FluentProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { BadgeState, useBadgeStyles_unstable } from '@fluentui/react-components';
import { tokens } from '@fluentui/react-theme';
import { CAP_THEME } from '../CAPTheme';

const textPadding = tokens.spacingHorizontalXXS;

const useCAPBadgeStyles = makeStyles({
root: {
padding: `0 calc(${tokens.spacingHorizontalSNudge} + ${textPadding})`,
},

tiny: {
padding: 'unset',
},

'extra-small': {
padding: 'unset',
},

small: {
padding: `0 calc(${tokens.spacingHorizontalXS} + ${textPadding})`,
},

medium: {
// Set by root
},

large: {
padding: `0 calc(${tokens.spacingHorizontalSNudge} + ${textPadding})`,
},

'extra-large': {
padding: `0 calc(${tokens.spacingHorizontalS} + ${textPadding})`,
},

// shape
'rounded-extra-large': { borderRadius: tokens.borderRadiusXLarge },
'rounded-large': { borderRadius: tokens.borderRadiusLarge },
'rounded-medium': { borderRadius: tokens.borderRadiusMedium },
'rounded-small': { borderRadius: tokens.borderRadiusMedium },
'rounded-extra-small': { borderRadius: tokens.borderRadiusSmall },
'rounded-tiny': { borderRadius: tokens.borderRadiusSmall },

'outline-brand': {
...shorthands.borderColor(tokens.colorBrandStroke2),
},
'outline-warning': {
...shorthands.borderColor(tokens.colorStatusWarningBorder1),
},
'outline-important': {
...shorthands.borderColor(tokens.colorNeutralStroke1),
},
'outline-danger': {
...shorthands.borderColor(tokens.colorStatusDangerBorder1),
},
'outline-success': {
...shorthands.borderColor(tokens.colorStatusSuccessBorder1),
},
'outline-informative': {
...shorthands.borderColor(tokens.colorNeutralStroke1),
},
'outline-subtle': {
...shorthands.borderColor(tokens.colorNeutralForegroundOnBrand),
},

'tint-brand': {
color: CAP_THEME.colorBrandForegroundCompound,
},

'ghost-brand': {
color: CAP_THEME.colorBrandForegroundCompound,
},

'filled-warning': {
color: tokens.colorNeutralForegroundOnBrand,
backgroundColor: tokens.colorStatusWarningBackground3,
},

'tint-informative': {
backgroundColor: tokens.colorNeutralBackground5,
...shorthands.borderColor(tokens.colorNeutralStroke1),
},

'filled-important': {
backgroundColor: tokens.colorNeutralBackgroundInverted,
color: tokens.colorNeutralForegroundOnBrand,
},

'tint-important': {
backgroundColor: tokens.colorNeutralBackground5,
color: tokens.colorNeutralForeground3,
...shorthands.borderColor(tokens.colorNeutralStroke1),
},

'filled-subtle': {
color: tokens.colorNeutralForeground3,
backgroundColor: tokens.colorNeutralBackground5,
},

'tint-subtle': {
...shorthands.borderColor(tokens.colorNeutralStroke1),
},
});

const useCAPBadgeIconStyles = makeStyles({
beforeTextSmall: {
marginRight: textPadding,
},
afterTextSmall: {
marginLeft: textPadding,
},
});

export function useCAPBadgeStylesHook(state: BadgeState) {
// Apply base Badge styles first
useBadgeStyles_unstable(state);

// Then override with CAP styles
const styles = useCAPBadgeStyles();
const iconStyles = useCAPBadgeIconStyles();

state.root.className = mergeClasses(
state.root.className,
styles.root,
styles[state.size],
state.shape === 'rounded' && styles[`rounded-${state.size}`],
`${state.appearance}-${state.color}` in styles &&
styles[`${state.appearance}-${state.color}` as keyof typeof styles],
);

// Override icon spacing for small size
if (state.icon && state.size === 'small') {
const iconPositionClass = state.iconPosition === 'after' ? iconStyles.afterTextSmall : iconStyles.beforeTextSmall;
state.icon.className = mergeClasses(state.icon.className, iconPositionClass);
}

return state;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { makeStyles, mergeClasses } from '@griffel/react';
import { ButtonState } from '@fluentui/react-components';
import { CAPTokens } from '../CAPTheme';

const useCAPButtonStyles = makeStyles({
root: {
borderRadius: '12px',
},
primary: {
backgroundColor: CAPTokens.buttonPrimaryBackgroundColor,

':hover': {
backgroundColor: CAPTokens.buttonPrimaryBackgroundColorHover,
},
},
secondary: {
backgroundColor: CAPTokens.buttonSecondaryBackgroundColor,

':hover': {
backgroundColor: CAPTokens.buttonSecondaryBackgroundColorHover,
},
},
outline: {
backgroundColor: CAPTokens.buttonOutlineBackgroundColor,

':hover': {
backgroundColor: CAPTokens.buttonOutlineBackgroundColorHover,
},
},
subtle: {},
tint: {
backgroundColor: CAPTokens.buttonTintBackgroundColor,

':hover': {
backgroundColor: CAPTokens.buttonTintBackgroundColorHover,
},
},
transparent: {},
});

export function useCAPButtonStylesHook(state: ButtonState) {
const styles = useCAPButtonStyles();
state.root.className = mergeClasses(state.root.className, styles.root, state.appearance && styles[state.appearance]);
return state;
}
Loading
Loading