Refactor the client to implement a channel navbar #76

Open
twig wants to merge 22 commits from navbar into main
13 changed files with 522 additions and 396 deletions

View file

@ -0,0 +1,85 @@
<template>
<div id="navbar" class="navbar-row">
<div id="channel-info" class="navbar-row">
<!-- h1 to help screen readers -->
<h1 id="channel-name">
# {{ channel.name }}
</h1>
<span id="channel-description">
{{ channel.description }}
</span>
</div>
<div id="buttons" class="navbar-row">
<a class="button" href="https://git.gorb.app/gorb/frontend">
<Icon name="lucide:code-xml" title="Source"/>
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import type { INavbar } from '~/types/interfaces';
const props = defineProps<INavbar>();
</script>
<style scoped>
.navbar-row {
display: flex;
flex-direction: row;
}
#navbar {
--font-size: calc(var(--navbar-height) * 0.45);
--side-margins: calc(var(--font-size) * 0.5);
min-height: var(--navbar-height);
max-height: var(--navbar-height);
width: 100%;
background: var(--optional-topbar-background);
background-color: var(--topbar-background-color);
border-bottom: 1px solid var(--padding-color);
}
#channel-info {
margin-left: var(--side-margins);
gap: calc(var(--font-size) * 0.75);
align-items: center;
}
#channel-name {
font-size: var(--font-size);
font-weight: 500;
}
#channel-description {
font-size: calc(var(--font-size) * 0.8);
text-overflow: ellipsis;
/* TODO make new theme colour? unsure of it's name, this is good enough for now */
color: var(--reply-text-color);
}
#buttons {
margin-right: var(--side-margins);
flex-grow: 1;
align-items: center;
justify-content: end;
}
.button {
color: var(--secondary-text-color);
transition: color 300ms;
right: 0;
}
.button:hover {
color: var(--primary-highlighted-color);
}
</style>

View file

@ -0,0 +1,91 @@
<template>
<ResizableSidebar
width="14rem" min-width="8rem" max-width="30rem"
border-sides="right" local-storage-name="middleLeftColumn">
<div id="guild-sidebar">
<div id="guild-top-container">
<span id="guild-name" :title="props.guild?.name">{{ props.guild?.name }}</span>
<button id="guild-settings-button" @click="toggleGuildSettings">
<Icon name="lucide:chevron-down" />
</button>
<GuildOptionsMenu v-if="showGuildSettings" />
</div>
<div id="channels-list">
<ChannelEntry v-for="channel of channels"
:name="channel.name"
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
</div>
</div>
</ResizableSidebar>
</template>
<script lang="ts" setup>
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
import ResizableSidebar from "../UserInterface/ResizableSidebar.vue";
import type { ChannelResponse, GuildResponse, INavbar } from "~/types/interfaces";
const props = defineProps<{
guild: GuildResponse
}>();
const route = useRoute();
const { fetchChannels } = useApi();
const showGuildSettings = ref(false);
const channels: ChannelResponse[] = await fetchChannels(props.guild.uuid);
function toggleGuildSettings(e: Event) {
e.preventDefault();
showGuildSettings.value = !showGuildSettings.value;
}
</script>
<style scoped>
#guild-top-container {
min-height: var(--navbar-height);
max-height: var(--navbar-height);
width: 100%;
display: flex;
justify-content: center;
position: relative;
border-bottom: 1px solid var(--padding-color);
}
#guild-name {
font-size: 1.5em;
overflow: hidden;
text-overflow: ellipsis;
}
#guild-settings-button {
background-color: transparent;
font-size: 1em;
color: white;
border: none;
padding: 0%;
}
#channels-list {
background: var(--optional-channel-list-background);
background-color: var(--sidebar-background-color);
display: flex;
flex-direction: column;
gap: .5em;
text-overflow: ellipsis;
padding-top: .5em;
padding-bottom: .5em;
max-height: calc(100% - 1em); /* 100% - top and bottom padding */
padding-left: .5em;
padding-right: .5em;
}
</style>

View file

@ -32,7 +32,13 @@ function hideModalPopup() {
<style> <style>
.member-item { .member-item {
position: relative; display: flex;
margin-top: .5em;
margin-bottom: .5em;
gap: .5em;
align-items: center;
text-align: left;
cursor: pointer;
} }
.member-avatar { .member-avatar {
@ -41,4 +47,9 @@ function hideModalPopup() {
min-width: 2.3em; min-width: 2.3em;
max-width: 2.3em; max-width: 2.3em;
} }
.member-display-name {
overflow: hidden;
text-overflow: ellipsis;
}
</style> </style>

View file

@ -0,0 +1,47 @@
<template>
<ResizableSidebar
width="14rem" min-width="5.5rem" max-width="30rem"
border-sides="left" local-storage-name="membersListWidth">
<div id="members-container">
<div id="members-list">
<MemberEntry v-for="member of members.objects" :member="member" tabindex="0"/>
</div>
</div>
</ResizableSidebar>
</template>
<script lang="ts" setup>
import ResizableSidebar from "../UserInterface/ResizableSidebar.vue";
import MemberEntry from "./MemberEntry.vue";
import type { GuildResponse } from "~/types/interfaces";
const props = defineProps<{
guild: GuildResponse
}>();
const { fetchMembers } = useApi();
// TODO implement paging
const members = await fetchMembers(props.guild.uuid)
</script>
<style scoped>
#members-container {
background: var(--optional-member-list-background);
padding-top: .5em;
padding-bottom: .5em;
max-height: calc(100% - 1em); /* 100% - top and bottom padding */
}
#members-list {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: scroll;
padding-left: .5em;
padding-right: .5em;
}
</style>

View file

@ -47,6 +47,8 @@ import { generateIrcColor } from '#imports';
const { getDisplayName } = useProfile() const { getDisplayName } = useProfile()
const { fetchMe } = useApi() const { fetchMe } = useApi()
// TODO this file is a mess, and we need to stop using fetchWithApi
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>(); const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
const me = await fetchMe() as UserResponse; const me = await fetchMe() as UserResponse;

View file

@ -135,8 +135,7 @@ function loadStoredWidth() {
.sidebar-content { .sidebar-content {
width: 100%; width: 100%;
padding-left: .25em; height: 100%;
padding-right: .25em;
} }
.sidebar-content > :first-child { .sidebar-content > :first-child {

View file

@ -0,0 +1,218 @@
<template>
<div id="sidebar-column">
<div class="side-column-segment">
<NuxtLink id="home-button" href="/me">
<img class="sidebar-icon" src="/public/icon.svg"/>
</NuxtLink>
</div>
<VerticalSpacer />
<div class="sidebar-column-segment" id="guild-column">
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`" id="guild-icon-container">
<NuxtImg v-if="guild.icon"
class="sidebar-icon guild-icon"
:alt="guild.name"
:src="guild.icon" />
<DefaultIcon v-else
class="sidebar-icon guild-icon"
:alt="guild.name"
:name="guild.name" :seed="guild.uuid"/>
</NuxtLink>
</div>
<VerticalSpacer />
<div class="sidebar-column-segment">
<div ref="createButtonContainer">
<button id="create-button" class="sidebar-bottom-buttons" @click.prevent="createDropdown">
<Icon id="create-icon" name="lucide:square-plus" alt="Create or join guild"/>
</button>
</div>
<NuxtLink id="settings-menu" class="sidebar-bottom-buttons" href="/settings">
<Icon name="lucide:settings" alt="Settings menu" />
</NuxtLink>
</div>
</div>
</template>
<script lang="ts" setup>
import { ModalBase } from '#components';
import { render } from 'vue';
import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
import DefaultIcon from '~/components/DefaultIcon.vue';
import Button from '~/components/UserInterface/Button.vue';
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
import type { GuildResponse } from '~/types/interfaces';
const { getDisplayName } = useProfile()
const { fetchMyGuilds, joinGuild, createGuild, createChannel } = useApi();
const createButtonContainer = ref<HTMLButtonElement>();
const guilds = await fetchMyGuilds();
// TODO we need to turn this into an actual modal
const options = [
{ name: "Join", value: "join", callback: async () => {
console.log("join guild!");
const div = document.createElement("div");
const guildJoinModal = h(ModalBase, {
title: "Join Guild",
id: "guild-join-modal",
onClose: () => {
unrender(div);
},
onCancel: () => {
unrender(div);
},
style: "height: 20dvh; width: 15dvw"
},
[
h("input", {
id: "guild-invite-input",
type: "text",
placeholder: "oyqICZ",
}),
h(Button, {
text: "Join",
variant: "normal",
callback: async () => {
const input = document.getElementById("guild-invite-input") as HTMLInputElement;
const invite = input.value;
if (invite.length == 6) {
try {
const joinedGuild = await joinGuild(invite);
guilds.push(joinedGuild);
return await navigateTo(`/servers/${joinedGuild.uuid}`);
} catch (error) {
alert(`Couldn't use invite: ${error}`);
}
}
}
})
]);
document.body.appendChild(div);
render(guildJoinModal, div);
}
},
{ name: "Create", value: "create", callback: async () => {
console.log("create guild");
const user = await useAuth().getUser();
const div = document.createElement("div");
const guildCreateModal = h(ModalBase, {
title: "Create a Guild",
id: "guild-join-modal",
onClose: () => {
unrender(div);
},
onCancel: () => {
unrender(div);
},
style: "height: 20dvh; width: 15dvw;"
},
[
h("input", {
id: "guild-name-input",
type: "text",
placeholder: `${getDisplayName(user!)}'s Awesome Bouncy Castle'`,
style: "width: 100%"
}),
h(Button, {
text: "Create!",
variant: "normal",
callback: async () => {
const input = document.getElementById("guild-name-input") as HTMLInputElement;
const name = input.value;
try {
const guild = (await createGuild(name)) as GuildResponse;
await createChannel(guild.uuid, "general");
} catch (error) {
alert(`Couldn't create guild: ${error}`);
}
}
})
]);
document.body.appendChild(div);
render(guildCreateModal, div);
}
}
];
function createDropdown() {
const dropdown = h(GuildDropdown, { options });
const div = document.createElement("div");
div.classList.add("dropdown", "destroy-on-click");
if (createButtonContainer.value) {
createButtonContainer.value.appendChild(div);
} else {
document.body.appendChild(div);
}
render(dropdown, div);
div.addEventListener("keyup", (e) => {
if (e.key == "Escape") {
unrender(div);
}
});
div.focus();
}
</script>
<style scoped>
#sidebar-column {
display: flex;
flex-direction: column;
padding-left: var(--sidebar-margin);
padding-right: var(--sidebar-margin);
padding-top: .5em;
background: var(--optional-sidebar-background);
background-color: var(--sidebar-background-color);
border-right: 1px solid var(--padding-color);
text-align: center;
}
#guild-column {
overflow-y: scroll;
flex-grow: 1;
gap: var(--sidebar-icon-gap);
}
#guild-icon-container {
text-decoration: none;
}
.sidebar-column-segment {
display: flex;
flex-direction: column;
scrollbar-width: none;
}
.guild-column-segment::-webkit-scrollbar {
display: none;
}
#home-button {
height: var(--sidebar-icon-width);
}
.guild-icon {
border-radius: var(--guild-icon-radius);
}
.sidebar-icon {
width: var(--sidebar-icon-width);
height: var(--sidebar-icon-width);
}
.sidebar-bottom-buttons {
color: var(--primary-color);
background-color: transparent;
border: none;
cursor: pointer;
font-size: 2.4rem;
padding: 0;
display: inline-block;
}
.sidebar-bottom-buttons:hover {
color: var(--primary-highlighted-color);
}
</style>

View file

@ -1,59 +1,17 @@
<template> <template>
<Loading v-show="loading" /> <Loading v-show="loading" />
<div :class="{ hidden: loading, visible: !loading }" id="client-root"> <div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar"> <div class="flex-container-row">
<div class="homebar-item"> <SidebarColumn />
<marquee>
gorb!!!!!
</marquee>
</div>
</div>
<div id="page-content">
<div id="left-column">
<div class="left-column-segment">
<NuxtLink id="home-button" href="/me">
<img class="sidebar-icon" src="/public/icon.svg"/>
</NuxtLink>
</div>
<VerticalSpacer />
<div class="left-column-segment" id="left-column-middle">
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`" id="guild-icon-container">
<NuxtImg v-if="guild.icon"
class="sidebar-icon guild-icon"
:alt="guild.name"
:src="guild.icon" />
<DefaultIcon v-else
class="sidebar-icon guild-icon"
:alt="guild.name"
:name="guild.name" :seed="guild.uuid"/>
</NuxtLink>
</div>
<VerticalSpacer />
<div class="left-column-segment">
<div ref="createButtonContainer">
<button id="create-button" class="sidebar-bottom-buttons" @click.prevent="createDropdown">
<Icon id="create-icon" name="lucide:square-plus" alt="Create or join guild"/>
</button>
</div>
<NuxtLink id="settings-menu" class="sidebar-bottom-buttons" href="/settings">
<Icon name="lucide:settings" alt="Settings menu" />
</NuxtLink>
</div>
</div>
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ModalBase } from '#components';
import { render } from 'vue';
import DefaultIcon from '~/components/DefaultIcon.vue';
import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
import Loading from '~/components/Popups/Loading.vue'; import Loading from '~/components/Popups/Loading.vue';
import Button from '~/components/UserInterface/Button.vue'; import SidebarColumn from '~/components/UserInterface/SidebarColumn.vue';
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
import type { GuildResponse } from '~/types/interfaces';
definePageMeta({ definePageMeta({
keepalive: true keepalive: true
@ -61,117 +19,6 @@ definePageMeta({
const loading = useState("loading", () => false); const loading = useState("loading", () => false);
const createButtonContainer = ref<HTMLButtonElement>();
const { getDisplayName } = useProfile()
const api = useApi();
const options = [
{ name: "Join", value: "join", callback: async () => {
console.log("join guild!");
const div = document.createElement("div");
const guildJoinModal = h(ModalBase, {
title: "Join Guild",
id: "guild-join-modal",
onClose: () => {
unrender(div);
},
onCancel: () => {
unrender(div);
},
style: "height: 20dvh; width: 15dvw"
},
[
h("input", {
id: "guild-invite-input",
type: "text",
placeholder: "oyqICZ",
}),
h(Button, {
text: "Join",
variant: "normal",
callback: async () => {
const input = document.getElementById("guild-invite-input") as HTMLInputElement;
const invite = input.value;
if (invite.length == 6) {
try {
const joinedGuild = await api.joinGuild(invite);
guilds.push(joinedGuild);
return await navigateTo(`/servers/${joinedGuild.uuid}`);
} catch (error) {
alert(`Couldn't use invite: ${error}`);
}
}
}
})
]);
document.body.appendChild(div);
render(guildJoinModal, div);
}
},
{ name: "Create", value: "create", callback: async () => {
console.log("create guild");
const user = await useAuth().getUser();
const div = document.createElement("div");
const guildCreateModal = h(ModalBase, {
title: "Create a Guild",
id: "guild-join-modal",
onClose: () => {
unrender(div);
},
onCancel: () => {
unrender(div);
},
style: "height: 20dvh; width: 15dvw;"
},
[
h("input", {
id: "guild-name-input",
type: "text",
placeholder: `${getDisplayName(user!)}'s Awesome Bouncy Castle'`,
style: "width: 100%"
}),
h(Button, {
text: "Create!",
variant: "normal",
callback: async () => {
const input = document.getElementById("guild-name-input") as HTMLInputElement;
const name = input.value;
try {
const guild = (await api.createGuild(name)) as GuildResponse;
await api.createChannel(guild.uuid, "general");
} catch (error) {
alert(`Couldn't create guild: ${error}`);
}
}
})
]);
document.body.appendChild(div);
render(guildCreateModal, div);
}
}
];
const guilds = await api.fetchMyGuilds();
function createDropdown() {
const dropdown = h(GuildDropdown, { options });
const div = document.createElement("div");
div.classList.add("dropdown", "destroy-on-click");
if (createButtonContainer.value) {
createButtonContainer.value.appendChild(div);
} else {
document.body.appendChild(div);
}
render(dropdown, div);
div.addEventListener("keyup", (e) => {
if (e.key == "Escape") {
unrender(div);
}
});
div.focus();
}
</script> </script>
<style> <style>
@ -179,8 +26,6 @@ function createDropdown() {
height: 100dvh; height: 100dvh;
width: 100dvw; width: 100dvw;
display: flex; display: flex;
flex-direction: column;
text-align: center;
} }
.hidden { .hidden {
@ -192,89 +37,20 @@ function createDropdown() {
transition: opacity 500ms; transition: opacity 500ms;
} }
#homebar {
min-height: 4dvh;
display: flex;
justify-content: space-evenly;
align-items: center;
background: var(--optional-topbar-background);
background-color: var(--topbar-background-color);
border-bottom: 1px solid var(--padding-color);
padding-left: 5dvw;
padding-right: 5dvw;
}
.homebar-item { .flex-container-row,
width: 100dvw; .flex-container-column {
}
#page-content {
display: flex; display: flex;
flex-direction: row;
flex-grow: 1;
overflow: auto; overflow: auto;
}
#left-column {
display: flex;
flex-direction: column;
padding-left: var(--sidebar-margin);
padding-right: var(--sidebar-margin);
padding-top: .5em;
background: var(--optional-sidebar-background);
background-color: var(--sidebar-background-color);
border-right: 1px solid var(--padding-color);
}
.left-column-segment {
display: flex;
flex-direction: column;
scrollbar-width: none;
}
.left-column-segment::-webkit-scrollbar {
display: none;
}
#left-column-middle {
overflow-y: scroll;
flex-grow: 1; flex-grow: 1;
gap: var(--sidebar-icon-gap);
} }
#home-button { .flex-container-row {
height: var(--sidebar-icon-width); flex-direction: row;
} }
#guild-icon-container { .flex-container-column {
text-decoration: none; flex-direction: column;
}
.guild-icon {
border-radius: var(--guild-icon-radius);
}
.sidebar-icon {
width: var(--sidebar-icon-width);
height: var(--sidebar-icon-width);
}
.sidebar-bottom-buttons {
color: var(--primary-color);
background-color: transparent;
border: none;
cursor: pointer;
font-size: 2.4rem;
padding: 0;
display: inline-block;
}
.sidebar-bottom-buttons:hover {
color: var(--primary-highlighted-color);
} }
</style> </style>

View file

@ -1,18 +1,4 @@
<template>
<NuxtLayout>
</NuxtLayout>
</template>
<script lang="ts" setup> <script lang="ts" setup>
await navigateTo("/me/", { replace: true }) await navigateTo("/me/", { replace: true })
definePageMeta({
layout: "client"
});
</script> </script>
<style>
</style>

View file

@ -8,6 +8,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue'; import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
onMounted(async () => {
updateNavbar({isDirectMessages: true})
})
onActivated(async () => {
updateNavbar({isDirectMessages: true})
})
</script> </script>
<style> <style>

View file

@ -1,163 +1,43 @@
<template> <template>
<NuxtLayout name="client"> <NuxtLayout name="client">
<ResizableSidebar <GuildSidebar v-if="guild" :guild="guild" />
width="14rem" min-width="8rem" max-width="30rem" <div class="flex-container-column">
border-sides="right" local-storage-name="middleLeftColumn"> <GuildChannelNavbar id="navbar"
<div id="middle-left-column" class="main-grid-row"> v-if="guild && channel"
<div id="server-name-container"> :guild="guild"
<span id="server-name" :title="server?.name">{{ server?.name }}</span> :channel="channel"/>
<button id="server-settings-button" @click="toggleGuildSettings">
<Icon id="server-settings-icon" name="lucide:chevron-down" /> <div class="flex-container-row">
</button>
<GuildOptionsMenu v-if="showGuildSettings" />
</div>
<div id="channels-list">
<ChannelEntry v-for="channel of channels" :name="channel.name"
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
</div>
</div>
</ResizableSidebar>
<MessageArea :channel-url="channelUrlPath" /> <MessageArea :channel-url="channelUrlPath" />
<ResizableSidebar <GuildMemberList v-if="guild" :guild="guild" />
width="14rem" min-width="5.5rem" max-width="30rem"
border-sides="left" local-storage-name="membersListWidth">
<div id="members-container">
<div id="members-list">
<MemberEntry v-for="member of members" :member="member" tabindex="0"/>
</div> </div>
</div> </div>
</ResizableSidebar>
</NuxtLayout> </NuxtLayout>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue";
import MemberEntry from "~/components/Guild/MemberEntry.vue";
import ResizableSidebar from "~/components/UserInterface/ResizableSidebar.vue";
import type { ChannelResponse, GuildMemberResponse, GuildResponse } from "~/types/interfaces";
const route = useRoute(); const route = useRoute();
const { fetchGuild, fetchChannel } = useApi()
const loading = useState("loading"); const channelId = route.params.channelId as string
const guildId = route.params.serverId as string
const channelUrlPath = `channels/${route.params.channelId}`; const channelUrlPath = `channels/${channelId}`;
const server = ref<GuildResponse | undefined>(); const guild = await fetchGuild(guildId)
const channels = ref<ChannelResponse[] | undefined>(); const channel = await fetchChannel(channelId)
const channel = ref<ChannelResponse | undefined>();
const members = ref<GuildMemberResponse[]>(); // function toggleInvitePopup(e: Event) {
// e.preventDefault();
// showInvitePopup.value = !showInvitePopup.value;
// }
const showInvitePopup = ref(false); // function handleMemberClick(member: GuildMemberResponse) {
const showGuildSettings = ref(false); // }
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("channelid: servers:", servers);
const { fetchMembers } = useApi();
onMounted(async () => {
console.log("mounting");
const guildUrl = `guilds/${route.params.serverId}`;
server.value = await fetchWithApi(guildUrl);
console.log("fetched guild");
await setArrayVariables();
console.log("set array variables");
});
onActivated(async () => {
console.log("activating");
const guildUrl = `guilds/${route.params.serverId}`;
server.value = await fetchWithApi(guildUrl);
console.log("fetched guild");
await setArrayVariables();
console.log("set array variables");
});
async function setArrayVariables() {
const membersRes = await fetchMembers(route.params.serverId as string);
members.value = membersRes.objects;
const guildUrl = `guilds/${route.params.serverId}`;
channels.value = await fetchWithApi(`${guildUrl}/channels`);
console.log("channels:", channels.value);
channel.value = await fetchWithApi(`/channels/${route.params.channelId}`);
console.log("channel:", channel.value);
}
function toggleGuildSettings(e: Event) {
e.preventDefault();
showGuildSettings.value = !showGuildSettings.value;
}
function toggleInvitePopup(e: Event) {
e.preventDefault();
showInvitePopup.value = !showInvitePopup.value;
}
function handleMemberClick(member: GuildMemberResponse) {
}
</script> </script>
<style> <style scoped>
#members-container {
background: var(--optional-member-list-background);
}
#members-list {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: scroll;
padding-left: 1.25em;
padding-right: 1.25em;
padding-top: 0.75em;
padding-bottom: 0.75em;
max-height: calc(100% - 0.75em * 2); /* 100% - top and bottom */
}
.member-item {
display: flex;
margin-top: .5em;
margin-bottom: .5em;
gap: .5em;
align-items: center;
text-align: left;
cursor: pointer;
}
#channels-list {
display: flex;
flex-direction: column;
gap: .5em;
text-overflow: ellipsis;
}
.member-display-name {
overflow: hidden;
text-overflow: ellipsis;
}
#server-name-container {
padding-top: 3dvh;
padding-bottom: 3dvh;
display: flex;
justify-content: center;
position: relative;
}
#server-name {
font-size: 1.5em;
overflow: hidden;
text-overflow: ellipsis;
}
#server-settings-button {
background-color: transparent;
font-size: 1em;
color: white;
border: none;
padding: 0%;
}
</style> </style>

View file

@ -9,6 +9,10 @@ complementaryColor = white
--sidebar-icon-gap: .25em; --sidebar-icon-gap: .25em;
--sidebar-margin: .5em; --sidebar-margin: .5em;
--navbar-height: 5dvh;
--navbar-icon-size: 3dvh;
--navbar-gap: calc(3dvh * .2);
--standard-radius: .5em; --standard-radius: .5em;
--button-radius: .6em; --button-radius: .6em;
--guild-icon-radius: 15%; --guild-icon-radius: 15%;

View file

@ -121,3 +121,22 @@ export interface ContextMenuInterface {
pointerY: number, pointerY: number,
items: ContextMenuItem[] items: ContextMenuItem[]
} }
export interface NavbarItem {
title: string,
icon: string,
hasPing?: boolean, // whether to draw a "ping" icon or not
callback: (...args: any[]) => any;
}
export interface INavbar {
guild: GuildResponse
channel: ChannelResponse
}
export interface NavbarOptions {
guild?: GuildResponse
channel?: ChannelResponse
isDirectMessages?: boolean
}