From e551ab0ca6a9d82c68cf141f1815846d2245d4f5 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 14 Feb 2026 10:19:41 +0000 Subject: [PATCH 01/24] feat: start on modal layout + story for testing --- .../src/components/base/BigOptionButton.vue | 30 +++++ packages/ui/src/components/base/index.ts | 1 + .../components/CustomSetupStage.vue | 85 ++++++++++++ .../components/FinalConfigStage.vue | 64 +++++++++ .../components/ModpackStage.vue | 119 +++++++++++++++++ .../components/WorldTypeStage.vue | 37 ++++++ .../create-world-context.ts | 122 ++++++++++++++++++ .../flows/create-world-flow-modal/index.vue | 44 +++++++ .../stages/custom-setup-stage.ts | 29 +++++ .../stages/final-config-stage.ts | 24 ++++ .../create-world-flow-modal/stages/index.ts | 13 ++ .../stages/modpack-stage.ts | 19 +++ .../stages/world-type-stage.ts | 14 ++ .../ui/src/components/servers/flows/index.ts | 7 + packages/ui/src/components/servers/index.ts | 1 + .../servers/CreateWorldFlowModal.stories.ts | 84 ++++++++++++ 16 files changed, 693 insertions(+) create mode 100644 packages/ui/src/components/base/BigOptionButton.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/components/FinalConfigStage.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/components/ModpackStage.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts create mode 100644 packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts create mode 100644 packages/ui/src/components/servers/flows/index.ts create mode 100644 packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts diff --git a/packages/ui/src/components/base/BigOptionButton.vue b/packages/ui/src/components/base/BigOptionButton.vue new file mode 100644 index 0000000000..86017a6137 --- /dev/null +++ b/packages/ui/src/components/base/BigOptionButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 06813a0af1..fbce3c5b62 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -5,6 +5,7 @@ export { default as AutoBrandIcon } from './AutoBrandIcon.vue' export { default as AutoLink } from './AutoLink.vue' export { default as Avatar } from './Avatar.vue' export { default as Badge } from './Badge.vue' +export { default as BigOptionButton } from './BigOptionButton.vue' export { default as BulletDivider } from './BulletDivider.vue' export { default as Button } from './Button.vue' export { default as ButtonStyled } from './ButtonStyled.vue' diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue new file mode 100644 index 0000000000..cead36d7ed --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue @@ -0,0 +1,85 @@ + + + diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/FinalConfigStage.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/FinalConfigStage.vue new file mode 100644 index 0000000000..03e5a0ab00 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/FinalConfigStage.vue @@ -0,0 +1,64 @@ + + + diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/ModpackStage.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/ModpackStage.vue new file mode 100644 index 0000000000..54c8a41e6d --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/ModpackStage.vue @@ -0,0 +1,119 @@ + + + diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue new file mode 100644 index 0000000000..d06d86f6de --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts new file mode 100644 index 0000000000..29a7eeb745 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts @@ -0,0 +1,122 @@ +import { computed, ref, type ComputedRef, type Ref, type ShallowRef } from 'vue' +import type { ComponentExposed } from 'vue-component-type-helpers' +import { createContext } from '../../../../providers' +import type { MultiStageModal, StageConfigInput } from '../../../base' +import { stageConfigs } from './stages' + +export type WorldType = 'modpack' | 'custom' | 'vanilla' +export type Gamemode = 'survival' | 'creative' | 'hardcore' +export type Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard' +export type LoaderVersionType = 'stable' | 'latest' | 'other' + +export interface ModpackSelection { + projectId: string + versionId: string + name: string + iconUrl?: string +} + +export interface CreateWorldContextValue { + // State + worldType: Ref + worldName: Ref + gamemode: Ref + difficulty: Ref + worldSeed: Ref + worldTypeOption: Ref + + // Loader/version state (custom setup) + selectedLoader: Ref + selectedGameVersion: Ref + loaderVersionType: Ref + selectedLoaderVersion: Ref + hideLoaderFields: ComputedRef + + // Modpack state + modpackSelection: Ref + modpackFile: Ref + + // Modal + modal: ShallowRef | null> + stageConfigs: StageConfigInput[] + + // Methods + reset: () => void + setWorldType: (type: WorldType) => void +} + +export const [injectCreateWorldContext, provideCreateWorldContext] = + createContext('CreateWorldFlowModal') + +export function createWorldContext( + modal: ShallowRef | null>, + emit: { + browseModpacks: () => void + createWorld: (config: CreateWorldContextValue) => void + }, +): CreateWorldContextValue { + const worldType = ref(null) + const worldName = ref('') + const gamemode = ref('survival') + const difficulty = ref('normal') + const worldSeed = ref('') + const worldTypeOption = ref('minecraft:normal') + + const selectedLoader = ref(null) + const selectedGameVersion = ref(null) + const loaderVersionType = ref('stable') + const selectedLoaderVersion = ref(null) + + const modpackSelection = ref(null) + const modpackFile = ref(null) + + const hideLoaderFields = computed(() => worldType.value === 'vanilla') + + function reset() { + worldType.value = null + worldName.value = '' + gamemode.value = 'survival' + difficulty.value = 'normal' + worldSeed.value = '' + worldTypeOption.value = 'minecraft:normal' + selectedLoader.value = null + selectedGameVersion.value = null + loaderVersionType.value = 'stable' + selectedLoaderVersion.value = null + modpackSelection.value = null + modpackFile.value = null + } + + function setWorldType(type: WorldType) { + worldType.value = type + if (type === 'modpack') { + modal.value?.setStage('modpack') + } else { + // both custom and vanilla go to custom-setup + // vanilla just hides loader fields via hideLoaderFields computed + modal.value?.setStage('custom-setup') + } + } + + const contextValue: CreateWorldContextValue = { + worldType, + worldName, + gamemode, + difficulty, + worldSeed, + worldTypeOption, + selectedLoader, + selectedGameVersion, + loaderVersionType, + selectedLoaderVersion, + hideLoaderFields, + modpackSelection, + modpackFile, + modal, + stageConfigs, + reset, + setWorldType, + } + + return contextValue +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue new file mode 100644 index 0000000000..beef4c2d83 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts new file mode 100644 index 0000000000..e30e1a87a8 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts @@ -0,0 +1,29 @@ +import { RightArrowIcon, XIcon } from '@modrinth/assets' +import type { StageConfigInput } from '../../../../base' +import { markRaw } from 'vue' +import CustomSetupStage from '../components/CustomSetupStage.vue' +import type { CreateWorldContextValue } from '../create-world-context' + +export const stageConfig: StageConfigInput = { + id: 'custom-setup', + title: 'Create world', + stageContent: markRaw(CustomSetupStage), + skip: (ctx) => ctx.worldType.value === 'modpack', + cannotNavigateForward: (ctx) => + !ctx.selectedGameVersion.value || + (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), + leftButtonConfig: (ctx) => ({ + label: 'Cancel', + icon: XIcon, + onClick: () => ctx.modal.value?.hide(), + }), + rightButtonConfig: (ctx) => ({ + label: 'Continue', + icon: RightArrowIcon, + iconPosition: 'after', + disabled: + !ctx.selectedGameVersion.value || + (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), + onClick: () => ctx.modal.value?.nextStage(), + }), +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts new file mode 100644 index 0000000000..bd16187b8a --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts @@ -0,0 +1,24 @@ +import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets' +import type { StageConfigInput } from '../../../../base' +import { markRaw } from 'vue' +import FinalConfigStage from '../components/FinalConfigStage.vue' +import type { CreateWorldContextValue } from '../create-world-context' + +export const stageConfig: StageConfigInput = { + id: 'final-config', + title: 'Create world', + stageContent: markRaw(FinalConfigStage), + cannotNavigateForward: (ctx) => !ctx.worldName.value.trim(), + leftButtonConfig: (ctx) => ({ + label: 'Back', + icon: LeftArrowIcon, + onClick: () => ctx.modal.value?.prevStage(), + }), + rightButtonConfig: (ctx) => ({ + label: 'Continue', + icon: RightArrowIcon, + iconPosition: 'after', + disabled: !ctx.worldName.value.trim(), + onClick: () => ctx.modal.value?.nextStage(), + }), +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts new file mode 100644 index 0000000000..1f6564d04a --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts @@ -0,0 +1,13 @@ +import type { StageConfigInput } from '../../../../base' +import type { CreateWorldContextValue } from '../create-world-context' +import { stageConfig as customSetupStageConfig } from './custom-setup-stage' +import { stageConfig as finalConfigStageConfig } from './final-config-stage' +import { stageConfig as modpackStageConfig } from './modpack-stage' +import { stageConfig as worldTypeStageConfig } from './world-type-stage' + +export const stageConfigs: StageConfigInput[] = [ + worldTypeStageConfig, + modpackStageConfig, + customSetupStageConfig, + finalConfigStageConfig, +] diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts new file mode 100644 index 0000000000..acc79bdd49 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts @@ -0,0 +1,19 @@ +import { LeftArrowIcon } from '@modrinth/assets' +import type { StageConfigInput } from '../../../../base' +import { markRaw } from 'vue' +import ModpackStage from '../components/ModpackStage.vue' +import type { CreateWorldContextValue } from '../create-world-context' + +export const stageConfig: StageConfigInput = { + id: 'modpack', + title: 'Create world', + stageContent: markRaw(ModpackStage), + skip: (ctx) => ctx.worldType.value !== 'modpack', + leftButtonConfig: (ctx) => ({ + label: 'Back', + icon: LeftArrowIcon, + onClick: () => ctx.modal.value?.setStage('world-type'), + }), + rightButtonConfig: null, + maxWidth: '520px', +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts new file mode 100644 index 0000000000..289ecc9c75 --- /dev/null +++ b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts @@ -0,0 +1,14 @@ +import type { StageConfigInput } from '../../../../base' +import { markRaw } from 'vue' +import WorldTypeStage from '../components/WorldTypeStage.vue' +import type { CreateWorldContextValue } from '../create-world-context' + +export const stageConfig: StageConfigInput = { + id: 'world-type', + title: 'Create world', + stageContent: markRaw(WorldTypeStage), + nonProgressStage: true, + leftButtonConfig: null, + rightButtonConfig: null, + maxWidth: '520px', +} diff --git a/packages/ui/src/components/servers/flows/index.ts b/packages/ui/src/components/servers/flows/index.ts new file mode 100644 index 0000000000..20a0d2f659 --- /dev/null +++ b/packages/ui/src/components/servers/flows/index.ts @@ -0,0 +1,7 @@ +export { default as CreateWorldFlowModal } from './create-world-flow-modal/index.vue' +export type { + CreateWorldContextValue, + Difficulty, + Gamemode, + WorldType, +} from './create-world-flow-modal/create-world-context' diff --git a/packages/ui/src/components/servers/index.ts b/packages/ui/src/components/servers/index.ts index 476693f12a..10935f8983 100644 --- a/packages/ui/src/components/servers/index.ts +++ b/packages/ui/src/components/servers/index.ts @@ -1,5 +1,6 @@ export * from './backups' export * from './files' +export * from './flows' export * from './icons' export * from './labels' export * from './marketing' diff --git a/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts b/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts new file mode 100644 index 0000000000..02d82e451b --- /dev/null +++ b/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import ButtonStyled from '../../components/base/ButtonStyled.vue' +import CreateWorldFlowModal from '../../components/servers/flows/create-world-flow-modal/index.vue' + +const meta = { + title: 'Servers/CreateWorldFlowModal', + component: CreateWorldFlowModal, + parameters: { + layout: 'centered', + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// ============================================ +// Default — Full Flow +// ============================================ + +export const Default: Story = { + render: () => ({ + components: { CreateWorldFlowModal, ButtonStyled }, + setup() { + const modalRef = ref | null>(null) + const openModal = () => modalRef.value?.show() + return { modalRef, openModal } + }, + template: /*html*/ ` +
+ + + + +
+ `, + }), +} + +// ============================================ +// Events Logging +// ============================================ + +export const WithEventLogging: Story = { + render: () => ({ + components: { CreateWorldFlowModal, ButtonStyled }, + setup() { + const modalRef = ref | null>(null) + const lastEvent = ref('') + const openModal = () => modalRef.value?.show() + + const onBrowseModpacks = () => { + lastEvent.value = 'browse-modpacks emitted' + } + const onCreateWorld = (config: any) => { + lastEvent.value = `create-world emitted — name: "${config.worldName.value}", mode: ${config.gamemode.value}` + } + const onHide = () => { + lastEvent.value = 'hide emitted' + } + return { modalRef, openModal, lastEvent, onBrowseModpacks, onCreateWorld, onHide } + }, + template: /*html*/ ` +
+ + + +

Last event: {{ lastEvent }}

+ +
+ `, + }), +} From 4e3fdd6898c9df7008e2b68778b3975104cd4ee3 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 14 Feb 2026 10:54:51 +0000 Subject: [PATCH 02/24] feat: modpack stage ready --- .../src/components/base/BigOptionButton.vue | 4 +- packages/ui/src/components/base/Combobox.vue | 2 +- .../instances/modals/ContentUpdaterModal.vue | 3 +- packages/ui/src/components/modal/NewModal.vue | 2 +- .../components/ModpackStage.vue | 195 ++++++++++++------ .../components/WorldTypeStage.vue | 6 +- .../create-world-context.ts | 31 +++ .../stages/custom-setup-stage.ts | 8 +- .../version/VersionChannelIndicator.vue | 30 ++- 9 files changed, 204 insertions(+), 77 deletions(-) diff --git a/packages/ui/src/components/base/BigOptionButton.vue b/packages/ui/src/components/base/BigOptionButton.vue index 86017a6137..7d6700b5b7 100644 --- a/packages/ui/src/components/base/BigOptionButton.vue +++ b/packages/ui/src/components/base/BigOptionButton.vue @@ -1,12 +1,12 @@ diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/WorldTypeStage.vue similarity index 66% rename from packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue rename to packages/ui/src/components/flows/creation-flow-modal/components/WorldTypeStage.vue index a3b14c6e13..d55dce20ba 100644 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/WorldTypeStage.vue +++ b/packages/ui/src/components/flows/creation-flow-modal/components/WorldTypeStage.vue @@ -21,17 +21,14 @@ @click="setWorldType('vanilla')" /> - diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts similarity index 79% rename from packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts rename to packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts index 1f1aa83864..700fd50b26 100644 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/create-world-context.ts +++ b/packages/ui/src/components/flows/creation-flow-modal/creation-flow-context.ts @@ -1,10 +1,12 @@ -import { computed, ref, type ComputedRef, type Ref, type ShallowRef } from 'vue' +import { computed, type ComputedRef, type Ref, ref, type ShallowRef } from 'vue' import type { ComponentExposed } from 'vue-component-type-helpers' -import { createContext } from '../../../../providers' -import type { ComboboxOption } from '../../../base/Combobox.vue' -import type { MultiStageModal, StageConfigInput } from '../../../base' + +import { createContext } from '../../../providers' +import type { MultiStageModal, StageConfigInput } from '../../base' +import type { ComboboxOption } from '../../base/Combobox.vue' import { stageConfigs } from './stages' +export type FlowType = 'world' | 'server-onboarding' | 'instance' export type WorldType = 'modpack' | 'custom' | 'vanilla' export type Gamemode = 'survival' | 'creative' | 'hardcore' export type Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard' @@ -23,7 +25,16 @@ export interface ModpackSearchHit { latestVersion?: string } -export interface CreateWorldContextValue { +export const flowTypeHeadings: Record = { + world: 'Create world', + 'server-onboarding': 'Set up server', + instance: 'Create instance', +} + +export interface CreationFlowContextValue { + // Flow + flowType: FlowType + // State worldType: Ref worldName: Ref @@ -52,23 +63,25 @@ export interface CreateWorldContextValue { // Modal modal: ShallowRef | null> - stageConfigs: StageConfigInput[] + stageConfigs: StageConfigInput[] // Methods reset: () => void setWorldType: (type: WorldType) => void + finish: () => void } -export const [injectCreateWorldContext, provideCreateWorldContext] = - createContext('CreateWorldFlowModal') +export const [injectCreationFlowContext, provideCreationFlowContext] = + createContext('CreationFlowModal') -export function createWorldContext( +export function createCreationFlowContext( modal: ShallowRef | null>, + flowType: FlowType, emit: { browseModpacks: () => void - createWorld: (config: CreateWorldContextValue) => void + create: (config: CreationFlowContextValue) => void }, -): CreateWorldContextValue { +): CreationFlowContextValue { const worldType = ref(null) const worldName = ref('') const gamemode = ref('survival') @@ -124,7 +137,12 @@ export function createWorldContext( } } - const contextValue: CreateWorldContextValue = { + function finish() { + emit.create(contextValue) + } + + const contextValue: CreationFlowContextValue = { + flowType, worldType, worldName, gamemode, @@ -147,6 +165,7 @@ export function createWorldContext( stageConfigs, reset, setWorldType, + finish, } return contextValue diff --git a/packages/ui/src/components/flows/creation-flow-modal/index.vue b/packages/ui/src/components/flows/creation-flow-modal/index.vue new file mode 100644 index 0000000000..6abdd2b9c9 --- /dev/null +++ b/packages/ui/src/components/flows/creation-flow-modal/index.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/custom-setup-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/custom-setup-stage.ts new file mode 100644 index 0000000000..b927e5226e --- /dev/null +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/custom-setup-stage.ts @@ -0,0 +1,35 @@ +import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets' +import { markRaw } from 'vue' + +import type { StageConfigInput } from '../../../base' +import CustomSetupStage from '../components/CustomSetupStage.vue' +import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context' + +export const stageConfig: StageConfigInput = { + id: 'custom-setup', + title: (ctx) => flowTypeHeadings[ctx.flowType], + stageContent: markRaw(CustomSetupStage), + skip: (ctx) => ctx.worldType.value === 'modpack', + cannotNavigateForward: (ctx) => + !ctx.selectedGameVersion.value || (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), + leftButtonConfig: (ctx) => ({ + label: 'Back', + icon: LeftArrowIcon, + onClick: () => ctx.modal.value?.setStage('world-type'), + }), + rightButtonConfig: (ctx) => ({ + label: ctx.flowType === 'world' ? 'Continue' : 'Finish', + icon: ctx.flowType === 'world' ? RightArrowIcon : null, + iconPosition: 'after', + color: ctx.flowType === 'world' ? undefined : ('brand' as const), + disabled: + !ctx.selectedGameVersion.value || (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), + onClick: () => { + if (ctx.flowType === 'world') { + ctx.modal.value?.nextStage() + } else { + ctx.finish() + } + }, + }), +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts similarity index 66% rename from packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts rename to packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts index bd16187b8a..5bd6241f52 100644 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/final-config-stage.ts +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/final-config-stage.ts @@ -1,13 +1,15 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets' -import type { StageConfigInput } from '../../../../base' import { markRaw } from 'vue' + +import type { StageConfigInput } from '../../../base' import FinalConfigStage from '../components/FinalConfigStage.vue' -import type { CreateWorldContextValue } from '../create-world-context' +import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context' -export const stageConfig: StageConfigInput = { +export const stageConfig: StageConfigInput = { id: 'final-config', - title: 'Create world', + title: (ctx) => flowTypeHeadings[ctx.flowType], stageContent: markRaw(FinalConfigStage), + skip: (ctx) => ctx.flowType !== 'world', cannotNavigateForward: (ctx) => !ctx.worldName.value.trim(), leftButtonConfig: (ctx) => ({ label: 'Back', diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts similarity index 65% rename from packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts rename to packages/ui/src/components/flows/creation-flow-modal/stages/index.ts index 1f6564d04a..d85f328c6e 100644 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/index.ts +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/index.ts @@ -1,11 +1,11 @@ -import type { StageConfigInput } from '../../../../base' -import type { CreateWorldContextValue } from '../create-world-context' +import type { StageConfigInput } from '../../../base' +import type { CreationFlowContextValue } from '../creation-flow-context' import { stageConfig as customSetupStageConfig } from './custom-setup-stage' import { stageConfig as finalConfigStageConfig } from './final-config-stage' import { stageConfig as modpackStageConfig } from './modpack-stage' import { stageConfig as worldTypeStageConfig } from './world-type-stage' -export const stageConfigs: StageConfigInput[] = [ +export const stageConfigs: StageConfigInput[] = [ worldTypeStageConfig, modpackStageConfig, customSetupStageConfig, diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/modpack-stage.ts similarity index 61% rename from packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts rename to packages/ui/src/components/flows/creation-flow-modal/stages/modpack-stage.ts index acc79bdd49..3135384790 100644 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/modpack-stage.ts +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/modpack-stage.ts @@ -1,12 +1,13 @@ import { LeftArrowIcon } from '@modrinth/assets' -import type { StageConfigInput } from '../../../../base' import { markRaw } from 'vue' + +import type { StageConfigInput } from '../../../base' import ModpackStage from '../components/ModpackStage.vue' -import type { CreateWorldContextValue } from '../create-world-context' +import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context' -export const stageConfig: StageConfigInput = { +export const stageConfig: StageConfigInput = { id: 'modpack', - title: 'Create world', + title: (ctx) => flowTypeHeadings[ctx.flowType], stageContent: markRaw(ModpackStage), skip: (ctx) => ctx.worldType.value !== 'modpack', leftButtonConfig: (ctx) => ({ diff --git a/packages/ui/src/components/flows/creation-flow-modal/stages/world-type-stage.ts b/packages/ui/src/components/flows/creation-flow-modal/stages/world-type-stage.ts new file mode 100644 index 0000000000..5d7b1bcad2 --- /dev/null +++ b/packages/ui/src/components/flows/creation-flow-modal/stages/world-type-stage.ts @@ -0,0 +1,15 @@ +import { markRaw } from 'vue' + +import type { StageConfigInput } from '../../../base' +import WorldTypeStage from '../components/WorldTypeStage.vue' +import { type CreationFlowContextValue, flowTypeHeadings } from '../creation-flow-context' + +export const stageConfig: StageConfigInput = { + id: 'world-type', + title: (ctx) => flowTypeHeadings[ctx.flowType], + stageContent: markRaw(WorldTypeStage), + nonProgressStage: true, + leftButtonConfig: null, + rightButtonConfig: null, + maxWidth: '520px', +} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue deleted file mode 100644 index cead36d7ed..0000000000 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/components/CustomSetupStage.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue b/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue deleted file mode 100644 index beef4c2d83..0000000000 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/index.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts deleted file mode 100644 index 09217d9523..0000000000 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/custom-setup-stage.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets' -import type { StageConfigInput } from '../../../../base' -import { markRaw } from 'vue' -import CustomSetupStage from '../components/CustomSetupStage.vue' -import type { CreateWorldContextValue } from '../create-world-context' - -export const stageConfig: StageConfigInput = { - id: 'custom-setup', - title: 'Create world', - stageContent: markRaw(CustomSetupStage), - skip: (ctx) => ctx.worldType.value === 'modpack', - cannotNavigateForward: (ctx) => - !ctx.selectedGameVersion.value || - (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), - leftButtonConfig: (ctx) => ({ - label: 'Back', - icon: LeftArrowIcon, - onClick: () => ctx.modal.value?.setStage('world-type'), - }), - rightButtonConfig: (ctx) => ({ - label: 'Continue', - icon: RightArrowIcon, - iconPosition: 'after', - disabled: - !ctx.selectedGameVersion.value || - (!ctx.hideLoaderFields.value && !ctx.selectedLoader.value), - onClick: () => ctx.modal.value?.nextStage(), - }), -} diff --git a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts b/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts deleted file mode 100644 index 289ecc9c75..0000000000 --- a/packages/ui/src/components/servers/flows/create-world-flow-modal/stages/world-type-stage.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { StageConfigInput } from '../../../../base' -import { markRaw } from 'vue' -import WorldTypeStage from '../components/WorldTypeStage.vue' -import type { CreateWorldContextValue } from '../create-world-context' - -export const stageConfig: StageConfigInput = { - id: 'world-type', - title: 'Create world', - stageContent: markRaw(WorldTypeStage), - nonProgressStage: true, - leftButtonConfig: null, - rightButtonConfig: null, - maxWidth: '520px', -} diff --git a/packages/ui/src/components/servers/flows/index.ts b/packages/ui/src/components/servers/flows/index.ts index 20a0d2f659..3003d2f794 100644 --- a/packages/ui/src/components/servers/flows/index.ts +++ b/packages/ui/src/components/servers/flows/index.ts @@ -1,7 +1,8 @@ -export { default as CreateWorldFlowModal } from './create-world-flow-modal/index.vue' export type { - CreateWorldContextValue, + CreationFlowContextValue, Difficulty, + FlowType, Gamemode, WorldType, -} from './create-world-flow-modal/create-world-context' +} from '../../flows/creation-flow-modal/creation-flow-context' +export { default as CreationFlowModal } from '../../flows/creation-flow-modal/index.vue' diff --git a/packages/ui/src/providers/index.ts b/packages/ui/src/providers/index.ts index 0b08f9b292..8785b07aa4 100644 --- a/packages/ui/src/providers/index.ts +++ b/packages/ui/src/providers/index.ts @@ -85,4 +85,5 @@ export * from './modal-behavior' export * from './page-context' export * from './project-page' export * from './server-context' +export * from './tags' export * from './web-notifications' diff --git a/packages/ui/src/providers/tags.ts b/packages/ui/src/providers/tags.ts new file mode 100644 index 0000000000..32ea2f933f --- /dev/null +++ b/packages/ui/src/providers/tags.ts @@ -0,0 +1,11 @@ +import type { Labrinth } from '@modrinth/api-client' +import type { Ref } from 'vue' + +import { createContext } from './index' + +export interface TagsContext { + gameVersions: Ref + loaders: Ref +} + +export const [injectTags, provideTags] = createContext('root', 'tags') diff --git a/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts b/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts deleted file mode 100644 index 02d82e451b..0000000000 --- a/packages/ui/src/stories/servers/CreateWorldFlowModal.stories.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' -import { ref } from 'vue' - -import ButtonStyled from '../../components/base/ButtonStyled.vue' -import CreateWorldFlowModal from '../../components/servers/flows/create-world-flow-modal/index.vue' - -const meta = { - title: 'Servers/CreateWorldFlowModal', - component: CreateWorldFlowModal, - parameters: { - layout: 'centered', - }, -} satisfies Meta - -export default meta -type Story = StoryObj - -// ============================================ -// Default — Full Flow -// ============================================ - -export const Default: Story = { - render: () => ({ - components: { CreateWorldFlowModal, ButtonStyled }, - setup() { - const modalRef = ref | null>(null) - const openModal = () => modalRef.value?.show() - return { modalRef, openModal } - }, - template: /*html*/ ` -
- - - - -
- `, - }), -} - -// ============================================ -// Events Logging -// ============================================ - -export const WithEventLogging: Story = { - render: () => ({ - components: { CreateWorldFlowModal, ButtonStyled }, - setup() { - const modalRef = ref | null>(null) - const lastEvent = ref('') - const openModal = () => modalRef.value?.show() - - const onBrowseModpacks = () => { - lastEvent.value = 'browse-modpacks emitted' - } - const onCreateWorld = (config: any) => { - lastEvent.value = `create-world emitted — name: "${config.worldName.value}", mode: ${config.gamemode.value}` - } - const onHide = () => { - lastEvent.value = 'hide emitted' - } - return { modalRef, openModal, lastEvent, onBrowseModpacks, onCreateWorld, onHide } - }, - template: /*html*/ ` -
- - - -

Last event: {{ lastEvent }}

- -
- `, - }), -} diff --git a/packages/ui/src/stories/servers/CreationFlowModal.stories.ts b/packages/ui/src/stories/servers/CreationFlowModal.stories.ts new file mode 100644 index 0000000000..67c8833452 --- /dev/null +++ b/packages/ui/src/stories/servers/CreationFlowModal.stories.ts @@ -0,0 +1,122 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import ButtonStyled from '../../components/base/ButtonStyled.vue' +import type { CreationFlowContextValue } from '../../components/flows/creation-flow-modal/creation-flow-context' +import CreationFlowModal from '../../components/flows/creation-flow-modal/index.vue' + +const meta = { + title: 'Servers/CreationFlowModal', + component: CreationFlowModal, + parameters: { + layout: 'centered', + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// ============================================ +// Default — Create World (full flow) +// ============================================ + +export const CreateWorld: Story = { + render: () => ({ + components: { CreationFlowModal, ButtonStyled }, + setup() { + const modalRef = ref | null>(null) + const lastEvent = ref('') + const openModal = () => modalRef.value?.show() + + const onCreate = (config: CreationFlowContextValue) => { + lastEvent.value = `create emitted — name: "${config.worldName.value}", mode: ${config.gamemode.value}` + } + return { modalRef, openModal, lastEvent, onCreate } + }, + template: /*html*/ ` +
+ + + +

Last event: {{ lastEvent }}

+ +
+ `, + }), +} + +// ============================================ +// Server Onboarding (no final config) +// ============================================ + +export const ServerOnboarding: Story = { + render: () => ({ + components: { CreationFlowModal, ButtonStyled }, + setup() { + const modalRef = ref | null>(null) + const lastEvent = ref('') + const openModal = () => modalRef.value?.show() + + const onCreate = (config: CreationFlowContextValue) => { + lastEvent.value = `create emitted — loader: ${config.selectedLoader.value}, version: ${config.selectedGameVersion.value}` + } + return { modalRef, openModal, lastEvent, onCreate } + }, + template: /*html*/ ` +
+ + + +

Last event: {{ lastEvent }}

+ +
+ `, + }), +} + +// ============================================ +// Instance (no final config) +// ============================================ + +export const Instance: Story = { + render: () => ({ + components: { CreationFlowModal, ButtonStyled }, + setup() { + const modalRef = ref | null>(null) + const lastEvent = ref('') + const openModal = () => modalRef.value?.show() + + const onCreate = (config: CreationFlowContextValue) => { + lastEvent.value = `create emitted — loader: ${config.selectedLoader.value}, version: ${config.selectedGameVersion.value}` + } + return { modalRef, openModal, lastEvent, onCreate } + }, + template: /*html*/ ` +
+ + + +

Last event: {{ lastEvent }}

+ +
+ `, + }), +} From 678ac1f40399fea6053489ece4d8d607696e665c Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 14 Feb 2026 12:04:25 +0000 Subject: [PATCH 04/24] feat: impl modal into server panel --- .../ui/servers/ServerSetupModal.vue | 166 +++++++++++++ .../src/pages/hosting/manage/[id].vue | 46 ++-- .../hosting/manage/[id]/options/loader.vue | 232 +++++++++++++++++- .../components/ConfirmStage.vue | 72 ++++++ .../components/CustomSetupStage.vue | 128 ++++++++-- .../components/ModpackStage.vue | 4 + .../creation-flow-context.ts | 44 +++- .../flows/creation-flow-modal/index.vue | 30 ++- .../stages/confirm-stage.ts | 23 ++ .../stages/custom-setup-stage.ts | 34 +-- .../flows/creation-flow-modal/stages/index.ts | 2 + .../ui/src/components/servers/flows/index.ts | 2 + 12 files changed, 723 insertions(+), 60 deletions(-) create mode 100644 apps/frontend/src/components/ui/servers/ServerSetupModal.vue create mode 100644 packages/ui/src/components/flows/creation-flow-modal/components/ConfirmStage.vue create mode 100644 packages/ui/src/components/flows/creation-flow-modal/stages/confirm-stage.ts diff --git a/apps/frontend/src/components/ui/servers/ServerSetupModal.vue b/apps/frontend/src/components/ui/servers/ServerSetupModal.vue new file mode 100644 index 0000000000..db899de805 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerSetupModal.vue @@ -0,0 +1,166 @@ + + + diff --git a/apps/frontend/src/pages/hosting/manage/[id].vue b/apps/frontend/src/pages/hosting/manage/[id].vue index 05ec9bdf7a..748602e6b8 100644 --- a/apps/frontend/src/pages/hosting/manage/[id].vue +++ b/apps/frontend/src/pages/hosting/manage/[id].vue @@ -195,18 +195,13 @@ > Setting up your server... -
-

- What would you like to install on your new server? -

- - -
+ diff --git a/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue b/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue index 26207583bc..dc6ca329e5 100644 --- a/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue +++ b/packages/ui/src/components/flows/creation-flow-modal/components/SetupTypeStage.vue @@ -17,13 +17,13 @@ @@ -39,7 +39,7 @@ diff --git a/packages/ui/src/components/flows/creation-flow-modal/index.vue b/packages/ui/src/components/flows/creation-flow-modal/index.vue index 3a11035037..1eb3e2be79 100644 --- a/packages/ui/src/components/flows/creation-flow-modal/index.vue +++ b/packages/ui/src/components/flows/creation-flow-modal/index.vue @@ -1,5 +1,5 @@ +