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:
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',
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user