refactor: content tab styling (#7091)

* refactor: managed content card

* fix: update filter pill styling to be chips-like

* fix: modpack exporting

* fixed: vanilla server projs + header

* refactor: filters

* refactor: detect external file icons + other metadata inside

* refactor: support json5 metadata

* fix: qa

* fix: qa

* fix: fmt + prepr

* fix: fmt

* fix: qa

* fix: headers

* feat: locking

* fix: clippy

* fix: qa

* fix: frozen loc

* fix: shared instances diff calc break

* fix: env filter + remove categories
This commit is contained in:
Calum H.
2026-08-13 14:55:35 +00:00
committed by GitHub
parent e087e571f3
commit 06b7c44dcc
126 changed files with 3654 additions and 3473 deletions
@@ -119,6 +119,7 @@ const etfItem: ContentCardTableItem = {
const importedModItem: ContentCardTableItem = {
id: 'imported123',
external: true,
project: {
id: 'imported123',
slug: 'import-mod',
@@ -1,633 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { fn } from 'storybook/test'
import { ref } from 'vue'
import NewModal from '../../components/modal/NewModal.vue'
import ContentCardItem from '../../layouts/shared/content-tab/components/ContentCardItem.vue'
import ContentModpackCard from '../../layouts/shared/content-tab/components/ContentModpackCard.vue'
import type {
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '../../layouts/shared/content-tab/types'
// 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 (&lt;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,
},
}
@@ -0,0 +1,204 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { fn } from 'storybook/test'
import ManagedContentCard from '../../layouts/shared/content-tab/components/managed-content-card/index.vue'
import type { ManagedContentCardData } from '../../layouts/shared/content-tab/types'
const cobblemonIcon =
'https://cdn.modrinth.com/data/5FFgwNNP/e7f9ee2e9d361623847853fe2ddce42f519ee64f.png'
const serverIcon =
'https://cdn.modrinth.com/data/PEMTFQFO/f4140c3a9042e4a825d47dbb64889dad9fdc066c.png'
const userAvatar = 'https://cdn.modrinth.com/user/LnK8MbX7/icon.png'
const figmaSummary = [
{ type: 'mod', count: 52 },
{ type: 'resourcepack', count: 2 },
{ type: 'shader', count: 1 },
] satisfies ManagedContentCardData['summary']
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
const modpackCard = {
kind: 'modpack',
manager: {
name: 'Cobblemon Modpack',
iconUrl: cobblemonIcon,
link: '/modpack/cobblemon-fabric',
},
summary: figmaSummary,
versionNumber: 'v1.6.1',
versionLink: '/modpack/cobblemon-fabric/version/v1.6.1',
updatedAt: twoDaysAgo,
} satisfies ManagedContentCardData
const serverCard = {
kind: 'server',
manager: {
name: 'Modrinth SMP',
iconUrl: serverIcon,
link: '/server/modrinth-smp',
},
summary: figmaSummary,
syncedAt: fiveMinutesAgo,
updateAvailable: true,
} satisfies ManagedContentCardData
const sharedInstanceCard = {
kind: 'shared-instance',
manager: {
name: 'Fetch',
iconUrl: userAvatar,
link: '/user/fetch',
},
summary: figmaSummary,
syncedAt: fiveMinutesAgo,
updateAvailable: true,
} satisfies ManagedContentCardData
const meta = {
title: 'Instances/ManagedContentCard',
component: ManagedContentCard,
parameters: {
layout: 'padded',
},
decorators: [
(story) => ({
components: { story },
template: '<div class="w-[1020px] max-w-full"><story /></div>',
}),
],
args: {
showViewContent: true,
showSettings: true,
showPrimaryAction: true,
onViewContent: fn(),
onSettings: fn(),
onPrimaryAction: fn(),
},
} satisfies Meta<typeof ManagedContentCard>
export default meta
type Story = StoryObj<typeof meta>
export const Modpack: Story = {
args: {
data: modpackCard,
},
}
export const Server: Story = {
args: {
data: serverCard,
},
}
export const SharedInstance: Story = {
args: {
data: sharedInstanceCard,
},
}
export const NarrowWidth: Story = {
args: {
data: serverCard,
},
render: (args) => ({
components: { ManagedContentCard },
setup: () => ({ args }),
template: /*html*/ `
<div class="w-[620px] max-w-full">
<ManagedContentCard v-bind="args" />
</div>
`,
}),
}
export const LongManagerName: Story = {
args: {
data: {
...sharedInstanceCard,
manager: {
...sharedInstanceCard.manager,
name: 'A shared-instance manager with a very long display name',
},
},
},
}
export const MissingManagerIcon: Story = {
args: {
data: {
...serverCard,
manager: {
name: 'Modrinth SMP',
},
},
},
}
export const ImportedModpack: Story = {
args: {
data: {
kind: 'modpack',
manager: {
name: 'Imported from Prism Launcher',
},
summary: figmaSummary,
},
showPrimaryAction: false,
},
}
export const LoadingCounts: Story = {
args: {
data: {
...modpackCard,
summary: undefined,
},
},
}
export const ZeroContent: Story = {
args: {
data: {
...sharedInstanceCard,
summary: [],
updateAvailable: false,
},
showPrimaryAction: false,
},
}
export const ApplyingUpdate: Story = {
args: {
data: serverCard,
disabled: true,
disabledText: 'Updating...',
},
}
export const AccountLocked: Story = {
args: {
data: {
...sharedInstanceCard,
updateAvailable: false,
},
showPrimaryAction: false,
},
}
export const EveryContentType: Story = {
args: {
data: {
...serverCard,
summary: [
{ type: 'mod', count: 52 },
{ type: 'plugin', count: 4 },
{ type: 'datapack', count: 3 },
{ type: 'resourcepack', count: 2 },
{ type: 'shader', count: 1 },
],
},
},
}
@@ -2,10 +2,9 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import { Button } from '../../components/base/buttons'
import ModpackContentModal from '../../layouts/shared/content-tab/components/modals/ModpackContentModal.vue'
import ManagedContentModal from '../../layouts/shared/content-tab/components/managed-content-modal/index.vue'
import type { ContentItem } from '../../layouts/shared/content-tab/types'
// Sample modpack content items (representing mods included in a modpack)
const sodiumItem: ContentItem = {
file_name: 'sodium-fabric-0.8.2+mc1.21.1.jar',
file_path: '',
@@ -201,7 +200,6 @@ const entityTextureFeaturesItem: ContentItem = {
update_version_id: null,
}
// Shader pack item
const complementaryShaderItem: ContentItem = {
file_name: 'ComplementaryReimagined_r5.3.zip',
file_path: '',
@@ -256,7 +254,6 @@ const bslShaderItem: ContentItem = {
update_version_id: null,
}
// Resource pack items
const faithfulItem: ContentItem = {
file_name: 'Faithful 32x - 1.21.zip',
file_path: '',
@@ -339,7 +336,6 @@ const stayTrueItem: ContentItem = {
update_version_id: null,
}
// Mixed content (mods + shaders + resource packs)
const mixedModpackContent: ContentItem[] = [
sodiumItem,
lithiumItem,
@@ -355,7 +351,6 @@ const mixedModpackContent: ContentItem[] = [
stayTrueItem,
]
// Mods only
const modsOnlyContent: ContentItem[] = [
sodiumItem,
lithiumItem,
@@ -366,7 +361,6 @@ const modsOnlyContent: ContentItem[] = [
entityTextureFeaturesItem,
]
// Large modpack content (40+ items for testing scrolling)
const largeModpackContent: ContentItem[] = [
...mixedModpackContent,
...Array.from({ length: 35 }, (_, i) => ({
@@ -393,35 +387,31 @@ const largeModpackContent: ContentItem[] = [
]
const meta = {
title: 'Instances/ModpackContentModal',
component: ModpackContentModal,
title: 'Instances/ManagedContentModal',
component: ManagedContentModal,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ModpackContentModal>
} satisfies Meta<typeof ManagedContentModal>
export default meta
type Story = StoryObj<typeof meta>
// ============================================
// Basic Examples
// ============================================
export const Default: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
return { modalRef, openModal }
},
template: /*html*/ `
<div>
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mixed)</Button>
<ModpackContentModal
<ManagedContentModal
ref="modalRef"
modpack-name="Cobblemon Official Modpack"
modpack-icon-url="https://cdn.modrinth.com/data/5FFgwNNP/icon.png"
source-name="Cobblemon Official Modpack"
source-icon-url="https://cdn.modrinth.com/data/5FFgwNNP/icon.png"
/>
</div>
`,
@@ -430,33 +420,28 @@ export const Default: Story = {
export const ModsOnly: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show(modsOnlyContent)
return { modalRef, openModal }
},
template: /*html*/ `
<div>
<Button type="colored" color="brand" @click="openModal">View Modpack Content (Mods Only)</Button>
<ModpackContentModal ref="modalRef" />
<ManagedContentModal ref="modalRef" />
</div>
`,
}),
}
// ============================================
// Loading State
// ============================================
export const LoadingState: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => {
modalRef.value?.showLoading()
// Simulate loading delay
setTimeout(() => {
modalRef.value?.show(mixedModpackContent)
}, 2000)
@@ -466,71 +451,59 @@ export const LoadingState: Story = {
template: /*html*/ `
<div>
<Button type="colored" color="brand" @click="openModal">View Content (With Loading)</Button>
<ModpackContentModal
<ManagedContentModal
ref="modalRef"
modpack-name="Fabulously Optimized"
modpack-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
source-name="Fabulously Optimized"
source-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
/>
</div>
`,
}),
}
// ============================================
// Empty State
// ============================================
export const EmptyContent: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show([])
return { modalRef, openModal }
},
template: /*html*/ `
<div>
<Button type="colored" color="brand" @click="openModal">View Empty Modpack</Button>
<ModpackContentModal ref="modalRef" />
<ManagedContentModal ref="modalRef" />
</div>
`,
}),
}
// ============================================
// Large Content List
// ============================================
export const LargeModpack: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show(largeModpackContent)
return { modalRef, openModal }
},
template: /*html*/ `
<div>
<Button type="colored" color="brand" @click="openModal">View Large Modpack (47 items)</Button>
<ModpackContentModal
<ManagedContentModal
ref="modalRef"
modpack-name="All the Mods 10"
modpack-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
source-name="All the Mods 10"
source-icon-url="https://cdn.modrinth.com/data/1KVo5zza/icon.png"
/>
</div>
`,
}),
}
// ============================================
// Search Functionality
// ============================================
export const SearchDemo: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
return { modalRef, openModal }
},
@@ -540,21 +513,17 @@ export const SearchDemo: Story = {
Click the button and try searching for "sodium", "shader", or "faithful" to test the search functionality.
</p>
<Button type="colored" color="brand" @click="openModal">Test Search</Button>
<ModpackContentModal ref="modalRef" />
<ManagedContentModal ref="modalRef" />
</div>
`,
}),
}
// ============================================
// Filter Demo
// ============================================
export const FilterDemo: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
return { modalRef, openModal }
},
@@ -564,22 +533,17 @@ export const FilterDemo: Story = {
Click the button and try the filter chips (Mods, Shaders, Resource Packs) to filter content by type.
</p>
<Button type="colored" color="brand" @click="openModal">Test Filters</Button>
<ModpackContentModal ref="modalRef" />
<ManagedContentModal ref="modalRef" />
</div>
`,
}),
}
// ============================================
// Mixed Owner Types
// ============================================
export const MixedOwnerTypes: Story = {
render: () => ({
components: { ModpackContentModal, Button },
components: { ManagedContentModal, Button },
setup() {
const modalRef = ref<InstanceType<typeof ModpackContentModal> | null>(null)
// Mix of user and organization owners
const modalRef = ref<InstanceType<typeof ManagedContentModal> | null>(null)
const mixedContent = [
sodiumItem, // User owner
fabricApiItem, // Organization owner
@@ -595,7 +559,7 @@ export const MixedOwnerTypes: Story = {
Shows content with different owner types: users (circular avatar) and organizations (rounded + icon).
</p>
<Button type="colored" color="brand" @click="openModal">View Mixed Owners</Button>
<ModpackContentModal ref="modalRef" />
<ManagedContentModal ref="modalRef" />
</div>
`,
}),