Skip to content

Commit 1e87719

Browse files
committed
feat: alignment with figma flows
1 parent f1f1a77 commit 1e87719

18 files changed

+635
-88
lines changed

packages/ui/src/components/base/BigOptionButton.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<button
3-
class="flex w-full items-center gap-3 rounded-[20px] border border-solid p-3 text-left transition-all hover:brightness-110 brightness-90 active:scale-[0.98]"
3+
class="flex w-full hover:cursor-pointer items-center gap-3 rounded-[20px] border border-solid p-3 text-left transition-all hover:brightness-110 brightness-90 active:scale-[0.98]"
44
:class="selected ? 'border-brand bg-brand-highlight' : 'border-surface-5 bg-surface-4'"
55
@click="$emit('click')"
66
>
@@ -19,17 +19,20 @@
1919
<span class="text-base font-semibold text-contrast">{{ title }}</span>
2020
<span class="text-sm font-medium text-primary">{{ description }}</span>
2121
</div>
22+
<ChevronRightIcon v-if="showChevron" class="size-5 shrink-0 text-secondary" />
2223
</button>
2324
</template>
2425

2526
<script setup lang="ts">
27+
import { ChevronRightIcon } from '@modrinth/assets'
2628
import type { Component } from 'vue'
2729
2830
defineProps<{
2931
icon: Component
3032
title: string
3133
description: string
3234
selected?: boolean
35+
showChevron?: boolean
3336
}>()
3437
3538
defineEmits<{

packages/ui/src/components/flows/creation-flow-modal/components/ConfirmStage.vue

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
</span>
4646
</div>
4747
<div
48-
v-if="ctx.selectedLoaderVersion.value && !ctx.hideLoaderFields.value"
48+
v-if="ctx.selectedLoaderVersion.value && !ctx.hideLoaderVersion.value"
4949
class="flex items-center justify-between"
5050
>
5151
<span class="text-secondary">Loader version</span>
@@ -94,27 +94,15 @@ import { computed } from 'vue'
9494
import Avatar from '../../../base/Avatar.vue'
9595
import Toggle from '../../../base/Toggle.vue'
9696
import { injectCreationFlowContext } from '../creation-flow-context'
97+
import { capitalize, formatLoaderLabel } from '../shared'
9798
9899
const ctx = injectCreationFlowContext()
99100
101+
const formatLoader = formatLoaderLabel
102+
100103
const selectedVersionLabel = computed(() => {
101104
const versionId = ctx.modpackSelection.value?.versionId
102105
if (!versionId) return null
103106
return ctx.modpackVersionOptions.value.find((o) => o.value === versionId)?.label ?? null
104107
})
105-
106-
const loaderDisplayNames: Record<string, string> = {
107-
fabric: 'Fabric',
108-
neoforge: 'NeoForge',
109-
forge: 'Forge',
110-
quilt: 'Quilt',
111-
paper: 'Paper',
112-
purpur: 'Purpur',
113-
vanilla: 'Vanilla',
114-
}
115-
116-
const formatLoader = (loader: string) =>
117-
loaderDisplayNames[loader] ?? loader.charAt(0).toUpperCase() + loader.slice(1)
118-
119-
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
120108
</script>

packages/ui/src/components/flows/creation-flow-modal/components/CustomSetupStage.vue

Lines changed: 122 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,56 @@
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
@@ -20,6 +59,7 @@
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"
@@ -29,12 +69,14 @@
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
@@ -55,30 +97,73 @@
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'
63135
import { computed, onMounted, ref, watch } from 'vue'
64136
65137
import { injectTags } from '../../../../providers'
138+
import Avatar from '../../../base/Avatar.vue'
139+
import ButtonStyled from '../../../base/ButtonStyled.vue'
66140
import Checkbox from '../../../base/Checkbox.vue'
67141
import Chips from '../../../base/Chips.vue'
68142
import Collapsible from '../../../base/Collapsible.vue'
69143
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
144+
import StyledInput from '../../../base/StyledInput.vue'
70145
import type { LoaderVersionType } from '../creation-flow-context'
71146
import { injectCreationFlowContext } from '../creation-flow-context'
147+
import { capitalize, formatLoaderLabel } from '../shared'
72148
73149
const ctx = injectCreationFlowContext()
74150
const {
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
83168
onMounted(() => {
84169
if (ctx.initialLoader && !selectedLoader.value) {
@@ -93,23 +178,35 @@ const tags = injectTags()
93178
94179
const 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
115212
const 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

packages/ui/src/components/flows/creation-flow-modal/components/FinalConfigStage.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import StyledInput from '../../../base/StyledInput.vue'
7777
import Toggle from '../../../base/Toggle.vue'
7878
import type { Difficulty, Gamemode, GeneratorSettingsMode } from '../creation-flow-context'
7979
import { injectCreationFlowContext } from '../creation-flow-context'
80+
import { capitalize } from '../shared'
8081
8182
const ctx = injectCreationFlowContext()
8283
const {
@@ -93,8 +94,6 @@ const {
9394
const gamemodeItems: Gamemode[] = ['survival', 'creative', 'hardcore']
9495
const difficultyItems: Difficulty[] = ['peaceful', 'easy', 'normal', 'hard']
9596
96-
const capitalize = (item: string) => item.charAt(0).toUpperCase() + item.slice(1)
97-
9897
const worldTypeOptions: ComboboxOption<string>[] = [
9998
{ value: 'minecraft:normal', label: 'Default' },
10099
{ value: 'minecraft:flat', label: 'Superflat' },

0 commit comments

Comments
 (0)