Improve accessibiltiy of env selector, improve mobile support, and message for those with no permission (#4304)

* Fix redirect from project ID

* improve fix

* improve accessibility of environment selector

* lint

* fix mobile accessibility of project settings and improve message for those without permission

* disable envs when multiple + lint
This commit is contained in:
Prospector
2025-08-31 10:23:21 -07:00
committed by GitHub
parent 28337c88f6
commit 8058993578
12 changed files with 163 additions and 96 deletions

View File

@@ -338,7 +338,7 @@ body {
--size-navbar-height: 3.5rem; --size-navbar-height: 3.5rem;
--size-mobile-navbar-height: 3.5rem; --size-mobile-navbar-height: 3.5rem;
--size-mobile-navbar-height-expanded: 13.75rem; --size-mobile-navbar-height-expanded: 26.5rem;
--spacing-card-lg: 1.5rem; --spacing-card-lg: 1.5rem;
--spacing-card-bg: 1rem; --spacing-card-bg: 1rem;
@@ -367,16 +367,8 @@ body {
--font-weight-heading: var(--font-weight-extrabold); --font-weight-heading: var(--font-weight-extrabold);
--font-weight-title: var(--font-weight-extrabold); --font-weight-title: var(--font-weight-extrabold);
@media screen and (min-width: 320px) { @media screen and (min-width: 354px) {
--size-mobile-navbar-height-expanded: 11.5rem; --size-mobile-navbar-height-expanded: 15.5rem;
}
@media screen and (min-width: 432px) {
--size-mobile-navbar-height-expanded: 9.25rem;
}
@media screen and (min-width: 765px) {
--size-mobile-navbar-height-expanded: 7rem;
} }
} }

View File

@@ -49,7 +49,7 @@
/ 100%; / 100%;
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
margin-top: var(--spacing-card-md); margin-top: 1.5rem;
} }
.normal-page__sidebar { .normal-page__sidebar {

View File

@@ -770,6 +770,15 @@
"project.download.title": { "project.download.title": {
"message": "Download {title}" "message": "Download {title}"
}, },
"project.environment.migration-no-permission.message": {
"message": "We've just overhauled the Environments system on Modrinth and new options are now available. You don't have permission to modify these settings, but please let another member of the project know that the environment metadata needs to be verified."
},
"project.environment.migration-no-permission.title": {
"message": "Environment metadata needs to be reviewed"
},
"project.environment.migration.learn-more": {
"message": "Learn more about this change"
},
"project.environment.migration.message": { "project.environment.migration.message": {
"message": "We've just overhauled the Environments system on Modrinth and new options are now available. Please visit your project's settings and verify that the metadata is correct." "message": "We've just overhauled the Environments system on Modrinth and new options are now available. Please visit your project's settings and verify that the metadata is correct."
}, },

View File

@@ -5,7 +5,7 @@
<div v-if="route.name.startsWith('type-id-settings')" class="normal-page no-sidebar"> <div v-if="route.name.startsWith('type-id-settings')" class="normal-page no-sidebar">
<div class="normal-page__header"> <div class="normal-page__header">
<div <div
class="mb-4 flex items-center gap-2 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold" class="mb-4 flex flex-wrap items-center gap-x-2 gap-y-3 border-0 border-b-[1px] border-solid border-divider pb-4 text-lg font-semibold"
> >
<nuxt-link <nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`" :to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
@@ -759,21 +759,31 @@
projectV3.environment[0] !== 'unknown' projectV3.environment[0] !== 'unknown'
" "
type="warning" type="warning"
:header="formatMessage(messages.environmentMigrationTitle)" :header="
formatMessage(
hasEditDetailsPermission
? messages.environmentMigrationTitle
: messages.environmentMigrationNoPermissionTitle,
)
"
class="mt-3" class="mt-3"
> >
{{ formatMessage(messages.environmentMigrationMessage) }} {{
<ButtonStyled color="orange"> formatMessage(
<nuxt-link hasEditDetailsPermission
v-tooltip=" ? messages.environmentMigrationMessage
hasEditDetailsPermission : messages.environmentMigrationNoPermissionMessage,
? undefined )
: formatMessage(commonProjectSettingsMessages.noPermissionDescription) }}
" <nuxt-link
:to="`/project/${project.id}/settings/environment`" to="/news/article/new-environments"
class="mt-3 w-fit" target="_blank"
:disabled="!hasEditDetailsPermission" class="mt-1 block w-fit font-semibold text-orange hover:underline"
> >
{{ formatMessage(messages.environmentMigrationLink) }}
</nuxt-link>
<ButtonStyled v-if="hasEditDetailsPermission" color="orange">
<nuxt-link :to="`/project/${project.id}/settings/environment`" class="mt-3 w-fit">
<SettingsIcon /> {{ formatMessage(messages.reviewEnvironmentSettings) }} <SettingsIcon /> {{ formatMessage(messages.reviewEnvironmentSettings) }}
</nuxt-link> </nuxt-link>
</ButtonStyled> </ButtonStyled>
@@ -966,7 +976,6 @@ import {
ButtonStyled, ButtonStyled,
Checkbox, Checkbox,
commonMessages, commonMessages,
commonProjectSettingsMessages,
injectNotificationManager, injectNotificationManager,
NewModal, NewModal,
OverflowMenu, OverflowMenu,
@@ -1152,6 +1161,19 @@ const messages = defineMessages({
id: 'project.environment.migration.title', id: 'project.environment.migration.title',
defaultMessage: 'Please review environment metadata', defaultMessage: 'Please review environment metadata',
}, },
environmentMigrationNoPermissionMessage: {
id: 'project.environment.migration-no-permission.message',
defaultMessage:
"We've just overhauled the Environments system on Modrinth and new options are now available. You don't have permission to modify these settings, but please let another member of the project know that the environment metadata needs to be verified.",
},
environmentMigrationNoPermissionTitle: {
id: 'project.environment.migration-no-permission.title',
defaultMessage: 'Environment metadata needs to be reviewed',
},
environmentMigrationLink: {
id: 'project.environment.migration.learn-more',
defaultMessage: 'Learn more about this change',
},
followersStat: { followersStat: {
id: 'project.stats.followers-label', id: 'project.stats.followers-label',
defaultMessage: 'follower{count, plural, one {} other {s}}', defaultMessage: 'follower{count, plural, one {} other {s}}',

View File

@@ -41,7 +41,7 @@ const dependencies = defineModel<any>('dependencies')
const organization = defineModel<any>('organization') const organization = defineModel<any>('organization')
</script> </script>
<template> <template>
<div class="experimental-styles-within grid grid-cols-[1fr_3fr] gap-4"> <div class="experimental-styles-within grid gap-4 lg:grid-cols-[1fr_3fr]">
<div> <div>
<aside class="universal-card"> <aside class="universal-card">
<NavStack> <NavStack>
@@ -140,7 +140,7 @@ const organization = defineModel<any>('organization')
</NavStack> </NavStack>
</aside> </aside>
</div> </div>
<div> <div class="min-w-0">
<NuxtPage <NuxtPage
v-model:project="project" v-model:project="project"
v-model:project-v3="projectV3" v-model:project-v3="projectV3"

View File

@@ -110,18 +110,6 @@ const messages = defineMessages({
</script> </script>
<template> <template>
<div> <div>
<UnsavedChangesPopup
v-if="supportsEnvironment && hasPermission"
:original="saved"
:modified="current"
:saving="saving"
:can-reset="!needsToVerify"
:text="needsToVerify ? messages.verifyLabel : undefined"
:save-label="needsToVerify ? messages.verifyButton : undefined"
:save-icon="needsToVerify ? CheckIcon : undefined"
@reset="reset"
@save="save"
/>
<div class="card experimental-styles-within"> <div class="card experimental-styles-within">
<h2 class="m-0 mb-2 block text-lg font-extrabold text-contrast"> <h2 class="m-0 mb-2 block text-lg font-extrabold text-contrast">
{{ formatMessage(commonProjectSettingsMessages.environment) }} {{ formatMessage(commonProjectSettingsMessages.environment) }}
@@ -166,8 +154,23 @@ const messages = defineMessages({
:body="formatMessage(messages.reviewOptionsDescription)" :body="formatMessage(messages.reviewOptionsDescription)"
class="mb-3" class="mb-3"
/> />
<ProjectSettingsEnvSelector v-model="current.environment" :disabled="!hasPermission" /> <ProjectSettingsEnvSelector
v-model="current.environment"
:disabled="!hasPermission || projectV3?.environment?.length > 1"
/>
</template> </template>
</div> </div>
<UnsavedChangesPopup
v-if="supportsEnvironment && hasPermission"
:original="saved"
:modified="current"
:saving="saving"
:can-reset="!needsToVerify"
:text="needsToVerify ? messages.verifyLabel : undefined"
:save-label="needsToVerify ? messages.verifyButton : undefined"
:save-icon="needsToVerify ? CheckIcon : undefined"
@reset="reset"
@save="save"
/>
</div> </div>
</template> </template>

View File

@@ -67,7 +67,9 @@
</label> </label>
<div class="text-input-wrapper"> <div class="text-input-wrapper">
<div class="text-input-wrapper__before"> <div class="text-input-wrapper__before">
https://modrinth.com/{{ $getProjectTypeForUrl(project.project_type, project.loaders) }}/ <span class="hidden sm:inline">https://modrinth.com</span>/{{
$getProjectTypeForUrl(project.project_type, project.loaders)
}}/
</div> </div>
<input <input
id="project-slug" id="project-slug"

View File

@@ -13,7 +13,7 @@
<div class="font-semibold flex justify-between gap-4"> <div class="font-semibold flex justify-between gap-4">
<slot name="header">{{ header }}</slot> <slot name="header">{{ header }}</slot>
</div> </div>
<div class="font-normal"> <div class="font-normal text-sm sm:text-base">
<slot>{{ body }}</slot> <slot>{{ body }}</slot>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,8 @@
<template> <template>
<button <button
role="radio"
:aria-checked="selected"
:aria-disabled="disabled"
class="px-4 py-3 text-left border-0 font-medium border-2 border-button-bg border-solid flex gap-2 transition-all cursor-pointer rounded-xl" class="px-4 py-3 text-left border-0 font-medium border-2 border-button-bg border-solid flex gap-2 transition-all cursor-pointer rounded-xl"
:class=" :class="
(selected ? 'text-contrast bg-button-bg' : 'text-primary bg-transparent') + (selected ? 'text-contrast bg-button-bg' : 'text-primary bg-transparent') +
@@ -10,8 +13,8 @@
:disabled="disabled" :disabled="disabled"
@click="emit('select')" @click="emit('select')"
> >
<RadioButtonCheckedIcon v-if="selected" class="text-brand h-5 w-5 shrink-0" /> <RadioButtonCheckedIcon v-if="selected" class="text-brand h-5 w-5 shrink-0" aria-hidden="true" />
<RadioButtonIcon v-else class="h-5 w-5 shrink-0" /> <RadioButtonIcon v-else class="h-5 w-5 shrink-0" aria-hidden="true" />
<slot /> <slot />
</button> </button>
</template> </template>

View File

@@ -50,20 +50,15 @@ const shown = computed(() => {
function localizeIfPossible(message: MessageDescriptor | string) { function localizeIfPossible(message: MessageDescriptor | string) {
return typeof message === 'string' ? message : formatMessage(message) return typeof message === 'string' ? message : formatMessage(message)
} }
const bodyText = computed(() => localizeIfPossible(props.text))
const saveText = computed(() =>
localizeIfPossible(props.saving ? props.savingLabel : props.saveLabel),
)
</script> </script>
<template> <template>
<Transition name="pop-in"> <Transition name="pop-in">
<div v-if="shown" class="fixed w-full z-10 left-0 bottom-0 p-4"> <div v-if="shown" class="fixed w-full z-10 left-0 p-4 unsaved-changes-popup">
<div <div
class="flex items-center rounded-2xl bg-bg-raised border-2 border-divider border-solid mx-auto max-w-[77rem] p-4" class="flex items-center gap-2 rounded-2xl bg-bg-raised border-2 border-divider border-solid mx-auto max-w-[77rem] p-4"
> >
<p class="m-0 font-semibold">{{ bodyText }}</p> <p class="m-0 font-semibold text-sm md:text-base">{{ localizeIfPossible(text) }}</p>
<div class="ml-auto flex gap-2"> <div class="ml-auto flex gap-2">
<ButtonStyled v-if="canReset" type="transparent"> <ButtonStyled v-if="canReset" type="transparent">
<button :disabled="saving" @click="(e) => emit('reset', e)"> <button :disabled="saving" @click="(e) => emit('reset', e)">
@@ -74,7 +69,7 @@ const saveText = computed(() =>
<button :disabled="saving" @click="(e) => emit('save', e)"> <button :disabled="saving" @click="(e) => emit('save', e)">
<SpinnerIcon v-if="saving" class="animate-spin" /> <SpinnerIcon v-if="saving" class="animate-spin" />
<component :is="saveIcon" v-else /> <component :is="saveIcon" v-else />
{{ saveText }} {{ localizeIfPossible(saving ? savingLabel : saveLabel) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
@@ -103,4 +98,19 @@ const saveText = computed(() =>
translate: 0 0.25rem; translate: 0 0.25rem;
opacity: 0; opacity: 0;
} }
.unsaved-changes-popup {
transition: bottom 0.25s ease-in-out;
bottom: 0;
}
@media (any-hover: none) and (max-width: 640px) {
.unsaved-changes-popup {
bottom: var(--size-mobile-navbar-height);
}
.expanded-mobile-nav .unsaved-changes-popup {
bottom: var(--size-mobile-navbar-height-expanded);
}
}
</style> </style>

View File

@@ -3,6 +3,7 @@ import type { EnvironmentV3 } from '@modrinth/utils'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl' import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { commonProjectSettingsMessages } from '../../../../utils'
import LargeRadioButton from '../../../base/LargeRadioButton.vue' import LargeRadioButton from '../../../base/LargeRadioButton.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -23,6 +24,15 @@ type EnvironmentRadioOption = {
description?: MessageDescriptor description?: MessageDescriptor
} }
const subOptionLabel = defineMessage({
id: 'project.settings.environment.suboption.accessibility-suboption-group-label',
defaultMessage: 'Suboptions of {option}',
})
const optionLabelFormat = defineMessage({
id: 'project.settings.environment.suboption.accessibility-option-label',
defaultMessage: '{title}: {description}',
})
const OUTER_OPTIONS = { const OUTER_OPTIONS = {
client: { client: {
title: defineMessage({ title: defineMessage({
@@ -225,58 +235,68 @@ const simulateSave = ref(false)
</script> </script>
<template> <template>
<template <div role="radiogroup" :aria-label="formatMessage(commonProjectSettingsMessages.environment)">
v-for="({ title, description, suboptions }, key, index) in OUTER_OPTIONS" <template
:key="`env-option-${key}`" v-for="({ title, description, suboptions }, key, index) in OUTER_OPTIONS"
> :key="`env-option-${key}`"
<LargeRadioButton
class="!w-full"
:class="{ 'mt-2': index > 0 }"
:selected="currentOuterOption === key"
:disabled="disabled"
@select="
() => {
if (currentOuterOption !== key) {
currentSubOption = suboptions ? (Object.keys(suboptions)[0] as SubOptionKey) : undefined
}
currentOuterOption = key
simulateSave = false
}
"
> >
<span class="flex flex-col">
<span>{{ formatMessage(title) }}</span>
<span v-if="description" class="text-sm text-secondary">{{
formatMessage(description)
}}</span>
</span>
</LargeRadioButton>
<div v-if="suboptions" class="pl-8">
<LargeRadioButton <LargeRadioButton
v-for="( class="!w-full"
{ title: suboptionTitle, description: suboptionDescription }, suboptionKey :class="{ 'mt-2': index > 0 }"
) in suboptions" :selected="currentOuterOption === key"
:key="`env-option-${key}-${suboptionKey}`"
class="!w-full mt-2"
:class="{
'opacity-50': currentOuterOption !== key,
}"
:selected="currentSubOption === suboptionKey"
:disabled="disabled" :disabled="disabled"
:aria-label="formatMessage(optionLabelFormat, { title: formatMessage(title), description: formatMessage(description)})"
@select=" @select="
() => { () => {
if (currentOuterOption !== key) {
currentSubOption = suboptions
? (Object.keys(suboptions)[0] as SubOptionKey)
: undefined
}
currentOuterOption = key currentOuterOption = key
currentSubOption = suboptionKey simulateSave = false
} }
" "
> >
<span class="flex flex-col"> <span class="flex flex-col">
<span>{{ formatMessage(suboptionTitle) }}</span> <span>{{ formatMessage(title) }}</span>
<span v-if="suboptionDescription" class="text-sm text-secondary">{{ <span v-if="description" class="text-sm text-secondary">{{
formatMessage(suboptionDescription) formatMessage(description)
}}</span> }}</span>
</span> </span>
</LargeRadioButton> </LargeRadioButton>
</div> <div
</template> v-if="suboptions"
class="pl-8"
role="radiogroup"
:aria-label="formatMessage(subOptionLabel, { option: formatMessage(title) })"
>
<LargeRadioButton
v-for="(
{ title: suboptionTitle, description: suboptionDescription }, suboptionKey
) in suboptions"
:key="`env-option-${key}-${suboptionKey}`"
class="!w-full mt-2"
:class="{
'opacity-50': currentOuterOption !== key,
}"
:selected="currentSubOption === suboptionKey"
:disabled="disabled"
@select="
() => {
currentOuterOption = key
currentSubOption = suboptionKey
}
"
>
<span class="flex flex-col">
<span>{{ formatMessage(suboptionTitle) }}</span>
<span v-if="suboptionDescription" class="text-sm text-secondary">{{
formatMessage(suboptionDescription)
}}</span>
</span>
</LargeRadioButton>
</div>
</template>
</div>
</template> </template>

View File

@@ -488,6 +488,12 @@
"project.settings.environment.singleplayer.title": { "project.settings.environment.singleplayer.title": {
"defaultMessage": "Singleplayer only" "defaultMessage": "Singleplayer only"
}, },
"project.settings.environment.suboption.accessibility-option-label": {
"defaultMessage": "{title}: {description}"
},
"project.settings.environment.suboption.accessibility-suboption-group-label": {
"defaultMessage": "Suboptions of {option}"
},
"project.settings.environment.title": { "project.settings.environment.title": {
"defaultMessage": "Environment" "defaultMessage": "Environment"
}, },