mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Add console breadcrumbs
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
<script lang="ts">
|
||||
import { createMenubar, melt } from '@melt-ui/svelte';
|
||||
import { Badge, Icon, BottomSheet, type SheetMenu } from '@appwrite.io/pink-svelte';
|
||||
import { IconChevronDown, IconChevronRight, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
type Project = {
|
||||
name: string;
|
||||
$id: string;
|
||||
isSelected: boolean;
|
||||
};
|
||||
type Organization = {
|
||||
name: string;
|
||||
$id: string;
|
||||
tierName: string;
|
||||
isSelected: boolean;
|
||||
projects: Array<Project>;
|
||||
};
|
||||
|
||||
const {
|
||||
elements: { menubar },
|
||||
builders: { createMenu }
|
||||
} = createMenubar();
|
||||
|
||||
const {
|
||||
elements: {
|
||||
trigger: triggerOrganizations,
|
||||
menu: menuOrganizations,
|
||||
item: itemOrganizations,
|
||||
separator: separatorOrganizations
|
||||
},
|
||||
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup }
|
||||
} = createMenu();
|
||||
|
||||
const {
|
||||
elements: { radioGroup: radioGroupOrganizations }
|
||||
} = createMenuRadioGroup({});
|
||||
|
||||
const {
|
||||
elements: { subMenu: subMenuOrganizations, subTrigger: subTriggerOrganizations }
|
||||
} = createSubmenuOrganizations();
|
||||
|
||||
const {
|
||||
elements: {
|
||||
trigger: triggerProjects,
|
||||
menu: menuProjects,
|
||||
item: itemProjects,
|
||||
separator: separatorProjects
|
||||
}
|
||||
} = createMenu();
|
||||
|
||||
export let organizations: Organization[] = [];
|
||||
|
||||
$: selectedOrg = organizations.find((organization) => organization.isSelected);
|
||||
$: selectedProject = selectedOrg?.projects.find((project) => project.isSelected);
|
||||
|
||||
let isSmallViewport = false;
|
||||
let organisationBottomSheetOpen = false;
|
||||
let projectsBottomSheetOpen = false;
|
||||
|
||||
const switchOrganization = {
|
||||
top: {
|
||||
title: 'Switch Organization',
|
||||
items: organizations.map((organization) => ({
|
||||
name: organization.name,
|
||||
href: `/console/organization-${organization?.$id}`
|
||||
}))
|
||||
},
|
||||
bottom: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create organization',
|
||||
leadingIcon: IconPlus,
|
||||
href: `/console/create-organization`
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
$: organizationsBottomSheet = !selectedOrg
|
||||
? switchOrganization
|
||||
: {
|
||||
top: {
|
||||
items: [
|
||||
{
|
||||
name: 'Organization settings',
|
||||
href: `/console/organization-${selectedOrg?.$id}/settings`
|
||||
}
|
||||
]
|
||||
},
|
||||
bottom: {
|
||||
items: [
|
||||
{
|
||||
name: 'Switch organization',
|
||||
trailingIcon: IconChevronRight,
|
||||
subMenu: switchOrganization
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
let projectsBottomSheet: SheetMenu;
|
||||
$: projectsBottomSheet = {
|
||||
top: {
|
||||
title: 'Switch project',
|
||||
items: !selectedOrg
|
||||
? []
|
||||
: selectedOrg.projects.map((project) => ({
|
||||
name: project.name,
|
||||
href: `/console/project-${project.$id}/overview`
|
||||
}))
|
||||
},
|
||||
bottom: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `/console/organization-${selectedOrg?.$id}?create-project`
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (window) {
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
const updateViewport = () => (isSmallViewport = mediaQuery.matches);
|
||||
|
||||
// Initial check
|
||||
updateViewport();
|
||||
|
||||
// Listen for changes
|
||||
mediaQuery.addEventListener('change', updateViewport);
|
||||
|
||||
return () => {
|
||||
// Cleanup listener
|
||||
mediaQuery.removeEventListener('change', updateViewport);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div use:melt={$menubar}>
|
||||
{#if !isSmallViewport}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
use:melt={$triggerOrganizations}
|
||||
aria-label="Open organizations tab">
|
||||
<span class="orgNameProject">{selectedOrg?.name ?? 'Organization'}</span>
|
||||
<span class="not-mobile"
|
||||
>{#if selectedOrg?.tierName}<Badge
|
||||
variant="secondary"
|
||||
content={selectedOrg?.tierName} />{/if}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
on:click={() => {
|
||||
organisationBottomSheetOpen = true;
|
||||
}}
|
||||
aria-label="Open organizations tab">
|
||||
<span class="orgNameProject">{selectedOrg?.name ?? 'Organization'}</span>
|
||||
<span class="not-mobile"
|
||||
><Badge variant="secondary" content={selectedOrg?.tierName ?? ''} /></span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="menu" use:melt={$menuOrganizations}>
|
||||
{#if selectedOrg}
|
||||
<a
|
||||
href={`/console/organization-${selectedOrg?.$id}/settings`}
|
||||
class="item"
|
||||
use:melt={$itemOrganizations}>
|
||||
Organization settings
|
||||
</a>
|
||||
<div class="separator" use:melt={$separatorOrganizations} />
|
||||
<div class="item switch-org" use:melt={$subTriggerOrganizations}>
|
||||
Switch organization
|
||||
<div class="rightSlot"><Icon icon={IconChevronRight} size="s" /></div>
|
||||
</div>
|
||||
<div class="menu subMenu" use:melt={$subMenuOrganizations}>
|
||||
<div use:melt={$radioGroupOrganizations}>
|
||||
{#each organizations as organization}
|
||||
<a
|
||||
href={`/console/organization-${organization?.$id}`}
|
||||
class="item"
|
||||
use:melt={$itemOrganizations}>
|
||||
{organization.name}
|
||||
</a>
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorOrganizations} />
|
||||
<a
|
||||
class="item"
|
||||
href="/console/create-organization"
|
||||
use:melt={$itemOrganizations}>
|
||||
<div class="leftSlot"><Icon icon={IconPlus} size="s" /></div>
|
||||
Create organization
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each organizations as organization}
|
||||
<a
|
||||
href={`/console/organization-${organization?.$id}`}
|
||||
class="item"
|
||||
use:melt={$itemOrganizations}>
|
||||
{organization.name}
|
||||
</a>
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorOrganizations} />
|
||||
<a class="item" href="/console/create-organization" use:melt={$itemOrganizations}>
|
||||
<div class="leftSlot"><Icon icon={IconPlus} size="s" /></div>
|
||||
Create organization
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedOrg && selectedProject}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
{#if !isSmallViewport}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
use:melt={$triggerProjects}
|
||||
aria-label="Open projects tab">
|
||||
<span class="orgNameProject">{selectedProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
on:click={() => {
|
||||
projectsBottomSheetOpen = true;
|
||||
}}
|
||||
aria-label="Open projects tab">
|
||||
<span class="orgNameProject">{selectedProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="menu" use:melt={$menuProjects}>
|
||||
{#each selectedOrg.projects as project}
|
||||
<a href={`/console/project-${project.$id}`} class="item" use:melt={$itemProjects}>
|
||||
{project.name}
|
||||
</a>
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorProjects} />
|
||||
<a
|
||||
class="item"
|
||||
use:melt={$itemProjects}
|
||||
href={`/console/organization-${selectedOrg?.$id}?create-project`}>
|
||||
<div class="leftSlot"><Icon icon={IconPlus} size="s" /></div>
|
||||
Create project
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet}
|
||||
></BottomSheet.Menu>
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} menu={projectsBottomSheet}
|
||||
></BottomSheet.Menu>
|
||||
|
||||
<style lang="scss">
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none !important;
|
||||
min-width: 220px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border-neutral, #ededf0);
|
||||
background: var(--color-bgcolor-neutral-primary, #fff);
|
||||
z-index: 20;
|
||||
|
||||
/* box-shadow/neutral/L */
|
||||
box-shadow:
|
||||
-2px 8px 16px 0px rgba(0, 0, 0, 0.02),
|
||||
-2px 20px 24px 0px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.not-mobile {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.subMenu {
|
||||
min-width: 220px;
|
||||
margin-inline: -4px;
|
||||
margin-block: -4px;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
border-radius: 0.125rem;
|
||||
z-index: 20;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
margin: 0 4px;
|
||||
padding: var(--space-3, 6px) var(--space-4, 8px) var(--space-3, 6px) var(--space-5, 10px);
|
||||
color: var(--color-fgcolor-neutral-secondary, #56565c);
|
||||
|
||||
/* Desktop/Body M 400 */
|
||||
font-family: var(--font-family-sansserif, Inter);
|
||||
font-size: var(--font-size-S, 14px);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 140%; /* 19.6px */
|
||||
letter-spacing: -0.063px;
|
||||
}
|
||||
|
||||
.orgNameProject {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 60px;
|
||||
|
||||
@media (min-width: 390px) {
|
||||
max-width: 95px;
|
||||
}
|
||||
|
||||
@media (min-width: 390px) {
|
||||
max-width: 105px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.item:first-of-type {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.item:last-of-type,
|
||||
.switch-org {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item[data-highlighted] {
|
||||
border-radius: var(--border-radius-S, 8px);
|
||||
background: var(--color-overlay-neutral-hover, rgba(25, 25, 28, 0.03));
|
||||
}
|
||||
.trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-1, 2px) var(--space-2, 4px);
|
||||
gap: var(--space-2, 4px);
|
||||
margin: 0 var(--space-5, 10px) 0 var(--space-5, 10px);
|
||||
|
||||
transition: color 0.2s ease;
|
||||
|
||||
color: var(--color-fgcolor-neutral-primary, #2d2d31);
|
||||
border-radius: var(--corner-radius-medium, 8px);
|
||||
|
||||
cursor: default;
|
||||
/* Body text/level 2 Regular */
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 150%; /* 21px */
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
background: var(--color-overlay-neutral-hover, rgba(25, 25, 28, 0.03));
|
||||
}
|
||||
|
||||
.trigger[data-highlighted] {
|
||||
outline: none;
|
||||
background: var(--color-bgcolor-neutral-secondary, #f4f4f7);
|
||||
}
|
||||
|
||||
.trigger[data-highlighted]:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--color-bgcolor-neutral-secondary, #f4f4f7);
|
||||
}
|
||||
|
||||
.trigger:focus {
|
||||
z-index: 30;
|
||||
box-shadow:
|
||||
var(--shadow-offsetx-0, 0px) var(--shadow-offsety-0, 0px) 0 2px
|
||||
var(--color-bgcolor-neutral-default, #fafafb),
|
||||
0 0 0 4px var(--color-border-focus, #818186);
|
||||
}
|
||||
.separator {
|
||||
margin: 5px 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-border-neutral);
|
||||
}
|
||||
|
||||
.rightSlot {
|
||||
margin-left: auto;
|
||||
padding-left: 1.25rem;
|
||||
display: flex;
|
||||
}
|
||||
.leftSlot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: var(--space-4, 8px);
|
||||
}
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-fgcolor-neutral-tertiary, #97979b);
|
||||
}
|
||||
</style>
|
||||
@@ -78,3 +78,4 @@ export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
|
||||
export { default as Navbar } from './navbar.svelte';
|
||||
export { default as BreadcrumbsConsole } from './breadcrumbsConsole.svelte';
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Navbar,
|
||||
BreadcrumbsConsole,
|
||||
Icon,
|
||||
Button,
|
||||
Layout,
|
||||
Link,
|
||||
Tooltip
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Navbar, Icon, Layout, Link, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
|
||||
import type { BaseNavbarProps } from '@appwrite.io/pink-svelte/dist/navbar/Base.svelte';
|
||||
import { IconBell, IconMenuAlt4, IconSearch } from '@appwrite.io/pink-icons-svelte';
|
||||
import { DropList, Support } from '$lib/components';
|
||||
import { DropList, Support, BreadcrumbsConsole } from '$lib/components';
|
||||
import { Feedback } from '$lib/components/feedback';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
@@ -41,17 +33,20 @@
|
||||
export let organizations: $$Props['organizations'];
|
||||
export let avatar: $$Props['avatar'];
|
||||
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
|
||||
export let showSideNavigation: boolean;
|
||||
</script>
|
||||
|
||||
<Navbar.Base {...$$props}>
|
||||
<div slot="left" class="left">
|
||||
<div class="only-mobile-tablet">
|
||||
<button
|
||||
class="sideNavToggle"
|
||||
on:click={() => {
|
||||
sideBarIsOpen = !sideBarIsOpen;
|
||||
}}><Icon icon={IconMenuAlt4} /></button>
|
||||
</div>
|
||||
{#if showSideNavigation}
|
||||
<div class="only-mobile-tablet">
|
||||
<button
|
||||
class="sideNavToggle"
|
||||
on:click={() => {
|
||||
sideBarIsOpen = !sideBarIsOpen;
|
||||
}}><Icon icon={IconMenuAlt4} /></button>
|
||||
</div>
|
||||
{/if}
|
||||
<img src={logo.src} alt={logo.alt} class="only-desktop" />
|
||||
<BreadcrumbsConsole {organizations} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { Navbar } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
@@ -26,18 +25,6 @@
|
||||
$showSubNavigation = url.searchParams.get('openNavbar') === 'true';
|
||||
});
|
||||
|
||||
const toggleMenu = () => {
|
||||
y = 0;
|
||||
$showSubNavigation = !$showSubNavigation;
|
||||
if (browser) {
|
||||
if ($showSubNavigation) {
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
} else {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel navigation when wizard is open and triggered by popstate
|
||||
*/
|
||||
@@ -91,7 +78,7 @@
|
||||
<svelte:component this={$activeHeaderAlert.component} />
|
||||
{/if}
|
||||
{#if showHeader}
|
||||
<Navbar {...navbarProps} bind:sideBarIsOpen />
|
||||
<Navbar {...navbarProps} bind:sideBarIsOpen bind:showSideNavigation />
|
||||
{/if}
|
||||
|
||||
{#if showSideNavigation}
|
||||
@@ -121,16 +108,6 @@
|
||||
on:click={() => {
|
||||
sideBarIsOpen = false;
|
||||
}}></button>
|
||||
<!-- <section class="main-content">-->
|
||||
<!-- {#if $page.data?.header}-->
|
||||
<!-- <svelte:component this={$page.data.header} />-->
|
||||
<!-- {/if}-->
|
||||
|
||||
<!-- <slot />-->
|
||||
<!-- {#if showFooter}-->
|
||||
<!-- <slot name="footer" />-->
|
||||
<!-- {/if}-->
|
||||
<!-- </section>-->
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -177,15 +154,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main-side {
|
||||
z-index: 25;
|
||||
}
|
||||
|
||||
@media (max-width: 550.99px), (min-width: 551px) and (max-width: 1198.99px) {
|
||||
.main-side {
|
||||
top: 4.5rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1199px) {
|
||||
.grid-with-side {
|
||||
grid-template-columns: auto 1fr !important;
|
||||
|
||||
Reference in New Issue
Block a user