feat: shared components for worlds + p2p instances (#5135)
* feat: base content card component * fix: tooltips + colors * feat: fix orgs * feat: add ContentModpackCard * fix: extract types * feat: selection v-model * add show icon in selected for combobox with stories * feat: add project combobox * clean up project combobox * feat: start install to play modal * fix: events * feat: figma alignments * feat: migrate toggle to tailwind * fix: row borders * feat: disabled state * feat: virtual list impl for card table based on window scroll * fix: lint * feat: virtualization + smaller contentcard items * feat: fix gap + border issues on last elm * fix: use TeleportOverflowMenu * fix: hasUpdate type * fix: fallback to svg if src is invalid on avatar component * fix: storybook * feat: start on updater modal * feat: finish content updater modal * feat: i18n pass * remove install to play modal from ui package * pnpm prepr * feat: reusable table component * feat: add column width prop for table and fix stories * feat: add table overflow menu story example * feat: add surface-1.5 and use in table * chore: export table in index * fix: allow more loose typing on columns * feat: update table component to derive key from column instead of data * feat: surface 1.5 for oled + refactor story for contentcardtable + yeet sorting funcs * fix: lint * feat: add no padding story for new modal --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
DownloadIcon,
|
||||
HeartIcon,
|
||||
SettingsIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Combobox from '../../components/base/Combobox.vue'
|
||||
@@ -44,3 +52,61 @@ export const Disabled: Story = {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const IconSlot: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: 'download', label: 'Download', icon: DownloadIcon },
|
||||
{ value: 'share', label: 'Share', icon: ShareIcon },
|
||||
{ value: 'favorite', label: 'Add to favorites', icon: HeartIcon },
|
||||
{ type: 'divider' },
|
||||
{ value: 'settings', label: 'Settings', icon: SettingsIcon },
|
||||
{ value: 'profile', label: 'Profile', icon: UserIcon },
|
||||
{ type: 'divider' },
|
||||
{ value: 'delete', label: 'Delete', icon: TrashIcon, disabled: true },
|
||||
],
|
||||
placeholder: 'Select an action',
|
||||
listbox: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const IconSlotSearchable: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: 'download', label: 'Download', icon: DownloadIcon },
|
||||
{ value: 'share', label: 'Share', icon: ShareIcon },
|
||||
{ value: 'favorite', label: 'Add to favorites', icon: HeartIcon },
|
||||
{ value: 'settings', label: 'Settings', icon: SettingsIcon },
|
||||
{ value: 'profile', label: 'Profile', icon: UserIcon },
|
||||
{ value: 'delete', label: 'Delete', icon: TrashIcon },
|
||||
],
|
||||
placeholder: 'Select an action',
|
||||
searchable: true,
|
||||
searchPlaceholder: 'Search actions...',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithSelectedOption: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: '1', label: 'Option 1' },
|
||||
{ value: '2', label: 'Option 2' },
|
||||
{ value: '3', label: 'Option 3' },
|
||||
],
|
||||
modelValue: '2',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithSelectedOptionAndIcon: Story = {
|
||||
args: {
|
||||
options: [
|
||||
{ value: 'download', label: 'Download', icon: DownloadIcon },
|
||||
{ value: 'share', label: 'Share', icon: ShareIcon },
|
||||
{ value: 'favorite', label: 'Add to favorites', icon: HeartIcon },
|
||||
{ value: 'settings', label: 'Settings', icon: SettingsIcon },
|
||||
{ value: 'profile', label: 'Profile', icon: UserIcon },
|
||||
],
|
||||
modelValue: 'favorite',
|
||||
showIconInSelected: true,
|
||||
},
|
||||
}
|
||||
|
||||
455
packages/ui/src/stories/base/Table.stories.ts
Normal file
455
packages/ui/src/stories/base/Table.stories.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { EditIcon, MoreVerticalIcon, TrashIcon } from '@modrinth/assets'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { 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 Table from '../../components/base/Table.vue'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
status: 'active' | 'inactive' | 'pending'
|
||||
role: string
|
||||
}
|
||||
|
||||
const sampleUsers: User[] = [
|
||||
{ id: '1', name: 'John Doe', email: 'john@example.com', status: 'active', role: 'Admin' },
|
||||
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', status: 'inactive', role: 'User' },
|
||||
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', status: 'pending', role: 'Editor' },
|
||||
{ id: '4', name: 'Alice Brown', email: 'alice@example.com', status: 'active', role: 'User' },
|
||||
{
|
||||
id: '5',
|
||||
name: 'Charlie Wilson',
|
||||
email: 'charlie@example.com',
|
||||
status: 'active',
|
||||
role: 'Admin',
|
||||
},
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Table',
|
||||
// @ts-ignore - Generic component
|
||||
component: Table,
|
||||
} satisfies Meta<typeof Table>
|
||||
|
||||
export default meta
|
||||
|
||||
export const Default: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
return { columns, data }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data" />
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSelection: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
const selectedIds = ref<string[]>([])
|
||||
return { columns, data, selectedIds }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="space-y-4">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
show-selection
|
||||
row-key="id"
|
||||
v-model:selected-ids="selectedIds"
|
||||
/>
|
||||
<p class="text-secondary">Selected IDs: {{ selectedIds.join(', ') || 'None' }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSorting: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name', enableSorting: true },
|
||||
{ key: 'email', label: 'Email', enableSorting: true },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'role', label: 'Role', enableSorting: true },
|
||||
]
|
||||
const data = sampleUsers
|
||||
const sortColumn = ref<string | undefined>('name')
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
function handleSort(column: string, direction: 'asc' | 'desc') {
|
||||
console.log(`Sorting by ${column} ${direction}`)
|
||||
}
|
||||
|
||||
return { columns, data, sortColumn, sortDirection, handleSort }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="space-y-4">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
v-model:sort-column="sortColumn"
|
||||
v-model:sort-direction="sortDirection"
|
||||
@sort="handleSort"
|
||||
/>
|
||||
<p class="text-secondary">Sort: {{ sortColumn }} ({{ sortDirection }})</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithColumnAlignment: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name', align: 'left' as const },
|
||||
{ key: 'email', label: 'Email', align: 'center' as const },
|
||||
{ key: 'status', label: 'Status', align: 'center' as const },
|
||||
{ key: 'role', label: 'Role', align: 'right' as const },
|
||||
]
|
||||
const data = sampleUsers
|
||||
return { columns, data }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data" />
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithCustomCellSlots: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table, Badge },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status', align: 'center' as const, width: '20%' },
|
||||
{ key: 'role', label: 'Role', width: '10%' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
|
||||
const statusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'green'
|
||||
case 'inactive':
|
||||
return 'red'
|
||||
case 'pending':
|
||||
return 'orange'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
return { columns, data, statusColor }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data">
|
||||
<template #cell-name="{ value, row }">
|
||||
<div class="font-semibold">{{ value }}</div>
|
||||
</template>
|
||||
<template #cell-email="{ value }">
|
||||
<a :href="'mailto:' + value" class="text-brand hover:underline">{{ value }}</a>
|
||||
</template>
|
||||
<template #cell-status="{ value }">
|
||||
<div class="flex justify-center">
|
||||
<Badge :color="statusColor(value)">{{ value }}</Badge>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithCustomHeaderSlots: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
return { columns, data }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data">
|
||||
<template #header-name="{ column }">
|
||||
<span class="text-brand font-bold uppercase">{{ column.label }} ✨</span>
|
||||
</template>
|
||||
<template #header-status="{ column }">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-green"></span>
|
||||
{{ column.label }}
|
||||
</span>
|
||||
</template>
|
||||
</Table>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithActionsColumn: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table, ButtonStyled, EditIcon, TrashIcon },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'actions', label: 'Actions', align: 'right' as const },
|
||||
]
|
||||
const data = sampleUsers
|
||||
|
||||
function handleEdit(row: User) {
|
||||
alert(`Edit user: ${row.name}`)
|
||||
}
|
||||
|
||||
function handleDelete(row: User) {
|
||||
alert(`Delete user: ${row.name}`)
|
||||
}
|
||||
|
||||
return { columns, data, handleEdit, handleDelete }
|
||||
},
|
||||
template: /* html */ `
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const FullFeatured: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table, Badge, ButtonStyled, EditIcon, TrashIcon },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name', enableSorting: true },
|
||||
{ key: 'email', label: 'Email', enableSorting: true },
|
||||
{ key: 'status', label: 'Status', align: 'center' as const, width: '100px' },
|
||||
{ key: 'role', label: 'Role', enableSorting: true },
|
||||
{ key: 'actions', label: 'Actions', align: 'right' as const, width: '200px' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
const selectedIds = ref<string[]>([])
|
||||
const sortColumn = ref<string | undefined>('name')
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
const statusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'green'
|
||||
case 'inactive':
|
||||
return 'red'
|
||||
case 'pending':
|
||||
return 'orange'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
function handleSort(column: string, direction: 'asc' | 'desc') {
|
||||
console.log(`Sorting by ${column} ${direction}`)
|
||||
}
|
||||
|
||||
function handleEdit(row: User) {
|
||||
alert(`Edit user: ${row.name}`)
|
||||
}
|
||||
|
||||
function handleDelete(row: User) {
|
||||
alert(`Delete user: ${row.name}`)
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
data,
|
||||
selectedIds,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
statusColor,
|
||||
handleSort,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="space-y-4">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
show-selection
|
||||
row-key="id"
|
||||
v-model:selected-ids="selectedIds"
|
||||
v-model:sort-column="sortColumn"
|
||||
v-model:sort-direction="sortDirection"
|
||||
@sort="handleSort"
|
||||
>
|
||||
<template #cell-name="{ value }">
|
||||
<span class="font-semibold">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-email="{ value }">
|
||||
<a :href="'mailto:' + value" class="text-brand hover:underline">{{ value }}</a>
|
||||
</template>
|
||||
<template #cell-status="{ value }">
|
||||
<div class="flex justify-center">
|
||||
<Badge :color="statusColor(value)">{{ value }}</Badge>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="flex gap-4 text-secondary text-sm">
|
||||
<span>Selected: {{ selectedIds.length }} items</span>
|
||||
<span>Sort: {{ sortColumn }} ({{ sortDirection }})</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithOverflowMenu: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table, Badge, ButtonStyled, OverflowMenu, MoreVerticalIcon, EditIcon, TrashIcon },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'status', label: 'Status', align: 'center' as const, width: '20%' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'actions', label: '', width: '48px' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
|
||||
const statusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'green'
|
||||
case 'inactive':
|
||||
return 'red'
|
||||
case 'pending':
|
||||
return 'orange'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
|
||||
const getMenuOptions = (row: User) => [
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => alert(`Edit user: ${row.name}`),
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
action: () => alert(`Duplicate user: ${row.name}`),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red' as const,
|
||||
hoverFilled: true,
|
||||
action: () => alert(`Delete user: ${row.name}`),
|
||||
},
|
||||
]
|
||||
|
||||
return { columns, data, statusColor, getMenuOptions }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data">
|
||||
<template #cell-name="{ value }">
|
||||
<span class="font-semibold">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-status="{ value }">
|
||||
<div class="flex justify-center">
|
||||
<Badge :color="statusColor(value)">{{ value }}</Badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex justify-end">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type Story = StoryObj<typeof meta>
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
small: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -29,11 +30,26 @@ export const Disabled: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
modelValue: false,
|
||||
small: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallChecked: Story = {
|
||||
args: {
|
||||
modelValue: true,
|
||||
small: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => ({
|
||||
components: { Toggle },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<span style="font-weight: 600;">Default Size</span>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" /> Off
|
||||
</div>
|
||||
@@ -43,6 +59,16 @@ export const AllStates: Story = {
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :disabled="true" /> Disabled
|
||||
</div>
|
||||
<span style="font-weight: 600; margin-top: 1rem;">Small Size</span>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :small="true" /> Off
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="true" :small="true" /> On
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<Toggle :model-value="false" :small="true" :disabled="true" /> Disabled
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
|
||||
469
packages/ui/src/stories/instances/ContentCardTable.stories.ts
Normal file
469
packages/ui/src/stories/instances/ContentCardTable.stories.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import { DownloadIcon, EyeIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||
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 ContentCardTable from '../../components/instances/ContentCardTable.vue'
|
||||
import type { ContentCardTableItem } from '../../components/instances/types'
|
||||
|
||||
// ============================================
|
||||
// Fixtures
|
||||
// ============================================
|
||||
|
||||
const fixtures = {
|
||||
sodium: {
|
||||
id: 'AANobbMI',
|
||||
project: {
|
||||
id: 'AANobbMI',
|
||||
slug: 'sodium',
|
||||
title: 'Sodium',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
|
||||
},
|
||||
version: {
|
||||
id: '59wygFUQ',
|
||||
version_number: 'mc1.21.11-0.8.2-fabric',
|
||||
file_name: 'sodium-fabric-0.8.2+mc1.21.11.jar',
|
||||
},
|
||||
owner: {
|
||||
id: 'DzLrfrbK',
|
||||
name: 'IMS',
|
||||
avatar_url: 'https://avatars3.githubusercontent.com/u/31803019?v=4',
|
||||
type: 'user' as const,
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
modMenu: {
|
||||
id: 'mOgUt4GM',
|
||||
project: {
|
||||
id: 'mOgUt4GM',
|
||||
slug: 'modmenu',
|
||||
title: 'Mod Menu',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png',
|
||||
},
|
||||
version: {
|
||||
id: 'QuU0ciaR',
|
||||
version_number: '16.0.0',
|
||||
file_name: 'modmenu-16.0.0.jar',
|
||||
},
|
||||
owner: { id: 'u2', name: 'Prospector', type: 'user' as const },
|
||||
enabled: true,
|
||||
},
|
||||
fabricApi: {
|
||||
id: 'P7dR8mSH',
|
||||
project: {
|
||||
id: 'P7dR8mSH',
|
||||
slug: 'fabric-api',
|
||||
title: 'Fabric API',
|
||||
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||
},
|
||||
version: {
|
||||
id: 'Lwa1Q6e4',
|
||||
version_number: '0.141.3+26.1',
|
||||
file_name: 'fabric-api-0.141.3+26.1.jar',
|
||||
},
|
||||
owner: {
|
||||
id: 'BZoBsPo6',
|
||||
name: 'FabricMC',
|
||||
avatar_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||
type: 'organization' as const,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
} satisfies Record<string, ContentCardTableItem>
|
||||
|
||||
const defaultItems: ContentCardTableItem[] = [fixtures.sodium, fixtures.modMenu, fixtures.fabricApi]
|
||||
|
||||
/** Generate n items for stress testing */
|
||||
function generateItems(count: number): ContentCardTableItem[] {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
...fixtures.sodium,
|
||||
id: `item-${i}`,
|
||||
project: { ...fixtures.sodium.project, title: `Mod ${i + 1}` },
|
||||
version: { ...fixtures.sodium.version!, version_number: `1.0.${i}` },
|
||||
enabled: i % 3 !== 0,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Meta
|
||||
// ============================================
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ContentCardTable',
|
||||
component: ContentCardTable,
|
||||
parameters: { layout: 'padded' },
|
||||
args: {
|
||||
items: defaultItems,
|
||||
showSelection: false,
|
||||
virtualized: true,
|
||||
'onUpdate:enabled': fn(),
|
||||
onDelete: fn(),
|
||||
onUpdate: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof ContentCardTable>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// ============================================
|
||||
// Core Stories
|
||||
// ============================================
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithSelection: Story = {
|
||||
args: { showSelection: true },
|
||||
render: (args) => ({
|
||||
components: { ContentCardTable },
|
||||
setup() {
|
||||
const selectedIds = ref<string[]>([])
|
||||
return { args, selectedIds }
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentCardTable v-bind="args" v-model:selected-ids="selectedIds" />
|
||||
<p class="text-sm text-secondary">
|
||||
Selected: <strong>{{ selectedIds.length }}</strong>
|
||||
<span v-if="selectedIds.length"> ({{ selectedIds.join(', ') }})</span>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: { items: [] },
|
||||
}
|
||||
|
||||
export const EmptyCustom: Story = {
|
||||
args: { items: [] },
|
||||
render: (args) => ({
|
||||
components: { ContentCardTable, ButtonStyled },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<ContentCardTable v-bind="args">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</ContentCardTable>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// States
|
||||
// ============================================
|
||||
|
||||
/** All possible item states in one view */
|
||||
export const AllStates: Story = {
|
||||
args: {
|
||||
showSelection: true,
|
||||
items: [
|
||||
{ ...fixtures.sodium, enabled: true },
|
||||
{ ...fixtures.modMenu, hasUpdate: true },
|
||||
{ ...fixtures.fabricApi, enabled: false },
|
||||
{
|
||||
id: 'long-name',
|
||||
project: {
|
||||
id: 'long-name',
|
||||
slug: 'long-mod',
|
||||
title: '[EMF] Entity Model Features - The Ultimate Entity Rendering Mod',
|
||||
icon_url: fixtures.sodium.project.icon_url,
|
||||
},
|
||||
version: {
|
||||
id: 'v1',
|
||||
version_number: '2.4.1-beta.15+mc1.21.1-fabric-loader0.16.0',
|
||||
file_name: 'emf-2.4.1-beta.15.jar',
|
||||
},
|
||||
owner: { id: 'u1', name: 'Traben', type: 'user' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'no-icon',
|
||||
project: { id: 'no-icon', slug: 'imported', title: 'Imported mod', icon_url: undefined },
|
||||
version: { id: 'v1', version_number: 'Unknown', file_name: 'imported.jar' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'no-avatar',
|
||||
project: {
|
||||
id: 'no-avatar',
|
||||
slug: 'no-avatar',
|
||||
title: 'No Owner Avatar',
|
||||
icon_url: fixtures.modMenu.project.icon_url,
|
||||
},
|
||||
version: { id: 'v1', version_number: '1.0.0', file_name: 'mod.jar' },
|
||||
owner: { id: 'u1', name: 'Anonymous', avatar_url: undefined, type: 'user' },
|
||||
enabled: true,
|
||||
},
|
||||
{ ...fixtures.modMenu, id: 'disabled-item', disabled: true, enabled: false },
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Demonstrates: enabled, update available, disabled toggle, long names (truncation), missing icon, missing avatar, fully disabled item.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/** Items with update badges */
|
||||
export const WithUpdates: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ ...fixtures.sodium, hasUpdate: true },
|
||||
{ ...fixtures.modMenu, hasUpdate: true },
|
||||
fixtures.fabricApi,
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/** Per-item disabled state (e.g., during async operations) */
|
||||
export const ItemsDisabled: Story = {
|
||||
args: {
|
||||
showSelection: true,
|
||||
items: [
|
||||
fixtures.sodium,
|
||||
{ ...fixtures.modMenu, disabled: true },
|
||||
{ ...fixtures.fabricApi, disabled: true },
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: { story: 'Items with `disabled: true` have all interactions disabled.' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Slots
|
||||
// ============================================
|
||||
|
||||
export const CustomButtons: Story = {
|
||||
args: { showSelection: true },
|
||||
render: (args) => ({
|
||||
components: { ContentCardTable, ButtonStyled, EyeIcon, FolderOpenIcon, DownloadIcon },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<ContentCardTable v-bind="args">
|
||||
<template #itemButtonsLeft="{ item }">
|
||||
<ButtonStyled v-tooltip="'Download'" circular type="transparent" color="green" color-fill="text">
|
||||
<button><DownloadIcon class="size-5" /></button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template #itemButtonsRight="{ item }">
|
||||
<ButtonStyled v-tooltip="'View'" circular type="transparent">
|
||||
<button><EyeIcon class="size-5 text-secondary" /></button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-tooltip="'Folder'" circular type="transparent">
|
||||
<button><FolderOpenIcon class="size-5 text-secondary" /></button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentCardTable>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithOverflowMenu: Story = {
|
||||
args: {
|
||||
showSelection: true,
|
||||
items: [
|
||||
{
|
||||
...fixtures.sodium,
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ id: 'folder', action: () => console.log('Folder') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' as const },
|
||||
],
|
||||
},
|
||||
{
|
||||
...fixtures.modMenu,
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' as const },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ContentCardTable },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<ContentCardTable v-bind="args">
|
||||
<template #view>View on Modrinth</template>
|
||||
<template #folder>Open folder</template>
|
||||
<template #remove>Remove</template>
|
||||
</ContentCardTable>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Interactive
|
||||
// ============================================
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: { showSelection: true },
|
||||
render: (args) => ({
|
||||
components: { ContentCardTable },
|
||||
setup() {
|
||||
const items = ref<ContentCardTableItem[]>(
|
||||
defaultItems.map((item) => ({ ...item, enabled: item.id !== fixtures.fabricApi.id })),
|
||||
)
|
||||
const selectedIds = ref<string[]>([])
|
||||
|
||||
const handleToggle = (id: string, value: boolean) => {
|
||||
const item = items.value.find((i) => i.id === id)
|
||||
if (item) item.enabled = value
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
items.value = items.value.filter((i) => i.id !== id)
|
||||
selectedIds.value = selectedIds.value.filter((i) => i !== id)
|
||||
}
|
||||
|
||||
return { args, items, selectedIds, handleToggle, handleDelete }
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentCardTable
|
||||
:items="items"
|
||||
:show-selection="args.showSelection"
|
||||
v-model:selected-ids="selectedIds"
|
||||
@update:enabled="handleToggle"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<p class="text-sm text-secondary">
|
||||
Items: <strong>{{ items.length }}</strong> · Selected: <strong>{{ selectedIds.length }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const BulkActions: Story = {
|
||||
render: () => ({
|
||||
components: { ContentCardTable, ButtonStyled },
|
||||
setup() {
|
||||
const items = ref<ContentCardTableItem[]>(
|
||||
defaultItems.map((item, i) => ({ ...item, enabled: i !== 2 })),
|
||||
)
|
||||
const selectedIds = ref<string[]>([])
|
||||
|
||||
const setEnabled = (value: boolean) => {
|
||||
items.value.forEach((item) => {
|
||||
if (selectedIds.value.includes(item.id)) item.enabled = value
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSelected = () => {
|
||||
items.value = items.value.filter((item) => !selectedIds.value.includes(item.id))
|
||||
selectedIds.value = []
|
||||
}
|
||||
|
||||
const handleToggle = (id: string, value: boolean) => {
|
||||
const item = items.value.find((i) => i.id === id)
|
||||
if (item) item.enabled = value
|
||||
}
|
||||
|
||||
return { items, selectedIds, setEnabled, deleteSelected, handleToggle }
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-secondary">{{ selectedIds.length }} selected</span>
|
||||
<template v-if="selectedIds.length">
|
||||
<ButtonStyled size="small" color="green">
|
||||
<button @click="setEnabled(true)">Enable</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="small" type="transparent">
|
||||
<button @click="setEnabled(false)">Disable</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="small" color="red">
|
||||
<button @click="deleteSelected">Delete</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
<ContentCardTable
|
||||
:items="items"
|
||||
show-selection
|
||||
v-model:selected-ids="selectedIds"
|
||||
@update:enabled="handleToggle"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Performance
|
||||
// ============================================
|
||||
|
||||
export const Virtualization: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'2000 items with virtualization. Toggle to compare DOM node count. Virtualized should render ~20-30 nodes vs 2000.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentCardTable },
|
||||
setup() {
|
||||
const items = ref(generateItems(2000))
|
||||
const selectedIds = ref<string[]>([])
|
||||
const virtualized = ref(true)
|
||||
const tableRef = ref<InstanceType<typeof ContentCardTable> | null>(null)
|
||||
const domNodes = ref(0)
|
||||
let raf: number
|
||||
|
||||
const updateNodeCount = () => {
|
||||
if (tableRef.value?.$el) {
|
||||
domNodes.value = (tableRef.value.$el as HTMLElement).querySelectorAll(
|
||||
'[data-content-card-item]',
|
||||
).length
|
||||
}
|
||||
raf = requestAnimationFrame(updateNodeCount)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
raf = requestAnimationFrame(updateNodeCount)
|
||||
})
|
||||
onUnmounted(() => cancelAnimationFrame(raf))
|
||||
|
||||
return { items, selectedIds, virtualized, tableRef, domNodes }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="sticky top-0 z-10 mb-4 flex items-center gap-3 rounded-lg bg-surface-2 p-3">
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input type="checkbox" v-model="virtualized" class="h-4 w-4 rounded" />
|
||||
<span class="font-medium text-contrast">Virtualization</span>
|
||||
</label>
|
||||
<span class="ml-auto font-mono text-sm">
|
||||
DOM: <span :class="domNodes > 100 ? 'text-red-500' : 'text-green-500'">{{ domNodes }}</span>
|
||||
/ {{ items.length }}
|
||||
</span>
|
||||
</div>
|
||||
<ContentCardTable
|
||||
ref="tableRef"
|
||||
:items="items"
|
||||
:virtualized="virtualized"
|
||||
show-selection
|
||||
v-model:selected-ids="selectedIds"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
633
packages/ui/src/stories/instances/ContentModpackCard.stories.ts
Normal file
633
packages/ui/src/stories/instances/ContentModpackCard.stories.ts
Normal file
@@ -0,0 +1,633 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { fn } from 'storybook/test'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ContentCardItem from '../../components/instances/ContentCardItem.vue'
|
||||
import ContentModpackCard from '../../components/instances/ContentModpackCard.vue'
|
||||
import type {
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
} from '../../components/instances/types'
|
||||
import NewModal from '../../components/modal/NewModal.vue'
|
||||
|
||||
// Real project data from Modrinth API
|
||||
const fabulouslyOptimizedProject: ContentModpackCardProject = {
|
||||
id: '1KVo5zza',
|
||||
slug: 'fabulously-optimized',
|
||||
title: 'Fabulously Optimized',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/1KVo5zza/9f1ded4949c2a9db5ca382d3bcc912c7245486b4_96.webp',
|
||||
description:
|
||||
'Beautiful graphics, speedy performance and familiar features in a simple package. 1.21.11 beta!',
|
||||
downloads: 8708191,
|
||||
followers: 3762,
|
||||
}
|
||||
|
||||
const cobblemonProject: ContentModpackCardProject = {
|
||||
id: '5FFgwNNP',
|
||||
slug: 'cobblemon-fabric',
|
||||
title: 'Cobblemon Official Modpack [Fabric]',
|
||||
icon_url: 'https://cdn.modrinth.com/data/5FFgwNNP/e7f9ee2e9d361623847853fe2ddce42f519ee64f.png',
|
||||
description: 'The official modpack of the Cobblemon mod, for Fabric!',
|
||||
downloads: 4940845,
|
||||
followers: 2051,
|
||||
}
|
||||
|
||||
const simplyOptimizedProject: ContentModpackCardProject = {
|
||||
id: 'BYfVnHa7',
|
||||
slug: 'sop',
|
||||
title: 'Simply Optimized',
|
||||
icon_url: 'https://cdn.modrinth.com/data/BYfVnHa7/845e93223da7e8d1ed1a33364b5bdb4c316ac518.png',
|
||||
description:
|
||||
'The leading, well-researched optimization modpack with a focus on pure performance.',
|
||||
downloads: 2903242,
|
||||
followers: 1387,
|
||||
}
|
||||
|
||||
// Version data from Modrinth API
|
||||
const fabulouslyOptimizedVersion: ContentModpackCardVersion = {
|
||||
id: 'YEEXo8mO',
|
||||
version_number: '1.12.1',
|
||||
date_published: '2022-02-10T06:53:28.379507Z',
|
||||
}
|
||||
|
||||
const cobblemonVersion: ContentModpackCardVersion = {
|
||||
id: 'bpaivauC',
|
||||
version_number: '1.5.2',
|
||||
date_published: '2024-05-27T07:12:36.043005Z',
|
||||
}
|
||||
|
||||
// Owner data from Modrinth API
|
||||
const userOwner: ContentOwner = {
|
||||
id: '2avTeeAE',
|
||||
name: 'robotkoer',
|
||||
avatar_url: 'https://cdn.modrinth.com/user/2avTeeAE/icon.png',
|
||||
type: 'user',
|
||||
}
|
||||
|
||||
const cobblemonOwner: ContentOwner = {
|
||||
id: 'AEFONbAM',
|
||||
name: 'Reisen',
|
||||
avatar_url:
|
||||
'https://cdn.modrinth.com/user/AEFONbAM/9e97453507a8245981d5cd825280f23be44f15ac.jpeg',
|
||||
type: 'user',
|
||||
}
|
||||
|
||||
// Categories (using Labrinth.Tags.v2.Category structure with optional action)
|
||||
const optimizationCategories: ContentModpackCardCategory[] = [
|
||||
{ name: 'Fabric', icon: 'fabric', project_type: 'modpack', header: 'loaders' },
|
||||
{ name: 'Lightweight', icon: 'lightweight', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Optimization', icon: 'optimization', project_type: 'modpack', header: 'categories' },
|
||||
]
|
||||
|
||||
const cobblemonCategories: ContentModpackCardCategory[] = [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Fabric', icon: 'fabric', project_type: 'modpack', header: 'loaders' },
|
||||
{ name: 'Lightweight', icon: 'lightweight', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ContentModpackCard',
|
||||
component: ContentModpackCard,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
argTypes: {
|
||||
project: {
|
||||
control: 'object',
|
||||
description:
|
||||
'Project information (id, slug, title, icon_url, description, downloads, followers)',
|
||||
},
|
||||
version: {
|
||||
control: 'object',
|
||||
description: 'Version information (id, version_number, date_published)',
|
||||
},
|
||||
owner: {
|
||||
control: 'object',
|
||||
description: 'Owner/author information (user or organization)',
|
||||
},
|
||||
categories: {
|
||||
control: 'object',
|
||||
description: 'Category tags with optional click actions',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Grays out the card when true',
|
||||
},
|
||||
overflowOptions: {
|
||||
control: 'object',
|
||||
description: 'Options for the overflow menu',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ContentModpackCard>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// ============================================
|
||||
// All Types Overview
|
||||
// ============================================
|
||||
|
||||
export const AllTypes: Story = {
|
||||
args: {
|
||||
project: fabulouslyOptimizedProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
const cards = [
|
||||
{
|
||||
label: 'Full featured (all actions)',
|
||||
project: fabulouslyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
hasUpdate: true,
|
||||
hasContent: true,
|
||||
hasUnlink: true,
|
||||
},
|
||||
{
|
||||
label: 'With update available only',
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: cobblemonOwner,
|
||||
categories: cobblemonCategories,
|
||||
hasUpdate: true,
|
||||
},
|
||||
{
|
||||
label: 'With content button only',
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
hasContent: true,
|
||||
},
|
||||
{
|
||||
label: 'Minimal (project only)',
|
||||
project: fabulouslyOptimizedProject,
|
||||
},
|
||||
{
|
||||
label: 'With version info only',
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
},
|
||||
{
|
||||
label: 'With owner only',
|
||||
project: simplyOptimizedProject,
|
||||
owner: userOwner,
|
||||
},
|
||||
{
|
||||
label: 'Disabled state',
|
||||
project: fabulouslyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
return { cards }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-for="card in cards" :key="card.label">
|
||||
<h3 class="text-sm font-medium text-secondary">{{ card.label }}</h3>
|
||||
<ContentModpackCard
|
||||
:project="card.project"
|
||||
:version="card.version"
|
||||
:owner="card.owner"
|
||||
:categories="card.categories"
|
||||
:disabled="card.disabled"
|
||||
@update="card.hasUpdate ? () => {} : undefined"
|
||||
@content="card.hasContent ? () => {} : undefined"
|
||||
@unlink="card.hasUnlink ? () => {} : undefined"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Basic Stories
|
||||
// ============================================
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
onUnlink: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const MinimalProjectOnly: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithVersion: Story = {
|
||||
args: {
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithUserOwner: Story = {
|
||||
args: {
|
||||
project: simplyOptimizedProject,
|
||||
version: fabulouslyOptimizedVersion,
|
||||
owner: userOwner,
|
||||
categories: [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const WithOrganizationOwner: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Action Button Stories
|
||||
// ============================================
|
||||
|
||||
export const WithUpdateButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithContentButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onContent: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithUnlinkButton: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
onUnlink: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithAllActions: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
onUnlink: fn(),
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ id: 'settings', action: () => console.log('Settings') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// State Stories
|
||||
// ============================================
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const LongTitle: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
title: 'Super Long Modpack Title That Should Display Properly On All Screen Sizes',
|
||||
description:
|
||||
'This is an extremely long description that should wrap properly and not break the layout. It contains lots of information about what this modpack includes and what makes it special compared to other modpacks available on the platform.',
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: {
|
||||
...userOwner,
|
||||
name: 'Really Long Organization Name Studios',
|
||||
},
|
||||
categories: [
|
||||
{ name: 'Adventure', icon: 'adventure', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Technology', icon: 'technology', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Magic', icon: 'magic', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Exploration', icon: 'exploration', project_type: 'modpack', header: 'categories' },
|
||||
{ name: 'Multiplayer', icon: 'multiplayer', project_type: 'modpack', header: 'categories' },
|
||||
],
|
||||
onUpdate: fn(),
|
||||
onContent: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const NoDescription: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
description: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoStats: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
downloads: undefined,
|
||||
followers: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Categories Stories
|
||||
// ============================================
|
||||
|
||||
export const WithClickableCategories: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
const clickedCategory = ref<string | null>(null)
|
||||
const categories: ContentModpackCardCategory[] = [
|
||||
{
|
||||
name: 'Adventure',
|
||||
icon: 'adventure',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Adventure'),
|
||||
},
|
||||
{
|
||||
name: 'Lightweight',
|
||||
icon: 'lightweight',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Lightweight'),
|
||||
},
|
||||
{
|
||||
name: 'Multiplayer',
|
||||
icon: 'multiplayer',
|
||||
project_type: 'modpack',
|
||||
header: 'categories',
|
||||
action: () => (clickedCategory.value = 'Multiplayer'),
|
||||
},
|
||||
]
|
||||
return { args, categories, clickedCategory }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentModpackCard
|
||||
:project="args.project"
|
||||
:version="args.version"
|
||||
:owner="args.owner"
|
||||
:categories="categories"
|
||||
/>
|
||||
<div class="text-sm text-secondary">
|
||||
Clicked category: <strong>{{ clickedCategory || 'None' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Overflow Menu Stories
|
||||
// ============================================
|
||||
|
||||
export const WithOverflowMenu: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<ContentModpackCard v-bind="args">
|
||||
<template #view>View on Modrinth</template>
|
||||
<template #settings>Settings</template>
|
||||
<template #remove>Remove modpack</template>
|
||||
</ContentModpackCard>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
overflowOptions: [
|
||||
{ id: 'view', action: () => console.log('View') },
|
||||
{ id: 'settings', action: () => console.log('Settings') },
|
||||
{ divider: true },
|
||||
{ id: 'remove', action: () => console.log('Remove'), color: 'red' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Interactive Stories
|
||||
// ============================================
|
||||
|
||||
export const WithContentModal: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard, NewModal, ContentCardItem },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const modpackContent = [
|
||||
{
|
||||
project: {
|
||||
id: '1',
|
||||
slug: 'sodium',
|
||||
title: 'Sodium',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
|
||||
},
|
||||
version: { id: 'v1', version_number: '0.8.2', file_name: 'sodium-fabric-0.8.2.jar' },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
id: '2',
|
||||
slug: 'modmenu',
|
||||
title: 'Mod Menu',
|
||||
icon_url:
|
||||
'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png',
|
||||
},
|
||||
version: { id: 'v2', version_number: '16.0.0', file_name: 'modmenu-16.0.0.jar' },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
id: '3',
|
||||
slug: 'fabric-api',
|
||||
title: 'Fabric API',
|
||||
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||
},
|
||||
version: { id: 'v3', version_number: '0.141.3', file_name: 'fabric-api-0.141.3.jar' },
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
cobblemonProject,
|
||||
cobblemonVersion,
|
||||
userOwner,
|
||||
optimizationCategories,
|
||||
modalRef,
|
||||
modpackContent,
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@content="modalRef?.show()"
|
||||
@update="() => alert('Update clicked')"
|
||||
/>
|
||||
<NewModal ref="modalRef" header="Modpack Content">
|
||||
<div class="flex flex-col gap-4">
|
||||
<ContentCardItem
|
||||
v-for="item in modpackContent"
|
||||
:key="item.project.id"
|
||||
:project="item.project"
|
||||
:version="item.version"
|
||||
/>
|
||||
</div>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Responsive Stories
|
||||
// ============================================
|
||||
|
||||
export const ResponsiveView: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
},
|
||||
render: () => ({
|
||||
components: { ContentModpackCard },
|
||||
setup() {
|
||||
return {
|
||||
cobblemonProject,
|
||||
cobblemonVersion,
|
||||
userOwner,
|
||||
optimizationCategories,
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-secondary mb-2">Desktop (full width)</h3>
|
||||
<div class="w-full">
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@update="() => {}"
|
||||
@content="() => {}"
|
||||
@unlink="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-secondary mb-2">Mobile (<640px)</h3>
|
||||
<div class="w-[360px]">
|
||||
<ContentModpackCard
|
||||
:project="cobblemonProject"
|
||||
:version="cobblemonVersion"
|
||||
:owner="userOwner"
|
||||
:categories="optimizationCategories"
|
||||
@update="() => {}"
|
||||
@content="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Edge Cases
|
||||
// ============================================
|
||||
|
||||
export const NoIcon: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
icon_url: undefined,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoOwnerAvatar: Story = {
|
||||
args: {
|
||||
project: cobblemonProject,
|
||||
version: cobblemonVersion,
|
||||
owner: {
|
||||
...userOwner,
|
||||
avatar_url: undefined,
|
||||
},
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
|
||||
export const HighDownloadCounts: Story = {
|
||||
args: {
|
||||
project: {
|
||||
...cobblemonProject,
|
||||
downloads: 1234567890,
|
||||
followers: 9876543,
|
||||
},
|
||||
version: cobblemonVersion,
|
||||
owner: userOwner,
|
||||
categories: optimizationCategories,
|
||||
},
|
||||
}
|
||||
411
packages/ui/src/stories/instances/ContentUpdaterModal.stories.ts
Normal file
411
packages/ui/src/stories/instances/ContentUpdaterModal.stories.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
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 ContentUpdaterModal from '../../components/instances/modals/ContentUpdaterModal.vue'
|
||||
|
||||
// Real version data from Modrinth API - Sodium (mod)
|
||||
const sodiumVersions: Labrinth.Versions.v2.Version[] = [
|
||||
{
|
||||
id: '59wygFUQ',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: true,
|
||||
name: 'Sodium 0.8.2 for Fabric 1.21.11',
|
||||
version_number: 'mc1.21.11-0.8.2-fabric',
|
||||
version_type: 'release',
|
||||
changelog:
|
||||
'This release fixes a critical bug with FRAPI, as well as allowing mods to set non-monochrome icons.\n\n## Changes\n- Fixed FRAPI compatibility issues\n- Added support for non-monochrome mod icons\n- Various performance improvements',
|
||||
date_published: '2025-12-22T20:35:06.214284Z',
|
||||
downloads: 150000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.11'],
|
||||
loaders: ['fabric', 'quilt'],
|
||||
},
|
||||
{
|
||||
id: '8jueyeK2',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: false,
|
||||
name: 'Sodium 0.8.2 for NeoForge 1.21.11',
|
||||
version_number: 'mc1.21.11-0.8.2-neoforge',
|
||||
version_type: 'release',
|
||||
changelog:
|
||||
'This release fixes a critical bug with FRAPI, as well as allowing mods to set non-monochrome icons.',
|
||||
date_published: '2025-12-22T20:34:32.101126Z',
|
||||
downloads: 80000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.11'],
|
||||
loaders: ['neoforge'],
|
||||
},
|
||||
{
|
||||
id: '2IxKzI1o',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: false,
|
||||
name: 'Sodium 0.8.1 for Fabric 1.21.11',
|
||||
version_number: 'mc1.21.11-0.8.1-fabric',
|
||||
version_type: 'release',
|
||||
changelog:
|
||||
'This release adds support for the Fabric Rendering API on 1.21.11, works around AMD driver bugs, and fixes configuration screen issues.\n\n## Bug Fixes\n- Fixed AMD driver compatibility\n- Fixed configuration screen crashes\n- Improved FRAPI support',
|
||||
date_published: '2025-12-18T03:16:30.884738Z',
|
||||
downloads: 250000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.11'],
|
||||
loaders: ['fabric', 'quilt'],
|
||||
},
|
||||
{
|
||||
id: 'MLXdfyIk',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: false,
|
||||
name: 'Sodium 0.8.0 for Fabric 1.21.11',
|
||||
version_number: 'mc1.21.11-0.8.0-fabric',
|
||||
version_type: 'beta',
|
||||
changelog:
|
||||
'This release brings many bug fixes, a brand new configuration screen, and support for Minecraft 1.21.11.\n\n## New Features\n- Completely redesigned configuration screen\n- Support for Minecraft 1.21.11\n\n## Bug Fixes\n- Fixed various rendering issues\n- Improved memory usage',
|
||||
date_published: '2025-12-09T17:11:11.360476Z',
|
||||
downloads: 180000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.11'],
|
||||
loaders: ['fabric', 'quilt'],
|
||||
},
|
||||
{
|
||||
id: 'sFfidWgd',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: false,
|
||||
name: 'Sodium 0.7.3 for Fabric 1.21.10',
|
||||
version_number: 'mc1.21.10-0.7.3-fabric',
|
||||
version_type: 'release',
|
||||
changelog: 'This release fixes a stuttering issue affecting Intel cards.',
|
||||
date_published: '2025-11-10T18:51:15.477709Z',
|
||||
downloads: 320000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.9', '1.21.10'],
|
||||
loaders: ['fabric', 'quilt'],
|
||||
},
|
||||
{
|
||||
id: '24jH02Sf',
|
||||
project_id: 'AANobbMI',
|
||||
author_id: 'TEZXhE2U',
|
||||
featured: false,
|
||||
name: 'Sodium 0.7.0 for Fabric 1.21.8',
|
||||
version_number: 'mc1.21.8-0.7.0-fabric',
|
||||
version_type: 'alpha',
|
||||
changelog:
|
||||
'Major performance optimizations with quad splitting translucency sorting, improved chunk meshing, terrain rendering enhancements, and entity/particle performance improvements.\n\n## Performance\n- 30% faster chunk rendering\n- Reduced memory allocations\n- Better CPU utilization',
|
||||
date_published: '2025-09-30T15:07:01.867787Z',
|
||||
downloads: 450000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.6', '1.21.7', '1.21.8'],
|
||||
loaders: ['fabric', 'quilt'],
|
||||
},
|
||||
]
|
||||
|
||||
// Real version data from Modrinth API - Cobblemon modpack
|
||||
const cobblemonVersions: Labrinth.Versions.v2.Version[] = [
|
||||
{
|
||||
id: 'DbQNxSJ0',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: true,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.7.1',
|
||||
version_number: '1.7.1',
|
||||
version_type: 'release',
|
||||
changelog:
|
||||
'Updated to Cobblemon 1.7.1.\n\n## Modified Mods\n- EMF\n- ETF\n- Balm\n- FancyMenu\n- JEED\n- JEI',
|
||||
date_published: '2025-11-29T02:27:41.839520Z',
|
||||
downloads: 85000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
{
|
||||
id: 'jMz7A3RO',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: false,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.7',
|
||||
version_number: '1.7',
|
||||
version_type: 'release',
|
||||
changelog:
|
||||
'Updated to Cobblemon 1.7.\n\n## Changes\n- Removed Medal\n- Updated Fabric API\n- Updated JEI\n- Updated rendering mods',
|
||||
date_published: '2025-11-22T00:48:57.491974Z',
|
||||
downloads: 120000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
{
|
||||
id: '98odLiu9',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: false,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.6.1.4',
|
||||
version_number: '1.6.1.4',
|
||||
version_type: 'release',
|
||||
changelog: 'Updated Medal to 1.0.3 to resolve a crash issue.',
|
||||
date_published: '2025-07-01T04:32:02.692075Z',
|
||||
downloads: 95000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
{
|
||||
id: 'ZGcN3At3',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: false,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.6.1.1',
|
||||
version_number: '1.6.1.1',
|
||||
version_type: 'beta',
|
||||
changelog:
|
||||
'## Added Mods\n- Advanced Loot Info\n- CIT Resewn\n- EMI variants\n- Medal\n- Tips\n\n## Removed Mods\n- Architectury\n- REI',
|
||||
date_published: '2025-06-11T01:10:12.921145Z',
|
||||
downloads: 78000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
{
|
||||
id: 'cqaC80tF',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: false,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.6.1',
|
||||
version_number: '1.6.1',
|
||||
version_type: 'release',
|
||||
changelog: 'Updated to Cobblemon 1.6.1 release.',
|
||||
date_published: '2025-01-26T06:37:28.977532Z',
|
||||
downloads: 210000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
{
|
||||
id: 'bpaivauC',
|
||||
project_id: '5FFgwNNP',
|
||||
author_id: 'AEFONbAM',
|
||||
featured: false,
|
||||
name: 'Cobblemon Official Modpack [Fabric] 1.5.2',
|
||||
version_number: '1.5.2',
|
||||
version_type: 'release',
|
||||
changelog: 'Updated to Cobblemon 1.5.2. Adjusted InvMove defaults to prevent REI conflicts.',
|
||||
date_published: '2024-05-27T07:12:36.043005Z',
|
||||
downloads: 350000,
|
||||
status: 'listed',
|
||||
files: [],
|
||||
dependencies: [],
|
||||
game_versions: ['1.20.1'],
|
||||
loaders: ['fabric'],
|
||||
},
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Instances/ContentUpdaterModal',
|
||||
component: ContentUpdaterModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
argTypes: {
|
||||
versions: {
|
||||
control: 'object',
|
||||
description: 'Array of versions to display',
|
||||
},
|
||||
currentGameVersion: {
|
||||
control: 'text',
|
||||
description: 'Current game version for compatibility checking',
|
||||
},
|
||||
currentLoader: {
|
||||
control: 'text',
|
||||
description: 'Current loader for compatibility checking',
|
||||
},
|
||||
currentVersionId: {
|
||||
control: 'text',
|
||||
description: 'ID of the currently installed version',
|
||||
},
|
||||
header: {
|
||||
control: 'text',
|
||||
description: 'Modal header text',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ContentUpdaterModal>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// ============================================
|
||||
// Mod Example (Sodium)
|
||||
// ============================================
|
||||
|
||||
export const ModExample: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentUpdaterModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
const handleUpdate = (version: Labrinth.Versions.v2.Version) => {
|
||||
console.log('Update to version:', version)
|
||||
alert(`Updating to ${version.name}`)
|
||||
}
|
||||
return { args, modalRef, openModal, handleUpdate }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Update Sodium</button>
|
||||
</ButtonStyled>
|
||||
<ContentUpdaterModal
|
||||
ref="modalRef"
|
||||
v-bind="args"
|
||||
@update="handleUpdate"
|
||||
@cancel="() => console.log('Cancelled')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
versions: sodiumVersions,
|
||||
currentGameVersion: '1.21.11',
|
||||
currentLoader: 'fabric',
|
||||
currentVersionId: '2IxKzI1o', // 0.8.1 is current
|
||||
header: 'Update mod',
|
||||
onUpdate: fn(),
|
||||
onCancel: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Modpack Example (Cobblemon)
|
||||
// ============================================
|
||||
|
||||
export const ModpackExample: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentUpdaterModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
const handleUpdate = (version: Labrinth.Versions.v2.Version) => {
|
||||
console.log('Update to version:', version)
|
||||
alert(`Updating to ${version.name}`)
|
||||
}
|
||||
return { args, modalRef, openModal, handleUpdate }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Update Cobblemon Modpack</button>
|
||||
</ButtonStyled>
|
||||
<ContentUpdaterModal
|
||||
ref="modalRef"
|
||||
v-bind="args"
|
||||
@update="handleUpdate"
|
||||
@cancel="() => console.log('Cancelled')"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
versions: cobblemonVersions,
|
||||
currentGameVersion: '1.21.1',
|
||||
currentLoader: 'fabric',
|
||||
currentVersionId: 'jMz7A3RO', // 1.7 is current
|
||||
header: 'Update modpack',
|
||||
onUpdate: fn(),
|
||||
onCancel: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// With Incompatible Versions
|
||||
// ============================================
|
||||
|
||||
export const WithIncompatibleVersions: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentUpdaterModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { args, modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Update (Shows Incompatible)</button>
|
||||
</ButtonStyled>
|
||||
<ContentUpdaterModal
|
||||
ref="modalRef"
|
||||
v-bind="args"
|
||||
@update="(v) => console.log('Update:', v)"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
versions: sodiumVersions,
|
||||
currentGameVersion: '1.21.10', // Older version - some versions won't be compatible
|
||||
currentLoader: 'fabric',
|
||||
currentVersionId: 'sFfidWgd',
|
||||
header: 'Update mod',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// All Version Types (Release, Beta, Alpha)
|
||||
// ============================================
|
||||
|
||||
export const AllVersionTypes: Story = {
|
||||
render: (args) => ({
|
||||
components: { ContentUpdaterModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof ContentUpdaterModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { args, modalRef, openModal }
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">View All Version Types</button>
|
||||
</ButtonStyled>
|
||||
<ContentUpdaterModal
|
||||
ref="modalRef"
|
||||
v-bind="args"
|
||||
@update="(v) => console.log('Update:', v)"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
// Sodium has release, beta, and alpha versions
|
||||
versions: sodiumVersions,
|
||||
currentGameVersion: '1.21.11',
|
||||
currentLoader: 'fabric',
|
||||
currentVersionId: '24jH02Sf', // Alpha version is current
|
||||
header: 'Update mod',
|
||||
},
|
||||
}
|
||||
@@ -211,3 +211,31 @@ export const NotClosable: Story = {
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const NoPadding: Story = {
|
||||
render: () => ({
|
||||
components: { NewModal, ButtonStyled },
|
||||
setup() {
|
||||
const modalRef = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const openModal = () => modalRef.value?.show()
|
||||
return { modalRef, openModal }
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openModal">Open Modal (No Padding)</button>
|
||||
</ButtonStyled>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
22
packages/ui/src/stories/project/ProjectCombobox.stories.ts
Normal file
22
packages/ui/src/stories/project/ProjectCombobox.stories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ProjectCombobox from '../../components/project/ProjectCombobox.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Project/ProjectCombobox',
|
||||
component: ProjectCombobox,
|
||||
} satisfies Meta<typeof ProjectCombobox>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'Select project',
|
||||
searchPlaceholder: 'Search by name or paste ID...',
|
||||
loadingMessage: 'Loading...',
|
||||
noResultsMessage: 'No results found',
|
||||
disabled: false,
|
||||
limit: 20,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user