Skip to content

Commit ca042bc

Browse files
committed
qa: 1
1 parent fd3010d commit ca042bc

File tree

13 files changed

+318
-168
lines changed

13 files changed

+318
-168
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<button
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]"
4-
:class="selected ? 'border-brand bg-brand-highlight' : 'border-surface-5 bg-surface-4'"
3+
class="group flex w-full hover:cursor-pointer items-center gap-3 rounded-[20px] p-3 text-left transition-all hover:brightness-110 active:scale-[0.98] border-none"
4+
:class="selected ? 'bg-brand-highlight' : 'bg-surface-4'"
55
@click="$emit('click')"
66
>
77
<div
@@ -19,7 +19,9 @@
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 class="size-5 shrink-0 text-secondary" />
22+
<ChevronRightIcon
23+
class="size-5 shrink-0 text-secondary opacity-0 transition-opacity duration-100 group-hover:opacity-100"
24+
/>
2325
</button>
2426
</template>
2527

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

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
<template>
22
<div ref="containerRef" class="relative inline-block w-full">
3+
<!-- Search mode: input trigger -->
4+
<StyledInput
5+
v-if="searchMode"
6+
ref="searchModeTriggerRef"
7+
v-model="searchQuery"
8+
:icon="SearchIcon"
9+
type="text"
10+
:placeholder="searchPlaceholder || placeholder"
11+
:disabled="disabled"
12+
wrapper-class="w-full"
13+
:input-class="
14+
isOpen
15+
? shouldRoundBottomCorners
16+
? '!rounded-b-none'
17+
: '!rounded-t-none'
18+
: ''
19+
"
20+
class="z-[9999] relative"
21+
@input="handleSearchModeInput"
22+
@keydown="handleSearchKeydown"
23+
@focus="handleSearchModeFocus"
24+
/>
25+
26+
<!-- Standard mode: button trigger -->
327
<span
28+
v-else
429
ref="triggerRef"
530
role="button"
631
tabindex="0"
@@ -56,7 +81,7 @@
5681
@mousedown.stop
5782
@keydown="handleDropdownKeydown"
5883
>
59-
<div v-if="searchable" class="p-4">
84+
<div v-if="isSearchable && !searchMode" class="p-4">
6085
<StyledInput
6186
ref="searchInputRef"
6287
v-model="searchQuery"
@@ -70,7 +95,10 @@
7095
/>
7196
</div>
7297

73-
<div v-if="searchable && filteredOptions.length > 0" class="h-px bg-surface-5"></div>
98+
<div
99+
v-if="searchable && !searchMode && filteredOptions.length > 0"
100+
class="h-px bg-surface-5"
101+
></div>
74102

75103
<div
76104
v-if="filteredOptions.length > 0"
@@ -114,6 +142,8 @@
114142
<div v-else-if="searchQuery" class="p-4 mb-2 text-center text-sm text-secondary">
115143
{{ noOptionsMessage }}
116144
</div>
145+
146+
<slot name="dropdown-footer"></slot>
117147
</div>
118148
</Teleport>
119149
</div>
@@ -178,6 +208,7 @@ const props = withDefaults(
178208
forceDirection?: 'up' | 'down'
179209
noOptionsMessage?: string
180210
disableSearchFilter?: boolean
211+
searchMode?: boolean
181212
}>(),
182213
{
183214
placeholder: 'Select an option',
@@ -208,12 +239,20 @@ const searchQuery = ref('')
208239
const focusedIndex = ref(-1)
209240
const containerRef = ref<HTMLElement>()
210241
const triggerRef = ref<HTMLElement>()
242+
const searchModeTriggerRef = ref<InstanceType<typeof StyledInput>>()
211243
const dropdownRef = ref<HTMLElement>()
212244
const searchInputRef = ref<HTMLInputElement>()
213245
const optionsContainerRef = ref<HTMLElement>()
214246
const optionRefs = ref<(HTMLElement | null)[]>([])
215247
const rafId = ref<number | null>(null)
216248
249+
const effectiveTriggerEl = computed(() => {
250+
if (props.searchMode && searchModeTriggerRef.value) {
251+
return (searchModeTriggerRef.value as unknown as { $el: HTMLElement }).$el as HTMLElement
252+
}
253+
return triggerRef.value
254+
})
255+
217256
const dropdownStyle = ref({
218257
top: '0px',
219258
left: '0px',
@@ -253,8 +292,10 @@ const optionsWithKeys = computed(() => {
253292
}))
254293
})
255294
295+
const isSearchable = computed(() => props.searchable || props.searchMode)
296+
256297
const filteredOptions = computed(() => {
257-
if (!searchQuery.value || !props.searchable || props.disableSearchFilter) {
298+
if (!searchQuery.value || !isSearchable.value || props.disableSearchFilter) {
258299
return optionsWithKeys.value
259300
}
260301
@@ -342,11 +383,11 @@ function calculateHorizontalPosition(
342383
}
343384
344385
async function updateDropdownPosition() {
345-
if (!triggerRef.value || !dropdownRef.value) return
386+
if (!effectiveTriggerEl.value || !dropdownRef.value) return
346387
347388
await nextTick()
348389
349-
const triggerRect = triggerRef.value.getBoundingClientRect()
390+
const triggerRect = effectiveTriggerEl.value.getBoundingClientRect()
350391
const dropdownRect = dropdownRef.value.getBoundingClientRect()
351392
const viewportHeight = window.innerHeight
352393
const viewportWidth = window.innerWidth
@@ -368,15 +409,19 @@ async function openDropdown() {
368409
if (props.disabled || isOpen.value) return
369410
370411
isOpen.value = true
371-
searchQuery.value = ''
412+
if (!props.searchMode) {
413+
searchQuery.value = ''
414+
}
372415
373416
emit('open')
374417
375418
await nextTick()
376419
await updateDropdownPosition()
377420
378421
setInitialFocus()
379-
focusSearchInput()
422+
if (!props.searchMode) {
423+
focusSearchInput()
424+
}
380425
startPositionTracking()
381426
}
382427
@@ -385,13 +430,17 @@ function closeDropdown() {
385430
386431
stopPositionTracking()
387432
isOpen.value = false
388-
searchQuery.value = ''
433+
if (!props.searchMode) {
434+
searchQuery.value = ''
435+
}
389436
focusedIndex.value = -1
390437
emit('close')
391438
392-
nextTick(() => {
393-
triggerRef.value?.focus()
394-
})
439+
if (!props.searchMode) {
440+
nextTick(() => {
441+
triggerRef.value?.focus()
442+
})
443+
}
395444
}
396445
397446
function handleTriggerClick() {
@@ -418,6 +467,9 @@ function handleOptionClick(option: ComboboxOption<T>, index: number) {
418467
emit('select', option)
419468
420469
if (option.type !== 'link') {
470+
if (props.searchMode) {
471+
searchQuery.value = ''
472+
}
421473
closeDropdown()
422474
}
423475
}
@@ -442,7 +494,9 @@ function focusOption(index: number) {
442494
if (isDivider(option) || option.disabled) return
443495
444496
focusedIndex.value = index
445-
optionRefs.value[index]?.focus()
497+
if (!props.searchMode) {
498+
optionRefs.value[index]?.focus()
499+
}
446500
optionRefs.value[index]?.scrollIntoView({ block: 'nearest' })
447501
}
448502
@@ -512,10 +566,43 @@ function handleSearchKeydown(event: KeyboardEvent) {
512566
closeDropdown()
513567
} else if (event.key === 'ArrowDown') {
514568
event.preventDefault()
569+
if (props.searchMode && !isOpen.value && searchQuery.value.trim()) {
570+
openDropdown()
571+
}
515572
focusNextOption()
516573
} else if (event.key === 'ArrowUp') {
517574
event.preventDefault()
518575
focusPreviousOption()
576+
} else if (event.key === 'Enter') {
577+
event.preventDefault()
578+
if (focusedIndex.value >= 0) {
579+
const option = filteredOptions.value[focusedIndex.value]
580+
if (option && !isDivider(option)) {
581+
handleOptionClick(option, focusedIndex.value)
582+
}
583+
}
584+
} else if (event.key === 'Tab' && props.searchMode && isOpen.value) {
585+
event.preventDefault()
586+
if (event.shiftKey) {
587+
focusPreviousOption()
588+
} else {
589+
focusNextOption()
590+
}
591+
}
592+
}
593+
594+
function handleSearchModeInput() {
595+
emit('searchInput', searchQuery.value)
596+
if (searchQuery.value.trim() && !isOpen.value) {
597+
openDropdown()
598+
} else if (!searchQuery.value.trim() && isOpen.value) {
599+
closeDropdown()
600+
}
601+
}
602+
603+
function handleSearchModeFocus() {
604+
if (searchQuery.value.trim() && !isOpen.value) {
605+
openDropdown()
519606
}
520607
}
521608
@@ -545,7 +632,7 @@ onClickOutside(
545632
() => {
546633
closeDropdown()
547634
},
548-
{ ignore: [triggerRef] },
635+
{ ignore: [triggerRef, containerRef] },
549636
)
550637
551638
onMounted(() => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
</template>
5959

6060
<progress
61-
v-if="nonProgressStage !== true"
61+
v-if="nonProgressStage !== true && !disableProgress"
6262
:value="progressValue"
6363
max="100"
6464
class="w-full h-1 appearance-none border-none absolute top-0 left-0"
@@ -145,6 +145,7 @@ const props = defineProps<{
145145
context: T
146146
breadcrumbs?: boolean
147147
fitContent?: boolean
148+
disableProgress?: boolean
148149
}>()
149150
150151
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')

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

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@
2727

2828
<!-- Loader chips -->
2929
<div v-if="!hideLoaderChips" class="flex flex-col gap-2">
30-
<span class="font-semibold text-contrast"
31-
>{{ ctx.flowType === 'instance' ? 'Loader' : 'Content loader'
32-
}}<span class="text-red"> *</span></span
33-
>
30+
<span class="font-semibold text-contrast">{{
31+
ctx.flowType === 'instance' ? 'Loader' : 'Content loader'
32+
}}</span>
3433
<Chips
3534
v-model="selectedLoader"
3635
:items="effectiveLoaders"
@@ -41,21 +40,27 @@
4140

4241
<!-- Game version -->
4342
<div class="flex flex-col gap-2">
44-
<span class="font-semibold text-contrast">Game version<span class="text-red"> *</span></span>
43+
<span class="font-semibold text-contrast">Game version</span>
4544
<Combobox
4645
v-model="selectedGameVersion"
4746
:options="gameVersionOptions"
4847
searchable
4948
placeholder="Select game version"
50-
/>
51-
<span class="text-sm text-secondary">It is recommended to use the latest version.</span>
52-
<Checkbox
53-
v-if="ctx.showSnapshotToggle"
54-
:model-value="ctx.showSnapshots.value"
55-
label="Show snapshots"
56-
class="text-sm"
57-
@update:model-value="ctx.showSnapshots.value = $event"
58-
/>
49+
force-direction="down"
50+
:max-height="150"
51+
>
52+
<template v-if="ctx.showSnapshotToggle" #dropdown-footer>
53+
<button
54+
class="flex w-full cursor-pointer items-center justify-center gap-1.5 border-0 border-t border-solid border-surface-5 bg-transparent py-3 text-center text-sm font-semibold text-secondary transition-colors hover:text-contrast"
55+
@mousedown.prevent
56+
@click="ctx.showSnapshots.value = !ctx.showSnapshots.value"
57+
>
58+
<EyeOffIcon v-if="ctx.showSnapshots.value" class="size-4" />
59+
<EyeIcon v-else class="size-4" />
60+
{{ ctx.showSnapshots.value ? 'Hide snapshots' : 'Show all versions' }}
61+
</button>
62+
</template>
63+
</Combobox>
5964
</div>
6065

6166
<!-- Loader version (instance flow: flat layout, other flows: collapsible) -->
@@ -66,10 +71,9 @@
6671
v-show="selectedLoader && selectedGameVersion"
6772
class="flex flex-col gap-2"
6873
>
69-
<span class="font-semibold text-contrast"
70-
>{{ isPaperLike ? 'Build number' : 'Loader version'
71-
}}<span class="text-red"> *</span></span
72-
>
74+
<span class="font-semibold text-contrast">{{
75+
isPaperLike ? 'Build number' : 'Loader version'
76+
}}</span>
7377
<Chips
7478
v-if="!isPaperLike"
7579
v-model="loaderVersionType"
@@ -90,10 +94,9 @@
9094
<!-- Other flows: collapsible wrapper -->
9195
<Collapsible v-else :collapsed="!selectedLoader || !selectedGameVersion" overflow-visible>
9296
<div class="flex flex-col gap-2">
93-
<span class="font-semibold text-contrast"
94-
>{{ isPaperLike ? 'Build number' : 'Loader version'
95-
}}<span class="text-red"> *</span></span
96-
>
97+
<span class="font-semibold text-contrast">{{
98+
isPaperLike ? 'Build number' : 'Loader version'
99+
}}</span>
97100
<Chips
98101
v-if="!isPaperLike"
99102
v-model="loaderVersionType"
@@ -116,13 +119,12 @@
116119
</template>
117120

118121
<script setup lang="ts">
119-
import { UploadIcon, XIcon } from '@modrinth/assets'
122+
import { EyeIcon, EyeOffIcon, UploadIcon, XIcon } from '@modrinth/assets'
120123
import { computed, onMounted, ref, watch } from 'vue'
121124
122125
import { injectFilePicker, injectTags } from '../../../../providers'
123126
import Avatar from '../../../base/Avatar.vue'
124127
import ButtonStyled from '../../../base/ButtonStyled.vue'
125-
import Checkbox from '../../../base/Checkbox.vue'
126128
import Chips from '../../../base/Chips.vue'
127129
import Collapsible from '../../../base/Collapsible.vue'
128130
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
@@ -151,8 +153,12 @@ const effectiveLoaders = computed(() => {
151153
152154
// Pre-select loader and game version from initial values
153155
onMounted(() => {
154-
if (ctx.initialLoader && !selectedLoader.value) {
155-
selectedLoader.value = ctx.initialLoader
156+
if (!selectedLoader.value) {
157+
if (ctx.initialLoader) {
158+
selectedLoader.value = ctx.initialLoader
159+
} else if (ctx.flowType === 'instance') {
160+
selectedLoader.value = 'fabric'
161+
}
156162
}
157163
if (ctx.initialGameVersion && !selectedGameVersion.value) {
158164
selectedGameVersion.value = ctx.initialGameVersion

0 commit comments

Comments
 (0)