1
0

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-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-bg: 1rem;
@@ -367,16 +367,8 @@ body {
--font-weight-heading: var(--font-weight-extrabold);
--font-weight-title: var(--font-weight-extrabold);
@media screen and (min-width: 320px) {
--size-mobile-navbar-height-expanded: 11.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;
@media screen and (min-width: 354px) {
--size-mobile-navbar-height-expanded: 15.5rem;
}
}

View File

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

View File

@@ -770,6 +770,15 @@
"project.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": {
"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 class="normal-page__header">
<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
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
@@ -759,21 +759,31 @@
projectV3.environment[0] !== 'unknown'
"
type="warning"
:header="formatMessage(messages.environmentMigrationTitle)"
:header="
formatMessage(
hasEditDetailsPermission
? messages.environmentMigrationTitle
: messages.environmentMigrationNoPermissionTitle,
)
"
class="mt-3"
>
{{ formatMessage(messages.environmentMigrationMessage) }}
<ButtonStyled color="orange">
<nuxt-link
v-tooltip="
hasEditDetailsPermission
? undefined
: formatMessage(commonProjectSettingsMessages.noPermissionDescription)
"
:to="`/project/${project.id}/settings/environment`"
class="mt-3 w-fit"
:disabled="!hasEditDetailsPermission"
>
{{
formatMessage(
hasEditDetailsPermission
? messages.environmentMigrationMessage
: messages.environmentMigrationNoPermissionMessage,
)
}}
<nuxt-link
to="/news/article/new-environments"
target="_blank"
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) }}
</nuxt-link>
</ButtonStyled>
@@ -966,7 +976,6 @@ import {
ButtonStyled,
Checkbox,
commonMessages,
commonProjectSettingsMessages,
injectNotificationManager,
NewModal,
OverflowMenu,
@@ -1152,6 +1161,19 @@ const messages = defineMessages({
id: 'project.environment.migration.title',
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: {
id: 'project.stats.followers-label',
defaultMessage: 'follower{count, plural, one {} other {s}}',

View File

@@ -41,7 +41,7 @@ const dependencies = defineModel<any>('dependencies')
const organization = defineModel<any>('organization')
</script>
<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>
<aside class="universal-card">
<NavStack>
@@ -140,7 +140,7 @@ const organization = defineModel<any>('organization')
</NavStack>
</aside>
</div>
<div>
<div class="min-w-0">
<NuxtPage
v-model:project="project"
v-model:project-v3="projectV3"

View File

@@ -110,18 +110,6 @@ const messages = defineMessages({
</script>
<template>
<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">
<h2 class="m-0 mb-2 block text-lg font-extrabold text-contrast">
{{ formatMessage(commonProjectSettingsMessages.environment) }}
@@ -166,8 +154,23 @@ const messages = defineMessages({
:body="formatMessage(messages.reviewOptionsDescription)"
class="mb-3"
/>
<ProjectSettingsEnvSelector v-model="current.environment" :disabled="!hasPermission" />
<ProjectSettingsEnvSelector
v-model="current.environment"
:disabled="!hasPermission || projectV3?.environment?.length > 1"
/>
</template>
</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>
</template>

View File

@@ -67,7 +67,9 @@
</label>
<div class="text-input-wrapper">
<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>
<input
id="project-slug"

View File

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

View File

@@ -1,5 +1,8 @@
<template>
<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="
(selected ? 'text-contrast bg-button-bg' : 'text-primary bg-transparent') +
@@ -10,8 +13,8 @@
:disabled="disabled"
@click="emit('select')"
>
<RadioButtonCheckedIcon v-if="selected" class="text-brand h-5 w-5 shrink-0" />
<RadioButtonIcon v-else class="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" aria-hidden="true" />
<slot />
</button>
</template>

View File

@@ -50,20 +50,15 @@ const shown = computed(() => {
function localizeIfPossible(message: MessageDescriptor | string) {
return typeof message === 'string' ? message : formatMessage(message)
}
const bodyText = computed(() => localizeIfPossible(props.text))
const saveText = computed(() =>
localizeIfPossible(props.saving ? props.savingLabel : props.saveLabel),
)
</script>
<template>
<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
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">
<ButtonStyled v-if="canReset" type="transparent">
<button :disabled="saving" @click="(e) => emit('reset', e)">
@@ -74,7 +69,7 @@ const saveText = computed(() =>
<button :disabled="saving" @click="(e) => emit('save', e)">
<SpinnerIcon v-if="saving" class="animate-spin" />
<component :is="saveIcon" v-else />
{{ saveText }}
{{ localizeIfPossible(saving ? savingLabel : saveLabel) }}
</button>
</ButtonStyled>
</div>
@@ -103,4 +98,19 @@ const saveText = computed(() =>
translate: 0 0.25rem;
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>

View File

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

View File

@@ -488,6 +488,12 @@
"project.settings.environment.singleplayer.title": {
"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": {
"defaultMessage": "Environment"
},