refactor: Button components (#6929)

* feat: refactor button components

* fix: qa

* fix: storybook

* a11y: pass

* fix: build

* fix: rename default ->md

* fix: lint

* docs: buttons.md

* refactor part 1

* refactor part 2

* fix: undo refactor for fresh restart

* refactor

* Revert "refactor"

This reverts commit 96d65902d7.

* refactor: part 1

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: qa

* fix: remove text-contrast

* fix: qa

* fix: v-tooltip

* fix: lint

* fix: split broken

* fix: passkey qa

* fix: qa

* fix: qa

* fix: prepr

* fix: splitbutton

* improve button group seam

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-08-04 13:54:04 -07:00
committed by GitHub
co-authored by Prospector
parent 6cfe999cb3
commit bb2193b6f5
398 changed files with 13688 additions and 13133 deletions
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Admonition from '../../components/base/Admonition.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
const meta = {
title: 'Base/Admonition',
@@ -78,7 +78,7 @@ export const HeaderWithTimestamp: Story = {
export const WithTopRightActions: Story = {
render: () => ({
components: { Admonition, ButtonStyled },
components: { Admonition, Button },
template: /*html*/ `
<div style="display: flex; flex-direction: column; gap: 1rem;">
<Admonition
@@ -88,9 +88,7 @@ export const WithTopRightActions: Story = {
>
Uploading server files...
<template #top-right-actions>
<ButtonStyled type="outlined" color="blue">
<button class="!border" type="button">Cancel</button>
</ButtonStyled>
<Button type="outlined" class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]" native-type="button">Cancel</Button>
</template>
</Admonition>
<Admonition
@@ -100,9 +98,7 @@ export const WithTopRightActions: Story = {
>
Something went wrong while extracting the archive.
<template #top-right-actions>
<ButtonStyled color="red">
<button type="button">Retry</button>
</ButtonStyled>
<Button type="colored" color="red" native-type="button">Retry</Button>
</template>
</Admonition>
<Admonition type="success" header="Extraction complete" :dismissible="true">
@@ -115,7 +111,7 @@ export const WithTopRightActions: Story = {
export const WithProgressBar: Story = {
render: () => ({
components: { Admonition, ButtonStyled },
components: { Admonition, Button },
template: /*html*/ `
<div style="display: flex; flex-direction: column; gap: 1rem;">
<Admonition
@@ -127,9 +123,7 @@ export const WithProgressBar: Story = {
>
128 KB / 1.2 MB (45%)
<template #top-right-actions>
<ButtonStyled type="outlined" color="blue">
<button class="!border" type="button">Cancel</button>
</ButtonStyled>
<Button type="outlined" class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]" native-type="button">Cancel</Button>
</template>
</Admonition>
<Admonition
@@ -141,9 +135,7 @@ export const WithProgressBar: Story = {
>
24 MB extracted — config/settings.yml
<template #top-right-actions>
<ButtonStyled type="outlined" color="blue">
<button class="!border" type="button">Cancel</button>
</ButtonStyled>
<Button type="outlined" class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]" native-type="button">Cancel</Button>
</template>
</Admonition>
<Admonition
@@ -1,83 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '../../components/base/Button.vue'
const meta = {
title: 'Base/Button',
component: Button,
render: (args) => ({
components: { Button },
setup() {
return { args }
},
template: /*html*/ `
<Button v-bind="args">Click me</Button>
`,
}),
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const Primary: Story = {
args: {
color: 'primary',
},
}
export const Danger: Story = {
args: {
color: 'danger',
},
}
export const AllColors: Story = {
render: () => ({
components: { Button },
template: /*html*/ `
<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
<Button color="default">Default</Button>
<Button color="primary">Primary</Button>
<Button color="danger">Danger</Button>
<Button color="red">Red</Button>
<Button color="orange">Orange</Button>
<Button color="green">Green</Button>
<Button color="blue">Blue</Button>
<Button color="purple">Purple</Button>
</div>
`,
}),
}
export const Large: Story = {
args: {
large: true,
},
}
export const Outline: Story = {
args: {
outline: true,
},
}
export const Transparent: Story = {
args: {
transparent: true,
},
}
export const Disabled: Story = {
args: {
disabled: true,
},
}
export const AsLink: Story = {
args: {
link: 'https://modrinth.com',
external: true,
},
}
@@ -1,226 +0,0 @@
import { DownloadIcon, HeartIcon, SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
const colors = ['standard', 'brand', 'red', 'orange', 'green', 'blue', 'purple'] as const
const types = [
'standard',
'outlined',
'transparent',
'highlight',
'highlight-colored-text',
'chip',
] as const
const sizes = ['small', 'standard', 'large'] as const
const meta = {
title: 'Base/ButtonStyled',
component: ButtonStyled,
argTypes: {
color: {
control: 'select',
options: [...colors, 'medal-promo'],
},
size: {
control: 'select',
options: [...sizes],
},
type: {
control: 'select',
options: [...types],
},
circular: { control: 'boolean' },
colorFill: {
control: 'select',
options: ['auto', 'background', 'text', 'none'],
},
hoverColorFill: {
control: 'select',
options: ['auto', 'background', 'text', 'none'],
},
highlighted: { control: 'boolean' },
highlightedStyle: {
control: 'select',
options: ['main-nav-primary', 'main-nav-secondary'],
},
},
args: {
color: 'standard',
size: 'standard',
type: 'standard',
circular: false,
colorFill: 'auto',
hoverColorFill: 'auto',
highlighted: false,
highlightedStyle: 'main-nav-primary',
},
render: (args) => ({
components: { ButtonStyled, DownloadIcon },
setup() {
return { args }
},
template: /*html*/ `
<ButtonStyled v-bind="args">
<button><DownloadIcon /> Button</button>
</ButtonStyled>
`,
}),
} satisfies Meta<typeof ButtonStyled>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
type: 'standard',
},
}
export const AllVariants: Story = {
render: () => ({
components: { ButtonStyled },
setup() {
return { colors, types }
},
template: /*html*/ `
<div style="overflow-x: auto;">
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<th style="padding: 0.5rem 1rem; text-align: left; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">Color / Type</th>
<th v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">{{ type }}</th>
</tr>
</thead>
<tbody>
<tr v-for="color in colors" :key="color" style="border-top: 1px solid var(--color-surface-5);">
<td style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.875rem;">{{ color }}</td>
<td v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center;">
<ButtonStyled :color="color" :type="type">
<button>Button</button>
</ButtonStyled>
</td>
</tr>
</tbody>
</table>
</div>
`,
}),
}
export const AllVariantsHighlighted: Story = {
render: () => ({
components: { ButtonStyled },
setup() {
return { colors, types }
},
template: /*html*/ `
<div style="overflow-x: auto;">
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<th style="padding: 0.5rem 1rem; text-align: left; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">Color / Type</th>
<th v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">{{ type }}</th>
</tr>
</thead>
<tbody>
<tr v-for="color in colors" :key="color" style="border-top: 1px solid var(--color-surface-5);">
<td style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.875rem;">{{ color }}</td>
<td v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center;">
<ButtonStyled :color="color" :type="type" highlighted>
<button>Button</button>
</ButtonStyled>
</td>
</tr>
</tbody>
</table>
</div>
`,
}),
}
export const Sizes: Story = {
render: () => ({
components: { ButtonStyled },
setup() {
return { sizes, types }
},
template: /*html*/ `
<div style="overflow-x: auto;">
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<th style="padding: 0.5rem 1rem; text-align: left; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">Size / Type</th>
<th v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">{{ type }}</th>
</tr>
</thead>
<tbody>
<tr v-for="size in sizes" :key="size" style="border-top: 1px solid var(--color-surface-5);">
<td style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.875rem;">{{ size }}</td>
<td v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center;">
<ButtonStyled :size="size" :type="type" color="brand">
<button>Button</button>
</ButtonStyled>
</td>
</tr>
</tbody>
</table>
</div>
`,
}),
}
export const WithIcons: Story = {
render: () => ({
components: { ButtonStyled, DownloadIcon, HeartIcon, SettingsIcon },
setup() {
return { types }
},
template: /*html*/ `
<div style="overflow-x: auto;">
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<th style="padding: 0.5rem 1rem; text-align: left; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">Variant</th>
<th v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center; font-size: 0.75rem; text-transform: uppercase; color: var(--color-secondary);">{{ type }}</th>
</tr>
</thead>
<tbody>
<tr style="border-top: 1px solid var(--color-surface-5);">
<td style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.875rem;">Icon + text</td>
<td v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center;">
<ButtonStyled :type="type" color="brand">
<button><DownloadIcon /> Download</button>
</ButtonStyled>
</td>
</tr>
<tr style="border-top: 1px solid var(--color-surface-5);">
<td style="padding: 0.5rem 1rem; font-weight: 600; font-size: 0.875rem;">Icon only</td>
<td v-for="type in types" :key="type" style="padding: 0.5rem 1rem; text-align: center;">
<ButtonStyled :type="type" color="brand" circular>
<button><HeartIcon /></button>
</ButtonStyled>
</td>
</tr>
</tbody>
</table>
</div>
`,
}),
}
export const Disabled: Story = {
render: () => ({
components: { ButtonStyled },
setup() {
return { types }
},
template: /*html*/ `
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;">
<ButtonStyled v-for="type in types" :key="type" :type="type" color="brand">
<button disabled>{{ type }}</button>
</ButtonStyled>
</div>
`,
}),
}
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import EmptyState from '../../components/base/EmptyState.vue'
const meta = {
@@ -42,7 +42,7 @@ export const Default: Story = {
export const WithActions: StoryObj = {
render: () => ({
components: { EmptyState, ButtonStyled },
components: { EmptyState, Button },
template: /*html*/ `
<EmptyState
type="empty-inbox"
@@ -50,9 +50,7 @@ export const WithActions: StoryObj = {
description="Create your first backup"
>
<template #actions>
<ButtonStyled color="brand">
<button>Create backup</button>
</ButtonStyled>
<Button type="colored" color="brand">Create backup</Button>
</template>
</EmptyState>
`,
@@ -1,122 +0,0 @@
import { PlayIcon, SlashIcon, StopCircleIcon, UpdatedIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import JoinedButtons from '../../components/base/JoinedButtons.vue'
const meta = {
title: 'Base/JoinedButtons',
component: JoinedButtons,
argTypes: {
color: {
control: 'select',
options: ['standard', 'brand', 'red', 'orange', 'green', 'blue', 'purple'],
},
size: {
control: 'select',
options: ['small', 'standard', 'large'],
},
disabled: { control: 'boolean' },
primaryDisabled: { control: 'boolean' },
dropdownDisabled: { control: 'boolean' },
primaryMuted: { control: 'boolean' },
},
} satisfies Meta<typeof JoinedButtons>
export default meta
type Story = StoryObj<typeof meta>
export const Start: Story = {
args: {
color: 'brand',
size: 'large',
actions: [
{
id: 'start',
label: 'Start',
icon: PlayIcon,
action: () => console.log('Start'),
},
],
},
}
export const StopWithKill: Story = {
args: {
color: 'red',
size: 'large',
actions: [
{
id: 'stop',
label: 'Stop',
icon: StopCircleIcon,
action: () => console.log('Stop'),
},
{
id: 'kill_server',
label: 'Kill server',
icon: SlashIcon,
action: () => console.log('Kill'),
},
],
},
}
export const Stopping: Story = {
args: {
color: 'red',
size: 'large',
primaryDisabled: true,
primaryMuted: true,
actions: [
{
id: 'stop',
label: 'Stopping',
icon: StopCircleIcon,
action: () => console.log('Stop'),
},
{
id: 'kill_server',
label: 'Kill server',
icon: SlashIcon,
action: () => console.log('Kill'),
},
],
},
}
export const Restart: Story = {
args: {
color: 'orange',
size: 'large',
actions: [
{
id: 'restart',
label: 'Restart',
icon: UpdatedIcon,
action: () => console.log('Restart'),
},
],
},
}
export const Disabled: Story = {
args: {
color: 'red',
size: 'large',
disabled: true,
actions: [
{
id: 'stop',
label: 'Stop',
icon: StopCircleIcon,
action: () => console.log('Stop'),
},
{
id: 'kill_server',
label: 'Kill server',
icon: SlashIcon,
action: () => console.log('Kill'),
},
],
},
}
@@ -1,81 +0,0 @@
import { MoreHorizontalIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import OverflowMenu from '../../components/base/OverflowMenu.vue'
const meta = {
title: 'Base/OverflowMenu',
component: OverflowMenu,
render: (args) => ({
components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
setup() {
return { args }
},
template: /*html*/ `
<ButtonStyled circular type="transparent">
<OverflowMenu v-bind="args">
<MoreHorizontalIcon class="h-5 w-5" />
<template #edit>Edit</template>
<template #delete>Delete</template>
<template #share>Share</template>
</OverflowMenu>
</ButtonStyled>
`,
}),
} satisfies Meta<typeof OverflowMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
options: [
{ id: 'edit', action: () => console.log('Edit clicked') },
{ id: 'share', action: () => console.log('Share clicked') },
{ divider: true },
{ id: 'delete', action: () => console.log('Delete clicked'), color: 'danger' },
],
},
}
export const WithDifferentPlacements: StoryObj = {
render: () => ({
components: { OverflowMenu, MoreHorizontalIcon, ButtonStyled },
template: /*html*/ `
<div class="flex gap-8 items-start">
<div class="flex flex-col items-center gap-2">
<span class="text-sm text-secondary">bottom-end (default)</span>
<ButtonStyled circular type="transparent">
<OverflowMenu
:options="[
{ id: 'edit', action: () => {} },
{ id: 'delete', action: () => {}, color: 'danger' },
]"
>
<MoreHorizontalIcon class="h-5 w-5" />
<template #edit>Edit</template>
<template #delete>Delete</template>
</OverflowMenu>
</ButtonStyled>
</div>
<div class="flex flex-col items-center gap-2">
<span class="text-sm text-secondary">bottom-start</span>
<ButtonStyled circular type="transparent">
<OverflowMenu
direction="left"
:options="[
{ id: 'edit', action: () => {} },
{ id: 'delete', action: () => {}, color: 'danger' },
]"
>
<MoreHorizontalIcon class="h-5 w-5" />
<template #edit>Edit</template>
<template #delete>Delete</template>
</OverflowMenu>
</ButtonStyled>
</div>
</div>
`,
}),
}
@@ -20,9 +20,13 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import AutoLink from '../../components/base/AutoLink.vue'
import Avatar from '../../components/base/Avatar.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import {
Button,
IconButton,
SplitButton,
TeleportOverflowMenu,
} from '../../components/base/buttons'
import FormattedTag from '../../components/base/FormattedTag.vue'
import JoinedButtons from '../../components/base/JoinedButtons.vue'
import PageHeader from '../../components/base/page-header/index.vue'
import PageHeaderMetadata from '../../components/base/page-header/metadata/index.vue'
import PageHeaderMetadataItem from '../../components/base/page-header/metadata/page-header-metadata-item.vue'
@@ -32,7 +36,6 @@ import PageHeaderMetadataTimeItem from '../../components/base/page-header/metada
import PageHeaderActions from '../../components/base/page-header/page-header-actions.vue'
import PageHeaderBadgeItem from '../../components/base/page-header/page-header-badge-item.vue'
import TagItem from '../../components/base/TagItem.vue'
import TeleportOverflowMenu from '../../components/base/TeleportOverflowMenu.vue'
import LoaderIcon from '../../components/servers/icons/LoaderIcon.vue'
import ServerIcon from '../../components/servers/icons/ServerIcon.vue'
@@ -87,9 +90,10 @@ const pageHeaderIcons = {
const pageHeaderComponents = {
AutoLink,
Avatar,
ButtonStyled,
Button,
FormattedTag,
JoinedButtons,
IconButton,
SplitButton,
PageHeader,
PageHeaderActions,
PageHeaderBadgeItem,
@@ -111,7 +115,7 @@ const meta = {
},
decorators: [
(story) => ({
components: { story },
components: { story, TeleportOverflowMenu },
template: '<div class="w-full"><story /></div>',
}),
],
@@ -157,17 +161,13 @@ export const ProjectHeader: Story = {
<template #actions>
<PageHeaderActions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="noop">
<DownloadIcon />
Download
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu :options="menuActions" aria-label="More actions">
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="noop">
<DownloadIcon />
Download
</Button>
<TeleportOverflowMenu type="quiet" size="xl" label="More actions" :options="menuActions">
<MoreVerticalIcon />
</TeleportOverflowMenu>
</PageHeaderActions>
</template>
</PageHeader>
@@ -207,12 +207,10 @@ export const CreatorHeader: Story = {
<template #actions>
<PageHeaderActions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="noop">
<HeartIcon />
Follow
</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="noop">
<HeartIcon />
Follow
</Button>
</PageHeaderActions>
</template>
</PageHeader>
@@ -222,10 +220,7 @@ export const CreatorHeader: Story = {
export const AppInstanceHeader: Story = {
render: () => ({
components: {
...pageHeaderComponents,
LoaderIcon,
},
components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -252,22 +247,16 @@ export const AppInstanceHeader: Story = {
<template #actions>
<PageHeaderActions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="noop">
<PlayIcon />
Play
</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button type="button" aria-label="Instance settings" @click="noop">
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu :options="menuActions" aria-label="More actions">
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="noop">
<PlayIcon />
Play
</Button>
<IconButton size="xl" label="Instance settings" native-type="button" @click="noop">
<SettingsIcon />
</IconButton>
<TeleportOverflowMenu type="quiet" size="xl" label="More actions" :options="menuActions">
<MoreVerticalIcon />
</TeleportOverflowMenu>
</PageHeaderActions>
</template>
</PageHeader>
@@ -277,10 +266,7 @@ export const AppInstanceHeader: Story = {
export const BrowseHeader: Story = {
render: () => ({
components: {
...pageHeaderComponents,
LoaderIcon,
},
components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -291,11 +277,9 @@ export const BrowseHeader: Story = {
template: `
<PageHeader title="Survival SMP" :divider="false" :bottom-padding="false" main-class="items-center" title-class="leading-8" truncate-title>
<template #leading>
<ButtonStyled circular size="large">
<button type="button" aria-label="Back to instance" @click="noop">
<LeftArrowIcon />
</button>
</ButtonStyled>
<IconButton size="xl" label="Back to instance" native-type="button" @click="noop">
<LeftArrowIcon />
</IconButton>
<Avatar src="" alt="Survival SMP" size="48px" tint-by="survival-smp" />
</template>
@@ -317,10 +301,7 @@ export const BrowseHeader: Story = {
export const ServerPanelRootHeader: Story = {
render: () => ({
components: {
...pageHeaderComponents,
ServerIcon,
},
components: { ...pageHeaderComponents, ServerIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -346,17 +327,13 @@ export const ServerPanelRootHeader: Story = {
<template #actions>
<PageHeaderActions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="noop">
<PlayIcon />
Start server
</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button type="button" aria-label="Server settings" @click="noop">
<SettingsIcon />
</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="noop">
<PlayIcon />
Start server
</Button>
<IconButton size="xl" label="Server settings" native-type="button" @click="noop">
<SettingsIcon />
</IconButton>
</PageHeaderActions>
</template>
</PageHeader>
@@ -366,10 +343,7 @@ export const ServerPanelRootHeader: Story = {
export const ServerPanelInstanceHeader: Story = {
render: () => ({
components: {
...pageHeaderComponents,
LoaderIcon,
},
components: { ...pageHeaderComponents, LoaderIcon, TeleportOverflowMenu },
setup() {
return {
...pageHeaderIcons,
@@ -381,11 +355,9 @@ export const ServerPanelInstanceHeader: Story = {
template: `
<PageHeader title="My World">
<template #leading>
<ButtonStyled circular size="large">
<button type="button" aria-label="All instances" @click="noop">
<LeftArrowIcon />
</button>
</ButtonStyled>
<IconButton size="xl" label="All instances" native-type="button" @click="noop">
<LeftArrowIcon />
</IconButton>
</template>
<template #metadata>
@@ -400,18 +372,24 @@ export const ServerPanelInstanceHeader: Story = {
<template #actions>
<PageHeaderActions>
<ButtonStyled color="brand" size="large">
<button type="button" @click="noop">
<PlayIcon />
Start instance
</button>
</ButtonStyled>
<JoinedButtons :actions="joinedActions" color="red" size="large" />
<ButtonStyled circular size="large">
<button type="button" aria-label="Instance settings" @click="noop">
<SettingsIcon />
</button>
</ButtonStyled>
<Button type="colored" color="brand" size="xl" native-type="button" @click="noop">
<PlayIcon />
Start instance
</Button>
<SplitButton
type="colored"
color="red"
size="xl"
:options="joinedActions.slice(1)"
menu-label="Force stop options"
@click="noop"
>
<StopCircleIcon />
Stop
</SplitButton>
<IconButton size="xl" label="Instance settings" native-type="button" @click="noop">
<SettingsIcon />
</IconButton>
</PageHeaderActions>
</template>
</PageHeader>
@@ -1,94 +0,0 @@
import { SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '../../components/base/Button.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import PopoutMenu from '../../components/base/PopoutMenu.vue'
const meta = {
title: 'Base/PopoutMenu',
component: PopoutMenu,
render: (args) => ({
components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
setup() {
return { args }
},
template: /*html*/ `
<ButtonStyled circular type="transparent">
<PopoutMenu v-bind="args">
<SettingsIcon class="h-5 w-5" />
<template #menu>
<div class="flex flex-col gap-1 p-1">
<Button transparent>Option 1</Button>
<Button transparent>Option 2</Button>
<Button transparent>Option 3</Button>
</div>
</template>
</PopoutMenu>
</ButtonStyled>
`,
}),
} satisfies Meta<typeof PopoutMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const WithTooltip: Story = {
args: {
tooltip: 'Click for more options',
},
}
export const DifferentPlacements: StoryObj = {
render: () => ({
components: { PopoutMenu, Button, ButtonStyled, SettingsIcon },
template: /*html*/ `
<div class="flex gap-8 items-start p-8">
<div class="flex flex-col items-center gap-2">
<span class="text-sm text-secondary">bottom-end (default)</span>
<ButtonStyled circular type="transparent">
<PopoutMenu placement="bottom-end">
<SettingsIcon class="h-5 w-5" />
<template #menu>
<div class="flex flex-col gap-1 p-1">
<Button transparent>Option 1</Button>
<Button transparent>Option 2</Button>
</div>
</template>
</PopoutMenu>
</ButtonStyled>
</div>
<div class="flex flex-col items-center gap-2">
<span class="text-sm text-secondary">bottom-start</span>
<ButtonStyled circular type="transparent">
<PopoutMenu placement="bottom-start">
<SettingsIcon class="h-5 w-5" />
<template #menu>
<div class="flex flex-col gap-1 p-1">
<Button transparent>Option 1</Button>
<Button transparent>Option 2</Button>
</div>
</template>
</PopoutMenu>
</ButtonStyled>
</div>
<div class="flex flex-col items-center gap-2">
<span class="text-sm text-secondary">top-end</span>
<ButtonStyled circular type="transparent">
<PopoutMenu placement="top-end">
<SettingsIcon class="h-5 w-5" />
<template #menu>
<div class="flex flex-col gap-1 p-1">
<Button transparent>Option 1</Button>
<Button transparent>Option 2</Button>
</div>
</template>
</PopoutMenu>
</ButtonStyled>
</div>
</div>
`,
}),
}
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Admonition from '../../components/base/Admonition.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import StackedAdmonitionsRaw, {
type StackedAdmonitionItem,
} from '../../components/base/StackedAdmonitions.vue'
@@ -383,7 +383,7 @@ interface RichItem extends StackedAdmonitionItem {
export const RichContent: Story = {
render: () => ({
components: { StackedAdmonitions, Admonition, ButtonStyled },
components: { StackedAdmonitions, Admonition, Button },
setup() {
const items = ref<RichItem[]>([
{
@@ -429,12 +429,8 @@ export const RichContent: Story = {
>
{{ item.body }}
<template #top-right-actions>
<ButtonStyled v-if="item.canCancel" type="outlined" color="blue">
<button class="!border" type="button" @click="dismiss(item.id)">Cancel</button>
</ButtonStyled>
<ButtonStyled v-if="item.canRetry" color="red">
<button type="button" @click="dismiss(item.id)">Retry</button>
</ButtonStyled>
<Button type="outlined" v-if="item.canCancel" class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]" native-type="button" @click="dismiss(item.id)">Cancel</Button>
<Button type="colored" color="red" v-if="item.canRetry" native-type="button" @click="dismiss(item.id)">Retry</Button>
</template>
</Admonition>
</template>
+71 -79
View File
@@ -3,8 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { computed, ref } from 'vue'
import Badge from '../../components/base/Badge.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import OverflowMenu from '../../components/base/OverflowMenu.vue'
import { Button, TeleportOverflowMenu } from '../../components/base/buttons'
import Table from '../../components/base/Table.vue'
interface User {
@@ -54,7 +53,7 @@ export default meta
export const Default: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -74,7 +73,7 @@ export const Default: StoryObj = {
export const HorizontalOverflow: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -103,7 +102,7 @@ export const HorizontalOverflow: StoryObj = {
export const CustomClasses: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', cellClass: '!overflow-visible py-3' },
@@ -129,7 +128,7 @@ export const CustomClasses: StoryObj = {
export const WithSelection: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -160,7 +159,7 @@ export const WithSelection: StoryObj = {
export const WithSelectionData: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -193,7 +192,7 @@ export const WithSelectionData: StoryObj = {
export const WithSelectionIds: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -233,7 +232,7 @@ export const WithSorting: StoryObj = {
},
},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -269,7 +268,7 @@ export const WithSorting: StoryObj = {
export const WithColumnAlignment: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', align: 'left' as const },
@@ -289,7 +288,7 @@ export const WithColumnAlignment: StoryObj = {
export const WithCustomCellSlots: StoryObj = {
args: {},
render: () => ({
components: { Table, Badge },
components: { Table, Badge, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -335,7 +334,7 @@ export const WithCustomCellSlots: StoryObj = {
export const WithCustomHeaderSlots: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -365,7 +364,7 @@ export const WithCustomHeaderSlots: StoryObj = {
export const WithHeaderSlot: StoryObj = {
args: {},
render: () => ({
components: { Table, ButtonStyled },
components: { Table, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -383,9 +382,7 @@ export const WithHeaderSlot: StoryObj = {
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div class="text-lg font-semibold text-contrast">Team Members</div>
<div class="flex items-center gap-2">
<ButtonStyled color="brand">
<button type="button">Invite member</button>
</ButtonStyled>
<Button type="colored" color="brand" native-type="button">Invite member</Button>
</div>
</div>
</template>
@@ -397,7 +394,7 @@ export const WithHeaderSlot: StoryObj = {
export const WithActionsColumn: StoryObj = {
args: {},
render: () => ({
components: { Table, ButtonStyled, EditIcon, TrashIcon },
components: { Table, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -421,18 +418,14 @@ export const WithActionsColumn: StoryObj = {
<Table :columns="columns" :data="data">
<template #cell-actions="{ row }">
<div class="flex items-center justify-end gap-2">
<ButtonStyled color="brand" type="transparent" @click="handleEdit(row)">
<button class="flex items-center gap-1">
<EditIcon class="size-4" />
Edit
</button>
</ButtonStyled>
<ButtonStyled color="red" type="transparent" @click="handleDelete(row)">
<button class="flex items-center gap-1">
<TrashIcon class="size-4" />
Delete
</button>
</ButtonStyled>
<Button type="quiet" color="brand" @click="handleEdit(row)" class="flex items-center gap-1">
<EditIcon class="size-4" />
Edit
</Button>
<Button type="quiet" color="red" @click="handleDelete(row)" class="flex items-center gap-1">
<TrashIcon class="size-4" />
Delete
</Button>
</div>
</template>
</Table>
@@ -443,7 +436,7 @@ export const WithActionsColumn: StoryObj = {
export const WithLocalizedActionsColumn: StoryObj = {
args: {},
render: () => ({
components: { Table, ButtonStyled, EditIcon, TrashIcon },
components: { Table, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Nombre' },
@@ -467,18 +460,14 @@ export const WithLocalizedActionsColumn: StoryObj = {
<Table :columns="columns" :data="data">
<template #cell-actions="{ row }">
<div class="flex items-center justify-end gap-2">
<ButtonStyled color="brand" type="transparent" @click="handleEdit(row)">
<button class="flex items-center gap-1">
<EditIcon class="size-4" />
Editar
</button>
</ButtonStyled>
<ButtonStyled color="red" type="transparent" @click="handleDelete(row)">
<button class="flex items-center gap-1">
<TrashIcon class="size-4" />
Eliminar
</button>
</ButtonStyled>
<Button type="quiet" color="brand" @click="handleEdit(row)" class="flex items-center gap-1">
<EditIcon class="size-4" />
Editar
</Button>
<Button type="quiet" color="red" @click="handleDelete(row)" class="flex items-center gap-1">
<TrashIcon class="size-4" />
Eliminar
</Button>
</div>
</template>
</Table>
@@ -489,7 +478,7 @@ export const WithLocalizedActionsColumn: StoryObj = {
export const FullFeatured: StoryObj = {
args: {},
render: () => ({
components: { Table, Badge, ButtonStyled, EditIcon, TrashIcon },
components: { Table, Badge, EditIcon, TrashIcon, Button, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -565,18 +554,14 @@ export const FullFeatured: StoryObj = {
</template>
<template #cell-actions="{ row }">
<div class="flex items-center justify-end gap-2">
<ButtonStyled color="brand" type="transparent" @click="handleEdit(row)">
<button class="flex items-center gap-1">
<EditIcon class="size-4" />
Edit
</button>
</ButtonStyled>
<ButtonStyled color="red" type="transparent" @click="handleDelete(row)">
<button class="flex items-center gap-1">
<TrashIcon class="size-4" />
Delete
</button>
</ButtonStyled>
<Button type="quiet" color="brand" @click="handleEdit(row)" class="flex items-center gap-1">
<EditIcon class="size-4" />
Edit
</Button>
<Button type="quiet" color="red" @click="handleDelete(row)" class="flex items-center gap-1">
<TrashIcon class="size-4" />
Delete
</Button>
</div>
</template>
</Table>
@@ -592,7 +577,7 @@ export const FullFeatured: StoryObj = {
export const VirtualizedLargeData: StoryObj = {
args: {},
render: () => ({
components: { Table, Badge },
components: { Table, Badge, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name', enableSorting: true },
@@ -710,7 +695,15 @@ export const VirtualizedLargeData: StoryObj = {
export const WithOverflowMenu: StoryObj = {
args: {},
render: () => ({
components: { Table, Badge, ButtonStyled, OverflowMenu, MoreVerticalIcon, EditIcon, TrashIcon },
components: {
Table,
Badge,
MoreVerticalIcon,
EditIcon,
TrashIcon,
Button,
TeleportOverflowMenu,
},
setup() {
const columns = [
{ key: 'name', label: 'Name' },
@@ -737,17 +730,19 @@ export const WithOverflowMenu: StoryObj = {
const getMenuOptions = (row: User) => [
{
id: 'edit',
label: 'Edit',
action: () => alert(`Edit user: ${row.name}`),
},
{
id: 'duplicate',
label: 'Duplicate',
action: () => alert(`Duplicate user: ${row.name}`),
},
{ divider: true },
{ type: 'divider' },
{
id: 'delete',
color: 'red' as const,
hoverFilled: true,
label: 'Delete',
tone: 'red',
action: () => alert(`Delete user: ${row.name}`),
},
]
@@ -766,26 +761,23 @@ export const WithOverflowMenu: StoryObj = {
</template>
<template #cell-actions="{ row }">
<div class="flex justify-end">
<ButtonStyled circular type="transparent">
<OverflowMenu
<TeleportOverflowMenu type="quiet" label="More options"
:options="getMenuOptions(row)"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #edit>
<EditIcon class="size-4" aria-hidden="true" />
Edit
</template>
<template #duplicate>
<EditIcon class="size-4" aria-hidden="true" />
Duplicate
</template>
<template #delete>
<TrashIcon class="size-4" aria-hidden="true" />
Delete
</template>
</OverflowMenu>
</ButtonStyled>
<MoreVerticalIcon aria-hidden="true" />
<template #edit>
<EditIcon class="size-4" aria-hidden="true" />
Edit
</template>
<template #duplicate>
<EditIcon class="size-4" aria-hidden="true" />
Duplicate
</template>
<template #delete>
<TrashIcon class="size-4" aria-hidden="true" />
Delete
</template>
</TeleportOverflowMenu>
</div>
</template>
</Table>
@@ -796,7 +788,7 @@ export const WithOverflowMenu: StoryObj = {
export const EmptyState: StoryObj = {
args: {},
render: () => ({
components: { Table },
components: { Table, TeleportOverflowMenu },
setup() {
const columns = [
{ key: 'name', label: 'Name' },