feat: app onboarding and library (#6923)
* refactor: remove unused legacy onboarded flag * sqlx prepare * feat: add onboarding checklist app db and add vue provider * feat: add welcome page * feat: add onboarding checklist component * feat: move checklist visibility source of truth in app db * feat: split up import modpack button * feat-small: style tweak * feat: remove discover section from homepage and add library * refactor: move welcome screen to component * refactor: move grid display into /library * refactor: rename existing components * refactor: split up the current library page files * feat: add create group modal * feat: add instance group definitions * feat: update accordian for instance groups * feat: add delete group * feat: implement rename instance groups * refactor: move instance group api out * feat: add context menu for groups * feat: style instance items in library * feat: add selected instance item state * feat: implement action bar for selection * feat: drag and drop to move between groups * feat: show ungrouped with header * feat: improve edit group name * small fix * small style fixes * feat: improve selection behaviour * fix: dont toggle group when context menu open * feat: drag and drop multiple selected * small * feat: add remove from group in action bar * feat: add delay on icon hiding in inline input * feat: improve icon hiding delay * fix: bad routes.js * feat: implement sort and filter options * refactor: dont prefix filenames * feat: change open/collapse group target * feat: improve ungrouped behaviour * feat: add shift to select multiple instances * fix: icon disappear transition * feat: add esc while dragging to cancel * feat: update sorting and moved label * feat: update context menu * fix: multiple context menus can be opened * refactor: instance groups to be identified by ID instead of name, names become display data * feat: creating new group always adds instance * feat: create group button, fix instance selection, and increase group name max length * feat: strokes * fix: show getting started checklist in all sidebar * feat: set size on microsoft login screen * fix: use ButtonStyled in Chips component * feat: creating groups with selected instances and fix some moving selection and duplication bugs * pnpm prepr * feat: add tooltip for instance already in group * feat: create instance with content in creation flow modal - search modpack becomes search projects - creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal) * feat: update instance card styles * refactor: separate out instance card content * update styles * refactor: instance card * fix: width of instance * feat: update empty state * feat: update sort and grouping dropdowns * feat: better context menu options * feat: add group action buttons * feat: add/remove favourites group * remove manual sort * feat: improve the "New group" jank * feat: add group by instance type and change group sorting * feat: welcome screen styles match figma * feat: not signed in skins demo * feat: new hosting empty state * feat: new instance card style * feat: add tooltip and spice up animation more * feat: improve the transitions for groups after dropping many instances * feat: set instance group membership for bulk moving * feat: remove fade out * fix: allow dragging from buttons inside instance card * fix: library page load in behaviour * small fix * feat: add group ordering and reordering * feat: add drag to reorder groups * fix: switching page not clearing selected instances * style fixes * style fix * fix spacing * feat: jump in changes * feat: add styled newly created instances * feat: improve jump in cards styles * fix: welcome screen style * fix: empty state styles * feat: max 5 jump in items * feat: max new instances to 3 * fix: search icon * feat: clear selection after move * feat: skins list use check icon styles * feat: list will be deleted instances and default sort last played * fix: stroke * remove: no more group settings in instance general settings * fix: resolving modpack server install * feat: icon editor * feat: add three hue shifted variants * fix: surprise me doesnt pick symbol * feat: add customize icon to create instance modal * feat: skins loading state style * fix: welcome logo * fix: copy * fix: left over usage of ButtonStyled from main's refactor * feat: some leaked merge stuff * feat: server empty state styles * remove: instance helper text * remove: auto link in set up stage * feat: auto focus on search when modal open * fix: delete instance modal style * remove: modpack meta repeating * fix: icon * fix: font weight * fix: dropdown filter preview heights * pnpm prepr * fix: sign into modrinth flashing change in position * feat: add avatar transparent corner gets padding * feat: drag and hold over group opens group * feat: add random icon for custom instance * feat: update loading icon * polish: empty state for search * polish: jump in cards * fix: avatar could skip checking for image if tauri loads it first * feat: add icon customize and randomizer in content install modal for add to new instances * fix: avatar cannot read image pixels due to cors * refactor: popup notifications toast type for instance sharing notifications * feat: apply random icons modal and add popup notification for it * polish: style * fix: set_webview_visible * QA * qa * feat: add inner white 15 opacity stroke around avatar * add to catalog * pnpm prepr * fix: app event import * feat: add new icons and backgrounds for icon editor * feat: recents list generate from randomized icons and save icon in editor + icon editor modal fixes * qa * refactor: recipe -> config * fix: event bus * polish * feat: add safe guard against randomizing empty icons * feat: navigate to instance after creation * fix: icon file names * i18n on welcome screen * i18n pass on PR * fix: inconsistent max group name length * refactor: use sqlx macro * feat: add instance icon background selection drag * feat: update catalog order * fix: remove gradient for welcome screen * remove: frog * fix: min width * feat: update corner size * fix: context menu styles * prepr * fix: instance name trimming * reorder * fix: instance card loading state stayed until hovering * feat: add icon randomizer blacklist * blog * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
@@ -9,7 +9,7 @@
|
||||
:initial-loader="initialLoader"
|
||||
:initial-game-version="initialGameVersion"
|
||||
:fade="props.initialSetup ? undefined : 'danger'"
|
||||
:search-modpacks="searchModpacks"
|
||||
:search-projects="searchModpacks"
|
||||
:get-project-versions="getProjectVersions"
|
||||
:finish-disabled="!canCompleteSetup"
|
||||
:finish-disabled-tooltip="!canCompleteSetup ? permissionDeniedMessage : undefined"
|
||||
|
||||
@@ -6,7 +6,7 @@ export type {
|
||||
Gamemode,
|
||||
GeneratorSettingsMode,
|
||||
LoaderVersionType,
|
||||
ModpackSearchResult,
|
||||
ProjectSearchResult,
|
||||
SetupType,
|
||||
} from '../../flows/creation-flow-modal/creation-flow-context'
|
||||
export { default as CreationFlowModal } from '../../flows/creation-flow-modal/index.vue'
|
||||
|
||||
|
After Width: | Height: | Size: 551 B |
@@ -1,139 +1,74 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-8 items-center justify-center py-10 max-w-[760px]">
|
||||
<!-- Left column -->
|
||||
<div class="flex flex-col gap-8 items-start pr-8 shrink-0">
|
||||
<!-- Heading -->
|
||||
<div class="flex flex-col gap-2 items-start w-[300px]">
|
||||
<p class="text-3xl leading-9 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.modrinthHostingLabel) }}
|
||||
</p>
|
||||
<p class="text-base font-normal text-primary">
|
||||
{{ formatMessage(messages.noServersDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature list -->
|
||||
<div class="flex flex-col gap-4 items-start w-full">
|
||||
<div class="flex gap-3 items-start">
|
||||
<div
|
||||
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center"
|
||||
>
|
||||
<PackageOpenIcon class="size-5 text-secondary" aria-hidden="true" />
|
||||
<div class="flex w-full max-w-[56rem] h-full flex-col items-stretch justify-between gap-8 py-6">
|
||||
<div class="flex w-full flex-wrap grow items-center justify-center gap-2">
|
||||
<div class="mx-auto flex w-full max-w-[20rem] flex-col items-start gap-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="m-0 text-3xl font-semibold leading-9 text-contrast">
|
||||
{{ formatMessage(messages.modrinthHostingLabel) }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-base font-semibold text-contrast">
|
||||
{{ formatMessage(messages.oneClickModInstallsTitle) }}
|
||||
</p>
|
||||
<p class="text-base font-normal text-primary">
|
||||
{{ formatMessage(messages.oneClickModInstallsDescription) }}
|
||||
</p>
|
||||
<p class="m-0 text-base text-primary">
|
||||
{{ formatMessage(messages.noServersDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-6">
|
||||
<div v-for="feature in features" :key="feature.key" class="flex items-start gap-4">
|
||||
<div
|
||||
class="feature-icon relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[0.875rem] border border-solid bg-surface-1 text-brand"
|
||||
>
|
||||
<div class="feature-icon-gradient absolute left-[-1px] top-[-1px] size-[6.25rem]" />
|
||||
<div class="feature-icon-shade absolute left-[-1px] top-[-1px] size-[6.25rem]" />
|
||||
<img
|
||||
:src="iconTexture"
|
||||
alt=""
|
||||
class="absolute left-1/2 top-1/2 h-[6.25rem] w-[9.8125rem] max-w-none -translate-x-1/2 -translate-y-1/2 object-cover opacity-40 mix-blend-luminosity"
|
||||
/>
|
||||
<component
|
||||
:is="feature.icon"
|
||||
class="feature-icon-glyph relative size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<p class="m-0 text-lg font-semibold leading-6 text-contrast">
|
||||
{{ feature.title }}
|
||||
</p>
|
||||
<p class="m-0 text-sm leading-5 text-primary">
|
||||
{{ feature.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 items-start">
|
||||
<div
|
||||
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<GlobeIcon class="size-5 text-secondary" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-base font-semibold text-contrast">
|
||||
{{ formatMessage(messages.simpleSetupTitle) }}
|
||||
</p>
|
||||
<p class="text-base font-normal text-primary">
|
||||
{{ formatMessage(messages.simpleSetupDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 items-start">
|
||||
<div
|
||||
class="bg-surface-4 border border-solid border-surface-5 rounded-full shrink-0 size-8 flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<UsersIcon class="size-5 text-secondary" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-base font-semibold text-contrast">
|
||||
{{ formatMessage(messages.playWithFriendsTitle) }}
|
||||
</p>
|
||||
<p class="text-base font-normal text-primary">
|
||||
{{ formatMessage(messages.playWithFriendsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA section -->
|
||||
<div class="flex flex-col gap-6 items-start">
|
||||
<div class="flex flex-col gap-3 items-start">
|
||||
<Button type="colored" color="brand" @click="onClickNewServer?.()">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<Button type="colored" color="brand" size="lg" @click="onClickNewServer?.()">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.newServerButton) }}
|
||||
</Button>
|
||||
|
||||
<AutoLink
|
||||
to="https://modrinth.com/hosting"
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 hover:brightness-125"
|
||||
class="flex items-center gap-1 hover:brightness-125 font-semibold"
|
||||
>
|
||||
{{ formatMessage(messages.learnMoreLink) }}
|
||||
<RightArrowIcon class="size-5 shrink-0" aria-hidden="true" />
|
||||
</AutoLink>
|
||||
</div>
|
||||
|
||||
<template v-if="!loggedIn">
|
||||
<div class="h-px w-full bg-surface-5" />
|
||||
|
||||
<div class="flex gap-3 items-center flex-wrap">
|
||||
<p class="text-base font-normal text-primary">
|
||||
{{ formatMessage(messages.alreadyHaveServerLabel) }}
|
||||
</p>
|
||||
<Button @click="onClickSignIn?.()">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ServerListEmptyPreview />
|
||||
</div>
|
||||
|
||||
<!-- Right column - mod icon grid -->
|
||||
<div
|
||||
class="relative flex h-[617px] shrink-0 items-center justify-center overflow-hidden rounded-[40px] pointer-events-none select-none [mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_35%,transparent_100%)]"
|
||||
>
|
||||
<div class="rotate-[15deg]">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="row in GRID_ROWS"
|
||||
:key="row"
|
||||
class="flex gap-4 items-center shrink-0"
|
||||
:class="animated ? (row % 2 === 1 ? 'drift-left' : 'drift-right relative left-14') : ''"
|
||||
>
|
||||
<div class="hidden drift-right drift-left"></div>
|
||||
<div
|
||||
v-for="col in GRID_COLS"
|
||||
:key="col"
|
||||
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
|
||||
>
|
||||
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
v-for="col in GRID_COLS"
|
||||
:key="col"
|
||||
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
|
||||
>
|
||||
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
v-for="col in GRID_COLS"
|
||||
:key="col"
|
||||
class="border border-surface-5 rounded-[20px] shrink-0 size-[112px] bg-surface-4 overflow-hidden"
|
||||
>
|
||||
<img :src="getGridImage(row - 1, col - 1)" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!loggedIn" class="flex flex-col items-center gap-4 text-center">
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ formatMessage(messages.alreadyHaveServerLabel) }}
|
||||
</p>
|
||||
<Button @click="onClickSignIn?.()">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -147,46 +82,21 @@ import {
|
||||
RightArrowIcon,
|
||||
UsersIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { AutoLink } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import iconTexture from '#ui/assets/welcome/icon-texture.png'
|
||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||
import { Button } from '#ui/components/base/buttons'
|
||||
|
||||
import { defineMessages, useVIntl } from '../../../composables/i18n'
|
||||
import imgAircraft from './grid-images/aircraft.png'
|
||||
import imgAlexs from "./grid-images/alex's.png"
|
||||
import imgArtifacts from './grid-images/artifacts.png'
|
||||
import imgBiomes from './grid-images/biomes.png'
|
||||
import imgCatac from './grid-images/catac.png'
|
||||
import imgCobble from './grid-images/cobble.png'
|
||||
import imgComforts from './grid-images/comforts.png'
|
||||
import imgCreate from './grid-images/create.png'
|
||||
import imgCreate1 from './grid-images/create1.png'
|
||||
import imgCreate2 from './grid-images/create2.png'
|
||||
import imgCreate3 from './grid-images/create3.png'
|
||||
import imgCreeper from './grid-images/creeper.png'
|
||||
import imgFriends from './grid-images/friends.png'
|
||||
import imgGeo from './grid-images/geo.png'
|
||||
import imgNaturalist from './grid-images/naturalist.png'
|
||||
import imgSeasons from './grid-images/seasons.png'
|
||||
import imgTravellers from './grid-images/travellers.png'
|
||||
import imgTree from './grid-images/tree.png'
|
||||
import imgYum1 from './grid-images/yum1.png'
|
||||
import imgYum2 from './grid-images/yum2.png'
|
||||
import imgYum3 from './grid-images/yum3.png'
|
||||
import imgYung from './grid-images/yung.png'
|
||||
import ServerListEmptyPreview from './ServerListEmptyPreview.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
animated?: boolean
|
||||
onClickNewServer?: () => void
|
||||
onClickSignIn?: () => void
|
||||
loggedIn?: boolean
|
||||
}>(),
|
||||
{ animated: false },
|
||||
)
|
||||
defineProps<{
|
||||
onClickNewServer?: () => void
|
||||
onClickSignIn?: () => void
|
||||
loggedIn?: boolean
|
||||
}>()
|
||||
|
||||
const GRID_ROWS = 6
|
||||
const GRID_COLS = 5
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -194,10 +104,6 @@ const messages = defineMessages({
|
||||
id: 'servers.list-empty.modrinth-hosting-label',
|
||||
defaultMessage: 'Modrinth Hosting',
|
||||
},
|
||||
noServersTitle: {
|
||||
id: 'servers.list-empty.no-servers-title',
|
||||
defaultMessage: 'No servers yet',
|
||||
},
|
||||
noServersDescription: {
|
||||
id: 'servers.list-empty.no-servers-description',
|
||||
defaultMessage: 'Install mods, invite friends, and play together all from the Modrinth App.',
|
||||
@@ -224,7 +130,7 @@ const messages = defineMessages({
|
||||
},
|
||||
playWithFriendsDescription: {
|
||||
id: 'servers.list-empty.play-with-friends-description',
|
||||
defaultMessage: 'Invite friends and get them set up right in the Modrinth App.',
|
||||
defaultMessage: 'Invite friends to play your server with one-click play from the Modrinth App.',
|
||||
},
|
||||
newServerButton: {
|
||||
id: 'servers.list-empty.new-server-button',
|
||||
@@ -232,7 +138,7 @@ const messages = defineMessages({
|
||||
},
|
||||
learnMoreLink: {
|
||||
id: 'servers.list-empty.learn-more-link',
|
||||
defaultMessage: 'Learn more about Modrinth Hosting',
|
||||
defaultMessage: 'Learn more',
|
||||
},
|
||||
alreadyHaveServerLabel: {
|
||||
id: 'servers.list-empty.already-have-server-label',
|
||||
@@ -240,70 +146,55 @@ const messages = defineMessages({
|
||||
},
|
||||
signInButton: {
|
||||
id: 'servers.list-empty.sign-in-button',
|
||||
defaultMessage: 'Sign in',
|
||||
defaultMessage: 'Sign in to Modrinth',
|
||||
},
|
||||
})
|
||||
|
||||
const GRID_IMAGES = [
|
||||
imgYum1,
|
||||
imgYum2,
|
||||
imgYum3,
|
||||
imgYung,
|
||||
imgCreeper,
|
||||
imgFriends,
|
||||
imgNaturalist,
|
||||
imgBiomes,
|
||||
imgCatac,
|
||||
imgCobble,
|
||||
imgGeo,
|
||||
imgCreate,
|
||||
imgCreate1,
|
||||
imgCreate2,
|
||||
imgCreate3,
|
||||
imgAircraft,
|
||||
imgArtifacts,
|
||||
imgComforts,
|
||||
imgTravellers,
|
||||
imgAlexs,
|
||||
imgSeasons,
|
||||
imgTree,
|
||||
]
|
||||
|
||||
function getGridImage(row: number, col: number): string {
|
||||
return GRID_IMAGES[(row * GRID_COLS + col) % GRID_IMAGES.length]
|
||||
}
|
||||
const features = computed(() => [
|
||||
{
|
||||
key: 'one-click-mod-installs',
|
||||
icon: PackageOpenIcon,
|
||||
title: formatMessage(messages.oneClickModInstallsTitle),
|
||||
description: formatMessage(messages.oneClickModInstallsDescription),
|
||||
},
|
||||
{
|
||||
key: 'simple-setup',
|
||||
icon: GlobeIcon,
|
||||
title: formatMessage(messages.simpleSetupTitle),
|
||||
description: formatMessage(messages.simpleSetupDescription),
|
||||
},
|
||||
{
|
||||
key: 'play-with-friends',
|
||||
icon: UsersIcon,
|
||||
title: formatMessage(messages.playWithFriendsTitle),
|
||||
description: formatMessage(messages.playWithFriendsDescription),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p {
|
||||
margin: 0;
|
||||
.feature-icon {
|
||||
border-color: color-mix(in srgb, var(--color-text-primary) 10%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--color-brand) 30%, var(--surface-1)),
|
||||
var(--shadow-card),
|
||||
0 0 3.75rem color-mix(in srgb, var(--color-brand) 10%, transparent);
|
||||
}
|
||||
|
||||
@keyframes drift-right {
|
||||
from {
|
||||
transform: translateX(-33%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
.feature-icon-gradient {
|
||||
background: linear-gradient(180deg, var(--color-green-800) 0%, var(--color-green-950) 100%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes drift-left {
|
||||
from {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-33%);
|
||||
}
|
||||
.feature-icon-shade {
|
||||
background: linear-gradient(
|
||||
-14deg,
|
||||
color-mix(in srgb, var(--color-green-950) 37%, transparent) 8%,
|
||||
transparent 86%
|
||||
);
|
||||
}
|
||||
|
||||
.drift-left {
|
||||
animation: drift-left linear infinite alternate;
|
||||
animation-duration: 400s;
|
||||
}
|
||||
|
||||
.drift-right {
|
||||
animation: drift-right linear infinite alternate;
|
||||
animation-duration: 400s;
|
||||
.feature-icon-glyph {
|
||||
filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--color-green-950) 12%, transparent));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div
|
||||
inert
|
||||
aria-hidden="true"
|
||||
class="relative mx-auto h-[38rem] w-full max-w-[25rem] select-none -mb-10"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 h-full overflow-hidden rounded-2xl border border-solid border-surface-3 bg-surface-2 shadow-card"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between border-0 border-b border-solid border-surface-3 p-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-contrast">
|
||||
<UserPlusIcon class="size-5 text-primary" />
|
||||
<span class="text-lg font-semibold">{{ formatMessage(messages.heading) }}</span>
|
||||
</div>
|
||||
<XIcon class="size-5 text-secondary" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 border-0 border-b border-solid border-surface-3 p-4">
|
||||
<div
|
||||
class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-xl bg-surface-3 px-3 text-sm text-secondary"
|
||||
>
|
||||
<SearchIcon class="size-4 shrink-0" />
|
||||
<span class="truncate text-secondary">{{
|
||||
formatMessage(messages.searchPlaceholder)
|
||||
}}</span>
|
||||
</div>
|
||||
<Button type="colored" color="brand" class="!cursor-default" disabled>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.addButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-1 py-3">
|
||||
<p class="m-0 pb-2 text-sm font-semibold text-primary px-4">
|
||||
{{ formatMessage(messages.friendsHeading, { count: totalFriendCount }) }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(friend, index) in friends"
|
||||
:key="friend.username"
|
||||
class="relative flex h-11 items-center justify-between gap-3 px-4"
|
||||
:class="[index > 4 ? 'opacity-40' : '', friend.showPointer ? 'bg-surface-2' : '']"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="relative flex shrink-0">
|
||||
<Avatar :src="friend.avatarUrl" size="1.5rem" class="!border-none" circle no-shadow />
|
||||
<span
|
||||
v-if="friend.presence"
|
||||
class="absolute bottom-0 right-[-1px] size-2 rounded-full border border-solid border-surface-1"
|
||||
:class="friend.presence === 'online' ? 'bg-brand' : 'bg-blue'"
|
||||
/>
|
||||
</div>
|
||||
<span class="truncate text-base font-medium text-primary">{{ friend.username }}</span>
|
||||
</div>
|
||||
<Button
|
||||
:type="friend.status === 'cancel' ? 'outlined' : 'base'"
|
||||
class="!cursor-default !font-medium !h-8"
|
||||
size="md"
|
||||
:disabled="friend.status === 'added'"
|
||||
:class="friend.status === 'added' ? '' : 'w-20'"
|
||||
>
|
||||
<CheckIcon v-if="friend.status === 'added'" aria-hidden="true" />
|
||||
{{ friendStatusLabel(friend.status) }}
|
||||
</Button>
|
||||
<div
|
||||
v-if="friend.showPointer"
|
||||
class="absolute right-[14.25rem] top-9 z-10 flex size-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 opacity-75"
|
||||
>
|
||||
<img :src="pointerIcon" alt="" class="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 border-0 border-t border-solid border-surface-3 bg-surface-2 p-4"
|
||||
>
|
||||
<p class="m-0 pb-2 text-base font-semibold text-contrast">
|
||||
{{ formatMessage(messages.inviteLinkHeading) }}
|
||||
</p>
|
||||
<div
|
||||
class="flex h-8 items-center justify-between gap-2 rounded-xl bg-surface-1 px-2.5 text-sm font-medium text-primary"
|
||||
>
|
||||
<span class="truncate">https://modrinth.com/server/abc123</span>
|
||||
<ClipboardCopyIcon class="size-4 shrink-0 text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 bottom-0 h-[28rem] [background:linear-gradient(to_bottom,transparent,var(--surface-1))]"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute left-[32%] top-[23rem] z-10 flex w-[21rem] max-w-[calc(100%-1rem)] gap-4 !overflow-hidden rounded-2xl border border-solid border-surface-4 bg-surface-2 px-4 py-3 shadow-card"
|
||||
>
|
||||
<div
|
||||
class="absolute top-[16px] left-[16px] size-[20px] rounded-full bg-contrast opacity-75 blur-xl"
|
||||
/>
|
||||
<div class="relative h-max">
|
||||
<Avatar :src="geometricallyAvatar" size="2.25rem" circle no-shadow />
|
||||
<span
|
||||
class="absolute bottom-0 right-[-1px] size-3 rounded-full border-2 border-solid border-surface-2 bg-brand"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-1">
|
||||
<div class="min-w-0 flex-1 text-base leading-5 text-primary">
|
||||
<p class="m-0">
|
||||
<span class="font-medium text-contrast">Geometrically</span>
|
||||
{{ formatMessage(messages.invitedYouTo) }}
|
||||
</p>
|
||||
<p class="m-0 flex items-center gap-1">
|
||||
<Avatar :src="modrinthSmpIcon" size="1.25rem" no-shadow />
|
||||
<span class="font-medium text-contrast">Modrinth SMP</span>
|
||||
<span>{{ formatMessage(messages.serverSuffix) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<XIcon class="size-5 shrink-0 text-secondary" />
|
||||
</div>
|
||||
<div class="mt-2.5 flex gap-2">
|
||||
<Button type="colored" color="brand" class="!cursor-default !font-medium">
|
||||
{{ formatMessage(messages.acceptButton) }}
|
||||
</Button>
|
||||
<Button type="outlined" class="!cursor-default !font-medium">
|
||||
{{ formatMessage(messages.declineButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
UserPlusIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
|
||||
import borisAvatar from '#ui/assets/servers/server-list-empty/boris.png'
|
||||
import fetchAvatar from '#ui/assets/servers/server-list-empty/fetch.png'
|
||||
import geometricallyAvatar from '#ui/assets/servers/server-list-empty/geometrically.png'
|
||||
import imb11Avatar from '#ui/assets/servers/server-list-empty/imb11.png'
|
||||
import joshAvatar from '#ui/assets/servers/server-list-empty/josh.png'
|
||||
import michaelAvatar from '#ui/assets/servers/server-list-empty/michael.png'
|
||||
import modrinthSmpIcon from '#ui/assets/servers/server-list-empty/modrinth-smp.png'
|
||||
import prospectorAvatar from '#ui/assets/servers/server-list-empty/prospector.png'
|
||||
import sayaAvatar from '#ui/assets/servers/server-list-empty/saya.png'
|
||||
import trumanAvatar from '#ui/assets/servers/server-list-empty/truman.png'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import Button from '#ui/components/base/buttons/Button.vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '../../../composables/i18n'
|
||||
import pointerIcon from './Pointer.png'
|
||||
|
||||
type FriendStatus = 'added' | 'cancel' | 'invite'
|
||||
type FriendPresence = 'online' | 'playing'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'sharing.invite-players-modal.heading',
|
||||
defaultMessage: 'Invite players',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
id: 'sharing.invite-players-modal.search-placeholder',
|
||||
defaultMessage: 'Enter Modrinth username',
|
||||
},
|
||||
addButton: {
|
||||
id: 'sharing.invite-players-modal.add',
|
||||
defaultMessage: 'Add',
|
||||
},
|
||||
friendsHeading: {
|
||||
id: 'sharing.invite-players-modal.friends-heading',
|
||||
defaultMessage: 'Your friends - {count}',
|
||||
},
|
||||
addedButton: {
|
||||
id: 'sharing.invite-players-modal.added',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'sharing.invite-players-modal.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
inviteButton: {
|
||||
id: 'sharing.invite-players-modal.invite',
|
||||
defaultMessage: 'Invite',
|
||||
},
|
||||
inviteLinkHeading: {
|
||||
id: 'sharing.invite-players-modal.invite-link-heading',
|
||||
defaultMessage: 'Or use an invite link',
|
||||
},
|
||||
invitedYouTo: {
|
||||
id: 'sharing.invite-players-toast.invited-you-to',
|
||||
defaultMessage: 'invited you to',
|
||||
},
|
||||
serverSuffix: {
|
||||
id: 'sharing.invite-players-toast.server-suffix',
|
||||
defaultMessage: 'server.',
|
||||
},
|
||||
acceptButton: {
|
||||
id: 'sharing.invite-players-toast.accept',
|
||||
defaultMessage: 'Accept',
|
||||
},
|
||||
declineButton: {
|
||||
id: 'sharing.invite-players-toast.decline',
|
||||
defaultMessage: 'Decline',
|
||||
},
|
||||
})
|
||||
|
||||
const friends: Array<{
|
||||
username: string
|
||||
avatarUrl: string
|
||||
status: FriendStatus
|
||||
presence?: FriendPresence
|
||||
showPointer?: boolean
|
||||
}> = [
|
||||
{ username: 'Josh', avatarUrl: joshAvatar, status: 'added' },
|
||||
{
|
||||
username: 'Prospector',
|
||||
avatarUrl: prospectorAvatar,
|
||||
status: 'invite',
|
||||
presence: 'online',
|
||||
showPointer: true,
|
||||
},
|
||||
{ username: 'Fetch', avatarUrl: fetchAvatar, status: 'cancel', presence: 'playing' },
|
||||
{ username: 'IMB11', avatarUrl: imb11Avatar, status: 'invite' },
|
||||
{ username: 'Truman', avatarUrl: trumanAvatar, status: 'invite' },
|
||||
{ username: 'Boris', avatarUrl: borisAvatar, status: 'invite' },
|
||||
{ username: 'Saya', avatarUrl: sayaAvatar, status: 'invite' },
|
||||
{ username: 'Michael', avatarUrl: michaelAvatar, status: 'invite' },
|
||||
]
|
||||
|
||||
const totalFriendCount = 11
|
||||
|
||||
function friendStatusLabel(status: FriendStatus): string {
|
||||
if (status === 'added') return formatMessage(messages.addedButton)
|
||||
if (status === 'cancel') return formatMessage(messages.cancelButton)
|
||||
return formatMessage(messages.inviteButton)
|
||||
}
|
||||
</script>
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |