Redesign: Add new tonal variant button (#40262)

This commit is contained in:
Echo
2026-08-24 15:04:35 +00:00
committed by GitHub
parent 98c7b86db5
commit 1bb1aba44d
14 changed files with 82 additions and 57 deletions

View File

@@ -54,6 +54,14 @@ a.base {
}
}
@mixin active {
&:active,
&[aria-pressed='true'],
&[aria-expanded='true'] {
@content;
}
}
// Sizes
.xs {
@@ -85,33 +93,64 @@ a.base {
&.accent {
--bg: var(--color-bg-brand-base);
--bg-hover: var(--color-bg-brand-base-hover);
}
.neutral {
// This technically is a duplicate, but it's added so TS realizes this class is an option.
--bg: var(--color-bg-inverted);
--fg: var(--color-text-inverted);
}
&.tonal:not(:active, [aria-pressed='true'], [aria-expanded='true']) {
--bg: var(--color-bg-highlight);
--fg: var(--color-text-primary);
--fg: var(--color-text-on-brand-base);
}
&.destructive {
--bg: var(--color-bg-error-base);
--bg-hover: var(--color-bg-error-base-hover);
--fg: var(--color-text-on-error-base);
}
}
// Tonal
.tonal {
--bg-color: var(--color-text-primary);
--bg-opacity: 4%;
--bg: rgb(from var(--bg-color) r g b / var(--bg-opacity));
--bg-hover-opacity: 10%;
--bg-hover: rgb(from var(--bg-color) r g b / var(--bg-hover-opacity));
&.neutral {
@include active {
--bg: var(--color-bg-inverted);
--fg: var(--color-text-inverted);
}
}
&.accent,
&.destructive {
--bg-opacity: 10%;
--bg-hover-opacity: 20%;
@include active {
--bg-opacity: 100%;
}
}
&.accent {
--bg-color: var(--color-bg-brand-base);
--fg: var(--color-text-brand);
@include active {
--fg: var(--color-text-on-brand-base);
}
}
&.destructive {
--bg-color: var(--color-bg-error-base);
--fg: var(--color-text-error);
@include active {
--fg: var(--color-text-on-error-base);
}
}
}
// Ghost
.ghost {
&:active,
&[aria-pressed='true'],
&[aria-expanded='true'] {
@include active {
--bg: var(--color-bg-inverted);
--fg: var(--color-text-inverted);
}
@@ -119,9 +158,7 @@ a.base {
&.accent {
--fg: var(--color-text-brand);
&:active,
&[aria-pressed='true'],
&[aria-expanded='true'] {
@include active {
--bg: var(--color-bg-brand-base);
--fg: var(--color-text-on-brand-base);
}
@@ -130,11 +167,9 @@ a.base {
&.destructive {
--fg: var(--color-text-error);
&:active,
&[aria-pressed='true'],
&[aria-expanded='true'] {
--fg: var(--color-text-on-error-base);
@include active {
--bg: var(--color-bg-error-base);
--fg: var(--color-text-on-error-base);
}
}
}

View File

@@ -44,11 +44,11 @@ const meta = {
},
variant: {
control: 'inline-radio',
options: ['solid', 'ghost'],
options: ['solid', 'tonal', 'ghost'],
},
color: {
control: 'inline-radio',
options: ['neutral', 'accent', 'tonal', 'destructive'],
options: ['neutral', 'accent', 'destructive'],
},
leadingIcon: iconArgType,
trailingIcon: iconArgType,

View File

@@ -16,8 +16,8 @@ export const buttonClasses = classes;
interface ButtonPropsBase<As extends 'a' | 'button'> {
size?: 'lg' | 'md' | 'sm' | 'xs';
variant?: 'solid' | 'ghost';
color?: 'accent' | 'neutral' | 'tonal' | 'destructive';
variant?: 'solid' | 'tonal' | 'ghost';
color?: 'accent' | 'neutral' | 'destructive';
onClick?: React.MouseEventHandler<
As extends 'button' ? HTMLButtonElement : HTMLAnchorElement
>;
@@ -36,8 +36,8 @@ type ButtonProps = ButtonButtonProps | ButtonAnchorProps | ButtonLinkProps;
const BaseButton: React.FC<ButtonProps> = ({
size = 'md',
variant = 'solid',
color = 'tonal',
variant = 'tonal',
color = 'neutral',
as: asComp = 'button',
children,
className,

View File

@@ -54,6 +54,7 @@ export const Card = <As extends React.ElementType>({
{!hideButton && (
<IconButton
variant='solid'
icon={TrashIcon}
onClick={onDelete}
color='destructive'

View File

@@ -1,24 +1,21 @@
import type { CSSProperties } from 'react';
import classNames from 'classnames';
import type { Merge } from 'type-fest';
import type { PolymorphicProps } from '@/types/polymorphic';
import { Popover } from '../popover';
import type { PopoverProps } from '../popover';
import classes from './styles.module.scss';
export type MenuCardProps<As extends React.ElementType> = Merge<
export type MenuCardProps<As extends React.ElementType> = PolymorphicProps<
{
as?: As;
children: React.ReactNode;
className?: string;
elevation?: 1 | 2;
maxWidth?: number | string;
style?: React.CSSProperties;
},
React.ComponentProps<As>
As
>;
export const MenuCard = <As extends React.ElementType>({
@@ -41,7 +38,7 @@ export const MenuCard = <As extends React.ElementType>({
'--_max-card-width':
typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,
...style,
} as CSSProperties
} as React.CSSProperties
}
>
{children}
@@ -85,7 +82,7 @@ export const PopoverMenuCard = <As extends React.ElementType>({
{({ props: popoverChildProps }) => (
<MenuCard
{...popoverChildProps}
{...props}
{...(props as React.ComponentPropsWithoutRef<As>)}
className={classNames(
className,
props.maxWidth && classes.popoverCard,

View File

@@ -1,4 +1,3 @@
import type React from 'react';
import {
createContext,
use,
@@ -8,7 +7,7 @@ import {
useState,
} from 'react';
import type { Merge } from 'type-fest';
import type { PolymorphicProps } from '@/types/polymorphic';
import { Button } from '../button/redesign';
@@ -243,18 +242,11 @@ export const Menu: React.FC<MenuProps> = ({ type = 'actions', children }) => {
return <MenuContext value={contextValue}>{children}</MenuContext>;
};
export type MenuTriggerProps<As extends React.ElementType> = Merge<
React.ComponentProps<As>,
{
as?: As;
}
>;
export const MenuTrigger = <As extends React.ElementType>({
as: asComp,
children,
...props
}: MenuTriggerProps<As>) => {
}: PolymorphicProps<object, As>) => {
const Component = asComp ?? Button;
const { menuTriggerProps } = useMenuContext();
return (
@@ -282,7 +274,7 @@ export const MenuList = <As extends React.ElementType>({
reference={popover.reference}
popoverElement={popover.popover}
container={null}
{...props}
{...(props as React.ComponentPropsWithoutRef<As>)}
{...menuListProps}
>
{type === 'navigation' ? <ul>{children}</ul> : children}

View File

@@ -38,7 +38,7 @@ const meta = {
<ModalActions align={actionAlign}>
<Button>Cancel</Button>
<Button color='neutral'>Save</Button>
<Button variant='solid'>Save</Button>
</ModalActions>
</ModalShell>
);

View File

@@ -83,7 +83,7 @@ export const ComposeFooter: React.FC<{ onEmojiPick: OnEmojiPick }> = ({
</span>
<Button
color='neutral'
variant='solid'
type='submit'
disabled={!canSubmit}
loading={isSubmitting}

View File

@@ -52,13 +52,13 @@ const ComposerModalCancelConfirm: React.FC<{ openNew?: boolean }> = ({
/>
<ModalActions>
<Button color='destructive' onClick={handleDelete}>
<Button variant='solid' color='destructive' onClick={handleDelete}>
<FormattedMessage
id='compose.cancel_modal.delete'
defaultMessage='Delete draft'
/>
</Button>
<Button color='neutral' onClick={handleContinue}>
<Button variant='solid' onClick={handleContinue}>
<FormattedMessage
id='compose.cancel_modal.continue'
defaultMessage='Continue draft'

View File

@@ -127,7 +127,7 @@ const ComposerModalRearrange: React.FC<{ onClose: () => void }> = ({
/>
</Button>
<Button color='neutral' onClick={handleSave}>
<Button variant='solid' onClick={handleSave}>
<FormattedMessage
id='compose.rearrange_modal.save'
defaultMessage='Save'

View File

@@ -55,7 +55,7 @@ const ComposerModalSwitch: React.FC = () => {
/>
</Button>
<Button size='sm' color='neutral' onClick={handleContinue}>
<Button size='sm' variant='solid' onClick={handleContinue}>
<FormattedMessage
id='compose.switch_modal.continue'
defaultMessage='Continue'

View File

@@ -181,8 +181,8 @@ export const ComposePoll: React.FC = () => {
className={classNames(
classes.pollDurationSelect,
buttonClasses.base,
buttonClasses.solid,
buttonClasses.tonal,
buttonClasses.neutral,
buttonClasses.xs,
)}
>

View File

@@ -74,7 +74,7 @@ export const ComposeRedesignButton: React.FC = () => {
<MenuTrigger
as={IconButton}
icon={PenNibIcon}
color='neutral'
variant='solid'
className={classes.button}
size='lg'
>

View File

@@ -76,7 +76,7 @@ export const ComposeUpload: React.FC<{
as={IconButton}
icon={DotsThreeIcon}
size='sm'
color='neutral'
variant='solid'
className={classes.mediaMenuButton}
>
<FormattedMessage