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
+4 -6
View File
@@ -153,16 +153,14 @@ For components that need user interaction to show:
```typescript
export const Default: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { Button, NewModal },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
return { modalRef }
},
template: /* html */ `
<div>
<ButtonStyled @click="modalRef?.show()">
<button>Open Modal</button>
</ButtonStyled>
<Button @click="modalRef?.show()">Open Modal</Button>
<NewModal ref="modalRef" header="Example Modal">
<p>Modal content</p>
</NewModal>
@@ -199,10 +197,10 @@ Components should use relative imports, not the package alias:
```typescript
// ❌ BAD - Causes circular dependency in Storybook
import { ButtonStyled } from '@modrinth/ui'
import { Button } from '@modrinth/ui'
// ✅ GOOD - Use relative imports
import ButtonStyled from '../base/ButtonStyled.vue'
import Button from '../components/base/buttons/Button.vue'
```
### 2. Object/Array Prop Defaults Must Be Factory Functions
@@ -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' },
@@ -0,0 +1,213 @@
import { DownloadIcon, ExternalIcon, HeartIcon, SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '../../components/base/buttons/Button.vue'
import ButtonLink from '../../components/base/buttons/ButtonLink.vue'
import IconButton from '../../components/base/buttons/IconButton.vue'
const types = ['base', 'colored', 'outlined', 'quiet'] as const
const sizes = ['sm', 'md', 'lg', 'xl'] as const
const colors = ['brand', 'red', 'orange', 'green', 'blue', 'purple', 'medal_promotion'] as const
const sizeColumns = [
{ value: 'sm', label: 'Small' },
{ value: 'md', label: 'Medium' },
{ value: 'lg', label: 'Large' },
{ value: 'xl', label: 'Extra large' },
] as const
const typeRows = [
{ label: 'Base', type: 'base' },
{ label: 'Outlined', type: 'outlined' },
{ label: 'Quiet', type: 'quiet' },
...colors.map((color) => ({
label: `Colored / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
type: 'colored' as const,
color,
})),
...colors.map((color) => ({
label: `Outlined / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
type: 'outlined' as const,
color,
})),
...colors.map((color) => ({
label: `Quiet / ${color.charAt(0).toUpperCase()}${color.slice(1)}`,
type: 'quiet' as const,
color,
})),
]
const meta = {
title: 'Buttons/Button',
component: Button,
argTypes: {
type: {
control: 'select',
options: types,
},
size: {
control: 'select',
options: sizes,
},
color: {
control: 'select',
options: colors,
},
nativeType: {
control: 'select',
options: ['button', 'submit', 'reset'],
},
disabled: { control: 'boolean' },
loading: { control: 'boolean' },
},
args: {
type: 'base',
size: 'md',
color: 'brand',
nativeType: 'button',
disabled: false,
loading: false,
},
render: (args) => ({
components: { Button, DownloadIcon },
setup() {
return { args }
},
template: /*html*/ `
<Button v-bind="args">
<DownloadIcon />
Download
</Button>
`,
}),
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Playground: Story = {}
export const AllTypes: Story = {
render: () => ({
components: { Button, DownloadIcon },
setup() {
return { sizeColumns, typeRows }
},
template: /*html*/ `
<div class="grid grid-cols-[max-content_repeat(4,max-content)] items-center gap-4 overflow-x-auto p-1">
<div />
<div v-for="size in sizeColumns" :key="size.value" class="font-semibold text-contrast">
{{ size.label }}
</div>
<template v-for="row in typeRows" :key="row.label">
<div class="whitespace-nowrap font-semibold text-secondary">{{ row.label }}</div>
<Button
v-for="size in sizeColumns"
:key="size.value"
:type="row.type"
:color="row.color"
:size="size.value"
>
<DownloadIcon />Button
</Button>
</template>
</div>
`,
}),
}
export const Quiet: Story = {
render: () => ({
components: { Button, DownloadIcon, IconButton, SettingsIcon },
template: /*html*/ `
<div class="flex flex-wrap items-center gap-4">
<Button type="quiet"><DownloadIcon />Quiet</Button>
<Button type="quiet" color="red"><DownloadIcon />Quiet destructive</Button>
<IconButton label="Settings" type="quiet"><SettingsIcon /></IconButton>
</div>
`,
}),
}
export const Sizes: Story = {
render: () => ({
components: { Button, DownloadIcon, IconButton },
setup() {
return { sizes }
},
template: /*html*/ `
<div class="flex flex-wrap items-center gap-4">
<template v-for="size in sizes" :key="size">
<Button :size="size"><DownloadIcon />{{ size }}</Button>
<IconButton :label="size" :size="size"><DownloadIcon /></IconButton>
</template>
</div>
`,
}),
}
export const Colors: Story = {
render: () => ({
components: { Button },
setup() {
return { colors }
},
template: /*html*/ `
<div class="flex flex-wrap items-center gap-4">
<Button v-for="color in colors" :key="color" type="colored" :color="color">
{{ color }}
</Button>
</div>
`,
}),
}
export const Content: Story = {
render: () => ({
components: { Button, DownloadIcon, SettingsIcon },
template: /*html*/ `
<div class="flex max-w-2xl flex-col items-start gap-4">
<Button>Text only</Button>
<Button><DownloadIcon />Leading icon</Button>
<Button>Trailing icon<SettingsIcon /></Button>
<Button class="w-full">Full width</Button>
<Button>Continue with a deliberately long translated action label</Button>
</div>
`,
}),
}
export const InteractionStates: Story = {
render: () => ({
components: { Button },
template: /*html*/ `
<div class="flex flex-wrap items-center gap-4">
<Button>Enabled</Button>
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
<Button type="colored">Colored</Button>
<Button type="colored" disabled>Colored disabled</Button>
<Button type="outlined">Outlined</Button>
<Button type="quiet">Quiet</Button>
</div>
`,
}),
}
export const LinksAndIconButton: Story = {
render: () => ({
components: { ButtonLink, ExternalIcon, HeartIcon, IconButton },
template: /*html*/ `
<div class="flex flex-wrap items-center gap-4">
<ButtonLink to="/library">Internal link</ButtonLink>
<ButtonLink href="https://modrinth.com" target="_blank" type="outlined">
Modrinth<ExternalIcon />
</ButtonLink>
<ButtonLink href="https://modrinth.com" disabled>Disabled link</ButtonLink>
<IconButton label="Favorite"><HeartIcon /></IconButton>
<IconButton label="Favorite" type="colored"><HeartIcon /></IconButton>
<IconButton label="Favorite" type="outlined"><HeartIcon /></IconButton>
<IconButton label="Favorite" type="quiet"><HeartIcon /></IconButton>
</div>
`,
}),
}
@@ -0,0 +1,82 @@
import { PlayIcon, SettingsIcon, StopCircleIcon, TrashIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '../../components/base/buttons/Button.vue'
import ButtonGroup from '../../components/base/buttons/ButtonGroup.vue'
import SplitButton from '../../components/base/buttons/SplitButton.vue'
import type { OverflowMenuOption } from '../../components/base/buttons/types'
const splitOptions: OverflowMenuOption[] = [
{
id: 'settings',
label: 'Server settings',
icon: SettingsIcon,
action: () => undefined,
},
{ type: 'divider' },
{
id: 'delete',
label: 'Delete server',
icon: TrashIcon,
tone: 'red',
action: () => undefined,
},
]
const meta = {
title: 'Buttons/Button Group',
component: ButtonGroup,
} satisfies Meta<typeof ButtonGroup>
export default meta
type Story = StoryObj<typeof meta>
export const Joined: Story = {
render: () => ({
components: { Button, ButtonGroup },
template: /*html*/ `
<ButtonGroup label="Pagination">
<Button type="outlined">Previous</Button>
<Button type="outlined">Next</Button>
</ButtonGroup>
`,
}),
}
export const Split: Story = {
render: () => ({
components: { PlayIcon, SplitButton },
setup() {
return { splitOptions }
},
template: /*html*/ `
<SplitButton
menu-label="More server actions"
group-label="Server actions"
type="colored"
:options="splitOptions"
>
<PlayIcon />Start server
</SplitButton>
`,
}),
}
export const IndependentDisabledStates: Story = {
render: () => ({
components: { SplitButton, StopCircleIcon },
setup() {
return { splitOptions }
},
template: /*html*/ `
<div class="flex flex-wrap gap-4">
<SplitButton menu-label="More actions" :options="splitOptions" primary-disabled>
<StopCircleIcon />Primary disabled
</SplitButton>
<SplitButton menu-label="More actions" :options="splitOptions" menu-disabled>
<StopCircleIcon />Menu disabled
</SplitButton>
</div>
`,
}),
}
@@ -0,0 +1,61 @@
import { UploadIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import FileButton from '../../components/base/buttons/FileButton.vue'
const meta = {
title: 'Buttons/File Button',
component: FileButton,
argTypes: {
type: {
control: 'select',
options: ['base', 'colored', 'outlined', 'quiet'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg', 'xl'],
},
color: {
control: 'select',
options: ['brand', 'red', 'orange', 'green', 'blue', 'purple', 'medal_promotion'],
},
},
args: {
prompt: 'Select file',
type: 'base',
size: 'md',
multiple: false,
disabled: false,
},
render: (args) => ({
components: { FileButton, UploadIcon },
setup() {
return { args }
},
template: /*html*/ `
<FileButton v-bind="args">
<UploadIcon />
</FileButton>
`,
}),
} satisfies Meta<typeof FileButton>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const MultipleImages: Story = {
args: {
prompt: 'Select images',
accept: 'image/*',
multiple: true,
type: 'colored',
},
}
export const Disabled: Story = {
args: {
disabled: true,
},
}
@@ -0,0 +1,104 @@
import {
DownloadIcon,
ExternalIcon,
MoreVerticalIcon,
SettingsIcon,
TrashIcon,
} from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import TeleportOverflowMenu from '../../components/base/buttons/TeleportOverflowMenu.vue'
import type { OverflowMenuOption } from '../../components/base/buttons/types'
const options: OverflowMenuOption[] = [
{
id: 'download',
label: 'Download',
icon: DownloadIcon,
action: () => undefined,
},
{
id: 'settings',
label: 'Project settings',
icon: SettingsIcon,
type: 'link',
to: '/settings',
},
{
id: 'website',
label: 'Open website',
icon: ExternalIcon,
type: 'link',
href: 'https://modrinth.com',
target: '_blank',
},
{
id: 'unavailable',
label: 'Unavailable action',
disabled: true,
tooltip: 'This action is currently unavailable',
action: () => undefined,
},
{ type: 'divider' },
{
id: 'delete',
label: 'Delete project',
icon: TrashIcon,
tone: 'red',
action: () => undefined,
},
]
const meta = {
title: 'Buttons/Teleport Overflow Menu',
component: TeleportOverflowMenu,
args: {
label: 'More actions',
options,
type: 'base',
size: 'md',
placement: 'bottom-end',
disabled: false,
hoverable: false,
},
render: (args) => ({
components: { MoreVerticalIcon, TeleportOverflowMenu },
setup() {
return { args }
},
template: /*html*/ `
<TeleportOverflowMenu v-bind="args">
<MoreVerticalIcon />
</TeleportOverflowMenu>
`,
}),
} satisfies Meta<typeof TeleportOverflowMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const ColoredTrigger: Story = {
args: {
type: 'colored',
},
}
export const OutlinedTrigger: Story = {
args: {
type: 'outlined',
},
}
export const QuietTrigger: Story = {
args: {
type: 'quiet',
},
}
export const Hoverable: Story = {
args: {
hoverable: true,
},
}
@@ -0,0 +1,50 @@
import { SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '../../components/base/buttons/Button.vue'
import TeleportPopoutMenu from '../../components/base/buttons/TeleportPopoutMenu.vue'
const meta = {
title: 'Buttons/Teleport Popout Menu',
component: TeleportPopoutMenu,
} satisfies Meta<typeof TeleportPopoutMenu>
export default meta
type Story = StoryObj<typeof meta>
export const ArbitraryContent: Story = {
render: () => ({
components: { Button, SettingsIcon, TeleportPopoutMenu },
template: /*html*/ `
<TeleportPopoutMenu label="Configure versions" placement="bottom-start">
<template #trigger>
<SettingsIcon />Configure
</template>
<template #panel="{ close }">
<div class="flex w-72 flex-col gap-4">
<label class="flex flex-col gap-2 font-semibold text-contrast">
Version name
<input class="rounded-lg bg-surface-4 px-3 py-2 text-primary ring-1 ring-surface-5" value="1.21.8" />
</label>
<Button type="colored" @click="close()">Apply</Button>
</div>
</template>
</TeleportPopoutMenu>
`,
}),
}
export const IconTrigger: Story = {
render: () => ({
components: { SettingsIcon, TeleportPopoutMenu },
template: /*html*/ `
<TeleportPopoutMenu label="Open settings" type="quiet" icon-only>
<template #trigger><SettingsIcon /></template>
<template #panel>
<p class="m-0 w-64">Arbitrary teleported content can live here.</p>
</template>
</TeleportPopoutMenu>
`,
}),
}
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { fn } from 'storybook/test'
import { onMounted, onUnmounted, ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button, IconButton } from '../../components/base/buttons'
import ContentCardTable from '../../layouts/shared/content-tab/components/ContentCardTable.vue'
import type { ContentCardTableItem } from '../../layouts/shared/content-tab/types'
@@ -539,7 +539,7 @@ export const InteractiveActions: Story = {
export const WithCustomItemButtons: Story = {
render: () => ({
components: { ContentCardTable, ButtonStyled, EyeIcon, FolderOpenIcon, DownloadIcon },
components: { ContentCardTable, EyeIcon, FolderOpenIcon, DownloadIcon, Button, IconButton },
setup() {
return { items: sampleItems }
},
@@ -551,23 +551,17 @@ export const WithCustomItemButtons: Story = {
@delete="(id) => console.log('Delete', id)"
>
<template #itemButtonsLeft="{ item }">
<ButtonStyled v-tooltip="'Download'" circular type="transparent" color="green" color-fill="text">
<button @click="console.log('Download', item.id)">
<DownloadIcon class="size-5" />
</button>
</ButtonStyled>
<IconButton type="quiet" color="green" label="Download" v-tooltip="'Download'" @click="console.log('Download', item.id)" class="!text-green [&>svg]:!text-green">
<DownloadIcon class="size-5" />
</IconButton>
</template>
<template #itemButtonsRight="{ item }">
<ButtonStyled v-tooltip="'View on Modrinth'" circular type="transparent">
<button @click="console.log('View', item.id)">
<EyeIcon class="size-5 text-secondary" />
</button>
</ButtonStyled>
<ButtonStyled v-tooltip="'Open folder'" circular type="transparent">
<button @click="console.log('Open folder', item.id)">
<FolderOpenIcon class="size-5 text-secondary" />
</button>
</ButtonStyled>
<IconButton type="quiet" label="View on Modrinth" v-tooltip="'View on Modrinth'" @click="console.log('View', item.id)">
<EyeIcon class="size-5 text-secondary" />
</IconButton>
<IconButton type="quiet" label="Open folder" v-tooltip="'Open folder'" @click="console.log('Open folder', item.id)">
<FolderOpenIcon class="size-5 text-secondary" />
</IconButton>
</template>
</ContentCardTable>
`,
@@ -582,15 +576,13 @@ export const WithEmptyState: Story = {
export const WithCustomEmptyState: Story = {
render: () => ({
components: { ContentCardTable, ButtonStyled },
components: { ContentCardTable, Button, IconButton },
template: /*html*/ `
<ContentCardTable :items="[]">
<template #empty>
<div class="flex flex-col items-center gap-4 py-8">
<span class="text-lg text-secondary">No mods installed</span>
<ButtonStyled color="green">
<button>Browse mods</button>
</ButtonStyled>
<Button type="colored" color="green">Browse mods</Button>
</div>
</template>
</ContentCardTable>
@@ -781,7 +773,7 @@ export const WithOverflowMenu: Story = {
export const BulkActionsDemo: Story = {
render: () => ({
components: { ContentCardTable, ButtonStyled },
components: { ContentCardTable, Button, IconButton },
setup() {
const items = ref<ContentCardTableItem[]>([
{ ...sodiumItem, enabled: true },
@@ -825,15 +817,9 @@ export const BulkActionsDemo: Story = {
<div class="flex items-center gap-2">
<span class="text-sm text-secondary">{{ selectedIds.length }} selected</span>
<template v-if="selectedIds.length > 0">
<ButtonStyled size="small" color="green">
<button @click="enableSelected">Enable</button>
</ButtonStyled>
<ButtonStyled size="small" type="transparent">
<button @click="disableSelected">Disable</button>
</ButtonStyled>
<ButtonStyled size="small" color="red">
<button @click="deleteSelected">Delete</button>
</ButtonStyled>
<Button type="colored" color="green" size="xs" @click="enableSelected" class="!h-6">Enable</Button>
<Button type="quiet" size="xs" @click="disableSelected" class="!h-6">Disable</Button>
<Button type="colored" color="red" size="xs" @click="deleteSelected" class="!h-6">Delete</Button>
</template>
</div>
<ContentCardTable
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ContentDependencyWarningModal from '../../layouts/shared/content-tab/components/modals/ContentDependencyWarningModal.vue'
import type { ContentCardTableItem } from '../../layouts/shared/content-tab/types'
@@ -165,7 +165,7 @@ type Story = StoryObj<typeof meta>
export const InstanceDependency: Story = {
render: () => ({
components: { ButtonStyled, ContentDependencyWarningModal },
components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
const deleted = ref(false)
@@ -186,9 +186,7 @@ export const InstanceDependency: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="orange">
<button @click="modalRef?.show()">Delete dependency</button>
</ButtonStyled>
<Button type="colored" color="orange" @click="modalRef?.show()">Delete dependency</Button>
<p v-if="deleted" class="m-0 text-sm text-secondary">Dependency deletion confirmed</p>
<ContentDependencyWarningModal
ref="modalRef"
@@ -210,7 +208,7 @@ export const InstanceDependency: Story = {
export const ServerDependency: Story = {
render: () => ({
components: { ButtonStyled, ContentDependencyWarningModal },
components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
const deleted = ref(false)
@@ -227,9 +225,7 @@ export const ServerDependency: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="orange">
<button @click="modalRef?.show()">Delete server dependency</button>
</ButtonStyled>
<Button type="colored" color="orange" @click="modalRef?.show()">Delete server dependency</Button>
<p v-if="deleted" class="m-0 text-sm text-secondary">Server dependency deletion confirmed</p>
<ContentDependencyWarningModal
ref="modalRef"
@@ -246,7 +242,7 @@ export const ServerDependency: Story = {
export const BulkDependencies: Story = {
render: () => ({
components: { ButtonStyled, ContentDependencyWarningModal },
components: { ContentDependencyWarningModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
const deleted = ref(false)
@@ -267,9 +263,7 @@ export const BulkDependencies: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="orange">
<button @click="modalRef?.show()">Delete selected dependencies</button>
</ButtonStyled>
<Button type="colored" color="orange" @click="modalRef?.show()">Delete selected dependencies</Button>
<p v-if="deleted" class="m-0 text-sm text-secondary">Bulk dependency deletion confirmed</p>
<ContentDependencyWarningModal
ref="modalRef"
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { fn } from 'storybook/test'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ContentUpdaterModal from '../../layouts/shared/content-tab/components/modals/content-updater-modal/index.vue'
// Real version data from Modrinth API - Sodium (mod)
@@ -265,7 +265,7 @@ type Story = StoryObj<typeof meta>
export const ModExample: Story = {
render: (args) => ({
components: { ContentUpdaterModal, ButtonStyled },
components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -277,9 +277,7 @@ export const ModExample: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Update Sodium</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Update Sodium</Button>
<ContentUpdaterModal
ref="modalRef"
v-bind="args"
@@ -306,7 +304,7 @@ export const ModExample: Story = {
export const ModpackExample: Story = {
render: (args) => ({
components: { ContentUpdaterModal, ButtonStyled },
components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -318,9 +316,7 @@ export const ModpackExample: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Update Cobblemon Modpack</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Update Cobblemon Modpack</Button>
<ContentUpdaterModal
ref="modalRef"
v-bind="args"
@@ -347,7 +343,7 @@ export const ModpackExample: Story = {
export const WithIncompatibleVersions: Story = {
render: (args) => ({
components: { ContentUpdaterModal, ButtonStyled },
components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -355,9 +351,7 @@ export const WithIncompatibleVersions: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Update (Shows Incompatible)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Update (Shows Incompatible)</Button>
<ContentUpdaterModal
ref="modalRef"
v-bind="args"
@@ -381,7 +375,7 @@ export const WithIncompatibleVersions: Story = {
export const AllVersionTypes: Story = {
render: (args) => ({
components: { ContentUpdaterModal, ButtonStyled },
components: { ContentUpdaterModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -389,9 +383,7 @@ export const AllVersionTypes: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View All Version Types</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View All Version Types</Button>
<ContentUpdaterModal
ref="modalRef"
v-bind="args"
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ModpackContentModal from '../../layouts/shared/content-tab/components/modals/ModpackContentModal.vue'
import type { ContentItem } from '../../layouts/shared/content-tab/types'
@@ -409,7 +409,7 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -417,9 +417,7 @@ export const Default: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View Modpack Content (Mixed)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mixed)</Button>
<ModpackContentModal
ref="modalRef"
modpack-name="Cobblemon Official Modpack"
@@ -432,7 +430,7 @@ export const Default: Story = {
export const ModsOnly: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show(modsOnlyContent)
@@ -440,9 +438,7 @@ export const ModsOnly: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View Modpack Content (Mods Only)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mods Only)</Button>
<ModpackContentModal ref="modalRef" />
</div>
`,
@@ -455,7 +451,7 @@ export const ModsOnly: Story = {
export const LoadingState: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => {
@@ -469,9 +465,7 @@ export const LoadingState: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View Content (With Loading)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Content (With Loading)</Button>
<ModpackContentModal
ref="modalRef"
modpack-name="Fabulously Optimized"
@@ -488,7 +482,7 @@ export const LoadingState: Story = {
export const EmptyContent: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show([])
@@ -496,9 +490,7 @@ export const EmptyContent: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View Empty Modpack</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Empty Modpack</Button>
<ModpackContentModal ref="modalRef" />
</div>
`,
@@ -511,7 +503,7 @@ export const EmptyContent: Story = {
export const LargeModpack: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show(largeModpackContent)
@@ -519,9 +511,7 @@ export const LargeModpack: Story = {
},
template: /*html*/ `
<div>
<ButtonStyled color="brand">
<button @click="openModal">View Large Modpack (47 items)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Large Modpack (47 items)</Button>
<ModpackContentModal
ref="modalRef"
modpack-name="All the Mods 10"
@@ -538,7 +528,7 @@ export const LargeModpack: Story = {
export const SearchDemo: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -549,9 +539,7 @@ export const SearchDemo: Story = {
<p class="text-sm text-secondary max-w-md text-center">
Click the button and try searching for "sodium", "shader", or "faithful" to test the search functionality.
</p>
<ButtonStyled color="brand">
<button @click="openModal">Test Search</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Test Search</Button>
<ModpackContentModal ref="modalRef" />
</div>
`,
@@ -564,7 +552,7 @@ export const SearchDemo: Story = {
export const FilterDemo: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
@@ -575,9 +563,7 @@ export const FilterDemo: Story = {
<p class="text-sm text-secondary max-w-md text-center">
Click the button and try the filter chips (Mods, Shaders, Resource Packs) to filter content by type.
</p>
<ButtonStyled color="brand">
<button @click="openModal">Test Filters</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Test Filters</Button>
<ModpackContentModal ref="modalRef" />
</div>
`,
@@ -590,7 +576,7 @@ export const FilterDemo: Story = {
export const MixedOwnerTypes: Story = {
render: () => ({
components: { ModpackContentModal, ButtonStyled },
components: { ModpackContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
// Mix of user and organization owners
@@ -608,9 +594,7 @@ export const MixedOwnerTypes: Story = {
<p class="text-sm text-secondary max-w-md text-center">
Shows content with different owner types: users (circular avatar) and organizations (rounded + icon).
</p>
<ButtonStyled color="brand">
<button @click="openModal">View Mixed Owners</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">View Mixed Owners</Button>
<ModpackContentModal ref="modalRef" />
</div>
`,
@@ -1,7 +1,7 @@
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ConfirmLeaveModal from '../../components/modal/ConfirmLeaveModal.vue'
const meta = {
@@ -14,7 +14,7 @@ type Story = StoryObj<typeof ConfirmLeaveModal>
export const Default: Story = {
render: () => ({
components: { ConfirmLeaveModal, ButtonStyled },
components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ConfirmLeaveModal> | null>(null)
const result = ref<string>('')
@@ -27,9 +27,7 @@ export const Default: Story = {
},
template: /* html */ `
<div>
<ButtonStyled color="red">
<button @click="openModal">Trigger Leave Confirmation</button>
</ButtonStyled>
<Button type="colored" color="red" @click="openModal">Trigger Leave Confirmation</Button>
<p v-if="result" class="mt-4 text-secondary">{{ result }}</p>
<ConfirmLeaveModal ref="modalRef" />
</div>
@@ -39,7 +37,7 @@ export const Default: Story = {
export const CustomMessages: Story = {
render: () => ({
components: { ConfirmLeaveModal, ButtonStyled },
components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ConfirmLeaveModal> | null>(null)
const openModal = () => modalRef.value?.prompt()
@@ -47,9 +45,7 @@ export const CustomMessages: Story = {
},
template: /* html */ `
<div>
<ButtonStyled color="red">
<button @click="openModal">Discard Draft?</button>
</ButtonStyled>
<Button type="colored" color="red" @click="openModal">Discard Draft?</Button>
<ConfirmLeaveModal
ref="modalRef"
title="Discard draft?"
@@ -65,7 +61,7 @@ export const CustomMessages: Story = {
export const WarningAdmonition: Story = {
render: () => ({
components: { ConfirmLeaveModal, ButtonStyled },
components: { ConfirmLeaveModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ConfirmLeaveModal> | null>(null)
const openModal = () => modalRef.value?.prompt()
@@ -73,9 +69,7 @@ export const WarningAdmonition: Story = {
},
template: /* html */ `
<div>
<ButtonStyled color="orange">
<button @click="openModal">Open Warning Variant</button>
</ButtonStyled>
<Button type="colored" color="orange" @click="openModal">Open Warning Variant</Button>
<ConfirmLeaveModal
ref="modalRef"
admonition-type="warning"
@@ -1,7 +1,7 @@
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import NewModal from '../../components/modal/NewModal.vue'
const meta = {
@@ -14,7 +14,7 @@ type Story = StoryObj<typeof NewModal>
export const Default: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -22,9 +22,7 @@ export const Default: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Modal</Button>
<NewModal ref="modalRef" header="Example Modal">
<p>This is the modal content.</p>
<p class="text-secondary mt-2">You can put any content here.</p>
@@ -36,7 +34,7 @@ export const Default: Story = {
export const WithActions: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -44,19 +42,13 @@ export const WithActions: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Modal with Actions</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Modal with Actions</Button>
<NewModal ref="modalRef" header="Confirm Action">
<p>Are you sure you want to proceed with this action?</p>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled>
<button @click="modalRef?.hide()">Cancel</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="modalRef?.hide()">Confirm</button>
</ButtonStyled>
<Button @click="modalRef?.hide()">Cancel</Button>
<Button type="colored" color="brand" @click="modalRef?.hide()">Confirm</Button>
</div>
</template>
</NewModal>
@@ -67,7 +59,7 @@ export const WithActions: Story = {
export const DangerFade: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -75,19 +67,13 @@ export const DangerFade: Story = {
},
template: `
<div>
<ButtonStyled color="red">
<button @click="openModal">Open Danger Modal</button>
</ButtonStyled>
<Button type="colored" color="red" @click="openModal">Open Danger Modal</Button>
<NewModal ref="modalRef" header="Delete Item" fade="danger">
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled>
<button @click="modalRef?.hide()">Cancel</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="modalRef?.hide()">Delete</button>
</ButtonStyled>
<Button @click="modalRef?.hide()">Cancel</Button>
<Button type="colored" color="red" @click="modalRef?.hide()">Delete</Button>
</div>
</template>
</NewModal>
@@ -98,7 +84,7 @@ export const DangerFade: Story = {
export const WarningFade: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -106,19 +92,13 @@ export const WarningFade: Story = {
},
template: `
<div>
<ButtonStyled color="orange">
<button @click="openModal">Open Warning Modal</button>
</ButtonStyled>
<Button type="colored" color="orange" @click="openModal">Open Warning Modal</Button>
<NewModal ref="modalRef" header="Warning" fade="warning">
<p>This action may have unintended consequences. Please review before proceeding.</p>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled>
<button @click="modalRef?.hide()">Cancel</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="modalRef?.hide()">Proceed</button>
</ButtonStyled>
<Button @click="modalRef?.hide()">Cancel</Button>
<Button type="colored" color="orange" @click="modalRef?.hide()">Proceed</Button>
</div>
</template>
</NewModal>
@@ -129,7 +109,7 @@ export const WarningFade: Story = {
export const Scrollable: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -137,9 +117,7 @@ export const Scrollable: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Scrollable Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Scrollable Modal</Button>
<NewModal ref="modalRef" header="Scrollable Content" scrollable max-content-height="300px">
<div class="space-y-4">
<p v-for="i in 20" :key="i">
@@ -148,9 +126,7 @@ export const Scrollable: Story = {
</div>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled color="brand">
<button @click="modalRef?.hide()">Close</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.hide()">Close</Button>
</div>
</template>
</NewModal>
@@ -161,7 +137,7 @@ export const Scrollable: Story = {
export const MergedHeader: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -169,9 +145,7 @@ export const MergedHeader: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Modal (Merged Header)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Modal (Merged Header)</Button>
<NewModal ref="modalRef" hide-header merge-header>
<div class="text-center py-8">
<h2 class="text-xl font-bold mb-4">Custom Header Area</h2>
@@ -185,7 +159,7 @@ export const MergedHeader: Story = {
export const NotClosable: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -193,17 +167,13 @@ export const NotClosable: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Non-Closable Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Non-Closable Modal</Button>
<NewModal ref="modalRef" header="Processing..." :closable="false" :close-on-esc="false" :close-on-click-outside="false">
<p>This modal cannot be closed by clicking outside or pressing escape.</p>
<p class="text-secondary mt-2">Only the action button can close it.</p>
<template #actions>
<div class="flex justify-end">
<ButtonStyled color="brand">
<button @click="modalRef?.hide()">I understand, close</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.hide()">I understand, close</Button>
</div>
</template>
</NewModal>
@@ -214,7 +184,7 @@ export const NotClosable: Story = {
export const NoPadding: Story = {
render: () => ({
components: { NewModal, ButtonStyled },
components: { NewModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
const openModal = () => modalRef.value?.show()
@@ -222,16 +192,12 @@ export const NoPadding: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Modal (No Padding)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Modal (No Padding)</Button>
<NewModal ref="modalRef" header="No Padding Modal" no-padding>
<p>This modal has no default padding on the content area.</p>
<template #actions>
<div class="flex gap-2 justify-end p-6 pt-0">
<ButtonStyled color="brand">
<button @click="modalRef?.hide()">Close</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.hide()">Close</Button>
</div>
</template>
</NewModal>
@@ -1,7 +1,7 @@
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ShareModal from '../../components/modal/ShareModal.vue'
const meta = {
@@ -20,7 +20,7 @@ export const LinkShare: Story = {
link: true,
},
render: (args) => ({
components: { ShareModal, ButtonStyled },
components: { ShareModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ShareModal> | null>(null)
const openModal = () => {
@@ -30,9 +30,7 @@ export const LinkShare: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Link Share Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Link Share Modal</Button>
<ShareModal ref="modalRef" v-bind="args" />
</div>
`,
@@ -47,7 +45,7 @@ export const TextShare: Story = {
link: false,
},
render: (args) => ({
components: { ShareModal, ButtonStyled },
components: { ShareModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ShareModal> | null>(null)
const openModal = () => {
@@ -57,9 +55,7 @@ export const TextShare: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="openModal">Open Text Share Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Open Text Share Modal</Button>
<ShareModal ref="modalRef" v-bind="args" />
</div>
`,
@@ -14,7 +14,7 @@ import {
import type { StoryObj } from '@storybook/vue3-vite'
import { defineComponent, h, ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import UnsavedChangesPopup from '../../components/base/UnsavedChangesPopup.vue'
import TabbedModal from '../../components/modal/TabbedModal.vue'
@@ -42,7 +42,7 @@ export default meta
export const Default: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -66,9 +66,7 @@ export const Default: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open Tabbed Modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open Tabbed Modal</Button>
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
</div>
`,
@@ -77,7 +75,7 @@ export const Default: StoryObj = {
export const WithTitleSlot: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled, SettingsIcon },
components: { TabbedModal, SettingsIcon, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -96,9 +94,7 @@ export const WithTitleSlot: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Title Slot</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Title Slot</Button>
<TabbedModal ref="modalRef" :tabs="tabs">
<template #title>
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
@@ -113,7 +109,7 @@ export const WithTitleSlot: StoryObj = {
export const WithFooter: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -137,9 +133,7 @@ export const WithFooter: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Footer</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Footer</Button>
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs">
<template #footer>
<div class="mt-auto text-secondary text-sm">
@@ -155,7 +149,7 @@ export const WithFooter: StoryObj = {
export const WithFloatingActionBar: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled, UnsavedChangesPopup },
components: { TabbedModal, UnsavedChangesPopup, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const dirty = ref(true)
@@ -170,9 +164,7 @@ export const WithFloatingActionBar: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="dirty = true; modalRef?.show()">Open with Floating Action Bar</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="dirty = true; modalRef?.show()">Open with Floating Action Bar</Button>
<TabbedModal
ref="modalRef"
header="Settings"
@@ -196,7 +188,7 @@ export const WithFloatingActionBar: StoryObj = {
export const WithBadge: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -221,9 +213,7 @@ export const WithBadge: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Badge</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Badge</Button>
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
</div>
`,
@@ -232,7 +222,7 @@ export const WithBadge: StoryObj = {
export const HiddenTabs: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -257,9 +247,7 @@ export const HiddenTabs: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Hidden Tab</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Hidden Tab</Button>
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
</div>
`,
@@ -268,7 +256,7 @@ export const HiddenTabs: StoryObj = {
export const ManyTabs: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -331,9 +319,7 @@ export const ManyTabs: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Many Tabs</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Many Tabs</Button>
<TabbedModal ref="modalRef" header="Settings" :tabs="tabs" />
</div>
`,
@@ -342,7 +328,7 @@ export const ManyTabs: StoryObj = {
export const ScrollableContent: StoryObj = {
render: () => ({
components: { TabbedModal, ButtonStyled },
components: { TabbedModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof TabbedModal> | null>(null)
const tabs = [
@@ -361,9 +347,7 @@ export const ScrollableContent: StoryObj = {
},
template: /* html */ `
<div>
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open with Scrollable Content</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open with Scrollable Content</Button>
<TabbedModal ref="modalRef" header="Scrollable Demo" :tabs="tabs" />
</div>
`,
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import UnknownFileWarningModal from '../../components/modal/UnknownFileWarningModal.vue'
const meta = {
@@ -17,15 +17,13 @@ type Story = StoryObj<typeof meta>
export const Modpack: Story = {
render: () => ({
components: { ButtonStyled, UnknownFileWarningModal },
components: { UnknownFileWarningModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof UnknownFileWarningModal> | null>(null)
return { modalRef }
},
template: /* html */ `
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open modpack warning</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open modpack warning</Button>
<UnknownFileWarningModal
ref="modalRef"
mode="modpack"
@@ -45,15 +43,13 @@ export const Modpack: Story = {
export const Mod: Story = {
render: () => ({
components: { ButtonStyled, UnknownFileWarningModal },
components: { UnknownFileWarningModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof UnknownFileWarningModal> | null>(null)
return { modalRef }
},
template: /* html */ `
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open file warning</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open file warning</Button>
<UnknownFileWarningModal
ref="modalRef"
mode="mod"
@@ -2,7 +2,7 @@ import { DownloadIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import ContentDiffModal from '../../layouts/shared/installation-settings/components/ContentDiffModal.vue'
import type { ContentDiffItem } from '../../layouts/shared/installation-settings/types'
@@ -66,15 +66,13 @@ const diffs: ContentDiffItem[] = [
export const ExternalFiles: Story = {
render: () => ({
components: { ButtonStyled, ContentDiffModal },
components: { ContentDiffModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ContentDiffModal> | null>(null)
return { diffs, DownloadIcon, modalRef }
},
template: /* html */ `
<ButtonStyled color="brand">
<button @click="modalRef?.show()">Open update warning</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show()">Open update warning</Button>
<ContentDiffModal
ref="modalRef"
header="Update to play"
@@ -2,7 +2,7 @@ import type { UploadHandle, UploadProgress } from '@modrinth/api-client'
import type { StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import UploadProgressModal from '../../components/modal/UploadProgressModal.vue'
const meta = {
@@ -52,7 +52,7 @@ function createMockUploadHandle(totalBytes: number, durationMs: number): UploadH
export const Default: Story = {
render: () => ({
components: { UploadProgressModal, ButtonStyled },
components: { UploadProgressModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof UploadProgressModal> | null>(null)
const startUpload = () => {
@@ -63,9 +63,7 @@ export const Default: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="startUpload">Upload 50MB file (3s)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="startUpload">Upload 50MB file (3s)</Button>
<UploadProgressModal ref="modalRef" />
</div>
`,
@@ -74,7 +72,7 @@ export const Default: Story = {
export const LargeFile: Story = {
render: () => ({
components: { UploadProgressModal, ButtonStyled },
components: { UploadProgressModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof UploadProgressModal> | null>(null)
const startUpload = () => {
@@ -85,9 +83,7 @@ export const LargeFile: Story = {
},
template: `
<div>
<ButtonStyled color="brand">
<button @click="startUpload">Upload 500MB file (8s)</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="startUpload">Upload 500MB file (8s)</Button>
<UploadProgressModal ref="modalRef" />
</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 NotificationPanel from '../../components/nav/NotificationPanel.vue'
import { injectNotificationManager } from '../../providers'
@@ -13,7 +13,7 @@ export default meta
export const Default: StoryObj = {
render: () => ({
components: { NotificationPanel, ButtonStyled },
components: { NotificationPanel, Button },
setup() {
const notificationManager = injectNotificationManager()
@@ -87,24 +87,12 @@ export const Default: StoryObj = {
template: /* html */ `
<div>
<div class="flex flex-wrap gap-2">
<ButtonStyled color="green">
<button @click="showSuccess">Success</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="showError">Error</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="showWarning">Warning</button>
</ButtonStyled>
<ButtonStyled color="blue">
<button @click="showInfo">Info</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showActions">Actions</button>
</ButtonStyled>
<ButtonStyled>
<button @click="clearAll">Clear All</button>
</ButtonStyled>
<Button type="colored" color="green" @click="showSuccess">Success</Button>
<Button type="colored" color="red" @click="showError">Error</Button>
<Button type="colored" color="orange" @click="showWarning">Warning</Button>
<Button type="colored" color="blue" @click="showInfo">Info</Button>
<Button @click="showActions">Actions</Button>
<Button @click="clearAll">Clear All</Button>
</div>
<NotificationPanel />
</div>
@@ -114,7 +102,7 @@ export const Default: StoryObj = {
export const LeftSide: StoryObj = {
render: () => ({
components: { NotificationPanel, ButtonStyled },
components: { NotificationPanel, Button },
setup() {
const notificationManager = injectNotificationManager()
notificationManager.setNotificationLocation('left')
@@ -161,21 +149,11 @@ export const LeftSide: StoryObj = {
template: /* html */ `
<div>
<div class="flex flex-wrap gap-2">
<ButtonStyled color="green">
<button @click="showSuccess">Success</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="showError">Error</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="showWarning">Warning</button>
</ButtonStyled>
<ButtonStyled color="blue">
<button @click="showInfo">Info</button>
</ButtonStyled>
<ButtonStyled>
<button @click="clearAll">Clear All</button>
</ButtonStyled>
<Button type="colored" color="green" @click="showSuccess">Success</Button>
<Button type="colored" color="red" @click="showError">Error</Button>
<Button type="colored" color="orange" @click="showWarning">Warning</Button>
<Button type="colored" color="blue" @click="showInfo">Info</Button>
<Button @click="clearAll">Clear All</Button>
</div>
<NotificationPanel />
</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 PopupNotificationPanel from '../../components/nav/PopupNotificationPanel.vue'
import { injectPopupNotificationManager } from '../../providers'
@@ -13,7 +13,7 @@ export default meta
export const Default: StoryObj = {
render: () => ({
components: { PopupNotificationPanel, ButtonStyled },
components: { PopupNotificationPanel, Button },
setup() {
const popupManager = injectPopupNotificationManager()
@@ -209,39 +209,17 @@ export const Default: StoryObj = {
template: /* html */ `
<div>
<div class="flex flex-wrap gap-2">
<ButtonStyled color="green">
<button @click="showSuccess">Install Complete</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="showError">Download Failed</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="showWarning">Update Available (Permanent)</button>
</ButtonStyled>
<ButtonStyled color="blue">
<button @click="showInfo">Download Complete</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showNoButtons">No Buttons</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showPermanent">Permanent</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showBlocking">Blocking</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showWaitingProgress">Waiting Progress</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showDeterminateProgress">Determinate Progress</button>
</ButtonStyled>
<ButtonStyled>
<button @click="showGroupedDownloads">Grouped Downloads</button>
</ButtonStyled>
<ButtonStyled>
<button @click="clearAll">Clear All</button>
</ButtonStyled>
<Button type="colored" color="green" @click="showSuccess">Install Complete</Button>
<Button type="colored" color="red" @click="showError">Download Failed</Button>
<Button type="colored" color="orange" @click="showWarning">Update Available (Permanent)</Button>
<Button type="colored" color="blue" @click="showInfo">Download Complete</Button>
<Button @click="showNoButtons">No Buttons</Button>
<Button @click="showPermanent">Permanent</Button>
<Button @click="showBlocking">Blocking</Button>
<Button @click="showWaitingProgress">Waiting Progress</Button>
<Button @click="showDeterminateProgress">Determinate Progress</Button>
<Button @click="showGroupedDownloads">Grouped Downloads</Button>
<Button @click="clearAll">Clear All</Button>
</div>
<PopupNotificationPanel />
</div>
@@ -3,7 +3,7 @@ import { DownloadIcon, MoreVerticalIcon } from '@modrinth/assets'
import type { GameVersionTag, Version } from '@modrinth/utils'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { ButtonLink, IconButton } from '../../components/base/buttons'
import ProjectPageVersions from '../../components/project/ProjectPageVersions.vue'
type StoryVersion = Version & {
@@ -148,28 +148,24 @@ const meta = {
versionLink: (version: Version) => `https://modrinth.com/mod/sodium/version/${version.id}`,
},
render: (args) => ({
components: { ButtonStyled, DownloadIcon, MoreVerticalIcon, ProjectPageVersions },
components: { DownloadIcon, MoreVerticalIcon, ProjectPageVersions, ButtonLink, IconButton },
setup() {
return { args }
},
template: /* html */ `
<ProjectPageVersions v-bind="args">
<template #actions="{ version }">
<ButtonStyled circular type="transparent">
<a
<ButtonLink type="quiet"
v-tooltip="'Download'"
:href="version.files[0]?.url"
:download="version.files[0]?.filename"
aria-label="Download"
>
<DownloadIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<button v-tooltip="'More options'" aria-label="More options">
<MoreVerticalIcon aria-hidden="true" />
</button>
</ButtonStyled>
class="!w-9 !px-0 !rounded-full">
<DownloadIcon aria-hidden="true" />
</ButtonLink>
<IconButton type="quiet" label="More options" v-tooltip="'More options'" aria-label="More options">
<MoreVerticalIcon aria-hidden="true" />
</IconButton>
</template>
</ProjectPageVersions>
`,
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import type { CreationFlowContextValue } from '../../components/flows/creation-flow-modal/creation-flow-context'
import CreationFlowModal from '../../components/flows/creation-flow-modal/index.vue'
@@ -23,7 +23,7 @@ type Story = StoryObj<typeof meta>
export const CreateWorld: Story = {
name: 'Create World (Hosting)',
render: () => ({
components: { CreationFlowModal, ButtonStyled },
components: { CreationFlowModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
const lastEvent = ref('')
@@ -36,9 +36,7 @@ export const CreateWorld: Story = {
},
template: /*html*/ `
<div class="flex flex-col gap-4 items-center">
<ButtonStyled color="brand">
<button @click="openModal">Create World</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Create World</Button>
<p v-if="lastEvent" class="text-sm text-secondary mt-2">Last event: {{ lastEvent }}</p>
<CreationFlowModal
ref="modalRef"
@@ -60,7 +58,7 @@ export const CreateWorld: Story = {
export const ServerOnboarding: Story = {
name: 'Server Setup (Legacy) (Hosting)',
render: () => ({
components: { CreationFlowModal, ButtonStyled },
components: { CreationFlowModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
const lastEvent = ref('')
@@ -73,9 +71,7 @@ export const ServerOnboarding: Story = {
},
template: /*html*/ `
<div class="flex flex-col gap-4 items-center">
<ButtonStyled color="brand">
<button @click="openModal">Set Up Server</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Set Up Server</Button>
<p v-if="lastEvent" class="text-sm text-secondary mt-2">Last event: {{ lastEvent }}</p>
<CreationFlowModal
ref="modalRef"
@@ -97,7 +93,7 @@ export const ServerOnboarding: Story = {
export const Instance: Story = {
name: 'Create Instance (App)',
render: () => ({
components: { CreationFlowModal, ButtonStyled },
components: { CreationFlowModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
const lastEvent = ref('')
@@ -110,9 +106,7 @@ export const Instance: Story = {
},
template: /*html*/ `
<div class="flex flex-col gap-4 items-center">
<ButtonStyled color="brand">
<button @click="openModal">Create Instance</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="openModal">Create Instance</Button>
<p v-if="lastEvent" class="text-sm text-secondary mt-2">Last event: {{ lastEvent }}</p>
<CreationFlowModal
ref="modalRef"
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import GrantAccessModal from '../../components/servers/access/GrantAccessModal.vue'
import type {
GrantServerAccessPayload,
@@ -27,7 +27,7 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => ({
components: { ButtonStyled, GrantAccessModal },
components: { GrantAccessModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof GrantAccessModal> | null>(null)
const lastAddedUser = ref('')
@@ -49,9 +49,7 @@ export const Default: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="brand">
<button @click="modalRef?.show($event)">Add user</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show($event)">Add user</Button>
<p v-if="lastAddedUser" class="m-0 text-sm text-secondary">Last added: {{ lastAddedUser }}</p>
<GrantAccessModal ref="modalRef" :search-users="searchUsers" @grant="handleGrant" />
</div>
@@ -61,7 +59,7 @@ export const Default: Story = {
export const ExistingMember: Story = {
render: () => ({
components: { ButtonStyled, GrantAccessModal },
components: { GrantAccessModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof GrantAccessModal> | null>(null)
const users = [
@@ -88,9 +86,7 @@ export const ExistingMember: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="brand">
<button @click="modalRef?.show($event)">Add existing user</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show($event)">Add existing user</Button>
<GrantAccessModal ref="modalRef" :members="members" :search-users="searchUsers" />
</div>
`,
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import RemoveAccessModal from '../../components/servers/access/RemoveAccessModal.vue'
const meta = {
@@ -17,7 +17,7 @@ type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => ({
components: { ButtonStyled, RemoveAccessModal },
components: { RemoveAccessModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof RemoveAccessModal> | null>(null)
const removed = ref(false)
@@ -28,9 +28,7 @@ export const Default: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="red">
<button @click="modalRef?.show()">Remove user</button>
</ButtonStyled>
<Button type="colored" color="red" @click="modalRef?.show()">Remove user</Button>
<p v-if="removed" class="m-0 text-sm text-secondary">User removed</p>
<RemoveAccessModal
ref="modalRef"
@@ -46,7 +44,7 @@ export const Default: Story = {
export const CancelInvite: Story = {
render: () => ({
components: { ButtonStyled, RemoveAccessModal },
components: { RemoveAccessModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof RemoveAccessModal> | null>(null)
const cancelled = ref(false)
@@ -57,9 +55,7 @@ export const CancelInvite: Story = {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="red">
<button @click="modalRef?.show()">Cancel invite</button>
</ButtonStyled>
<Button type="colored" color="red" @click="modalRef?.show()">Cancel invite</Button>
<p v-if="cancelled" class="m-0 text-sm text-secondary">Invite cancelled</p>
<RemoveAccessModal
ref="modalRef"
@@ -2,7 +2,7 @@ import { RotateCounterClockwiseIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Admonition from '../../components/base/Admonition.vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
type AdmonitionType = 'info' | 'warning' | 'critical' | 'success'
type ActionType = 'Cancel' | 'Retry' | 'Dismiss'
@@ -191,7 +191,7 @@ const sections: CopySection[] = [
export const AllCopy: Story = {
render: () => ({
components: { Admonition, ButtonStyled, RotateCounterClockwiseIcon },
components: { Admonition, RotateCounterClockwiseIcon, Button },
setup() {
return { sections }
},
@@ -221,19 +221,11 @@ export const AllCopy: Story = {
"
#top-right-actions
>
<ButtonStyled v-if="item.action === 'Cancel'" type="outlined" color="blue">
<button class="!border" type="button">Cancel</button>
</ButtonStyled>
<ButtonStyled
v-else
type="outlined"
color="red"
>
<button class="!border" type="button">
<RotateCounterClockwiseIcon class="size-5" />
Retry
</button>
</ButtonStyled>
<Button type="outlined" v-if="item.action === 'Cancel'" class="!border !text-blue [&>svg]:!text-blue !shadow-[inset_0_0_0_1px_var(--color-blue)]" native-type="button">Cancel</Button>
<Button type="outlined" v-else class="!border !text-red [&>svg]:!text-red !shadow-[inset_0_0_0_1px_var(--color-red)]" native-type="button">
<RotateCounterClockwiseIcon class="size-5" />
Retry
</Button>
</template>
</Admonition>
</div>
@@ -246,7 +238,7 @@ export const AllCopy: Story = {
export const TitleTreatmentExperiment: Story = {
render: () => ({
components: { Admonition, ButtonStyled, RotateCounterClockwiseIcon },
components: { Admonition, RotateCounterClockwiseIcon, Button },
template: /* html */ `
<div style="max-width: 840px;">
<Admonition
@@ -256,12 +248,10 @@ export const TitleTreatmentExperiment: Story = {
>
Something went wrong while creating World backup. Please try again or contact support if the issue continues.
<template #top-right-actions>
<ButtonStyled type="outlined" color="red">
<button class="!border" type="button">
<RotateCounterClockwiseIcon class="size-5" />
Retry
</button>
</ButtonStyled>
<Button type="outlined" class="!border !text-red [&>svg]:!text-red !shadow-[inset_0_0_0_1px_var(--color-red)]" native-type="button">
<RotateCounterClockwiseIcon class="size-5" />
Retry
</Button>
</template>
</Admonition>
</div>
@@ -2,7 +2,7 @@ import type { Labrinth } from '@modrinth/api-client'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button } from '../../components/base/buttons'
import InvitePlayersModal from '../../components/sharing/invite-players-modal/index.vue'
import type {
InviteLinkSettings,
@@ -98,7 +98,7 @@ function createSearchUsers() {
function createRender(args: Record<string, unknown>) {
return {
components: { ButtonStyled, InvitePlayersModal },
components: { InvitePlayersModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof InvitePlayersModal> | null>(null)
const friends = ref<InvitePlayersUser[]>(createFriends())
@@ -166,9 +166,7 @@ function createRender(args: Record<string, unknown>) {
},
template: /* html */ `
<div class="flex flex-col items-center gap-4">
<ButtonStyled color="brand">
<button @click="modalRef?.show($event)">Open modal</button>
</ButtonStyled>
<Button type="colored" color="brand" @click="modalRef?.show($event)">Open modal</Button>
<p v-if="lastAction" class="m-0 text-sm text-secondary">{{ lastAction }}</p>
<InvitePlayersModal
ref="modalRef"
@@ -1,7 +1,7 @@
import { EditIcon, TrashIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ButtonStyled from '../../components/base/ButtonStyled.vue'
import { Button, IconButton } from '../../components/base/buttons'
import SkinButton from '../../components/skin/SkinButton.vue'
const frontImage = `data:image/svg+xml,${encodeURIComponent(`
@@ -62,7 +62,7 @@ export const ActiveUnselected: Story = {
export const WithActions: Story = {
render: (args) => ({
components: { ButtonStyled, EditIcon, SkinButton, TrashIcon },
components: { EditIcon, SkinButton, TrashIcon, Button, IconButton },
setup() {
return { args }
},
@@ -70,16 +70,12 @@ export const WithActions: Story = {
<div class="w-[156px]">
<SkinButton v-bind="args">
<template #overlay-buttons>
<ButtonStyled color="brand">
<button class="pointer-events-auto">
<EditIcon /> Edit
</button>
</ButtonStyled>
<ButtonStyled circular color="red">
<button class="pointer-events-auto" aria-label="Delete skin">
<TrashIcon />
</button>
</ButtonStyled>
<Button type="colored" color="brand" class="pointer-events-auto">
<EditIcon /> Edit
</Button>
<IconButton type="colored" color="red" label="Delete skin" class="pointer-events-auto">
<TrashIcon />
</IconButton>
</template>
</SkinButton>
</div>