Fix a number of light mode issues and get rid of scrollbar jumping on menus (#4760)

* Fix DEV-466, Fixes #4692 as well as a bunch of other poor contrast and inconsistency issues in light mode. Adds shadows to buttons and makes scrollbar gutter stable.

* lintttt & only do scrollbar gutter on website

* try to fix following hydration issue

* try another clientonly approach

* fix home page link animation

* lint

* remove dropdown style from checkbox & improve shadow consistency

* liiiint
This commit is contained in:
Prospector
2025-11-13 15:21:43 -08:00
committed by GitHub
parent c27f787c91
commit 94c0003c19
40 changed files with 384 additions and 693 deletions

View File

@@ -765,7 +765,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
> >
<LibraryIcon /> <LibraryIcon />
</NavButton> </NavButton>
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div> <div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
<suspense> <suspense>
<QuickInstanceSwitcher /> <QuickInstanceSwitcher />
</suspense> </suspense>

View File

@@ -284,7 +284,7 @@ onUnmounted(() => {
z-index: 11; z-index: 11;
gap: 0.5rem; gap: 0.5rem;
padding: 1rem; padding: 1rem;
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
width: max-content; width: max-content;
user-select: none; user-select: none;
-ms-user-select: none; -ms-user-select: none;
@@ -380,7 +380,7 @@ onUnmounted(() => {
text-align: left; text-align: left;
&.expanded { &.expanded {
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
padding: 1rem; padding: 1rem;
} }
} }

View File

@@ -119,7 +119,7 @@ onBeforeUnmount(() => {
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-floating); box-shadow: var(--shadow-floating);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
margin: 0; margin: 0;
position: fixed; position: fixed;
z-index: 1000000; z-index: 1000000;
@@ -163,7 +163,7 @@ onBeforeUnmount(() => {
} }
.divider { .divider {
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
margin: var(--gap-sm); margin: var(--gap-sm);
pointer-events: none; pointer-events: none;
} }

View File

@@ -34,7 +34,7 @@
</div> </div>
<div class="input-row"> <div class="input-row">
<p class="input-label">Game version</p> <p class="input-label">Game version</p>
<div class="versions"> <div class="flex gap-4 items-center">
<multiselect <multiselect
v-model="game_version" v-model="game_version"
class="selector" class="selector"
@@ -45,7 +45,7 @@
open-direction="top" open-direction="top"
:show-labels="false" :show-labels="false"
/> />
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Show all versions" /> <Checkbox v-model="showSnapshots" class="shrink-0" label="Show all versions" />
</div> </div>
</div> </div>
<div v-if="loader !== 'vanilla'" class="input-row"> <div v-if="loader !== 'vanilla'" class="input-row">
@@ -546,12 +546,6 @@ const next = async () => {
font-style: italic; font-style: italic;
} }
.versions {
display: flex;
flex-direction: row;
gap: 1rem;
}
:deep(button.checkbox) { :deep(button.checkbox) {
border: none; border: none;
} }

View File

@@ -69,7 +69,7 @@ onUnmounted(() => {
<SpinnerIcon class="animate-spin w-4 h-4" /> <SpinnerIcon class="animate-spin w-4 h-4" />
</div> </div>
</NavButton> </NavButton>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div> <div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -293,7 +293,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
padding: var(--gap-sm) var(--gap-lg); padding: var(--gap-sm) var(--gap-lg);
} }
@@ -356,7 +356,7 @@ onBeforeUnmount(() => {
gap: 1rem; gap: 1rem;
overflow: auto; overflow: auto;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
&.hidden { &.hidden {
transform: translateY(-100%); transform: translateY(-100%);
@@ -454,7 +454,7 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
overflow: auto; overflow: auto;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
padding: var(--gap-md); padding: var(--gap-md);
&.hidden { &.hidden {

View File

@@ -130,7 +130,7 @@ onUnmounted(() => {
/> />
</template> </template>
<div <div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover" class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow rounded-xl smart-clickable:highlight-on-hover"
> >
<Avatar <Avatar
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined" :src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"

View File

@@ -181,7 +181,7 @@ const messages = defineMessages({
/> />
</template> </template>
<div <div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl" class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow smart-clickable:highlight-on-hover rounded-xl"
:class="{ :class="{
'world-item-highlighted': highlighted, 'world-item-highlighted': highlighted,
}" }"

View File

@@ -427,7 +427,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]"> <div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
<div <div
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto" class="bg-bg-raised card-shadow rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
> >
<img <img
:src="ExcitedRinthbot" :src="ExcitedRinthbot"

View File

@@ -1290,3 +1290,7 @@ svg.inline-svg {
} }
} }
} }
.card-shadow {
box-shadow: var(--shadow-card);
}

View File

@@ -37,6 +37,7 @@ html {
--icon-32: 2rem; --icon-32: 2rem;
interpolate-size: allow-keywords; interpolate-size: allow-keywords;
scrollbar-gutter: stable;
} }
.light-mode { .light-mode {
@@ -89,7 +90,7 @@ html {
--color-hr: var(--color-text); --color-hr: var(--color-text);
--color-table-border: #dfe2e5; --color-table-border: #dfe2e5;
--color-table-alternate-row: #f2f4f7; --color-table-alternate-row: #f0f1f2;
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp'); --landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp'); --landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');

View File

@@ -1,151 +0,0 @@
<template>
<div
class="checkbox-outer button-within"
:class="{ disabled, checked: modelValue }"
role="presentation"
@click="toggle"
>
<button
class="checkbox"
role="checkbox"
:disabled="disabled"
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }"
:aria-label="description ?? label"
:aria-checked="modelValue"
>
<CheckIcon v-if="modelValue && !collapsingToggleStyle" aria-hidden="true" />
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
</button>
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
<p v-if="label" aria-hidden="true">
{{ label }}
</p>
<slot v-else />
</div>
</template>
<script>
import { CheckIcon, DropdownIcon } from '@modrinth/assets'
export default {
components: {
CheckIcon,
DropdownIcon,
},
props: {
label: {
type: String,
required: false,
default: '',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
description: {
type: String,
required: false,
default: null,
},
modelValue: Boolean,
clickEvent: {
type: Function,
required: false,
default: () => {},
},
collapsingToggleStyle: {
type: Boolean,
required: false,
default: false,
},
},
emits: ['update:modelValue'],
methods: {
toggle() {
if (!this.disabled) {
this.$emit('update:modelValue', !this.modelValue)
}
},
},
}
</script>
<style lang="scss" scoped>
.checkbox-outer {
display: flex;
align-items: center;
cursor: pointer;
p {
user-select: none;
padding: 0.2rem 0;
margin: 0;
}
&.disabled {
cursor: not-allowed;
}
&.checked {
outline: 2px solid transparent;
outline-offset: 4px;
border-radius: 0.25rem;
}
}
.checkbox {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-width: 1rem;
min-height: 1rem;
padding: 0;
margin: 0 0.5rem 0 0;
color: var(--color-button-text);
background-color: var(--color-button-bg);
border-radius: var(--size-rounded-control);
box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
&.checked {
background-color: var(--color-brand);
}
svg {
color: var(--color-accent-contrast, var(--color-brand-inverted));
stroke-width: 0.2rem;
height: 0.8rem;
width: 0.8rem;
flex-shrink: 0;
}
&.collapsing {
background-color: transparent !important;
box-shadow: none;
svg {
color: inherit;
height: 1rem;
width: 1rem;
transition: transform 0.25s ease-in-out;
}
&.checked {
svg {
transform: rotate(180deg);
}
}
}
&:disabled {
box-shadow: none;
cursor: not-allowed;
}
}
</style>

View File

@@ -1,6 +1,8 @@
<template> <template>
<nav :aria-label="ariaLabel" class="w-full"> <nav :aria-label="ariaLabel" class="w-full">
<ul class="m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"> <ul
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"
>
<slot v-if="hasSlotContent" /> <slot v-if="hasSlotContent" />
<template v-else> <template v-else>

View File

@@ -152,7 +152,7 @@ const onSubmitHandler = () => {
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
margin-top: var(--gap-md); margin-top: var(--gap-md);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
.table-row { .table-row {

View File

@@ -356,7 +356,7 @@ svg {
:deep(.apexcharts-yaxistooltip) { :deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important; box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
} }
@@ -371,7 +371,7 @@ svg {
:deep(.apexcharts-xaxistooltip) { :deep(.apexcharts-xaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
color: var(--color-base) !important; color: var(--color-base) !important;

View File

@@ -891,7 +891,7 @@ const defaultRanges: RangeObject[] = [
flex-direction: column; flex-direction: column;
background-color: var(--color-bg); background-color: var(--color-bg);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
gap: var(--gap-md); gap: var(--gap-md);
padding: var(--gap-md); padding: var(--gap-md);
margin-top: var(--gap-md); margin-top: var(--gap-md);
@@ -920,7 +920,7 @@ const defaultRanges: RangeObject[] = [
width: 100%; width: 100%;
height: 1rem; height: 1rem;
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
overflow: hidden; overflow: hidden;

View File

@@ -159,10 +159,10 @@ defineExpose({
flex-direction: column; flex-direction: column;
gap: var(--gap-xs); gap: var(--gap-xs);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
box-shadow: var(--shadow-floating); box-shadow: var(--shadow-card);
color: var(--color-base); color: var(--color-base);
font-size: var(--font-size-nm); font-size: var(--font-size-nm);
@@ -192,7 +192,7 @@ svg {
:deep(.apexcharts-yaxistooltip) { :deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important; box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
} }

View File

@@ -1,77 +0,0 @@
<template>
<Checkbox
class="filter"
:model-value="activeFilters.includes(facetName)"
:description="displayName"
@update:model-value="toggle()"
>
<div class="filter-text">
<div v-if="icon" aria-hidden="true" class="icon" v-html="icon" />
<div v-else class="icon">
<slot />
</div>
<span aria-hidden="true"> {{ displayName }}</span>
</div>
</Checkbox>
</template>
<script>
import Checkbox from '~/components/ui/Checkbox.vue'
export default {
components: {
Checkbox,
},
props: {
facetName: {
type: String,
default: '',
},
displayName: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
activeFilters: {
type: Array,
default() {
return []
},
},
},
emits: ['toggle'],
methods: {
toggle() {
this.$emit('toggle', this.facetName)
},
},
}
</script>
<style lang="scss" scoped>
.filter {
margin-bottom: 0.5rem;
:deep(.filter-text) {
display: flex;
align-items: center;
.icon {
height: 1rem;
svg {
margin-right: 0.25rem;
width: 1rem;
height: 1rem;
}
}
}
span {
user-select: none;
}
}
</style>

View File

@@ -245,13 +245,20 @@ import {
LockOpenIcon, LockOpenIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Admonition, Avatar, ButtonStyled, Combobox, CopyCode, NewModal } from '@modrinth/ui' import {
Admonition,
Avatar,
ButtonStyled,
Checkbox,
Combobox,
CopyCode,
NewModal,
} from '@modrinth/ui'
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue' import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils' import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Accordion from '~/components/ui/Accordion.vue' import Accordion from '~/components/ui/Accordion.vue'
import Checkbox from '~/components/ui/Checkbox.vue'
import ContentVersionFilter, { import ContentVersionFilter, {
type ListedGameVersion, type ListedGameVersion,
type ListedPlatform, type ListedPlatform,

View File

@@ -36,7 +36,7 @@
v-for="(option, index) in filteredOptions" v-for="(option, index) in filteredOptions"
:key="isDivider(option) ? `divider-${index}` : option.id" :key="isDivider(option) ? `divider-${index}` : option.id"
> >
<div v-if="isDivider(option)" class="h-px w-full bg-button-bg"></div> <div v-if="isDivider(option)" class="h-px w-full bg-surface-5"></div>
<ButtonStyled v-else type="transparent" role="menuitem" :color="option.color"> <ButtonStyled v-else type="transparent" role="menuitem" :color="option.color">
<button <button
v-if="typeof option.action === 'function'" v-if="typeof option.action === 'function'"
@@ -288,19 +288,10 @@ const handleMouseOver = (index: number) => {
// Scrolling is disabled for keyboard navigation // Scrolling is disabled for keyboard navigation
const disableBodyScroll = () => { const disableBodyScroll = () => {
// Make opening not shift page when there's a vertical scrollbar
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth
if (scrollBarWidth > 0) {
document.body.style.paddingRight = `${scrollBarWidth}px`
} else {
document.body.style.paddingRight = ''
}
document.body.style.overflow = 'hidden' document.body.style.overflow = 'hidden'
} }
const enableBodyScroll = () => { const enableBodyScroll = () => {
document.body.style.paddingRight = ''
document.body.style.overflow = '' document.body.style.overflow = ''
} }

View File

@@ -261,9 +261,14 @@ import {
SendIcon, SendIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { CopyCode, injectNotificationManager, MarkdownEditor, OverflowMenu } from '@modrinth/ui' import {
Checkbox,
CopyCode,
injectNotificationManager,
MarkdownEditor,
OverflowMenu,
} from '@modrinth/ui'
import Checkbox from '~/components/ui/Checkbox.vue'
import Modal from '~/components/ui/Modal.vue' import Modal from '~/components/ui/Modal.vue'
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue' import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
import { useImageUpload } from '~/composables/image-upload.ts' import { useImageUpload } from '~/composables/image-upload.ts'

View File

@@ -220,8 +220,15 @@
class="experimental-styles-within desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-6 py-4 lg:grid-cols-[auto_1fr_auto]" class="experimental-styles-within desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-6 py-4 lg:grid-cols-[auto_1fr_auto]"
> >
<div> <div>
<NuxtLink to="/" :aria-label="formatMessage(messages.modrinthHomePage)"> <NuxtLink
<TextLogo aria-hidden="true" class="h-7 w-auto text-contrast" /> to="/"
:aria-label="formatMessage(messages.modrinthHomePage)"
class="group hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness]"
>
<TextLogo
aria-hidden="true"
class="h-7 w-auto text-contrast transition-transform group-active:scale-[0.98]"
/>
</NuxtLink> </NuxtLink>
</div> </div>
<div <div
@@ -369,7 +376,7 @@
formatMessage(navMenuMessages.discoverContent) formatMessage(navMenuMessages.discoverContent)
}}</span> }}</span>
<span class="contents md:hidden">{{ formatMessage(navMenuMessages.discover) }}</span> <span class="contents md:hidden">{{ formatMessage(navMenuMessages.discover) }}</span>
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" /> <DropdownIcon aria-hidden="true" class="h-5 w-5" />
<template #mods> <template #mods>
<BoxIcon aria-hidden="true" /> <BoxIcon aria-hidden="true" />

View File

@@ -547,14 +547,8 @@
</div> </div>
</template> </template>
</Tooltip> </Tooltip>
<ClientOnly> <ButtonStyled size="large" circular>
<ButtonStyled <ClientOnly>
size="large"
circular
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
>
<button <button
v-if="auth.user" v-if="auth.user"
v-tooltip=" v-tooltip="
@@ -579,94 +573,74 @@
> >
<HeartIcon aria-hidden="true" /> <HeartIcon aria-hidden="true" />
</nuxt-link> </nuxt-link>
</ButtonStyled> <template #fallback>
<ButtonStyled size="large" circular>
<PopoutMenu
v-if="auth.user"
:tooltip="
collections.some((x) => x.projects.includes(project.id))
? formatMessage(commonMessages.savedLabel)
: formatMessage(commonMessages.saveButton)
"
from="top-right"
:aria-label="formatMessage(commonMessages.saveButton)"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list text-primary">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">{{ formatMessage(messages.noCollectionsFound) }}</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.createNewCollection) }}
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<template #fallback>
<ButtonStyled size="large" circular>
<button
v-if="auth.user"
:v-tooltip="formatMessage(commonMessages.followButton)"
:aria-label="formatMessage(commonMessages.followButton)"
@click="userFollowProject(project)"
>
<HeartIcon aria-hidden="true" />
</button>
<nuxt-link <nuxt-link
v-else
v-tooltip="formatMessage(commonMessages.followButton)" v-tooltip="formatMessage(commonMessages.followButton)"
to="/auth/sign-in" to="/auth/sign-in"
:aria-label="formatMessage(commonMessages.followButton)" :aria-label="formatMessage(commonMessages.followButton)"
> >
<HeartIcon aria-hidden="true" /> <HeartIcon aria-hidden="true" />
</nuxt-link> </nuxt-link>
</ButtonStyled> </template>
<ButtonStyled size="large" circular> </ClientOnly>
<nuxt-link </ButtonStyled>
v-tooltip="formatMessage(commonMessages.saveButton)" <ButtonStyled size="large" circular>
to="/auth/sign-in" <PopoutMenu
:aria-label="formatMessage(commonMessages.saveButton)" v-if="auth.user"
:tooltip="
collections.some((x) => x.projects.includes(project.id))
? formatMessage(commonMessages.savedLabel)
: formatMessage(commonMessages.saveButton)
"
from="top-right"
:aria-label="formatMessage(commonMessages.saveButton)"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list text-primary">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">{{ formatMessage(messages.noCollectionsFound) }}</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
> >
<BookmarkIcon aria-hidden="true" /> <PlusIcon aria-hidden="true" />
</nuxt-link> {{ formatMessage(messages.createNewCollection) }}
</ButtonStyled> </button>
</template> </template>
</ClientOnly> </PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular> <ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link <nuxt-link
v-tooltip="formatMessage(commonMessages.settingsLabel)" v-tooltip="formatMessage(commonMessages.settingsLabel)"
@@ -1672,10 +1646,12 @@ const projectTypeDisplay = computed(() =>
), ),
) )
const following = computed( const following = computed(() => {
() => if (!user.value?.follows) {
user.value && user.value.follows && user.value.follows.find((x) => x.id === project.value.id), return false
) }
return !!user.value.follows.find((x) => x.id === project.value.id)
})
const title = computed(() => `${project.value.title} - Minecraft ${projectTypeDisplay.value}`) const title = computed(() => `${project.value.title} - Minecraft ${projectTypeDisplay.value}`)
const description = computed( const description = computed(

View File

@@ -36,7 +36,7 @@
</p> </p>
<template v-else> <template v-else>
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`"> <template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
<div class="label"> <div class="label mb-3">
<h4> <h4>
<span class="label__title">{{ formatCategoryHeader(header) }}</span> <span class="label__title">{{ formatCategoryHeader(header) }}</span>
</h4> </h4>
@@ -134,6 +134,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets' import { SaveIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets'
import { Checkbox } from '@modrinth/ui'
import { import {
formatCategory, formatCategory,
formatCategoryHeader, formatCategoryHeader,
@@ -143,8 +144,6 @@ import {
} from '@modrinth/utils' } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Checkbox from '~/components/ui/Checkbox.vue'
interface Category { interface Category {
name: string name: string
header: string header: string
@@ -337,7 +336,7 @@ const saveChanges = () => {
margin-bottom: var(--spacing-card-md); margin-bottom: var(--spacing-card-md);
:deep(.category-selector) { :deep(.category-selector) {
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
.category-selector__label { .category-selector__label {
display: flex; display: flex;

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<Modal ref="editLinksModal" header="Edit links"> <NewModal ref="editLinksModal" header="Edit links">
<div class="universal-modal links-modal"> <div class="universal-modal links-modal !p-0">
<p> <p>
Any links you specify below will be overwritten on each of the selected projects. Any you Any links you specify below will be overwritten on each of the selected projects. Any you
leave blank will be ignored. You can clear a link from all selected projects using the leave blank will be ignored. You can clear a link from all selected projects using the
@@ -139,10 +139,8 @@
<Checkbox <Checkbox
v-if="selectedProjects.length > 3" v-if="selectedProjects.length > 3"
v-model="editLinks.showAffected" v-model="editLinks.showAffected"
:label="editLinks.showAffected ? 'Less' : 'More'" label="Show all projects"
description="Show all loaders" description="Show all projects"
:border="false"
:collapsing-toggle-style="true"
/> />
<div class="push-right input-group"> <div class="push-right input-group">
<button class="iconified-button" @click="$refs.editLinksModal.hide()"> <button class="iconified-button" @click="$refs.editLinksModal.hide()">
@@ -155,7 +153,7 @@
</button> </button>
</div> </div>
</div> </div>
</Modal> </NewModal>
<ModalCreation ref="modal_creation" /> <ModalCreation ref="modal_creation" />
<section class="universal-card"> <section class="universal-card">
<div class="header__row"> <div class="header__row">
@@ -335,13 +333,13 @@ import {
commonMessages, commonMessages,
CopyCode, CopyCode,
injectNotificationManager, injectNotificationManager,
NewModal,
ProjectStatusBadge, ProjectStatusBadge,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import Modal from '~/components/ui/Modal.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js' import { getProjectTypeForUrl } from '~/helpers/projects.js'
useHead({ title: 'Projects - Modrinth' }) useHead({ title: 'Projects - Modrinth' })

View File

@@ -40,7 +40,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div <div
class="flex flex-row justify-between border-0 !border-b-[2px] border-solid border-button-bg p-1.5 md:p-2" class="flex flex-row justify-between border-0 !border-b-[2px] border-solid border-divider p-1.5 md:p-2"
> >
<span class="my-auto flex flex-row items-center gap-2 text-sm leading-none md:text-base" <span class="my-auto flex flex-row items-center gap-2 text-sm leading-none md:text-base"
><span ><span
@@ -58,7 +58,7 @@
<div <div
v-for="(date, i) in dateSegments" v-for="(date, i) in dateSegments"
:key="date.date" :key="date.date"
class="flex flex-row justify-between border-0 !border-b-[2px] border-solid border-button-bg p-1.5 md:p-2" class="flex flex-row justify-between border-0 !border-b-[2px] border-solid border-divider p-1.5 md:p-2"
> >
<span class="my-auto flex flex-row items-center gap-2 text-sm leading-none md:text-base"> <span class="my-auto flex flex-row items-center gap-2 text-sm leading-none md:text-base">
<span <span
@@ -123,7 +123,7 @@
}}</span> }}</span>
<div class="grid grid-cols-1 gap-x-4 gap-y-2 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-x-4 gap-y-2 md:grid-cols-2 lg:grid-cols-3">
<button <button
class="relative flex flex-col overflow-hidden rounded-2xl bg-gradient-to-r from-green to-green-700 p-4 text-inverted shadow-md transition-all duration-200 hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:brightness-100 md:p-5" class="relative flex flex-col overflow-hidden rounded-2xl bg-gradient-to-r from-green to-green-700 p-4 text-inverted shadow-md transition-all duration-200 hover:brightness-105 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:brightness-100 md:p-5"
:disabled="hasTinMismatch" :disabled="hasTinMismatch"
@click="openWithdrawModal" @click="openWithdrawModal"
> >

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="normal-page__content"> <div>
<Modal ref="editLinksModal" header="Edit links"> <NewModal ref="editLinksModal" header="Edit links">
<div class="universal-modal links-modal"> <div class="universal-modal links-modal !p-0">
<p> <p>
Any links you specify below will be overwritten on each of the selected projects. Any you Any links you specify below will be overwritten on each of the selected projects. Any you
leave blank will be ignored. You can clear a link from all selected projects using the leave blank will be ignored. You can clear a link from all selected projects using the
@@ -25,16 +25,15 @@
" "
maxlength="2048" maxlength="2048"
/> />
<Button <button
v-tooltip="'Clear link'" v-tooltip="'Clear link'"
aria-label="Clear link" aria-label="Clear link"
class="square-button label-button" class="square-button label-button"
:data-active="editLinks.issues.clear" :data-active="editLinks.issues.clear"
icon-only
@click="editLinks.issues.clear = !editLinks.issues.clear" @click="editLinks.issues.clear = !editLinks.issues.clear"
> >
<TrashIcon /> <TrashIcon />
</Button> </button>
</div> </div>
<label <label
for="source-code-input" for="source-code-input"
@@ -53,15 +52,15 @@
editLinks.source.clear ? 'Existing link will be cleared' : 'Enter a valid URL' editLinks.source.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
" "
/> />
<Button <button
v-tooltip="'Clear link'" v-tooltip="'Clear link'"
aria-label="Clear link" aria-label="Clear link"
class="square-button label-button"
:data-active="editLinks.source.clear" :data-active="editLinks.source.clear"
icon-only
@click="editLinks.source.clear = !editLinks.source.clear" @click="editLinks.source.clear = !editLinks.source.clear"
> >
<TrashIcon /> <TrashIcon />
</Button> </button>
</div> </div>
<label <label
for="wiki-page-input" for="wiki-page-input"
@@ -80,15 +79,15 @@
editLinks.wiki.clear ? 'Existing link will be cleared' : 'Enter a valid URL' editLinks.wiki.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
" "
/> />
<Button <button
v-tooltip="'Clear link'" v-tooltip="'Clear link'"
aria-label="Clear link" aria-label="Clear link"
class="square-button label-button"
:data-active="editLinks.wiki.clear" :data-active="editLinks.wiki.clear"
icon-only
@click="editLinks.wiki.clear = !editLinks.wiki.clear" @click="editLinks.wiki.clear = !editLinks.wiki.clear"
> >
<TrashIcon /> <TrashIcon />
</Button> </button>
</div> </div>
<label for="discord-invite-input" title="An invitation link to your Discord server."> <label for="discord-invite-input" title="An invitation link to your Discord server.">
<span class="label__title">Discord invite</span> <span class="label__title">Discord invite</span>
@@ -106,15 +105,15 @@
: 'Enter a valid Discord invite URL' : 'Enter a valid Discord invite URL'
" "
/> />
<Button <button
v-tooltip="'Clear link'" v-tooltip="'Clear link'"
aria-label="Clear link" aria-label="Clear link"
class="square-button label-button"
:data-active="editLinks.discord.clear" :data-active="editLinks.discord.clear"
icon-only
@click="editLinks.discord.clear = !editLinks.discord.clear" @click="editLinks.discord.clear = !editLinks.discord.clear"
> >
<TrashIcon /> <TrashIcon />
</Button> </button>
</div> </div>
</section> </section>
<p> <p>
@@ -140,35 +139,35 @@
<Checkbox <Checkbox
v-if="selectedProjects.length > 3" v-if="selectedProjects.length > 3"
v-model="editLinks.showAffected" v-model="editLinks.showAffected"
:label="editLinks.showAffected ? 'Less' : 'More'" label="Show all projects"
description="Show all loaders" description="Show all projects"
:border="false"
:collapsing-toggle-style="true"
/> />
<div class="push-right input-group"> <div class="push-right input-group">
<Button @click="$refs.editLinksModal.hide()"> <button class="iconified-button" @click="$refs.editLinksModal.hide()">
<XIcon /> <XIcon />
Cancel Cancel
</Button> </button>
<Button color="primary" @click="onBulkEditLinks"> <button class="iconified-button brand-button" @click="onBulkEditLinks">
<SaveIcon /> <SaveIcon />
Save changes Save changes
</Button> </button>
</div> </div>
</div> </div>
</Modal> </NewModal>
<ModalCreation ref="modal_creation" :organization-id="organization.id" /> <ModalCreation ref="modal_creation" :organization-id="organization.id" />
<div class="universal-card"> <section class="universal-card">
<h2>Projects</h2> <div class="header__row">
<div class="input-group"> <h2 class="header__title text-2xl">Projects</h2>
<Button color="primary" @click="$refs.modal_creation.show()"> <div class="input-group">
<PlusIcon /> <button class="iconified-button brand-button" @click="$refs.modal_creation.show()">
{{ formatMessage(commonMessages.createAProjectButton) }} <PlusIcon />
</Button> {{ formatMessage(commonMessages.createAProjectButton) }}
<OrganizationProjectTransferModal </button>
:projects="usersOwnedProjects || []" <OrganizationProjectTransferModal
@submit="onProjectTransferSubmit" :projects="usersOwnedProjects || []"
/> @submit="onProjectTransferSubmit"
/>
</div>
</div> </div>
<p v-if="sortedProjects.length < 1"> <p v-if="sortedProjects.length < 1">
You don't have any projects yet. Click the green button above to begin. You don't have any projects yet. Click the green button above to begin.
@@ -176,10 +175,14 @@
<template v-else> <template v-else>
<p>You can edit multiple projects at once by selecting them below.</p> <p>You can edit multiple projects at once by selecting them below.</p>
<div class="input-group"> <div class="input-group">
<Button :disabled="selectedProjects.length === 0" @click="$refs.editLinksModal.show()"> <button
class="iconified-button"
:disabled="selectedProjects.length === 0"
@click="$refs.editLinksModal.show()"
>
<EditIcon /> <EditIcon />
Edit links Edit links
</Button> </button>
<div class="push-right"> <div class="push-right">
<div class="labeled-control-row"> <div class="labeled-control-row">
Sort by Sort by
@@ -195,21 +198,20 @@
sortedProjects = updateSort(sortedProjects, sortBy, descending) sortedProjects = updateSort(sortedProjects, sortBy, descending)
" "
/> />
<Button <button
v-tooltip="descending ? 'Descending' : 'Ascending'" v-tooltip="descending ? 'Descending' : 'Ascending'"
class="square-button" class="square-button"
icon-only
@click="updateDescending()" @click="updateDescending()"
> >
<SortDescIcon v-if="descending" /> <SortDescIcon v-if="descending" />
<SortAscIcon v-else /> <SortAscIcon v-else />
</Button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="table"> <div class="grid-table">
<div class="table-head table-row"> <div class="grid-table__row grid-table__header">
<div class="check-cell table-cell"> <div>
<Checkbox <Checkbox
:model-value="selectedProjects === sortedProjects" :model-value="selectedProjects === sortedProjects"
@update:model-value=" @update:model-value="
@@ -219,15 +221,19 @@
" "
/> />
</div> </div>
<div class="table-cell">Icon</div> <div>Icon</div>
<div class="table-cell">Name</div> <div>Name</div>
<div class="table-cell">ID</div> <div>ID</div>
<div class="table-cell">Type</div> <div>Type</div>
<div class="table-cell">Status</div> <div>Status</div>
<div class="table-cell" /> <div />
</div> </div>
<div v-for="project in sortedProjects" :key="`project-${project.id}`" class="table-row"> <div
<div class="check-cell table-cell"> v-for="project in sortedProjects"
:key="`project-${project.id}`"
class="grid-table__row"
>
<div>
<Checkbox <Checkbox
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS" :disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:model-value="selectedProjects.includes(project)" :model-value="selectedProjects.includes(project)"
@@ -238,8 +244,13 @@
" "
/> />
</div> </div>
<div class="table-cell"> <div>
<nuxt-link tabindex="-1" :to="`/project/${project.slug ? project.slug : project.id}`"> <nuxt-link
tabindex="-1"
:to="`/${getProjectTypeForUrl(project.project_types[0] ?? 'project', project.loaders)}/${
project.slug ? project.slug : project.id
}`"
>
<Avatar <Avatar
:src="project.icon_url" :src="project.icon_url"
aria-hidden="true" aria-hidden="true"
@@ -249,7 +260,7 @@
</nuxt-link> </nuxt-link>
</div> </div>
<div class="table-cell"> <div>
<span class="project-title"> <span class="project-title">
<IssuesIcon <IssuesIcon
v-if="project.moderator_message" v-if="project.moderator_message"
@@ -258,48 +269,52 @@
<nuxt-link <nuxt-link
class="hover-link wrap-as-needed" class="hover-link wrap-as-needed"
:to="`/project/${project.slug ? project.slug : project.id}`" :to="`/${getProjectTypeForUrl(project.project_types[0] ?? 'project', project.loaders)}/${
project.slug ? project.slug : project.id
}`"
> >
{{ project.name }} {{ project.name }}
</nuxt-link> </nuxt-link>
</span> </span>
</div> </div>
<div class="table-cell"> <div>
<CopyCode :text="project.id" /> <CopyCode :text="project.id" />
</div> </div>
<div class="table-cell"> <div>
<BoxIcon /> {{
<span>{{
formatProjectType( formatProjectType(
$getProjectTypeForDisplay(project.project_types[0] ?? 'project', project.loaders), getProjectTypeForUrl(project.project_types[0] ?? 'project', project.loaders),
) )
}}</span> }}
</div> </div>
<div class="table-cell"> <div>
<Badge v-if="project.status" :type="project.status" class="status" /> <ProjectStatusBadge v-if="project.status" :status="project.status" />
</div> </div>
<div class="table-cell"> <div class="flex !flex-row items-center !justify-end gap-2">
<nuxt-link <ButtonStyled circular>
class="btn icon-only" <nuxt-link
:to="`/project/${project.slug ? project.slug : project.id}/settings`" v-tooltip="formatMessage(commonMessages.settingsLabel)"
> :to="`/${getProjectTypeForUrl(project.project_types[0] ?? 'project', project.loaders)}/${
<SettingsIcon /> project.slug ? project.slug : project.id
</nuxt-link> }/settings`"
>
<SettingsIcon />
</nuxt-link>
</ButtonStyled>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</div> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
BoxIcon,
EditIcon, EditIcon,
IssuesIcon, IssuesIcon,
PlusIcon, PlusIcon,
@@ -312,19 +327,20 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
import { import {
Avatar, Avatar,
Badge, ButtonStyled,
Button,
Checkbox, Checkbox,
commonMessages, commonMessages,
CopyCode, CopyCode,
injectNotificationManager, injectNotificationManager,
Modal, NewModal,
ProjectStatusBadge,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue' import OrganizationProjectTransferModal from '~/components/ui/OrganizationProjectTransferModal.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js'
import { injectOrganizationContext } from '~/providers/organization-context.ts' import { injectOrganizationContext } from '~/providers/organization-context.ts'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
@@ -423,10 +439,12 @@ const updateSort = (inputProjects, sort, descending) => {
break break
case 'Type': case 'Type':
sortedArray = inputProjects.slice().sort((a, b) => { sortedArray = inputProjects.slice().sort((a, b) => {
if (a.project_type < b.project_type) { const aType = a.project_types?.[0] ?? 'project'
const bType = b.project_types?.[0] ?? 'project'
if (aType < bType) {
return -1 return -1
} }
if (a.project_type > b.project_type) { if (aType > bType) {
return 1 return 1
} }
return 0 return 0
@@ -456,109 +474,107 @@ watch(
}, },
) )
const emptyLinksData = { const editLinks = reactive({
showAffected: false, showAffected: false,
source: { source: { val: '', clear: false },
val: '', discord: { val: '', clear: false },
clear: false, wiki: { val: '', clear: false },
}, issues: { val: '', clear: false },
discord: { })
val: '',
clear: false,
},
wiki: {
val: '',
clear: false,
},
issues: {
val: '',
clear: false,
},
}
const editLinks = ref(emptyLinksData)
const updateDescending = () => { const updateDescending = () => {
descending.value = !descending.value descending.value = !descending.value
sortedProjects.value = updateSort(sortedProjects.value, sortBy.value, descending.value) sortedProjects.value = updateSort(sortedProjects.value, sortBy.value, descending.value)
} }
const onBulkEditLinks = useClientTry(async () => { const onBulkEditLinks = async () => {
const linkData = editLinks.value try {
const baseData = {
issues_url: editLinks.value.issues.clear ? null : editLinks.value.issues.val.trim(),
source_url: editLinks.value.source.clear ? null : editLinks.value.source.val.trim(),
wiki_url: editLinks.value.wiki.clear ? null : editLinks.value.wiki.val.trim(),
discord_url: editLinks.value.discord.clear ? null : editLinks.value.discord.val.trim(),
}
const filteredData = Object.fromEntries(Object.entries(baseData).filter(([, v]) => v !== ''))
const baseData = {} await useBaseFetch(`projects?ids=${JSON.stringify(selectedProjects.value.map((x) => x.id))}`, {
method: 'PATCH',
body: JSON.stringify(filteredData),
})
if (linkData.issues.clear) { editLinksModal.value?.hide()
baseData.issues_url = null addNotification({
} else if (linkData.issues.val.trim().length > 0) { title: 'Success',
baseData.issues_url = linkData.issues.val.trim() text: "Bulk edited selected project's links.",
type: 'success',
})
selectedProjects.value = []
editLinks.value.issues.val = ''
editLinks.value.source.val = ''
editLinks.value.wiki.val = ''
editLinks.value.discord.val = ''
editLinks.value.issues.clear = false
editLinks.value.source.clear = false
editLinks.value.wiki.clear = false
editLinks.value.discord.clear = false
} catch (e) {
addNotification({
title: 'An error occurred',
text: e?.data?.description || e?.message || e || 'Unknown error',
type: 'error',
})
console.error(e)
} }
}
if (linkData.source.clear) {
baseData.source_url = null
} else if (linkData.source.val.trim().length > 0) {
baseData.source_url = linkData.source.val.trim()
}
if (linkData.wiki.clear) {
baseData.wiki_url = null
} else if (linkData.wiki.val.trim().length > 0) {
baseData.wiki_url = linkData.wiki.val.trim()
}
if (linkData.discord.clear) {
baseData.discord_url = null
} else if (linkData.discord.val.trim().length > 0) {
baseData.discord_url = linkData.discord.val.trim()
}
await useBaseFetch(`projects?ids=${JSON.stringify(selectedProjects.value.map((x) => x.id))}`, {
method: 'PATCH',
body: JSON.stringify(baseData),
})
editLinksModal.value.hide()
addNotification({
title: 'Success',
text: "Bulk edited selected project's links.",
type: 'success',
})
selectedProjects.value = []
editLinks.value = emptyLinksData
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.table { .grid-table {
display: grid; display: grid;
border-radius: var(--radius-md); grid-template-columns:
min-content min-content minmax(min-content, 2fr)
minmax(min-content, 1fr) minmax(min-content, 1fr) minmax(min-content, 1fr) min-content;
border-radius: var(--size-rounded-sm);
overflow: hidden; overflow: hidden;
margin-top: var(--gap-md); margin-top: var(--spacing-card-md);
border: 1px solid var(--color-button-bg); outline: 1px solid transparent;
background-color: var(--color-raised-bg);
.table-row { .grid-table__row {
grid-template-columns: 2.75rem 3.75rem 2fr 1fr 1fr 1fr 3.5rem; display: contents;
}
.table-cell { > div {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: var(--gap-xs); justify-content: center;
padding: var(--gap-md); padding: var(--spacing-card-sm);
padding-left: 0;
}
.check-cell { &:first-child {
padding-left: var(--gap-md); padding-left: var(--spacing-card-bg);
}
&:last-child {
padding-right: var(--spacing-card-bg);
}
}
&:nth-child(2n + 1) > div {
background-color: var(--color-table-alternate-row);
}
&.grid-table__header > div {
background-color: var(--color-bg);
font-weight: bold;
color: var(--color-text-dark);
padding-top: var(--spacing-card-bg);
padding-bottom: var(--spacing-card-bg);
}
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.table-row { .grid-table__row {
display: grid; display: grid;
grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings'; grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings';
grid-template-columns: grid-template-columns:
@@ -596,7 +612,7 @@ const onBulkEditLinks = useClientTry(async () => {
} }
} }
.table-head { .grid-table__header {
grid-template: 'checkbox settings'; grid-template: 'checkbox settings';
grid-template-columns: min-content minmax(min-content, 1fr); grid-template-columns: min-content minmax(min-content, 1fr);
@@ -611,7 +627,7 @@ const onBulkEditLinks = useClientTry(async () => {
} }
@media screen and (max-width: 560px) { @media screen and (max-width: 560px) {
.table-row { .grid-table__row {
display: grid; display: grid;
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings'; grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content; grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
@@ -621,7 +637,7 @@ const onBulkEditLinks = useClientTry(async () => {
} }
} }
.table-head { .grid-table__header {
grid-template: 'checkbox settings'; grid-template: 'checkbox settings';
grid-template-columns: min-content minmax(min-content, 1fr); grid-template-columns: min-content minmax(min-content, 1fr);
} }
@@ -652,13 +668,13 @@ const onBulkEditLinks = useClientTry(async () => {
flex-direction: row; flex-direction: row;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
gap: var(--gap-sm); gap: var(--spacing-card-md);
white-space: nowrap; white-space: nowrap;
} }
.small-select { .small-select {
width: fit-content;
width: -moz-fit-content; width: -moz-fit-content;
width: fit-content;
} }
.label-button[data-active='true'] { .label-button[data-active='true'] {
@@ -688,16 +704,4 @@ const onBulkEditLinks = useClientTry(async () => {
margin: 0 0 var(--spacing-card-sm) 0; margin: 0 0 var(--spacing-card-sm) 0;
} }
} }
h1 {
margin-block: var(--gap-sm) var(--gap-lg);
font-size: 2em;
line-height: 1em;
}
:deep(.checkbox-outer) {
button.checkbox {
border: none;
}
}
</style> </style>

View File

@@ -107,7 +107,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col gap-4 rounded-xl bg-bg-raised p-6"> <div class="card-shadow flex flex-col gap-4 rounded-xl bg-bg-raised p-6">
<template v-if="!prefilled || !currentItemValid"> <template v-if="!prefilled || !currentItemValid">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-lg font-bold text-contrast"> <span class="text-lg font-bold text-contrast">

View File

@@ -88,7 +88,10 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
<div v-if="server && projectType.id === 'modpack'" class="rounded-2xl bg-bg-raised"> <div
v-if="server && projectType.id === 'modpack'"
class="card-shadow rounded-2xl bg-bg-raised"
>
<div class="flex flex-row items-center gap-2 px-6 py-4 text-contrast"> <div class="flex flex-row items-center gap-2 px-6 py-4 text-contrast">
<h3 class="m-0 text-lg">Options</h3> <h3 class="m-0 text-lg">Options</h3>
</div> </div>
@@ -107,7 +110,10 @@
the selected modpack. the selected modpack.
</div> </div>
</div> </div>
<div v-if="server && projectType.id !== 'modpack'" class="rounded-2xl bg-bg-raised p-4"> <div
v-if="server && projectType.id !== 'modpack'"
class="card-shadow rounded-2xl bg-bg-raised p-4"
>
<Checkbox <Checkbox
v-model="serverHideInstalled" v-model="serverHideInstalled"
label="Hide installed content" label="Hide installed content"
@@ -126,7 +132,7 @@
:class=" :class="
filtersMenuOpen filtersMenuOpen
? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0' ? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
: 'rounded-2xl bg-bg-raised' : 'card-shadow rounded-2xl bg-bg-raised'
" "
button-class="button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none" button-class="button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
content-class="mb-4 mx-3" content-class="mb-4 mx-3"

View File

@@ -825,7 +825,7 @@ a,
.v-popper--theme-dropdown, .v-popper--theme-dropdown,
.v-popper--theme-dropdown.v-popper--theme-ribbit-popout { .v-popper--theme-dropdown.v-popper--theme-ribbit-popout {
.v-popper__inner { .v-popper__inner {
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
padding: var(--gap-sm) !important; padding: var(--gap-sm) !important;
width: fit-content !important; width: fit-content !important;
border-radius: var(--radius-md) !important; border-radius: var(--radius-md) !important;
@@ -834,7 +834,7 @@ a,
} }
.v-popper__arrow-outer { .v-popper__arrow-outer {
border-color: var(--color-button-bg) !important; border-color: var(--color-divider) !important;
} }
.v-popper__arrow-inner { .v-popper__arrow-inner {

View File

@@ -76,7 +76,7 @@ pre {
padding: 1em 1em 1em 1em; padding: 1em 1em 1em 1em;
border-width: 5px; border-width: 5px;
border-radius: 2em; border-radius: 2em;
border-color: var(--color-button-bg); border-color: var(--color-divider);
overflow-x: hidden; overflow-x: hidden;
code { code {

View File

@@ -3,7 +3,7 @@
--surface-2: #f5f5f5; --surface-2: #f5f5f5;
--surface-3: #f8f8f8; --surface-3: #f8f8f8;
--surface-4: #ffffff; --surface-4: #ffffff;
--surface-5: #e6e6e6; --surface-5: #dddddd;
--color-red-50: #fef2f2; --color-red-50: #fef2f2;
--color-red-100: #fee5e7; --color-red-100: #fee5e7;
@@ -97,8 +97,8 @@
--color-button-border: rgba(161, 161, 161, 0.35); --color-button-border: rgba(161, 161, 161, 0.35);
--color-scrollbar: #96a2b0; --color-scrollbar: #96a2b0;
--color-divider: var(--surface-2); --color-divider: var(--surface-5);
--color-divider-dark: #c8cdd3; --color-divider-dark: var(--surface-5);
--color-base: var(--color-text-default); --color-base: var(--color-text-default);
--color-secondary: var(--color-text-tertiary); --color-secondary: var(--color-text-tertiary);
@@ -207,6 +207,8 @@ html {
--color-link: var(--color-blue) !important; --color-link: var(--color-blue) !important;
--color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future --color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future
--color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future --color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future
--shadow-button: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.15);
} }
.light-mode, .light-mode,

View File

@@ -164,23 +164,31 @@ function setColorFill(
} }
const colorVariables = computed(() => { const colorVariables = computed(() => {
const defaultShadow =
props.type === 'standard' || props.type === 'highlight' || props.highlighted
? 'var(--shadow-button)'
: 'none'
if (props.highlighted) { if (props.highlighted) {
const colors = { const colors = {
bg: bg:
props.highlightedStyle === 'main-nav-primary' props.highlightedStyle === 'main-nav-primary'
? 'var(--color-brand-highlight)' ? 'var(--color-button-bg-selected)'
: 'var(--color-button-bg)', : 'var(--color-button-bg)',
text: 'var(--color-contrast)', text:
props.highlightedStyle === 'main-nav-primary'
? 'var(--color-button-text-selected)'
: 'var(--color-contrast)',
icon: icon:
props.type === 'chip' props.type === 'chip'
? 'var(--color-contrast)' ? 'var(--color-contrast)'
: props.highlightedStyle === 'main-nav-primary' : props.highlightedStyle === 'main-nav-primary'
? 'var(--color-brand)' ? 'var(--color-button-text-selected)'
: 'var(--color-contrast)', : 'var(--color-contrast)',
} }
const hoverColors = JSON.parse(JSON.stringify(colors)) const hoverColors = JSON.parse(JSON.stringify(colors))
const boxShadow = const boxShadow =
props.type === 'chip' && colorVar.value ? `0 0 0 2px ${colorVar.value}` : 'none' props.type === 'chip' && colorVar.value ? `0 0 0 2px ${colorVar.value}` : defaultShadow
return `--_bg: ${colors.bg}; --_text: ${colors.text}; --_icon: ${colors.icon}; --_hover-bg: ${hoverColors.bg}; --_hover-text: ${hoverColors.text}; --_hover-icon: ${hoverColors.icon}; --_box-shadow: ${boxShadow};` return `--_bg: ${colors.bg}; --_text: ${colors.text}; --_icon: ${colors.icon}; --_hover-bg: ${hoverColors.bg}; --_hover-text: ${hoverColors.text}; --_hover-icon: ${hoverColors.icon}; --_box-shadow: ${boxShadow};`
} }
@@ -217,7 +225,8 @@ const colorVariables = computed(() => {
) )
} }
const boxShadow = props.type === 'chip' && colorVar.value ? `0 0 0 2px ${colorVar.value}` : 'none' const boxShadow =
props.type === 'chip' && colorVar.value ? `0 0 0 2px ${colorVar.value}` : defaultShadow
return `--_bg: ${colors.bg}; --_text: ${colors.text}; --_hover-bg: ${hoverColors.bg}; --_hover-text: ${hoverColors.text}; --_box-shadow: ${boxShadow};` return `--_bg: ${colors.bg}; --_text: ${colors.text}; --_hover-bg: ${hoverColors.bg}; --_hover-text: ${hoverColors.text}; --_box-shadow: ${boxShadow};`
}) })

View File

@@ -1,31 +1,38 @@
<template> <template>
<div <button
class="checkbox-outer button-within" class="group bg-transparent border-none p-0 m-0 flex items-center gap-3 checkbox-outer outline-offset-4 text-contrast"
:class="{ disabled }" :disabled="disabled"
role="presentation" :class="
disabled
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness]'
"
:aria-label="description"
:aria-checked="modelValue"
role="checkbox"
@click="toggle" @click="toggle"
> >
<button <span
class="checkbox border-none" class="w-5 h-5 rounded-md flex items-center justify-center border-[1px] border-solid"
role="checkbox" :class="
:disabled="disabled" (modelValue
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }" ? 'bg-brand border-button-border text-brand-inverted'
:aria-label="description" : 'bg-surface-2 border-surface-5') +
:aria-checked="modelValue" (disabled ? '' : ' checkbox-shadow group-active:scale-95')
"
> >
<MinusIcon v-if="indeterminate" aria-hidden="true" /> <MinusIcon v-if="indeterminate" aria-hidden="true" stroke-width="3" />
<CheckIcon v-else-if="modelValue && !collapsingToggleStyle" aria-hidden="true" /> <CheckIcon v-else-if="modelValue" aria-hidden="true" stroke-width="3" />
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" /> </span>
</button>
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label --> <!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
<p v-if="label" aria-hidden="true" class="checkbox-label"> <span v-if="label" aria-hidden="true">
{{ label }} {{ label }}
</p> </span>
<slot v-else /> <slot v-else />
</div> </button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CheckIcon, DropdownIcon, MinusIcon } from '@modrinth/assets' import { CheckIcon, MinusIcon } from '@modrinth/assets'
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [boolean] 'update:modelValue': [boolean]
@@ -38,7 +45,6 @@ const props = withDefaults(
description?: string description?: string
modelValue: boolean modelValue: boolean
clickEvent?: () => void clickEvent?: () => void
collapsingToggleStyle?: boolean
indeterminate?: boolean indeterminate?: boolean
}>(), }>(),
{ {
@@ -47,7 +53,6 @@ const props = withDefaults(
description: '', description: '',
modelValue: false, modelValue: false,
clickEvent: () => {}, clickEvent: () => {},
collapsingToggleStyle: false,
indeterminate: false, indeterminate: false,
}, },
) )
@@ -60,86 +65,7 @@ function toggle() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.checkbox-outer { .checkbox-shadow {
display: flex; box-shadow: 1px 1px 2px 0 rgba(0, 0, 0, 0.08);
align-items: center;
cursor: pointer;
p {
user-select: none;
padding: 0.2rem 0;
margin: 0;
}
&.disabled {
cursor: not-allowed;
}
}
.checkbox {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-width: 1rem;
min-height: 1rem;
padding: 0;
margin: 0 0.5rem 0 0;
color: var(--color-contrast);
background-color: var(--color-button-bg);
border-radius: var(--radius-xs);
box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
&.checked {
background-color: var(--color-brand);
svg {
color: var(--color-accent-contrast);
}
}
svg {
color: var(--color-secondary);
stroke-width: 0.2rem;
height: 0.8rem;
width: 0.8rem;
flex-shrink: 0;
}
&.collapsing {
background-color: transparent !important;
box-shadow: none;
svg {
color: inherit;
height: 1rem;
width: 1rem;
transition: transform 0.25s ease-in-out;
@media (prefers-reduced-motion) {
transition: none !important;
}
}
&.checked {
svg {
transform: rotate(180deg);
}
}
}
&:disabled {
box-shadow: none;
cursor: not-allowed;
}
}
.checkbox-label {
color: var(--color-base);
} }
</style> </style>

View File

@@ -891,7 +891,7 @@ function openVideoModal() {
} }
.markdown-body-wrapper { .markdown-body-wrapper {
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
border-radius: var(--radius-md); border-radius: var(--radius-md);
width: 100%; width: 100%;
padding: var(--radius-md); padding: var(--radius-md);

View File

@@ -14,7 +14,7 @@
<div <div
v-if="isDivider(option)" v-if="isDivider(option)"
:key="`divider-${index}`" :key="`divider-${index}`"
class="h-px mx-3 my-2 bg-button-bg" class="h-px mx-3 my-2 bg-surface-5"
></div> ></div>
<Button <Button
v-else v-else

View File

@@ -286,7 +286,7 @@ svg {
:deep(.apexcharts-yaxistooltip) { :deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important; box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
} }
@@ -301,7 +301,7 @@ svg {
:deep(.apexcharts-xaxistooltip) { :deep(.apexcharts-xaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
color: var(--color-base) !important; color: var(--color-base) !important;

View File

@@ -161,10 +161,10 @@ const chartOptions = ref({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap-xs); gap: var(--gap-xs);
border: 1px solid var(--color-button-bg); border: 1px solid var(--color-divider);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
box-shadow: var(--shadow-floating); box-shadow: var(--shadow-card);
color: var(--color-base); color: var(--color-base);
font-size: var(--font-size-nm); font-size: var(--font-size-nm);
width: 100%; width: 100%;
@@ -190,7 +190,7 @@ svg {
:deep(.apexcharts-yaxistooltip) { :deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important; background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important; border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-button-bg) !important; border: 1px solid var(--color-divider) !important;
box-shadow: var(--shadow-floating) !important; box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important; font-size: var(--font-size-nm) !important;
} }

View File

@@ -151,21 +151,10 @@ const visible = ref(false)
const scrollContainer = ref<HTMLElement | null>(null) const scrollContainer = ref<HTMLElement | null>(null)
const { showTopFade, showBottomFade, checkScrollState } = useScrollIndicator(scrollContainer) const { showTopFade, showBottomFade, checkScrollState } = useScrollIndicator(scrollContainer)
// make modal opening not shift page when there's a vertical scrollbar
function addBodyPadding() {
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth
if (scrollBarWidth > 0) {
document.body.style.paddingRight = `${scrollBarWidth}px`
} else {
document.body.style.paddingRight = ''
}
}
function show(event?: MouseEvent) { function show(event?: MouseEvent) {
props.onShow?.() props.onShow?.()
open.value = true open.value = true
addBodyPadding()
document.body.style.overflow = 'hidden' document.body.style.overflow = 'hidden'
window.addEventListener('mousedown', updateMousePosition) window.addEventListener('mousedown', updateMousePosition)
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
@@ -184,7 +173,6 @@ function hide() {
props.onHide?.() props.onHide?.()
visible.value = false visible.value = false
document.body.style.overflow = '' document.body.style.overflow = ''
document.body.style.paddingRight = ''
window.removeEventListener('mousedown', updateMousePosition) window.removeEventListener('mousedown', updateMousePosition)
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
setTimeout(() => { setTimeout(() => {

View File

@@ -50,7 +50,7 @@
<template v-for="(version, index) in currentVersions" :key="index"> <template v-for="(version, index) in currentVersions" :key="index">
<!-- Row divider --> <!-- Row divider -->
<div <div
class="versions-grid-row h-px w-full bg-button-bg" class="versions-grid-row h-px w-full bg-surface-5"
:class="{ :class="{
'max-sm:!hidden': index === 0, 'max-sm:!hidden': index === 0,
}" }"