Files
console/src/lib/components/breadcrumbs.svelte
T
2025-02-13 14:04:47 +01:00

458 lines
16 KiB
Svelte

<script lang="ts">
import { createMenubar, melt } from '@melt-ui/svelte';
import { Badge, Icon, type SheetMenu, ActionMenu, Card } from '@appwrite.io/pink-svelte';
import {
IconChevronDown,
IconChevronRight,
IconPlus,
IconPlusSm
} from '@appwrite.io/pink-icons-svelte';
import { BottomSheet } from '$lib/components';
import { isSmallViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { newOrgModal } from '$lib/stores/organization';
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 organisationBottomSheetOpen = false;
let projectsBottomSheetOpen = false;
function createOrg() {
if (isCloud) {
goto(`${base}/create-organization`);
} else newOrgModal.set(true);
}
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,
onClick: createOrg
}
]
}
};
$: organizationsBottomSheet = !selectedOrg
? switchOrganization
: {
top: {
items: [
{
name: 'Organization overview',
href: `/console/organization-${selectedOrg?.$id}`
}
]
},
bottom:
organizations.length > 1
? {
items: [
{
name: 'Switch organization',
trailingIcon: IconChevronRight,
subMenu: switchOrganization
}
]
}
: {
items: [
{
name: 'Create organization',
leadingIcon: IconPlus,
onClick: createOrg
}
]
}
};
let projectsBottomSheet: SheetMenu;
$: projectsBottomSheet = {
top:
selectedOrg?.projects.length > 1
? {
title: 'Switch project',
items: !selectedOrg
? []
: selectedOrg?.projects
.map((project, index) => {
if (index < 4) {
return {
name: project.name,
href: `/console/project-${project.$id}/overview`
};
} else if (index === 4) {
return {
name: 'All projects',
href: `/console/organization-${selectedOrg?.$id}`
};
}
return null;
})
.filter((project) => project !== null)
}
: {
items: [
{
name: 'Create project',
trailingIcon: IconPlus,
href: `/console/organization-${selectedOrg?.$id}?create-project`
}
]
},
bottom:
selectedOrg?.projects.length > 1
? {
items: [
{
name: 'Create project',
trailingIcon: IconPlus,
href: `/console/organization-${selectedOrg?.$id}?create-project`
}
]
}
: undefined
};
</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="orgName">{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="orgName" class:noProjects={!selectedProject}
>{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}>
<Card.Base padding="xxxs" shadow={true}>
{#if selectedOrg}
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`/console/organization-${selectedOrg?.$id}`}
>Organization overview</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
{#if organizations.length > 1}
<div class="separator" use:melt={$separatorOrganizations} />
<div use:melt={$subTriggerOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
>Switch organization</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
<div class="menu subMenu" use:melt={$subMenuOrganizations}>
<Card.Base padding="xxxs" shadow={true}>
<div use:melt={$radioGroupOrganizations}>
{#each organizations as organization}
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${organization?.$id}`}
>{organization.name}</ActionMenu.Item.Anchor>
</ActionMenu.Root>
</div>
{/each}
<div class="separator" use:melt={$separatorOrganizations} />
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Button
leadingIcon={IconPlusSm}
on:click={createOrg}
>Create organization</ActionMenu.Item.Button
></ActionMenu.Root>
</div>
</div>
</Card.Base>
</div>
{:else}
<div class="separator" use:melt={$separatorOrganizations} />
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Button leadingIcon={IconPlusSm} on:click={createOrg}
>Create organization</ActionMenu.Item.Button
></ActionMenu.Root>
</div>
{/if}
{:else}
{#each organizations as organization}
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${organization?.$id}`}
>{organization.name}</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
{/each}
<div class="separator" use:melt={$separatorOrganizations} />
<div use:melt={$itemOrganizations}>
<ActionMenu.Root>
<ActionMenu.Item.Button leadingIcon={IconPlusSm} on:click={createOrg}
>Create organization</ActionMenu.Item.Button
></ActionMenu.Root>
</div>
{/if}
</Card.Base>
</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="projectName">{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="projectName">{selectedProject.name}</span>
<Icon icon={IconChevronDown} size="s" />
</button>
{/if}
<div class="menu" use:melt={$menuProjects}>
<Card.Base padding="xxxs" shadow={true}>
{#if selectedOrg.projects.length > 1}
{#each selectedOrg.projects as project, index}
{#if index < 4}
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor href={`/console/project-${project.$id}`}
>{project.name}</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
{:else if index === 4}
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
href={`/console/organization-${selectedOrg.$id}`}
>All projects</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
{/if}
{/each}
<div class="separator" use:melt={$separatorProjects} />
{/if}
<div use:melt={$itemProjects}>
<ActionMenu.Root>
<ActionMenu.Item.Anchor
leadingIcon={IconPlusSm}
href={`/console/organization-${selectedOrg?.$id}?create-project`}
>Create project</ActionMenu.Item.Anchor
></ActionMenu.Root>
</div>
</Card.Base>
</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: var(--border-radius-m, 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;
}
.orgName,
.projectName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60px;
@media (min-width: 390px) {
max-width: 95px;
}
@media (min-width: 400px) {
max-width: 105px;
}
@media (min-width: 800px) {
max-width: 125px;
}
@media (min-width: 1024px) {
max-width: 150px;
}
}
.noProjects {
max-width: 150px;
}
:global(.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-1, 2px) var(--space-1, 2px) var(--space-3, 6px);
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));
}
:global(.trigger[data-highlighted]) {
outline: none;
background: var(--color-bgcolor-neutral-secondary, #f4f4f7);
}
:global(.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 {
height: 1px;
margin-block: 2px;
margin-inline-start: calc(var(--base-4) * -1);
width: calc(100% + var(--base-8));
background-color: var(--color-border-neutral);
}
.breadcrumb-separator {
color: var(--color-fgcolor-neutral-tertiary, #97979b);
}
</style>