Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/assets/generated-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type IconComponent = FunctionalComponent<SVGAttributes>

import _AffiliateIcon from './icons/affiliate.svg?component'
import _AlignLeftIcon from './icons/align-left.svg?component'
import _AnvilIcon from './icons/anvil.svg?component'
import _ArchiveIcon from './icons/archive.svg?component'
import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component'
import _ArrowBigUpIcon from './icons/arrow-big-up.svg?component'
Expand Down Expand Up @@ -479,6 +480,8 @@ import _UserXIcon from './icons/user-x.svg?component'
import _UsersIcon from './icons/users.svg?component'
import _VersionIcon from './icons/version.svg?component'
import _VideoIcon from './icons/video.svg?component'
import _Volume2Icon from './icons/volume-2.svg?component'
import _VolumeXIcon from './icons/volume-x.svg?component'
import _WikiIcon from './icons/wiki.svg?component'
import _WindowIcon from './icons/window.svg?component'
import _WorldIcon from './icons/world.svg?component'
Expand All @@ -491,6 +494,7 @@ import _ZoomOutIcon from './icons/zoom-out.svg?component'

export const AffiliateIcon = _AffiliateIcon
export const AlignLeftIcon = _AlignLeftIcon
export const AnvilIcon = _AnvilIcon
export const ArchiveIcon = _ArchiveIcon
export const ArrowBigRightDashIcon = _ArrowBigRightDashIcon
export const ArrowBigUpIcon = _ArrowBigUpIcon
Expand Down Expand Up @@ -963,6 +967,8 @@ export const UserXIcon = _UserXIcon
export const UsersIcon = _UsersIcon
export const VersionIcon = _VersionIcon
export const VideoIcon = _VideoIcon
export const Volume2Icon = _Volume2Icon
export const VolumeXIcon = _VolumeXIcon
export const WikiIcon = _WikiIcon
export const WindowIcon = _WindowIcon
export const WorldIcon = _WorldIcon
Expand Down
19 changes: 19 additions & 0 deletions packages/assets/icons/anvil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions packages/assets/icons/volume-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions packages/assets/icons/volume-x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions packages/ui/src/components/base/IntlFormatted.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ const formattedParts = computed(() => {

for (const slotName of slotNames) {
const normalizedName = slotName.startsWith('~') ? slotName.slice(1) : slotName
slotHandlers[normalizedName] = (chunks) => {
let handlerName = `intlSlot${Object.keys(slotHandlers).length}`
while (Object.prototype.hasOwnProperty.call(props.values ?? {}, handlerName)) handlerName += '_'
slotHandlers[handlerName] = (chunks) => {
const slot = slots[slotName]
if (slot) {
const nodes = slot({
Expand All @@ -69,10 +71,12 @@ const formattedParts = computed(() => {
return markRaw(chunks) as VNode[]
}

msg = msg.replace(
new RegExp(`\\{${normalizedName}\\}`, 'g'),
`<${normalizedName}></${normalizedName}>`,
)
msg = msg
.replaceAll(`<${normalizedName}>`, `<${handlerName}>`)
.replaceAll(`</${normalizedName}>`, `</${handlerName}>`)
if (!Object.prototype.hasOwnProperty.call(props.values ?? {}, normalizedName)) {
msg = msg.replaceAll(`{${normalizedName}}`, `<${handlerName}></${handlerName}>`)
}
}

try {
Expand Down
89 changes: 79 additions & 10 deletions packages/ui/src/components/base/MultiStageModal.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<template>
<NewModal
ref="modal"
:header="resolvedTitle"
:scrollable="true"
max-content-height="72vh"
:on-hide="onModalHide"
:on-after-hide="onModalAfterHide"
:on-after-show="onModalAfterShow"
:before-hide="handleBeforeHide"
:closable="true"
:close-on-click-outside="closeOnClickOutside"
:width="resolvedMaxWidth"
:fade="fade"
:disable-close="resolveCtxFn(currentStage.disableClose, context)"
:hide-header="resolveCtxFn(currentStage.hideHeader, context)"
:merge-header="resolveCtxFn(currentStage.mergeHeader, context)"
:disable-close="disableClose || resolveCtxFn(currentStage.disableClose, context)"
>
<template #title>
<div
Expand Down Expand Up @@ -65,12 +71,13 @@
class="w-full h-1 appearance-none border-none absolute top-0 left-0"
></progress>

<component :is="currentStage?.stageContent" />
<component :is="currentStage?.stageContent" :key="currentStage?.id" />

<template #actions>
<div
:key="currentStage?.id"
class="flex flex-col justify-end gap-2 sm:flex-row"
:class="leftButtonConfig || rightButtonConfig ? 'mt-4' : ''"
:class="leftButtonConfig || rightButtonConfig ? actionsTopMarginClass : ''"
>
<Button
v-if="leftButtonConfig"
Expand All @@ -92,6 +99,7 @@
:color="rightButtonConfig.color === 'standard' ? undefined : rightButtonConfig.color"
:class="rightButtonConfig.buttonClass"
:disabled="rightButtonConfig.disabled || rightButtonConfig.loading"
:aria-disabled="rightButtonConfig.ariaDisabled || undefined"
@click="rightButtonConfig.onClick"
>
<SpinnerIcon
Expand Down Expand Up @@ -122,7 +130,7 @@
<script lang="ts">
import { ChevronRightIcon, SpinnerIcon } from '@modrinth/assets'
import type { Component } from 'vue'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { computed, nextTick, ref, shallowRef, useTemplateRef, watch } from 'vue'

import type { ButtonColor } from '#ui/components/base/buttons'
import { Button } from '#ui/components/base/buttons'
Expand All @@ -135,6 +143,7 @@ export interface StageButtonConfig {
iconPosition?: 'before' | 'after'
color?: ButtonColor | 'standard'
disabled?: boolean
ariaDisabled?: boolean
loading?: boolean
tooltip?: string
iconClass?: string | null
Expand All @@ -154,10 +163,14 @@ export interface StageConfigInput<T> {
nonProgressStage?: MaybeCtxFn<T, boolean>
cannotNavigateForward?: MaybeCtxFn<T, boolean>
disableClose?: MaybeCtxFn<T, boolean>
hideHeader?: MaybeCtxFn<T, boolean>
mergeHeader?: MaybeCtxFn<T, boolean>
beforeHide?: MaybeCtxFn<T, boolean>
leftButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
rightButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
/** Max width for the modal content and header defined in px (e.g., '460px', '600px'). Defaults to '460px'. */
maxWidth?: MaybeCtxFn<T, string>
actionsTopMargin?: MaybeCtxFn<T, 'sm' | 'md'>
}

export function resolveCtxFn<T, R>(value: MaybeCtxFn<T, R>, ctx: T): R {
Expand All @@ -175,14 +188,24 @@ const props = withDefaults(
fade?: 'standard' | 'warning' | 'danger'
disableProgress?: boolean
closeOnClickOutside?: boolean
animateStages?: boolean
disableClose?: boolean
}>(),
{
closeOnClickOutside: true,
animateStages: false,
disableClose: false,
},
)

const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
const currentStageIndex = ref<number>(0)
const displayedStageIndex = ref(0)
const frozenButtons = shallowRef<{
left: StageButtonConfig | null
right: StageButtonConfig | null
} | null>(null)
let transitioning = false

function show() {
modal.value?.show()
Expand Down Expand Up @@ -238,7 +261,16 @@ const prevStage = () => {
}
}

const currentStage = computed(() => props.stages[currentStageIndex.value])
const currentStage = computed(() => props.stages[displayedStageIndex.value])
const actionsTopMarginClass = computed(() => {
const margin = currentStage.value?.actionsTopMargin
return margin && resolveCtxFn(margin, props.context) === 'sm' ? 'mt-2' : 'mt-4'
})

function handleBeforeHide(): boolean {
const stage = currentStage.value
return stage?.beforeHide ? resolveCtxFn(stage.beforeHide, props.context) : true
}

const resolvedTitle = computed(() => {
const stage = currentStage.value
Expand All @@ -247,12 +279,14 @@ const resolvedTitle = computed(() => {
})

const leftButtonConfig = computed(() => {
if (frozenButtons.value) return frozenButtons.value.left
const stage = currentStage.value
if (!stage) return null
return resolveCtxFn(stage.leftButtonConfig, props.context)
})

const rightButtonConfig = computed(() => {
if (frozenButtons.value) return frozenButtons.value.right
const stage = currentStage.value
if (!stage) return null
return resolveCtxFn(stage.rightButtonConfig, props.context)
Expand All @@ -278,7 +312,7 @@ const progressValue = computed(() => {
}

const completedCount = props.stages
.slice(0, currentStageIndex.value + 1)
.slice(0, displayedStageIndex.value + 1)
.filter(isProgressStage).length
const totalCount = props.stages.filter(isProgressStage).length

Expand Down Expand Up @@ -374,18 +408,53 @@ watch([breadcrumbStages, currentStageIndex], () => nextTick(() => updateScrollSh
immediate: true,
})

watch(currentStageIndex, () => {
scrollToCurrentBreadcrumb()
})
watch(
currentStageIndex,
async () => {
if (transitioning) return
transitioning = true
try {
do {
const nextIndex = currentStageIndex.value
frozenButtons.value = {
left: leftButtonConfig.value,
right: rightButtonConfig.value,
}
const displayStage = () => {
displayedStageIndex.value = nextIndex
frozenButtons.value = null
}
if (props.animateStages && modal.value) {
await modal.value.transitionContent(displayStage)
} else {
displayStage()
}
} while (displayedStageIndex.value !== currentStageIndex.value)
} finally {
frozenButtons.value = null
transitioning = false
}
scrollToCurrentBreadcrumb()
},
{ flush: 'sync' },
)

const emit = defineEmits<{
(e: 'refresh-data' | 'hide'): void
(e: 'refresh-data' | 'hide' | 'after-hide' | 'after-show'): void
}>()

function onModalHide() {
emit('hide')
}

function onModalAfterHide() {
emit('after-hide')
}

function onModalAfterShow() {
emit('after-show')
}

defineExpose({
show,
hide,
Expand Down
13 changes: 9 additions & 4 deletions packages/ui/src/components/base/NavTabs.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div
v-if="filteredLinks.length > 1"
v-if="filteredLinks.length > (showSingleTab ? 0 : 1)"
:class="pageNav ? '-mx-6 -mt-2 mb-1 overflow-x-auto px-6 py-2' : 'contents'"
v-bind="pageNav ? $attrs : {}"
>
Expand Down Expand Up @@ -100,12 +100,15 @@ const props = withDefaults(
mode?: 'navigation' | 'local'
activeIndex?: number
pageNav?: boolean
/** Keeps the tabs rendered when only one is shown, so the layout doesn't shift as tabs are filtered. */
showSingleTab?: boolean
}>(),
{
mode: 'navigation',
query: undefined,
activeIndex: undefined,
pageNav: false,
showSingleTab: false,
},
)

Expand Down Expand Up @@ -182,6 +185,8 @@ function computeActiveIndex(): { index: number; isSubpage: boolean } {
}
}

let subpageIndex = -1

for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
const decodedPath = decodeURIComponent(route.path)
Expand All @@ -204,12 +209,12 @@ function computeActiveIndex(): { index: number; isSubpage: boolean } {
(decodedPath.length === decodedHref.length || decodedPath[decodedHref.length] === '/')) ||
link.subpages?.some((subpage) => decodedPath.includes(subpage))

if (isSubpageMatch) {
return { index: i, isSubpage: true }
if (isSubpageMatch && subpageIndex === -1) {
subpageIndex = i
}
}

return { index: -1, isSubpage: false }
return { index: subpageIndex, isSubpage: subpageIndex !== -1 }
}

function getTabElement(index: number): HTMLElement | null {
Expand Down
Loading
Loading