Improve UI chrome

master
John Montagu, the 4th Earl of Sandvich 2024-12-21 17:18:52 -08:00
parent 95efd01eef
commit 54daa904be
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
19 changed files with 231 additions and 96 deletions

View File

@ -34,7 +34,7 @@
--surface-0: #ccd0da; --surface-0: #ccd0da;
--base: #eff1f5; --base: #eff1f5;
--base-extra: #f5f6f7; --base-0: #fcfdfe;
--mantle: #e6e9ef; --mantle: #e6e9ef;
--crust: #dce0e8; --crust: #dce0e8;
@ -58,6 +58,7 @@
--lavender: #7287fd; --lavender: #7287fd;
--accent: var(--lavender); --accent: var(--lavender);
--accent-0: color-mix(in srgb, var(--accent), var(--text) 50%);
--lavender-transparent: color-mix(in srgb, var(--lavender), transparent 80%); --lavender-transparent: color-mix(in srgb, var(--lavender), transparent 80%);
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%); --accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
--accent-transparent-50: color-mix(in srgb, var(--accent), transparent 50%); --accent-transparent-50: color-mix(in srgb, var(--accent), transparent 50%);
@ -84,18 +85,19 @@
--sapphire: #7dc4e4; --sapphire: #7dc4e4;
--blue: #8aadf4; --blue: #8aadf4;
--lavender: #b7bdf8; --lavender: #b7bdf8;
--text: #cad3f5; --text: #dad3d5;
--subtext-1: #b8c0e0; --subtext-1: #bec0c0;
--subtext-0: #a5adcb; --subtext-0: #abada5;
--overlay-2: #939ab7; --overlay-2: #9a9a9a;
--overlay-1: #8087a2; --overlay-1: #8b8b8b;
--overlay-0: #6e738d; --overlay-0: #717171;
--surface-2: #5b6078; --surface-2: #575757;
--surface-1: #494d64; --surface-1: #3f3f3f;
--surface-0: #363a4f; --surface-0: #2f2f2f;
--base: #24273a; --base: #181818;
--mantle: #1e2030; --base-0: #242424;
--crust: #181926; --mantle: #121212;
--crust: #0f0f0f;
--destructive: var(--red); --destructive: var(--red);
*/ */
} }

View File

@ -24,8 +24,8 @@ button {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: var(--text); color: var(--text);
background-color: var(--crust); background-color: var(--base-0);
border: none; border: 1px solid var(--surface-0);
padding: 6px 20px; padding: 6px 20px;
line-height: 1.6; line-height: 1.6;
border-radius: 4px; border-radius: 4px;
@ -46,6 +46,10 @@ button {
transition-duration: 200ms; transition-duration: 200ms;
} }
button.no-border {
border: none;
}
button.icon-end { button.icon-end {
justify-content: space-between; justify-content: space-between;
} }
@ -53,6 +57,7 @@ button.icon-end {
button.icon { button.icon {
background-color: transparent; background-color: transparent;
padding: 8px; padding: 8px;
border: none;
} }
button.icon:hover { button.icon:hover {
@ -94,6 +99,7 @@ button:hover {
button.accent { button.accent {
background-color: var(--accent); background-color: var(--accent);
color: var(--base); color: var(--base);
border-color: var(--accent-0);
/*text-transform: uppercase;*/ /*text-transform: uppercase;*/
} }
@ -248,6 +254,11 @@ textarea {
resize: vertical; resize: vertical;
} }
input:focus, textarea:focus {
box-shadow: 1px 1px 8px var(--surface-0);
outline: none;
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -281,7 +292,7 @@ hr {
} }
[role="menu"], [role="listbox"] { [role="menu"], [role="listbox"] {
background-color: var(--base); background-color: var(--base-0);
border: 1px solid var(--overlay-0); border: 1px solid var(--overlay-0);
border-radius: 4px; border-radius: 4px;
padding: 4px 0 4px 0; padding: 4px 0 4px 0;
@ -299,9 +310,10 @@ hr {
} }
[role="menu"] button { [role="menu"] button {
background-color: var(--base); background-color: var(--base-0);
width: 100%; width: 100%;
border-radius: 0; border-radius: 0;
border: none;
} }
[role="menu"] button.destructive { [role="menu"] button.destructive {
@ -335,7 +347,7 @@ hr {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
background-color: var(--base); background-color: var(--base-0);
z-index: 10; z-index: 10;
animation: smooth-appear 0.2s ease; animation: smooth-appear 0.2s ease;
} }
@ -363,3 +375,49 @@ div.banner.info {
background-color: var(--lavender-transparent); background-color: var(--lavender-transparent);
color: var(--lavender); color: var(--lavender);
} }
table {
border-collapse: separate;
border-spacing: 0;
}
th:first-child {
border-top-left-radius: 4px;
border-left: 1px solid var(--surface-0);
}
th {
text-align: left;
padding: 0.5rem 1rem;
border-top: 1px solid var(--surface-0);
border-bottom: 1px solid var(--surface-0);
font-size: 10pt;
}
th:last-child {
border-top-right-radius: 4px;
border-right: 1px solid var(--surface-0);
}
td {
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--surface-0);
background-color: var(--base-0);
font-size: 10pt;
}
td:first-child {
border-left: 1px solid var(--surface-0);
}
td:last-child {
border-right: 1px solid var(--surface-0);
}
tr:last-child > td:first-child {
border-bottom-left-radius: 4px;
}
tr:last-child > td:last-child {
border-bottom-right-radius: 4px;
}

View File

@ -8,6 +8,7 @@ const scheduleStore = useScheduleStore();
const model = defineModel<number[]>({ required: true }); const model = defineModel<number[]>({ required: true });
const selectedTime = defineModel("selectedTime"); const selectedTime = defineModel("selectedTime");
const selectedIndex = defineModel("selectedIndex");
const hoveredIndex = defineModel("hoveredIndex"); const hoveredIndex = defineModel("hoveredIndex");
@ -142,11 +143,20 @@ function onSlotMouseUp(_: MouseEvent) {
} }
function onSlotClick(dayIndex: number, hour: number) { function onSlotClick(dayIndex: number, hour: number) {
let index = dayIndex * 24 + hour;
if (isEditing.value) { if (isEditing.value) {
return; return;
} }
if (selectedIndex.value == index) {
selectedIndex.value = -1;
selectedTime.value = undefined;
return;
}
selectedTime.value = getTimeAtCell(dayIndex, hour); selectedTime.value = getTimeAtCell(dayIndex, hour);
selectedIndex.value = index;
scheduleStore.selectIndex(24 * dayIndex + hour); scheduleStore.selectIndex(24 * dayIndex + hour);
} }
@ -235,6 +245,7 @@ function getHour(offset: number, tz?: string) {
:class="{ :class="{
'time-slot': true, 'time-slot': true,
'height-24px': true, 'height-24px': true,
'selected': selectedIndex == 24 * dayIndex + hour,
}" }"
:selection=" :selection="
selectionInside(dayIndex, hour) ? selectionValue selectionInside(dayIndex, hour) ? selectionValue
@ -315,9 +326,34 @@ function getHour(offset: number, tz?: string) {
font-weight: 700; font-weight: 700;
} }
.time-slot:hover, .time-slot.selected {
outline: 2px inset var(--subtext-0);
}
.time-slot.selected {
outline-style: solid;
animation: pulse 1s infinite;
}
.time-slot.selected:hover {
outline-style: solid;
}
@keyframes pulse {
0% {
outline-color: var(--overlay-0);
}
50% {
outline-color: var(--text);
}
100% {
outline-color: var(--overlay-0);
}
}
.time-slot:hover { .time-slot:hover {
background-color: var(--crust); background-color: var(--crust);
outline: 2px inset var(--subtext-0); outline-style: dashed;
} }
.time-slot:nth-child(2n):not(:last-child) { .time-slot:nth-child(2n):not(:last-child) {

View File

@ -109,7 +109,8 @@ h3 {
.event-card { .event-card {
display: flex; display: flex;
align-items: center; align-items: center;
/*background-color: white;*/ background-color: var(--base-0);
border-radius: 8px;
align-items: stretch; align-items: stretch;
} }
@ -139,7 +140,7 @@ h3 {
.details { .details {
padding: 1rem; padding: 1rem;
border: 1px solid var(--text); border: 1px solid var(--surface-0);
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -113,11 +113,12 @@ const selectedOption = computed({
<style scoped> <style scoped>
.event-confirm-button { .event-confirm-button {
display: flex; display: flex;
gap: 2px; gap: 0px;
} }
.left { .left {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
border-right: none;
} }
.right { .right {
@ -128,5 +129,10 @@ const selectedOption = computed({
.confirmed button.recolor { .confirmed button.recolor {
background-color: var(--text); background-color: var(--text);
color: var(--base); color: var(--base);
border-color: var(--text);
}
.confirmed .left.recolor {
border-right: 1px solid var(--surface-0);
} }
</style> </style>

View File

@ -1,16 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EventWithPlayerSchema, TeamSchema } from "@/client"; import type { EventWithPlayerSchema, TeamSchema } from "@/client";
import EventCard from "./EventCard.vue"; import EventCard from "./EventCard.vue";
import { onMounted, ref } from "vue";
import { useTeamDetails } from "@/composables/team-details";
import { useTeamsStore } from "@/stores/teams";
import { useTeamsEventsStore } from "@/stores/teams/events";
import LoaderContainer from "./LoaderContainer.vue";
import { computed } from "@vue/reactivity";
const props = defineProps<{ const { teamId, team } = useTeamDetails();
events: EventWithPlayerSchema[];
teamContext: TeamSchema; const teamsStore = useTeamsStore();
}>(); const teamsEventsStore = useTeamsEventsStore();
const isLoading = ref(false);
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
onMounted(() => {
isLoading.value = true;
teamsStore.fetchTeam(teamId.value)
.then(() => {
teamsEventsStore.fetchTeamEvents(teamId.value)
.finally(() => isLoading.value = false);
});
});
</script> </script>
<template> <template>
<div class="events-list" v-if="props.events?.length > 0"> <LoaderContainer v-if="isLoading" height="160">
<EventCard v-for="event in props.events" :key="event.event.id" :event="event" /> <rect x="0" y="0" rx="4" ry="4" width="100%" height="160" />
</LoaderContainer>
<div class="events-list" v-else-if="events?.length > 0">
<EventCard v-for="event in events" :key="event.event.id" :event="event" />
</div> </div>
<div class="events-list" v-else> <div class="events-list" v-else>
<em class="subtext"> <em class="subtext">
@ -19,7 +39,7 @@ const props = defineProps<{
:to="{ :to="{
name: 'schedule', name: 'schedule',
query: { query: {
teamId: props.teamContext.id teamId,
} }
}" }"
> >

View File

@ -42,14 +42,16 @@ function revokeInvite() {
<td> <td>
{{ createdAt }} {{ createdAt }}
</td> </td>
<td class="buttons"> <td>
<div class="buttons">
<button @click="copyLink"> <button @click="copyLink">
<i class="bi bi-link margin" /> <i class="bi bi-link margin" />
Copy Link Copy Link
</button> </button>
<button class="destructive" @click="revokeInvite"> <button class="icon" @click="revokeInvite">
<i class="bi bi-trash" /> <i class="bi bi-trash" />
</button> </button>
</div>
</td> </td>
</tr> </tr>
</template> </template>

View File

@ -65,7 +65,8 @@ const props = defineProps<{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
border: 1px solid var(--text); border: 1px solid var(--surface-0);
background-color: var(--base-0);
border-radius: 8px; border-radius: 8px;
gap: 0.5rem; gap: 0.5rem;
} }

View File

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { useRoute, useRouter, RouterLink } from "vue-router"; import { useRoute, useRouter, RouterLink } from "vue-router";
import { computed } from "vue"; import { computed, onMounted, ref } from "vue";
import { useTeamDetails } from "../composables/team-details"; import { useTeamDetails } from "../composables/team-details";
import PlayerTeamCard from "../components/PlayerTeamCard.vue"; import PlayerTeamCard from "../components/PlayerTeamCard.vue";
import LoaderContainer from "./LoaderContainer.vue";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -15,16 +16,13 @@ const {
availableMembersNextHour, availableMembersNextHour,
teamMembers, teamMembers,
} = useTeamDetails(); } = useTeamDetails();
const isLoading = ref(false);
function leaveTeam() { onMounted(() => {
teamsStore.leaveTeam(team.value.id) isLoading.value = true;
.then(() => { teamsStore.fetchTeamMembers(team.value.id)
teamsStore.fetchTeams() .finally(() => isLoading.value = false);
.then(() => { });
router.push("/");
})
});
}
</script> </script>
<template> <template>
@ -39,7 +37,21 @@ function leaveTeam() {
<div class="team-details-button-group"> <div class="team-details-button-group">
</div> </div>
</div> </div>
<table class="member-table"> <LoaderContainer v-if="isLoading">
<rect x="0" y="10" rx="3" ry="3" width="100%" height="10" />
<rect x="0" y="30" rx="3" ry="3" width="100%" height="10" />
<rect x="0" y="50" rx="3" ry="3" width="100%" height="10" />
<rect x="0" y="70" rx="3" ry="3" width="100%" height="10" />
</LoaderContainer>
<table class="member-table" v-else>
<thead>
<tr>
<th>Username</th>
<th>Roles</th>
<th>Playtime</th>
<th></th>
</tr>
</thead>
<tbody> <tbody>
<PlayerTeamCard <PlayerTeamCard
v-for="member in teamMembers" v-for="member in teamMembers"
@ -67,12 +79,6 @@ table.member-table {
width: 100%; width: 100%;
} }
table.member-table th {
text-align: left;
padding-left: 2em;
font-weight: 700;
}
.team-details-button-group { .team-details-button-group {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@ -82,14 +82,14 @@ const playtime = computed(() => {
<style scoped> <style scoped>
.player-card { .player-card {
background-color: white; background-color: var(--base-0);
padding: 1em; padding: 1em;
border-radius: 8px; border-radius: 8px;
user-select: none; user-select: none;
display: flex; display: flex;
gap: 1em; gap: 1em;
align-items: center; align-items: center;
border: 2px solid white; border: 2px solid var(--surface-0);
box-shadow: 1px 1px 8px var(--surface-0); box-shadow: 1px 1px 8px var(--surface-0);
} }
@ -122,13 +122,13 @@ const playtime = computed(() => {
} }
.player-card.no-player { .player-card.no-player {
border: 2px solid var(--overlay-0); border: 2px dashed var(--overlay-0);
box-shadow: none; box-shadow: none;
} }
.player-card.no-player.selected { .player-card.no-player.selected {
background-color: var(--accent-transparent); background-color: var(--accent-transparent);
border: 2px solid var(--accent); border: 2px dashed var(--accent);
color: var(--accent); color: var(--accent);
} }

View File

@ -180,7 +180,7 @@ const rightIndicator = computed(() => {
No roles No roles
</span> </span>
<div class="edit-group"> <div class="edit-group">
<button v-if="!isEditing" @click="isEditing = true"> <button v-if="!isEditing" class="icon" @click="isEditing = true">
<i class="bi bi-pencil-fill edit-icon" /> <i class="bi bi-pencil-fill edit-icon" />
</button> </button>
</div> </div>
@ -197,10 +197,10 @@ const rightIndicator = computed(() => {
<td> <td>
<div class="edit-group"> <div class="edit-group">
<template v-if="isEditing"> <template v-if="isEditing">
<button class="editing" @click="cancelEdit()"> <button class="editing icon" @click="cancelEdit()">
<i class="bi bi-x-lg" /> <i class="bi bi-x-lg" />
</button> </button>
<button class="editing" @click="updateRoles()"> <button class="editing icon" @click="updateRoles()">
<i class="bi bi-check-lg" /> <i class="bi bi-check-lg" />
</button> </button>
</template> </template>
@ -314,10 +314,6 @@ a.player-name:hover {
opacity: 1; opacity: 1;
} }
.edit-group > button.editing {
opacity: 1;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.player-card > td { .player-card > td {
padding: 0.5em 0em; padding: 0.5em 0em;

View File

@ -18,7 +18,7 @@ function logout() {
<template> <template>
<DropdownMenuRoot> <DropdownMenuRoot>
<DropdownMenuTrigger className="profile-button"> <DropdownMenuTrigger className="profile-button no-border">
{{ authStore.username }} {{ authStore.username }}
<i class="bi bi-chevron-down" /> <i class="bi bi-chevron-down" />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -47,6 +47,7 @@ function toggle(isMain: boolean) {
</div> </div>
<button <button
:class="{ :class="{
'no-border': true,
'center': true, 'center': true,
'selected': roleObject?.isMain 'selected': roleObject?.isMain
}" }"
@ -56,6 +57,7 @@ function toggle(isMain: boolean) {
</button> </button>
<button <button
:class="{ :class="{
'no-border': true,
'right': true, 'right': true,
'selected': !(roleObject?.isMain ?? true) 'selected': !(roleObject?.isMain ?? true)
}" }"

View File

@ -21,11 +21,11 @@ function incrementDate(delta: number) {
<template> <template>
<div class="scroll-box"> <div class="scroll-box">
<button class="transparent eq" @click="incrementDate(-1)" :disabled="isDisabled"> <button class="transparent icon" @click="incrementDate(-1)" :disabled="isDisabled">
<i class="bi bi-caret-left-fill"></i> <i class="bi bi-caret-left-fill"></i>
</button> </button>
<span class="date-range">{{ dateStart }} &ndash; {{ dateEnd }}</span> <span class="date-range">{{ dateStart }} &ndash; {{ dateEnd }}</span>
<button class="transparent eq" @click="incrementDate(1)" :disabled="isDisabled"> <button class="transparent icon" @click="incrementDate(1)" :disabled="isDisabled">
<i class="bi bi-caret-right-fill"></i> <i class="bi bi-caret-right-fill"></i>
</button> </button>
</div> </div>

View File

@ -189,7 +189,6 @@ main {
.radio-group { .radio-group {
display: flex; display: flex;
gap: 2px;
} }
button.radio { button.radio {
@ -207,6 +206,7 @@ button.radio.selected {
button.left { button.left {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
border-right-width: 0px;
} }
button.radio.left.selected { button.radio.left.selected {

View File

@ -37,8 +37,8 @@ onMounted(() => {
let doFetchTeam = () => { let doFetchTeam = () => {
teamsStore.fetchTeam(teamId.value) teamsStore.fetchTeam(teamId.value)
.then(() => { .then(() => {
teamsStore.fetchTeamMembers(teamId.value); //teamsStore.fetchTeamMembers(teamId.value);
teamsEventsStore.fetchTeamEvents(teamId.value); //teamsEventsStore.fetchTeamEvents(teamId.value);
matchesStore.fetchRecentMatchesForTeam(teamId.value, 5); matchesStore.fetchRecentMatchesForTeam(teamId.value, 5);
isLoading.value = false; isLoading.value = false;
}); });
@ -89,7 +89,7 @@ onMounted(() => {
</div> </div>
<div class="right"> <div class="right">
<h2>Upcoming Events</h2> <h2>Upcoming Events</h2>
<EventList :events="events" :team-context="team" /> <EventList />
<h2 id="recent-matches-header"> <h2 id="recent-matches-header">
Recent Matches Recent Matches
<RouterLink class="button" :to="{ name: 'team-settings/matches' }"> <RouterLink class="button" :to="{ name: 'team-settings/matches' }">
@ -98,15 +98,15 @@ onMounted(() => {
</button> </button>
</RouterLink> </RouterLink>
</h2> </h2>
<em class="subtext" v-if="!matches">
No recent matches.
</em>
<MatchCard <MatchCard
v-else v-if="matches?.length > 0"
v-for="match in matches" v-for="match in matches"
:team-match="match" :team-match="match"
:team="team" :team="team"
/> />
<em class="subtext" v-else>
No recent matches.
</em>
</div> </div>
</div> </div>
</template> </template>
@ -123,6 +123,7 @@ onMounted(() => {
.content-container { .content-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 1rem;
} }
.content-container > div.left { .content-container > div.left {
@ -164,6 +165,7 @@ onMounted(() => {
@media (max-width: 1024px) { @media (max-width: 1024px) {
.content-container { .content-container {
flex-direction: column; flex-direction: column;
gap: unset;
} }
} }
</style> </style>

View File

@ -27,6 +27,16 @@ onMounted(() => {
<template> <template>
<div class="invites" v-if="team"> <div class="invites" v-if="team">
<h2>Invites</h2> <h2>Invites</h2>
<p class="small aside">
Invite players to your team by creating an invite link.
All invites are usable only once.
</p>
<div class="create-invite-group">
<button class="accent" @click="createInvite">
<i class="bi bi-person-fill-add margin" />
Create Invite
</button>
</div>
<table id="invite-table" v-if="invites?.length > 0"> <table id="invite-table" v-if="invites?.length > 0">
<thead> <thead>
<tr> <tr>
@ -36,6 +46,8 @@ onMounted(() => {
<th> <th>
Creation time Creation time
</th> </th>
<th>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -45,22 +57,12 @@ onMounted(() => {
/> />
</tbody> </tbody>
</table> </table>
<div class="create-invite-group">
<button class="accent" @click="createInvite">
<i class="bi bi-person-fill-add margin" />
Create Invite
</button>
<span class="small aside">
Invites are usable once and expire after 24 hours.
</span>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
#invite-table { #invite-table {
width: 100%; width: 100%;
border: 1px solid var(--overlay-0);
border-radius: 8px; border-radius: 8px;
margin: 8px 0; margin: 8px 0;
} }
@ -75,5 +77,6 @@ onMounted(() => {
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
justify-content: end;
} }
</style> </style>

View File

@ -25,10 +25,10 @@ onMounted(() => {
<i class="bi bi-trophy-fill margin"></i> <i class="bi bi-trophy-fill margin"></i>
Matches Matches
</h2> </h2>
</div>
<div class="button-group"> <div class="button-group">
<AddMatchDialog /> <AddMatchDialog />
</div> </div>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -71,9 +71,4 @@ onMounted(() => {
table { table {
width: 100%; width: 100%;
} }
th {
text-align: left;
font-weight: 800;
}
</style> </style>

View File

@ -46,7 +46,7 @@ onMounted(() => {
Matches Matches
</RouterLink> </RouterLink>
<hr> <hr>
<button class="destructive-on-hover icon-end" @click="leaveTeam"> <button class="destructive-on-hover icon-end no-border" @click="leaveTeam">
Leave team Leave team
<i class="bi bi-box-arrow-left" /> <i class="bi bi-box-arrow-left" />
</button> </button>
@ -122,4 +122,9 @@ nav.sidebar button:hover {
background-color: var(--crust); background-color: var(--crust);
color: var(--text); color: var(--text);
} }
nav.sidebar button.destructive-on-hover:hover {
background-color: var(--destructive);
color: var(--base);
}
</style> </style>