11<template >
22 <div class =" space-y-6" >
3- <div v-if =" !hideLoaderFields" class =" flex flex-col gap-2" >
3+ <!-- Instance-specific: Icon upload -->
4+ <div v-if =" ctx.flowType === 'instance'" class =" flex items-center gap-4" >
5+ <Avatar :src =" ctx.instanceIconUrl.value ?? undefined" size =" 5rem" :rounded =" true" />
6+ <div class =" flex flex-col gap-2" >
7+ <ButtonStyled type =" outlined" >
8+ <button class =" !border-surface-5" @click =" triggerIconInput" >
9+ <UploadIcon />
10+ Select icon
11+ </button >
12+ </ButtonStyled >
13+ <ButtonStyled type =" outlined" >
14+ <button
15+ class =" !border-surface-5"
16+ :disabled =" !ctx.instanceIcon.value"
17+ @click =" removeIcon"
18+ >
19+ <XIcon />
20+ Remove icon
21+ </button >
22+ </ButtonStyled >
23+ </div >
24+ <input
25+ ref =" iconInput"
26+ type =" file"
27+ accept =" image/*"
28+ class =" hidden"
29+ @change =" onIconSelected"
30+ />
31+ </div >
32+
33+ <!-- Instance-specific: Name field -->
34+ <div v-if =" ctx.flowType === 'instance'" class =" flex flex-col gap-2" >
35+ <span class =" font-semibold text-contrast" >Name</span >
36+ <StyledInput v-model =" ctx.instanceName.value" placeholder =" Enter instance name" />
37+ </div >
38+
39+ <!-- Loader chips -->
40+ <div v-if =" !hideLoaderChips" class =" flex flex-col gap-2" >
441 <span class =" font-semibold text-contrast"
5- >Content loader<span class =" text-red" > *</span ></span
42+ >{{ ctx.flowType === 'instance' ? 'Loader' : 'Content loader'
43+ }}<span class =" text-red" > *</span ></span
644 >
745 <Chips
846 v-model =" selectedLoader"
9- :items =" ctx.availableLoaders "
47+ :items =" effectiveLoaders "
1048 :format-label =" formatLoaderLabel"
1149 :never-empty =" false"
1250 />
1351 </div >
1452
53+ <!-- Game version -->
1554 <div class =" flex flex-col gap-2" >
1655 <span class =" font-semibold text-contrast" >Game version<span class =" text-red" > *</span ></span >
1756 <Combobox
2059 searchable
2160 placeholder =" Select game version"
2261 />
62+ <span class =" text-sm text-secondary" >It is recommended to use the latest version.</span >
2363 <Checkbox
2464 v-if =" ctx.showSnapshotToggle"
2565 :model-value =" ctx.showSnapshots.value"
2969 />
3070 </div >
3171
32- <Collapsible
33- v-if =" !hideLoaderFields"
34- :collapsed =" !selectedLoader || !selectedGameVersion"
35- overflow-visible
36- >
37- <div class =" flex flex-col gap-2" >
72+ <!-- Loader version (instance flow: flat layout, other flows: collapsible) -->
73+ <template v-if =" ! hideLoaderVersion " >
74+ <!-- Instance flow: no collapsible wrapper -->
75+ <div
76+ v-if =" ctx.flowType === 'instance'"
77+ v-show =" selectedLoader && selectedGameVersion"
78+ class =" flex flex-col gap-2"
79+ >
3880 <span class =" font-semibold text-contrast"
3981 >{{ isPaperLike ? 'Build number' : 'Loader version'
4082 }}<span class =" text-red" > *</span ></span
5597 />
5698 </div >
5799 </div >
58- </Collapsible >
100+
101+ <!-- Other flows: collapsible wrapper -->
102+ <Collapsible
103+ v-else
104+ :collapsed =" !selectedLoader || !selectedGameVersion"
105+ overflow-visible
106+ >
107+ <div class =" flex flex-col gap-2" >
108+ <span class =" font-semibold text-contrast"
109+ >{{ isPaperLike ? 'Build number' : 'Loader version'
110+ }}<span class =" text-red" > *</span ></span
111+ >
112+ <Chips
113+ v-if =" !isPaperLike"
114+ v-model =" loaderVersionType"
115+ :items =" loaderVersionTypeItems"
116+ :format-label =" capitalize"
117+ />
118+ <div v-if =" isPaperLike || loaderVersionType === 'other'" >
119+ <Combobox
120+ v-model =" selectedLoaderVersion"
121+ :options =" loaderVersionOptions"
122+ :no-options-message =" loaderVersionsLoading ? 'Loading...' : 'No versions available'"
123+ searchable
124+ :placeholder =" isPaperLike ? 'Select build number' : 'Select loader version'"
125+ />
126+ </div >
127+ </div >
128+ </Collapsible >
129+ </template >
59130 </div >
60131</template >
61132
62133<script setup lang="ts">
134+ import { UploadIcon , XIcon } from ' @modrinth/assets'
63135import { computed , onMounted , ref , watch } from ' vue'
64136
65137import { injectTags } from ' ../../../../providers'
138+ import Avatar from ' ../../../base/Avatar.vue'
139+ import ButtonStyled from ' ../../../base/ButtonStyled.vue'
66140import Checkbox from ' ../../../base/Checkbox.vue'
67141import Chips from ' ../../../base/Chips.vue'
68142import Collapsible from ' ../../../base/Collapsible.vue'
69143import Combobox , { type ComboboxOption } from ' ../../../base/Combobox.vue'
144+ import StyledInput from ' ../../../base/StyledInput.vue'
70145import type { LoaderVersionType } from ' ../creation-flow-context'
71146import { injectCreationFlowContext } from ' ../creation-flow-context'
147+ import { capitalize , formatLoaderLabel } from ' ../shared'
72148
73149const ctx = injectCreationFlowContext ()
74150const {
75151 selectedLoader,
76152 selectedGameVersion,
77153 loaderVersionType,
78154 selectedLoaderVersion,
79- hideLoaderFields,
155+ hideLoaderChips,
156+ hideLoaderVersion,
80157} = ctx
81158
159+ // For instance flow, prepend 'vanilla' to available loaders
160+ const effectiveLoaders = computed (() => {
161+ if (ctx .flowType === ' instance' ) {
162+ return [' vanilla' , ... ctx .availableLoaders .filter ((l ) => l !== ' vanilla' )]
163+ }
164+ return ctx .availableLoaders
165+ })
166+
82167// Pre-select loader and game version from initial values
83168onMounted (() => {
84169 if (ctx .initialLoader && ! selectedLoader .value ) {
@@ -93,23 +178,35 @@ const tags = injectTags()
93178
94179const loaderVersionTypeItems: LoaderVersionType [] = [' stable' , ' latest' , ' other' ]
95180
96- const capitalize = (item : string ) => item .charAt (0 ).toUpperCase () + item .slice (1 )
181+ const isPaperLike = computed (
182+ () => selectedLoader .value === ' paper' || selectedLoader .value === ' purpur' ,
183+ )
184+
185+ // Icon upload handling
186+ const iconInput = ref <HTMLInputElement | null >(null )
97187
98- const loaderDisplayNames: Record <string , string > = {
99- fabric: ' Fabric' ,
100- neoforge: ' NeoForge' ,
101- forge: ' Forge' ,
102- quilt: ' Quilt' ,
103- paper: ' Paper' ,
104- purpur: ' Purpur' ,
105- vanilla: ' Vanilla' ,
188+ function triggerIconInput() {
189+ iconInput .value ?.click ()
106190}
107191
108- const formatLoaderLabel = (item : string ) => loaderDisplayNames [item ] ?? capitalize (item )
192+ function onIconSelected(event : Event ) {
193+ const input = event .target as HTMLInputElement
194+ const file = input .files ?.[0 ]
195+ if (file ) {
196+ ctx .instanceIcon .value = file
197+ ctx .instanceIconUrl .value = URL .createObjectURL (file )
198+ }
199+ // Reset input so the same file can be re-selected
200+ input .value = ' '
201+ }
109202
110- const isPaperLike = computed (
111- () => selectedLoader .value === ' paper' || selectedLoader .value === ' purpur' ,
112- )
203+ function removeIcon() {
204+ if (ctx .instanceIconUrl .value ) {
205+ URL .revokeObjectURL (ctx .instanceIconUrl .value )
206+ }
207+ ctx .instanceIcon .value = null
208+ ctx .instanceIconUrl .value = null
209+ }
113210
114211// Game versions from tags provider, filtered by loader support
115212const gameVersionOptions = computed <ComboboxOption <string >[]>(() => {
@@ -118,7 +215,7 @@ const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
118215 : tags .gameVersions .value .filter ((v ) => v .version_type === ' release' )
119216
120217 // For loaders with per-version entries (NeoForge, Forge, Paper, Purpur), only show supported versions
121- if (selectedLoader .value ) {
218+ if (selectedLoader .value && selectedLoader . value !== ' vanilla ' ) {
122219 let apiLoader = selectedLoader .value
123220 if (apiLoader === ' neoforge' ) apiLoader = ' neo'
124221
0 commit comments