From 3d51d8bc288312d5e9f4003326a5874bc900b64e Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 22 Jul 2026 15:56:27 +0200 Subject: [PATCH] Redesign buttons (#39898) --- .../components/button/redesign.module.scss | 150 ++++++++++++++++++ .../components/button/redesign.stories.tsx | 100 ++++++++++++ .../mastodon/components/button/redesign.tsx | 133 ++++++++++++++++ .../mastodon/components/circular_progress.tsx | 9 +- 4 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 app/javascript/mastodon/components/button/redesign.module.scss create mode 100644 app/javascript/mastodon/components/button/redesign.stories.tsx create mode 100644 app/javascript/mastodon/components/button/redesign.tsx diff --git a/app/javascript/mastodon/components/button/redesign.module.scss b/app/javascript/mastodon/components/button/redesign.module.scss new file mode 100644 index 00000000000..92a12a79b29 --- /dev/null +++ b/app/javascript/mastodon/components/button/redesign.module.scss @@ -0,0 +1,150 @@ +.base { + --bg: transparent; + --fg: var(--color-text-primary); + + background-color: var(--bg); + border-radius: var(--radius-round); + color: var(--fg); + transition: + background-color 200ms, + color 200ms; + border: 0; + appearance: none; + cursor: pointer; + display: inline-flex; + justify-content: center; + align-items: center; + font-size: var(--fs-sm); + line-height: 1; + box-sizing: border-box; + gap: var(--space-2xs); + + &:focus-visible, + &:focus-within { + outline: 2px solid var(--fg); + } + + &:disabled, + &:hover:disabled { + cursor: auto; + } +} + +// Override default anchor styles +a.base { + &:focus { + border-radius: var(--radius-round); + } + + &:hover { + text-decoration: inherit; + } +} + +.xs { + padding: var(--space-3xs) var(--space-xs); + font-size: var(--fs-2xs); + min-height: 24px; +} + +.sm { + padding: var(--space-xs); + min-height: 32px; +} + +.md { + padding: var(--space-xs) var(--space-sm); + min-height: 40px; +} + +.lg { + padding: var(--space-sm) var(--space-md); + min-height: 48px; +} + +.solid { + --bg: var(--color-bg-inverted); + --bg-hover: color-mix(in srgb, var(--bg) 90%, var(--fg)); + --fg: var(--color-text-inverted); + + background-color: var(--bg); + border-radius: var(--radius-round); + color: var(--fg); + transition: + background-color 200ms, + color 200ms; + + &:hover { + background-color: var(--bg-hover); + } + + &:active { + background-color: color-mix(in srgb, var(--bg-hover) 80%, var(--fg)); + } + + &.accent { + --bg: var(--color-bg-brand-base); + --bg-hover: var(--color-bg-brand-base-hover); + } + + &.tonal { + --bg: var(--color-bg-highlight); + --fg: var(--color-text-primary); + } + + &.destructive { + --bg: var(--color-bg-error-base); + --bg-hover: var(--color-bg-error-base-hover); + } + + &:disabled { + --bg: var(--color-bg-disabled); + --bg-hover: var(--bg); + --fg: var(--color-text-primary); + } +} + +.text { + &.accent { + --fg: var(--color-text-brand); + + &:hover { + --fg: var(--color-text-brand-soft); + } + } + + &.neutral, + &.tonal { + &:hover { + --fg: rgb(from var(--color-text-primary) r g b / 80%); + } + } + + &.destructive { + --fg: var(--color-text-error); + } + + &:disabled, + &:hover:disabled { + --fg: var(--color-text-disabled); + } +} + +.icon { + width: var(--fs-lg); +} + +.iconOnly { + aspect-ratio: 1; +} + +.loading { + color: inherit; + height: 0.8em; + width: 0.8em; + + .iconOnly & { + height: 1em; + width: 1em; + } +} diff --git a/app/javascript/mastodon/components/button/redesign.stories.tsx b/app/javascript/mastodon/components/button/redesign.stories.tsx new file mode 100644 index 00000000000..12aae037b84 --- /dev/null +++ b/app/javascript/mastodon/components/button/redesign.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; + +import ChatIcon from '@/material-icons/400-24px/chat.svg?react'; +import DownloadIcon from '@/material-icons/400-24px/download.svg?react'; +import HeadphonesIcon from '@/material-icons/400-24px/headphones.svg?react'; + +import { Button, IconButton } from './redesign'; + +const iconArgType = { + control: 'select', + options: ['chat', 'download', 'audio'], + mapping: { + chat: ChatIcon, + download: DownloadIcon, + audio: HeadphonesIcon, + }, +} as const; + +const meta = { + title: 'Redesign/Button', + component: Button, + args: { + children: 'Action', + size: 'md', + variant: 'solid', + color: 'neutral', + disabled: false, + loading: false, + as: 'button', + onClick: fn(), + }, + argTypes: { + children: { + control: 'text', + }, + as: { + control: 'inline-radio', + options: ['button', 'a', 'link'], + }, + size: { + control: 'inline-radio', + options: ['xs', 'sm', 'md', 'lg'], + }, + variant: { + control: 'inline-radio', + options: ['solid', 'text'], + }, + color: { + control: 'inline-radio', + options: ['neutral', 'accent', 'tonal', 'destructive'], + }, + leadingIcon: iconArgType, + trailingIcon: iconArgType, + onClick: { + table: { + disable: true, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const IconLeading: Story = { + args: { + leadingIcon: ChatIcon, + }, +}; + +export const IconTrailing: Story = { + args: { + trailingIcon: HeadphonesIcon, + }, +}; + +export const IconOnly: Story = { + render(args) { + return ; + }, + args: { + leadingIcon: ChatIcon, + }, +}; + +export const Link: Story = { + render(args) { + if (args.as === 'link') { + return