From 6328d4d92aea7c8c30407bcd4a1b14acf01aaad9 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 09:47:13 +0000 Subject: [PATCH 01/22] feat: base content card component --- packages/ui/src/components/index.ts | 1 + .../src/components/instances/ContentCard.vue | 118 ++++ packages/ui/src/components/instances/index.ts | 2 + .../stories/instances/ContentCard.stories.ts | 666 ++++++++++++++++++ 4 files changed, 787 insertions(+) create mode 100644 packages/ui/src/components/instances/ContentCard.vue create mode 100644 packages/ui/src/components/instances/index.ts create mode 100644 packages/ui/src/stories/instances/ContentCard.stories.ts 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..ac377d9eb1 --- /dev/null +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -0,0 +1,118 @@ + + + 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..2188053ece --- /dev/null +++ b/packages/ui/src/stories/instances/ContentCard.stories.ts @@ -0,0 +1,666 @@ +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 WithOwner: Story = { + args: { + project: fabricApiProject, + 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(), + }, +} From 548c1f3be4e0364b37974453ffc8ef9e92afb324 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 09:57:56 +0000 Subject: [PATCH 02/22] fix: tooltips + colors --- .../ui/src/components/instances/ContentCard.vue | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index ac377d9eb1..8c275e045b 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -85,12 +85,14 @@ const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) - @@ -100,9 +102,9 @@ const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) @update:model-value="(val) => emit('update:enabled', val)" /> - - From 847d752dae26ac65ea6594ed82078de0f6b496af Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 10:06:56 +0000 Subject: [PATCH 03/22] feat: fix orgs --- packages/ui/src/components/instances/ContentCard.vue | 11 +++++++++-- .../ui/src/stories/instances/ContentCard.stories.ts | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index 8c275e045b..9a2823079a 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -1,6 +1,6 @@ + + diff --git a/packages/ui/src/components/instances/index.ts b/packages/ui/src/components/instances/index.ts index 7d9dd736a3..4401ca4322 100644 --- a/packages/ui/src/components/instances/index.ts +++ b/packages/ui/src/components/instances/index.ts @@ -1,2 +1,9 @@ export type { ContentCardOwner, ContentCardProject, ContentCardVersion } from './ContentCard.vue' export { default as ContentCard } from './ContentCard.vue' +export type { + ContentModpackCardCategory, + ContentModpackCardOwner, + ContentModpackCardProject, + ContentModpackCardVersion, +} from './ContentModpackCard.vue' +export { default as ContentModpackCard } from './ContentModpackCard.vue' diff --git a/packages/ui/src/stories/instances/ContentModpackCard.stories.ts b/packages/ui/src/stories/instances/ContentModpackCard.stories.ts new file mode 100644 index 0000000000..6f27e7cf6f --- /dev/null +++ b/packages/ui/src/stories/instances/ContentModpackCard.stories.ts @@ -0,0 +1,613 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { fn } from 'storybook/test' +import { ref } from 'vue' + +import ContentCard from '../../components/instances/ContentCard.vue' +import type { + ContentModpackCardCategory, + ContentModpackCardOwner, + ContentModpackCardProject, + ContentModpackCardVersion, +} from '../../components/instances/ContentModpackCard.vue' +import ContentModpackCard from '../../components/instances/ContentModpackCard.vue' +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: ContentModpackCardOwner = { + id: '2avTeeAE', + name: 'robotkoer', + avatar_url: 'https://cdn.modrinth.com/user/2avTeeAE/icon.png', + type: 'user', +} + +const cobblemonOwner: ContentModpackCardOwner = { + id: 'AEFONbAM', + name: 'Reisen', + avatar_url: + 'https://cdn.modrinth.com/user/AEFONbAM/9e97453507a8245981d5cd825280f23be44f15ac.jpeg', + type: 'user', +} + +// Categories +const optimizationCategories: ContentModpackCardCategory[] = [ + { name: 'Fabric' }, + { name: 'Lightweight' }, + { name: 'Multiplayer' }, + { name: 'Optimization' }, +] + +const cobblemonCategories: ContentModpackCardCategory[] = [ + { name: 'Adventure' }, + { name: 'Fabric' }, + { name: 'Lightweight' }, + { name: 'Multiplayer' }, +] + +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 + +export default meta +type Story = StoryObj + +// ============================================ +// 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*/ ` +
+ +
+ `, + }), +} + +// ============================================ +// 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' }], + }, +} + +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' }, + { name: 'Technology' }, + { name: 'Magic' }, + { name: 'Exploration' }, + { name: 'Multiplayer' }, + ], + 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(null) + const categories: ContentModpackCardCategory[] = [ + { name: 'Adventure', action: () => (clickedCategory.value = 'Adventure') }, + { name: 'Lightweight', action: () => (clickedCategory.value = 'Lightweight') }, + { name: 'Multiplayer', action: () => (clickedCategory.value = 'Multiplayer') }, + ] + return { args, categories, clickedCategory } + }, + template: /*html*/ ` +
+ +
+ Clicked category: {{ clickedCategory || 'None' }} +
+
+ `, + }), + args: { + project: cobblemonProject, + version: cobblemonVersion, + owner: userOwner, + }, +} + +// ============================================ +// Overflow Menu Stories +// ============================================ + +export const WithOverflowMenu: Story = { + render: (args) => ({ + components: { ContentModpackCard }, + setup() { + return { args } + }, + template: /*html*/ ` + + + + + + `, + }), + 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, ContentCard }, + setup() { + const modalRef = ref | 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*/ ` +
+ + +
+ +
+
+
+ `, + }), +} + +// ============================================ +// Responsive Stories +// ============================================ + +export const ResponsiveView: Story = { + args: { + project: cobblemonProject, + }, + render: () => ({ + components: { ContentModpackCard }, + setup() { + return { + cobblemonProject, + cobblemonVersion, + userOwner, + optimizationCategories, + } + }, + template: /*html*/ ` +
+
+

Desktop (full width)

+
+ +
+
+
+

Mobile (<640px)

+
+ +
+
+
+ `, + }), +} + +// ============================================ +// 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, + }, +} From 74cc23054daea41d0e4d7eda8d34f3fc6f111e55 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 09:42:34 +0000 Subject: [PATCH 05/22] fix: extract types --- .../src/components/instances/ContentCard.vue | 22 +------ .../instances/ContentModpackCard.vue | 32 ++------- packages/ui/src/components/instances/index.ts | 9 +-- packages/ui/src/components/instances/types.ts | 33 ++++++++++ .../stories/instances/ContentCard.stories.ts | 6 +- .../instances/ContentModpackCard.stories.ts | 66 ++++++++++++------- 6 files changed, 93 insertions(+), 75 deletions(-) create mode 100644 packages/ui/src/components/instances/types.ts diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index 6389ea9d5a..aab05bbf4b 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -1,35 +1,17 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue index 73f552282a..16c57e7ace 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue @@ -62,11 +62,9 @@ />
-
@@ -134,7 +132,7 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue index c9699c2f28..50ff0fb78b 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/startup.vue @@ -69,12 +69,7 @@
- +
import { IssuesIcon, UpdatedIcon } from '@modrinth/assets' -import { ButtonStyled, Combobox, injectNotificationManager } from '@modrinth/ui' +import { ButtonStyled, Combobox, injectNotificationManager, Toggle } from '@modrinth/ui' import SaveBanner from '~/components/ui/servers/SaveBanner.vue' import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts' @@ -232,9 +227,3 @@ function resetToDefault() { invocation.value = originalInvocation.value ?? '' } - - diff --git a/apps/frontend/src/pages/settings/index.vue b/apps/frontend/src/pages/settings/index.vue index 70d27f9800..a737dbc3bc 100644 --- a/apps/frontend/src/pages/settings/index.vue +++ b/apps/frontend/src/pages/settings/index.vue @@ -119,65 +119,78 @@

{{ formatMessage(toggleFeatures.title) }}

{{ formatMessage(toggleFeatures.description) }}

-
- - -
-
- - -
-
- - -
-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/packages/assets/styles/classes.scss b/packages/assets/styles/classes.scss index d4d6a72c0a..a0d62bf970 100644 --- a/packages/assets/styles/classes.scss +++ b/packages/assets/styles/classes.scss @@ -70,10 +70,7 @@ :where(input) { box-sizing: border-box; max-height: 40px; - - &:not(.stylized-toggle) { - max-width: 100%; - } + max-width: 100%; } :where(.adjacent-input, &.adjacent-input) { @@ -118,10 +115,6 @@ &:not(&.small) { flex-direction: column; align-items: start; - - .stylized-toggle { - flex-basis: 0; - } } } } @@ -766,60 +759,6 @@ a, } } -.switch { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - -webkit-tap-highlight-color: transparent; - cursor: pointer; -} - -.stylized-toggle { - @extend .button-base; - - box-sizing: content-box; - min-height: 32px; - height: 32px; - min-width: 52px; - max-width: 52px; - border-radius: var(--radius-max); - display: inline-block; - position: relative; - margin: 0; - transition: all 0.2s ease; - background: var(--color-button-bg); - - &:after { - content: ''; - position: absolute; - top: 7px; - left: 7px; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--color-gray); - transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35); - outline: 2px solid transparent; - - @media (prefers-reduced-motion) { - transition: none; - } - } - - &:checked { - background-color: var(--color-brand); - - &:after { - transform: translatex(20px); - background: var(--color-accent-contrast); - } - } - - &:hover &:focus { - background: var(--color-button-bg); - } -} - // TOOLTIPS .v-popper--theme-dropdown, diff --git a/packages/ui/src/components/base/Toggle.vue b/packages/ui/src/components/base/Toggle.vue index b3eb3624d3..fd49647ae5 100644 --- a/packages/ui/src/components/base/Toggle.vue +++ b/packages/ui/src/components/base/Toggle.vue @@ -5,9 +5,9 @@ role="switch" :aria-checked="modelValue" :disabled="disabled" - class="relative inline-block rounded-full m-0 transition-all duration-200 cursor-pointer border-none" + class="relative inline-flex shrink-0 rounded-full m-0 transition-all duration-200 cursor-pointer border-none" :class="[ - small ? 'h-5 w-[38px]' : 'h-8 w-[52px]', + small ? 'h-5 !w-[38px]' : 'h-8 !w-[52px]', modelValue ? 'bg-brand' : 'bg-button-bg', disabled ? 'opacity-50 cursor-not-allowed' : 'btn-wrapper', ]" From 1850de7b8d4b2d9dfad63a26cd632b76ac06a006 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 22 Jan 2026 11:01:19 +0000 Subject: [PATCH 14/22] fix: row borders --- packages/tooling-config/tailwind/tailwind-preset.ts | 2 +- packages/ui/src/components/instances/ContentCardTable.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tooling-config/tailwind/tailwind-preset.ts b/packages/tooling-config/tailwind/tailwind-preset.ts index 3c6ca2a69e..e566b3f8ac 100644 --- a/packages/tooling-config/tailwind/tailwind-preset.ts +++ b/packages/tooling-config/tailwind/tailwind-preset.ts @@ -224,7 +224,7 @@ const config: Config = { hr: 'var(--color-hr)', table: { border: 'var(--color-table-border)', - alternateRow: ' var(--color-table-alternate-row)', + alternateRow: 'var(--color-table-alternate-row)', }, }, backgroundImage: { diff --git a/packages/ui/src/components/instances/ContentCardTable.vue b/packages/ui/src/components/instances/ContentCardTable.vue index 129e97848d..dd0f398777 100644 --- a/packages/ui/src/components/instances/ContentCardTable.vue +++ b/packages/ui/src/components/instances/ContentCardTable.vue @@ -146,9 +146,9 @@ function handleSort(column: ContentCardTableSortColumn) { :show-checkbox="showSelection" :selected="isItemSelected(item.id)" :class="[ - index % 2 === 0 ? 'bg-surface-1' : 'bg-surface-2', + index % 2 === 1 ? 'bg-surface-1' : 'bg-surface-2', index === items.length - 1 && 'rounded-b-[20px]', - 'border-t border-surface-3', + 'border-t border-solid border-[1px] border-surface-3', ]" @update:selected="(val) => toggleItemSelection(item.id, val ?? false)" @update:enabled="(val) => emit('update:enabled', item.id, val)" From 144bde8976f39519939c626c24a4133f15768d4a Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 22 Jan 2026 11:18:28 +0000 Subject: [PATCH 15/22] feat: disabled state --- .../components/instances/ContentCardItem.vue | 9 +++-- .../components/instances/ContentCardTable.vue | 4 +-- packages/ui/src/components/instances/types.ts | 1 + .../instances/ContentCardItem.stories.ts | 2 +- .../instances/ContentCardTable.stories.ts | 36 +++++++++++++------ 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCardItem.vue b/packages/ui/src/components/instances/ContentCardItem.vue index cca7356213..a9b9f23ea1 100644 --- a/packages/ui/src/components/instances/ContentCardItem.vue +++ b/packages/ui/src/components/instances/ContentCardItem.vue @@ -51,6 +51,7 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate @@ -104,7 +105,7 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate color-fill="text" hover-color-fill="background" > - @@ -112,18 +113,20 @@ const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate - - +