Files
Rocketmc/packages/ui/src/stories/instances/ContentModpackCard.stories.ts
T
Calum H. 7d92e4ec7f feat: content tab rewrite for worlds (#5136)
* feat: base content card component

* fix: tooltips + colors

* feat: fix orgs

* feat: base content tab internals rewrite

* feat: fix invalidmodal

* feat: add ContentModpackCard

* fix: extract types

* draft: layout

* feat: unlink modal

* feat: impl content tab

* fix: lint

* fix: toggling

* temp: disable updating stuff

* feat: selection v-model

* feat: bulk selection

* feat: mods tab rough draft

* feat: use fuse.js

* feat: add project combobox

* clean up project combobox

* feat: start install to play modal

* fix: events

* feat: use v-on

* feat: bulk actions + fix floating action bar width

* 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: use ContentCardTable + ContentCardItems

* feat: fix gap + border issues on last elm

* feat: cleanup + use proper searching

* fix: use TeleportOverflowMenu

* 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

* feat: impl modal

* feat(app): backend changes for content tab refactor (#5237)

* feat: include_changelog=false for updater modal

* fix: hash overrides

* feat: update checking for modpack

* feat: qa

* feat: modpack content modal

* fix: padding in table to match modals + tightness

* fix: lint

* feat: delete modal

* feat: fix toggle bugs

* fix: prepr

* fix: duplicate messages

* qa: full width search

* qa: use bg-surface-1.5

* qa: animation for filter pills

* qa: standardize hover colors

* fix: border-[1px] is border

* qa: mass de-select actually mass selecting

* qa: match figma designs for floating action bar

* qa: modal fixes

* q: modal fixes x2

* fix: table border

* qa: confirm modals

* qa: modal alignment

* qa: re-add stuck heading + dedupe logic

* qa: dedupe virtual scrolling + remove dead components

* qa: responsiveness for content table + link fixes

* qa: version column link, tooltips + lint fixes

* qa: instance busy protections

* fix: installation freeze bug

* chore: remove old mods page

* refactor: deduplicate layout

* chore: delete old content page(s)

* qa

* qa

* qa

* feat: sort btn - to iterate

* fix: ml

* feat: date added

* fix: lint

* fix: formatting.ts removal

* feat: get_dependencies_as_content_items

* qa: final QA changes

* refactor: deduplicate + polish content.rs

* feat: hook up content.vue with v1

* feat: hide v1 content api behind frontend feature flag

* fix: query keys + copy on empty state

* chore: i18n pass

* feat: reimpl unlink + upload endpoint

* feat: use bulk endpoints v1

* fix: lint

* fix: flags

* fix: responsiveness via container queries

* fix: lint

* qa: 1

* qa: fixes

* qa: fix ssr issues with browse content

* qa: header page divider

* qa: modals

* fix: prepr

* fix: issues

* fix: lint

* fix: toggle v1 ff

* qa: 5

* qa: delete modal copy

* feat: creation flow modals (#5383)

* refactor: delete content v0 usages + impl

* feat: qa + fixes

* feat: installing banner using state event

* feat: fix modpack card bugs + filtering issues

* refactor: delete backups v0 api module

* feat: v1 servers GET endpoint

* fix: backups

* feat: swap to kyros upload v1 addon

* fix: use tanstack for loader.vue

* feat: finish install from discovery modal

* qa: bug fixes

* feat: set up installation settings

* fix: lint

* fix: typos

* fix: bugs

* fix: disable inline content

* feat: content tab improvements — upload UX, installation settings, and client-only indicators

   Upload cancellation and navigation guard:
   - Add ConfirmLeaveModal that prompts when navigating away during upload
   - Cancel in-flight XHR uploads when user confirms leaving the page
   - Add beforeunload handler to warn on browser/tab close during upload
   - Track uploadedBytes/totalBytes in UploadState for progress display
   - Replace Collapsible with Transition for upload progress admonition
   - Show byte progress and percentage in upload banner
   - Clamp upload progress to prevent exceeding 100%

   Installation settings (server.properties):
   - Add KnownPropertiesFields and PropertiesFields types to Archon types
   - Add buildProperties() to creation flow context to collect gamemode,
     difficulty, seed, world type, structures, and generator settings
   - Pass properties through installContent on onboarding, discovery, and
     ServerSetupModal flows

   Server setup and discovery flow improvements:
   - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent
   - Replace loaderApiNames lookup with toApiLoader() helper
   - Remove eraseDataOnInstall toggle — always use soft_override: false
   - Simplify modpack install on discovery page to use first available version
     and route through creation flow modal for both onboarding and non-onboarding
   - Differentiate post-install navigation: content page for onboarding,
     loader options for existing servers

   Modpack update flow:
   - Replace updateModpack() call with installContent() using soft_override: true
     to support version selection in the content updater modal

   Client-only mod indicators:
   - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment)
   - Add environment to ContentItem and isClientOnly to ContentCardTableItem
   - Show orange TriangleAlertIcon with tooltip on client-only mods in content table
   - Add "Client-only" filter pill to content filtering (controlled via
     showClientOnlyFilter on ContentManagerContext)
   - Apply client-only indicators in both ContentPageLayout and ModpackContentModal

   Misc:
   - Add CLAUDE.md note about using prepr commands for lint checks
   - Export ConfirmLeaveModal from instances barrel

* fix: piping

* fix: switch content disable for linked server instances

* feat: client only filter

* fix: prepr

* feat: hasUpdate shape update

* feat: bulk update endpoint impl for content in panel

* feat: websocket state impl again with new phases

* fix: ws

* fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug

* fix: qa bugs

* fix: lint, a11y and i18n

* refactor: set up layouts folder properly

* fix: linked data cache stuff + lint

* feat: move installationsettings to shared layout

* fix: lint

* fix: issues

* feat: temp fuck staging up

* fix: lockfile

* fix: data sync issues on loader.vue

* fix: lint

* Hide shader configuration files from content list (#5499)

* feat: workaround search problem + split out reset

* fix: qa

* fix: changelog not showing on first open

* fix: qa + optimistic updating improvements

* fix: prepr+lint

* fix: qa

* feat: qa

* fix: lint

* fix: lint

* fix: build

* fix: build

* fix: type errors

* fix: fade and JAVA_HOME passthrough

* feat: qa

* feat: impl diff shit

* fix: qa

* fix: app qa

* feat: update diff modal

* fix: endpoint

* fix: qa

* fix: qa

* fix: use bulk in modpack modal

* feat: abort signal impl + fix issues

* fix: diff modal trunc

* feat: qa

* fix: qa

* feat: tooltip content tab

* fix: prepr

* fix: dismiss on settings btn

* feat: qa

* feat: dont clear handlers on disconnect

* fix: lint

* fix: wrangler + introduce staging-archon env file

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
2026-03-12 13:24:32 -07:00

634 lines
16 KiB
TypeScript

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,
},
}