Add console breadcrumbs

This commit is contained in:
ernstmul
2025-01-15 12:19:34 +01:00
parent 0e83b4af5c
commit 6689b93ce9
4 changed files with 434 additions and 50 deletions
@@ -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>
+1
View File
@@ -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';
+12 -17
View File
@@ -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 -33
View File
@@ -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;