diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 1f00fa63a7..7204b200dc 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -5,6 +5,7 @@ export * from './brand' export * from './changelog' export * from './chart' export * from './content' +export * from './instances' export * from './modal' export * from './nav' export * from './page' diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue new file mode 100644 index 0000000000..9a2823079a --- /dev/null +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -0,0 +1,127 @@ + + + diff --git a/packages/ui/src/components/instances/index.ts b/packages/ui/src/components/instances/index.ts new file mode 100644 index 0000000000..7d9dd736a3 --- /dev/null +++ b/packages/ui/src/components/instances/index.ts @@ -0,0 +1,2 @@ +export type { ContentCardOwner, ContentCardProject, ContentCardVersion } from './ContentCard.vue' +export { default as ContentCard } from './ContentCard.vue' diff --git a/packages/ui/src/stories/instances/ContentCard.stories.ts b/packages/ui/src/stories/instances/ContentCard.stories.ts new file mode 100644 index 0000000000..7edf6bd744 --- /dev/null +++ b/packages/ui/src/stories/instances/ContentCard.stories.ts @@ -0,0 +1,675 @@ +import { EditIcon, EyeIcon, FolderOpenIcon, LinkIcon } from '@modrinth/assets' +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 type { + ContentCardOwner, + ContentCardProject, + ContentCardVersion, +} from '../../components/instances/ContentCard.vue' +import ContentCard from '../../components/instances/ContentCard.vue' + +// Real project data from Modrinth API +const sodiumProject: ContentCardProject = { + id: 'AANobbMI', + slug: 'sodium', + title: 'Sodium', + icon_url: + 'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp', +} + +const modMenuProject: ContentCardProject = { + id: 'mOgUt4GM', + slug: 'modmenu', + title: 'Mod Menu', + icon_url: 'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png', +} + +const fabricApiProject: ContentCardProject = { + id: 'P7dR8mSH', + slug: 'fabric-api', + title: 'Fabric API', + icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png', +} + +// Version data +const sodiumVersion: ContentCardVersion = { + id: '59wygFUQ', + version_number: 'mc1.21.11-0.8.2-fabric', + file_name: 'sodium-fabric-0.8.2+mc1.21.11.jar', +} + +const modMenuVersion: ContentCardVersion = { + id: 'QuU0ciaR', + version_number: '16.0.0', + file_name: 'modmenu-16.0.0.jar', +} + +const fabricApiVersion: ContentCardVersion = { + id: 'Lwa1Q6e4', + version_number: '0.141.3+26.1', + file_name: 'fabric-api-0.141.3+26.1.jar', +} + +// Owner data +const sodiumOwner: ContentCardOwner = { + id: 'DzLrfrbK', + name: 'IMS', + avatar_url: 'https://avatars3.githubusercontent.com/u/31803019?v=4', + type: 'user', +} + +const fabricApiOwner: ContentCardOwner = { + id: 'BZoBsPo6', + name: 'FabricMC', + avatar_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png', + type: 'organization', +} + +const meta = { + title: 'Instances/ContentCard', + component: ContentCard, + parameters: { + layout: 'padded', + }, + argTypes: { + project: { + control: 'object', + description: 'Project information (id, slug, title, icon_url)', + }, + version: { + control: 'object', + description: 'Version information (id, version_number, file_name)', + }, + owner: { + control: 'object', + description: 'Owner/author information', + }, + enabled: { + control: 'boolean', + description: 'Toggle state - toggle hidden if undefined', + }, + disabled: { + control: 'boolean', + description: 'Grays out the card when true', + }, + overflowOptions: { + control: 'object', + description: 'Options for the overflow menu', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// ============================================ +// All Types Overview +// ============================================ + +export const AllTypes: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const toggleOn = ref(true) + const toggleOff = ref(false) + + const cards = [ + { + label: 'Full featured (all actions)', + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: toggleOn, + hasUpdate: true, + hasDelete: true, + hasOverflow: true, + }, + { + label: 'With toggle only', + project: modMenuProject, + version: modMenuVersion, + owner: { id: 'u2', name: 'Prospector', type: 'user' }, + enabled: toggleOn, + }, + { + label: 'With update available', + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + hasUpdate: true, + }, + { + label: 'Minimal (project only)', + project: sodiumProject, + }, + { + label: 'With version info only', + project: modMenuProject, + version: modMenuVersion, + }, + { + label: 'With owner only', + project: fabricApiProject, + owner: fabricApiOwner, + }, + { + label: 'Disabled state', + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: toggleOff, + disabled: true, + }, + { + label: 'Delete button only', + project: modMenuProject, + version: modMenuVersion, + hasDelete: true, + }, + { + label: 'Toggle off', + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + enabled: toggleOff, + }, + ] + + return { cards } + }, + template: /*html*/ ` +
+ +
+ `, + }), +} + +// ============================================ +// Basic Stories +// ============================================ + +export const Default: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: true, + overflowOptions: [ + { id: 'view', action: () => console.log('View clicked') }, + { id: 'edit', action: () => console.log('Edit clicked') }, + { divider: true }, + { id: 'remove', action: () => console.log('Remove clicked'), color: 'red' }, + ], + onDelete: fn(), + onUpdate: fn(), + 'onUpdate:enabled': fn(), + }, +} + +export const MinimalProjectOnly: Story = { + args: { + project: sodiumProject, + }, +} + +export const WithVersion: Story = { + args: { + project: modMenuProject, + version: modMenuVersion, + }, +} + +export const WithUserOwner: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + }, +} + +export const WithOrganizationOwner: Story = { + args: { + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + }, +} + +export const WithToggle: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + enabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const ToggleDisabled: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + enabled: false, + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Action Button Stories +// ============================================ + +export const WithDeleteButton: Story = { + args: { + project: modMenuProject, + version: modMenuVersion, + owner: sodiumOwner, + onDelete: fn(), + }, +} + +export const WithUpdateButton: Story = { + args: { + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + onUpdate: fn(), + }, +} + +export const WithAllActions: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: true, + onDelete: fn(), + onUpdate: fn(), + 'onUpdate:enabled': fn(), + overflowOptions: [ + { id: 'view', action: () => console.log('View') }, + { id: 'openFolder', action: () => console.log('Open folder') }, + { divider: true }, + { id: 'copyLink', action: () => console.log('Copy link') }, + ], + }, +} + +// ============================================ +// State Stories +// ============================================ + +export const Disabled: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: false, + disabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const LongProjectName: Story = { + args: { + project: { + id: 'test123', + slug: 'very-long-project-name', + title: '[EMF] Entity Model Features - The Ultimate Entity Rendering Mod', + icon_url: sodiumProject.icon_url, + }, + version: { + id: 'v1', + version_number: '2.4.1', + file_name: 'Entity_model_features_fabric_1.21.1-2.4.1.jar', + }, + owner: { + id: 'u1', + name: 'Traben', + type: 'user', + }, + enabled: true, + onDelete: fn(), + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Overflow Menu Stories +// ============================================ + +export const WithOverflowMenu: Story = { + render: (args) => ({ + components: { ContentCard, EditIcon, EyeIcon, FolderOpenIcon, LinkIcon }, + setup() { + return { args } + }, + template: /*html*/ ` + + + + + + + `, + }), + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + overflowOptions: [ + { id: 'view', action: () => console.log('View') }, + { id: 'edit', action: () => console.log('Edit') }, + { id: 'openFolder', action: () => console.log('Open folder') }, + { divider: true }, + { id: 'copyLink', action: () => console.log('Copy link') }, + ], + }, +} + +// ============================================ +// Slot Stories +// ============================================ + +export const WithAdditionalButtons: Story = { + render: (args) => ({ + components: { ContentCard, ButtonStyled, EyeIcon, FolderOpenIcon }, + setup() { + return { args } + }, + template: /*html*/ ` + + + + + `, + }), + args: { + project: modMenuProject, + version: modMenuVersion, + enabled: true, + onDelete: fn(), + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Interactive Stories +// ============================================ + +export const InteractiveToggle: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const enabled = ref(true) + return { + enabled, + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + } + }, + template: /*html*/ ` +
+ +
+ Mod is currently: {{ enabled ? 'Enabled' : 'Disabled' }} +
+
+ `, + }), +} + +// ============================================ +// List Stories +// ============================================ + +export const ModList: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const mods = ref([ + { project: sodiumProject, version: sodiumVersion, owner: sodiumOwner, enabled: true }, + { + project: modMenuProject, + version: modMenuVersion, + owner: { id: 'u2', name: 'Prospector', type: 'user' as const }, + enabled: true, + }, + { + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + enabled: true, + }, + ]) + + const handleDelete = (index: number) => { + mods.value.splice(index, 1) + } + + const handleToggle = (index: number, value: boolean) => { + mods.value[index].enabled = value + } + + return { mods, handleDelete, handleToggle } + }, + template: /*html*/ ` +
+ + + + +
+ `, + }), +} + +export const MixedStates: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + return { + sodiumProject, + sodiumVersion, + sodiumOwner, + modMenuProject, + modMenuVersion, + fabricApiProject, + fabricApiVersion, + fabricApiOwner, + } + }, + template: /*html*/ ` +
+ + + + + + + + +
+ `, + }), +} + +// ============================================ +// Responsive Stories +// ============================================ + +export const ResponsiveView: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + return { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + } + }, + template: /*html*/ ` +
+
+

Desktop (version info visible)

+
+ +
+
+
+

Mobile (<768px - version info hidden)

+
+ +
+
+
+ `, + }), +} + +// ============================================ +// Edge Cases +// ============================================ + +export const NoIcon: Story = { + args: { + project: { + id: 'test', + slug: 'no-icon-mod', + title: 'Mod Without Icon', + icon_url: undefined, + }, + version: { + id: 'v1', + version_number: '1.0.0', + file_name: 'no-icon-mod-1.0.0.jar', + }, + enabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const NoOwnerAvatar: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: { + id: 'u1', + name: 'Anonymous', + avatar_url: undefined, + type: 'user', + }, + enabled: true, + 'onUpdate:enabled': fn(), + }, +}