App redesign (#2946)

* Start of app redesign

* format

* continue progress

* Content page nearly done

* Fix recursion issues with content page

* Fix update all alignment

* Discover page progress

* Settings progress

* Removed unlocked-size hack that breaks web

* Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues

* Ads + run prettier

* Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor

* fix ads not hiding when plus log in

* rev lockfile changes/conflicts

* Fix sign in page

* Add generated

* (mostly) Data driven search

* Fix search mobile issue

* profile fixes

* Project versions page, fix typescript on UI lib and misc fixes

* Remove unused gallery component

* Fix linkfunction err

* Search filter controls at top, localization for locked filters

* Fix provided filter names

* Fix navigating from instance browse to main browse

* Friends frontend (#2995)

* Friends system frontend

* (almost) finish frontend

* finish friends, fix lint

* Fix lint

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>

* Refresh macOS app icon

* Update web search UI more

* Fix link opens

* Fix frontend build

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2024-12-11 19:54:18 -08:00
committed by GitHub
parent 6ec1dcf088
commit c39bb78e38
257 changed files with 15713 additions and 9475 deletions

View File

@@ -3,7 +3,7 @@
<ButtonStyled v-if="!!slots.title" :type="type">
<button class="!w-full" @click="() => (isOpen ? close() : open())">
<slot name="title" /><DropdownIcon
class="ml-auto size-5 transition-transform duration-300"
class="ml-auto size-5 text-contrast transition-transform duration-300"
:class="{ 'rotate-180': isOpen }"
/>
</button>
@@ -62,6 +62,9 @@ defineOptions({
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
animation: height-animate 500ms ease-in-out both;
content-visibility: auto;
animation-composition: replace;
}
@media (prefers-reduced-motion) {
@@ -77,4 +80,13 @@ defineOptions({
.accordion-content > div {
overflow: hidden;
}
@keyframes height-animate {
from {
block-size: initial;
}
to {
block-size: auto;
}
}
</style>

View File

@@ -4,7 +4,7 @@
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
"
>
<template v-if="color"> <span class="circle" /> {{ $capitalizeString(type) }}</template>
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
<!-- User roles -->
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
@@ -36,25 +36,28 @@
<template v-else-if="type === 'closed'"> <CloseIcon /> Closed</template>
<!-- Other -->
<template v-else> <span class="circle" /> {{ $capitalizeString(type) }} </template>
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
</span>
</template>
<script setup>
import { GlobeIcon, LinkIcon } from "@modrinth/assets";
import ModrinthIcon from "~/assets/images/logo.svg?component";
import PlusIcon from "~/assets/images/utils/plus.svg?component";
import ModeratorIcon from "~/assets/images/sidebar/admin.svg?component";
import CreatorIcon from "~/assets/images/utils/box.svg?component";
import DraftIcon from "~/assets/images/utils/file-text.svg?component";
import CrossIcon from "~/assets/images/utils/x.svg?component";
import ArchiveIcon from "~/assets/images/utils/archive.svg?component";
import ProcessingIcon from "~/assets/images/utils/updated.svg?component";
import CheckIcon from "~/assets/images/utils/check.svg?component";
import LockIcon from "~/assets/images/utils/lock.svg?component";
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
import {
GlobeIcon,
LinkIcon,
ModrinthIcon,
PlusIcon,
ScaleIcon as ModeratorIcon,
BoxIcon as CreatorIcon,
FileTextIcon as DraftIcon,
XIcon as CrossIcon,
ArchiveIcon,
UpdatedIcon as ProcessingIcon,
CheckIcon,
LockIcon,
CalendarIcon,
XCircleIcon as CloseIcon,
} from "@modrinth/assets";
import { capitalizeString } from "@modrinth/utils";
defineProps({
type: {

View File

@@ -1,7 +1,7 @@
<template>
<nav
ref="scrollContainer"
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<NuxtLink
v-for="(link, index) in filteredLinks"
@@ -11,7 +11,7 @@
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
:class="{
'text-brand': activeIndex === index && !subpageSelected,
'text-button-textSelected': activeIndex === index && !subpageSelected,
'text-contrast': activeIndex === index && subpageSelected,
}"
>
@@ -20,7 +20,7 @@
</NuxtLink>
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-brand-highlight'
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
}`"
:style="{
left: sliderLeftPx,
@@ -161,4 +161,8 @@ watch(
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
.card-shadow {
box-shadow: var(--shadow-card);
}
</style>

View File

@@ -1,220 +0,0 @@
<template>
<div class="experimental-styles-within flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<ManySelect
v-model="selectedPlatforms"
:options="filterOptions.platform"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Platform
<template #option="{ option }">
{{ formatCategory(option) }}
</template>
</ManySelect>
<ManySelect
v-model="selectedGameVersions"
:options="filterOptions.gameVersion"
search
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Game versions
<template #footer>
<Checkbox v-model="showSnapshots" class="mx-1" :label="`Show all versions`" />
</template>
</ManySelect>
<ManySelect
v-model="selectedChannels"
:options="filterOptions.channel"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Channels
<template #option="{ option }">
{{ option === "release" ? "Release" : option === "beta" ? "Beta" : "Alpha" }}
</template>
</ManySelect>
</div>
<div class="flex flex-wrap items-center gap-1 empty:hidden">
<button
v-if="selectedChannels.length + selectedGameVersions.length + selectedPlatforms.length > 1"
class="tag-list__item text-contrast transition-transform active:scale-[0.95]"
@click="clearFilters"
>
<XCircleIcon />
Clear all filters
</button>
<button
v-for="channel in selectedChannels"
:key="`remove-filter-${channel}`"
class="tag-list__item transition-transform active:scale-[0.95]"
:style="`--_color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'});--_bg-color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'}-highlight)`"
@click="toggleFilter('channel', channel)"
>
<XIcon />
{{ channel.slice(0, 1).toUpperCase() + channel.slice(1) }}
</button>
<button
v-for="version in selectedGameVersions"
:key="`remove-filter-${version}`"
class="tag-list__item transition-transform active:scale-[0.95]"
@click="toggleFilter('gameVersion', version)"
>
<XIcon />
{{ version }}
</button>
<button
v-for="platform in selectedPlatforms"
:key="`remove-filter-${platform}`"
class="tag-list__item transition-transform active:scale-[0.95]"
:style="`--_color: var(--color-platform-${platform})`"
@click="toggleFilter('platform', platform)"
>
<XIcon />
{{ formatCategory(platform) }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { FilterIcon, XCircleIcon, XIcon } from "@modrinth/assets";
import { ManySelect, Checkbox } from "@modrinth/ui";
import { formatCategory } from "@modrinth/utils";
import type { ModrinthVersion } from "@modrinth/utils";
const props = defineProps<{ versions: ModrinthVersion[] }>();
const emit = defineEmits(["switch-page"]);
const allChannels = ref(["release", "beta", "alpha"]);
const route = useNativeRoute();
const router = useNativeRouter();
const tags = useTags();
const showSnapshots = ref(false);
type FilterType = "channel" | "gameVersion" | "platform";
type Filter = string;
const filterOptions = computed(() => {
const filters: Record<FilterType, Filter[]> = {
channel: [],
gameVersion: [],
platform: [],
};
const platformSet = new Set();
const gameVersionSet = new Set();
const channelSet = new Set();
for (const version of props.versions) {
for (const loader of version.loaders) {
platformSet.add(loader);
}
for (const gameVersion of version.game_versions) {
gameVersionSet.add(gameVersion);
}
channelSet.add(version.version_type);
}
if (channelSet.size > 0) {
filters.channel = Array.from(channelSet) as Filter[];
filters.channel.sort((a, b) => allChannels.value.indexOf(a) - allChannels.value.indexOf(b));
}
if (gameVersionSet.size > 0) {
const gameVersions = tags.value.gameVersions.filter((x) => gameVersionSet.has(x.version));
filters.gameVersion = gameVersions
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
.map((x) => x.version);
}
if (platformSet.size > 0) {
filters.platform = Array.from(platformSet) as Filter[];
}
return filters;
});
const selectedChannels = ref<string[]>([]);
const selectedGameVersions = ref<string[]>([]);
const selectedPlatforms = ref<string[]>([]);
selectedChannels.value = route.query.c ? getArrayOrString(route.query.c) : [];
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
async function toggleFilters(type: FilterType, filters: Filter[]) {
for (const filter of filters) {
await toggleFilter(type, filter);
}
await router.replace({
query: {
...route.query,
c: selectedChannels.value,
g: selectedGameVersions.value,
l: selectedPlatforms.value,
},
});
emit("switch-page", 1);
}
async function toggleFilter(type: FilterType, filter: Filter, skipRouter = false) {
if (type === "channel") {
selectedChannels.value = selectedChannels.value.includes(filter)
? selectedChannels.value.filter((x) => x !== filter)
: [...selectedChannels.value, filter];
} else if (type === "gameVersion") {
selectedGameVersions.value = selectedGameVersions.value.includes(filter)
? selectedGameVersions.value.filter((x) => x !== filter)
: [...selectedGameVersions.value, filter];
} else if (type === "platform") {
selectedPlatforms.value = selectedPlatforms.value.includes(filter)
? selectedPlatforms.value.filter((x) => x !== filter)
: [...selectedPlatforms.value, filter];
}
if (!skipRouter) {
await updateFilters();
}
}
async function updateFilters() {
await router.replace({
query: {
...route.query,
c: selectedChannels.value,
g: selectedGameVersions.value,
l: selectedPlatforms.value,
},
});
emit("switch-page", 1);
}
async function clearFilters() {
selectedChannels.value = [];
selectedGameVersions.value = [];
selectedPlatforms.value = [];
await router.replace({
query: {
...route.query,
c: undefined,
g: undefined,
l: undefined,
},
});
emit("switch-page", 1);
}
defineExpose({
toggleFilter,
toggleFilters,
});
</script>

View File

@@ -1,46 +0,0 @@
<template>
<div
class="grid grid-cols-[min-content_auto_min-content_min-content] items-center gap-2 rounded-2xl border-[1px] border-button-bg bg-bg p-2"
>
<VersionChannelIndicator :channel="version.version_type" />
<div class="flex min-w-0 flex-col gap-1">
<h1 class="my-0 truncate text-nowrap text-base font-extrabold leading-none text-contrast">
{{ version.version_number }}
</h1>
<p class="m-0 truncate text-nowrap text-xs font-semibold text-secondary">
{{ version.name }}
</p>
</div>
<ButtonStyled color="brand">
<a :href="downloadUrl" class="min-w-0" @click="emit('onDownload')">
<DownloadIcon aria-hidden="true" /> Download
</a>
</ButtonStyled>
<ButtonStyled circular>
<nuxt-link
:to="`/project/${props.version.project_id}/version/${props.version.id}`"
class="min-w-0"
aria-label="Open project page"
@click="emit('onNavigate')"
>
<ExternalIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, VersionChannelIndicator } from "@modrinth/ui";
import { DownloadIcon, ExternalIcon } from "@modrinth/assets";
const props = defineProps<{
version: Version;
}>();
const downloadUrl = computed(() => {
const primary: VersionFile = props.version.files.find((x) => x.primary) || props.version.files[0];
return primary.url;
});
const emit = defineEmits(["onDownload", "onNavigate"]);
</script>

View File

@@ -108,7 +108,7 @@
type="search"
name="search"
autocomplete="off"
class="h-8 min-h-[unset] w-full border-[1px] border-solid border-button-bg bg-transparent py-2 pl-9"
class="h-8 min-h-[unset] w-full border-[1px] border-solid border-divider bg-transparent py-2 pl-9"
placeholder="Search..."
@input="$emit('update:searchQuery', ($event.target as HTMLInputElement).value)"
/>

View File

@@ -25,7 +25,7 @@
</div>
<div
class="border-0 border-b border-solid"
:class="danger ? 'border-[#cb2245] dark:border-[#612d38]' : 'border-button-bg'"
:class="danger ? 'border-[#cb2245] dark:border-[#612d38]' : 'border-divider'"
></div>
<div class="mt-2 h-full w-full overflow-auto px-6">
<slot />

View File

@@ -25,7 +25,7 @@
v-if="isOpen"
ref="menuRef"
data-pyro-telepopover-root
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-button-bg bg-bg-raised p-2 shadow-lg"
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-divider bg-bg-raised p-2 shadow-lg"
:style="menuStyle"
role="menu"
tabindex="-1"
@@ -272,7 +272,7 @@ const handleItemClick = (option: Option, index: number) => {
const handleMouseOver = (index: number) => {
selectedIndex.value = index;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
};
// Scrolling is disabled for keyboard navigation
@@ -295,7 +295,7 @@ const enableBodyScroll = () => {
const focusFirstMenuItem = () => {
if (menuItemsRef.value.length > 0) {
menuItemsRef.value[0].focus();
menuItemsRef.value[0].focus?.();
}
};
@@ -312,26 +312,26 @@ const handleKeydown = (event: KeyboardEvent) => {
case "ArrowDown":
event.preventDefault();
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
break;
case "ArrowUp":
event.preventDefault();
selectedIndex.value =
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
break;
case "Home":
event.preventDefault();
if (menuItemsRef.value.length > 0) {
selectedIndex.value = 0;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
case "End":
event.preventDefault();
if (menuItemsRef.value.length > 0) {
selectedIndex.value = filteredOptions.value.length - 1;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
case "Enter":
@@ -344,7 +344,7 @@ const handleKeydown = (event: KeyboardEvent) => {
case "Escape":
event.preventDefault();
closeMenu();
triggerRef.value?.focus();
triggerRef.value?.focus?.();
break;
case "Tab":
event.preventDefault();
@@ -355,7 +355,7 @@ const handleKeydown = (event: KeyboardEvent) => {
} else {
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
}
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
default:
@@ -366,7 +366,7 @@ const handleKeydown = (event: KeyboardEvent) => {
);
if (matchIndex !== -1) {
selectedIndex.value = matchIndex;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
if (typeAheadTimeout.value) {
clearTimeout(typeAheadTimeout.value);

View File

@@ -8,10 +8,9 @@
}"
>
<template v-if="members[message.author_id]">
<ConditionalNuxtLink
<AutoLink
class="message__icon"
:is-link="!noLinks"
:to="`/user/${members[message.author_id].username}`"
:to="noLinks ? '' : `/user/${members[message.author_id].username}`"
tabindex="-1"
aria-hidden="true"
>
@@ -21,19 +20,16 @@
circle
:raised="raised"
/>
</ConditionalNuxtLink>
</AutoLink>
<span :class="`message__author role-${members[message.author_id].role}`">
<LockIcon
v-if="message.body.private"
v-tooltip="'Only visible to moderators'"
class="private-icon"
/>
<ConditionalNuxtLink
:is-link="!noLinks"
:to="`/user/${members[message.author_id].username}`"
>
<AutoLink :to="noLinks ? '' : `/user/${members[message.author_id].username}`">
{{ members[message.author_id].username }}
</ConditionalNuxtLink>
</AutoLink>
<ScaleIcon v-if="members[message.author_id].role === 'moderator'" v-tooltip="'Moderator'" />
<ModrinthIcon
v-else-if="members[message.author_id].role === 'admin'"
@@ -107,7 +103,7 @@ import {
ModrinthIcon,
ScaleIcon,
} from "@modrinth/assets";
import { OverflowMenu, ConditionalNuxtLink } from "@modrinth/ui";
import { AutoLink, OverflowMenu } from "@modrinth/ui";
import { renderString } from "@modrinth/utils";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
@@ -287,7 +283,7 @@ a:active + .message__author a,
}
.moderation-color,
role-moderator {
.role-moderator {
color: var(--color-orange);
}