chore: update navigation component to support collapsing and expanding the navigation menu

This commit is contained in:
Daniel Kranich 2024-02-22 17:18:29 -09:00 committed by Mattias Granlund
parent 16de23fbc4
commit 8b387a306a
6 changed files with 348 additions and 133 deletions

View File

@ -2,25 +2,29 @@
import Icon from '$lib/components/Icon.svelte';
import type { User } from '$lib/backend/cloud';
import { goto } from '$app/navigation';
import type { Persisted } from '$lib/persisted/persisted';
export let user: User | undefined;
export let pop = false;
export let isNavCollapsed: Persisted<boolean>;
</script>
<button class="btn" class:pop on:click={() => goto('/settings/')}>
<span class="name text-base-13 text-semibold">
{#if user}
{#if user.name}
{user.name}
{:else if user.given_name}
{user.given_name}
{:else if user.email}
{user.email}
{#if !$isNavCollapsed}
<span class="name text-base-13 text-semibold">
{#if user}
{#if user.name}
{user.name}
{:else if user.given_name}
{user.given_name}
{:else if user.email}
{user.email}
{/if}
{:else}
Account
{/if}
{:else}
Account
{/if}
</span>
</span>
{/if}
{#if user?.picture}
<img class="profile-picture" src={user.picture} alt="Avatar" />
{:else}

View File

@ -2,14 +2,16 @@
import SyncButton from './SyncButton.svelte';
import Badge from '$lib/components/Badge.svelte';
import Icon from '$lib/components/Icon.svelte';
import { page } from '$app/stores';
import type { Project } from '$lib/backend/projects';
import type { GitHubService } from '$lib/github/service';
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
import { page } from '$app/stores';
import type { Persisted } from '$lib/persisted/persisted';
export let project: Project;
export let baseBranchService: BaseBranchService;
export let githubService: GitHubService;
export let isNavCollapsed: Persisted<boolean>;
$: base$ = baseBranchService.base$;
$: selected = $page.url.href.endsWith('/base');
@ -17,50 +19,82 @@
let baseContents: HTMLElement;
</script>
<a href="/{project.id}/base" class="base-branch-card" class:selected bind:this={baseContents}>
<div class="icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="4" fill="#FB7D61" />
<path d="M8 4L12 8L8 12L4 8L8 4Z" fill="white" />
</svg>
</div>
{#if $isNavCollapsed}
<a
href="/{project.id}/base"
class="base-branch-card-collapsed"
class:selected
bind:this={baseContents}
>
<div class="icon">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="16" height="16" rx="4" fill="#FB7D61" />
<path d="M8 4L12 8L8 12L4 8L8 4Z" fill="white" />
</svg>
</div>
<span class="text-base-13 text-semibold trunk-label-collapsed">Trunk</span>
{#if ($base$?.behind || 0) > 0}
<Badge count={$base$?.behind || 0} help="Unmerged upstream commits" />
{/if}
</a>
{:else}
<a href="/{project.id}/base" class="base-branch-card" class:selected bind:this={baseContents}>
<div class="icon">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="16" height="16" rx="4" fill="#FB7D61" />
<path d="M8 4L12 8L8 12L4 8L8 4Z" fill="white" />
</svg>
</div>
<div class="content">
<div class="row_1">
<span class="text-base-14 text-semibold trunk-label">Trunk</span>
{#if ($base$?.behind || 0) > 0}
<Badge count={$base$?.behind || 0} help="Unmerged upstream commits" />
{/if}
<SyncButton
projectId={project.id}
{baseBranchService}
{githubService}
cloudEnabled={project?.api?.sync || false}
/>
<div class="content">
<div class="row_1">
<span class="text-base-14 text-semibold trunk-label">Trunk</span>
{#if ($base$?.behind || 0) > 0}
<Badge count={$base$?.behind || 0} help="Unmerged upstream commits" />
{/if}
<SyncButton
projectId={project.id}
{baseBranchService}
{githubService}
cloudEnabled={project?.api?.sync || false}
/>
</div>
<div class="row_2 text-base-12">
{#if $base$?.remoteUrl.includes('github.com')}
<!-- GitHub logo -->
<svg
style="width:0.75rem; height: 0.75rem"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.98091 0.599976C3.45242 0.599976 0.599976 3.47344 0.599976 7.02832C0.599976 9.86992 2.42763 12.2753 4.96308 13.1266C5.28007 13.1906 5.39619 12.9883 5.39619 12.8181C5.39619 12.6691 5.38574 12.1582 5.38574 11.626C3.61072 12.0092 3.24109 10.8597 3.24109 10.8597C2.95583 10.1147 2.53317 9.92321 2.53317 9.92321C1.9522 9.52941 2.57549 9.52941 2.57549 9.52941C3.21993 9.57199 3.55808 10.1893 3.55808 10.1893C4.12847 11.1683 5.04758 10.8917 5.41735 10.7214C5.47011 10.3063 5.63926 10.0189 5.81885 9.85934C4.40314 9.71031 2.91364 9.15691 2.91364 6.68768C2.91364 5.98525 3.16703 5.41055 3.56853 4.9636C3.50518 4.80399 3.28327 4.14401 3.63201 3.26068C3.63201 3.26068 4.17078 3.09036 5.38561 3.92053C5.90572 3.77982 6.4421 3.70824 6.98091 3.70763C7.51968 3.70763 8.06891 3.78221 8.57607 3.92053C9.79103 3.09036 10.3298 3.26068 10.3298 3.26068C10.6785 4.14401 10.4565 4.80399 10.3932 4.9636C10.8052 5.41055 11.0482 5.98525 11.0482 6.68768C11.0482 9.15691 9.55867 9.6996 8.13238 9.85934C8.36487 10.0615 8.56549 10.4446 8.56549 11.0513C8.56549 11.9133 8.55504 12.6052 8.55504 12.818C8.55504 12.9883 8.67129 13.1906 8.98815 13.1267C11.5236 12.2751 13.3513 9.86992 13.3513 7.02832C13.3617 3.47344 10.4988 0.599976 6.98091 0.599976Z"
fill="currentColor"
/>
</svg>
{:else}
<Icon name="branch" />
{/if}
{$base$?.branchName}
</div>
</div>
<div class="row_2 text-base-12">
{#if $base$?.remoteUrl.includes('github.com')}
<!-- GitHub logo -->
<svg
style="width:0.75rem; height: 0.75rem"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.98091 0.599976C3.45242 0.599976 0.599976 3.47344 0.599976 7.02832C0.599976 9.86992 2.42763 12.2753 4.96308 13.1266C5.28007 13.1906 5.39619 12.9883 5.39619 12.8181C5.39619 12.6691 5.38574 12.1582 5.38574 11.626C3.61072 12.0092 3.24109 10.8597 3.24109 10.8597C2.95583 10.1147 2.53317 9.92321 2.53317 9.92321C1.9522 9.52941 2.57549 9.52941 2.57549 9.52941C3.21993 9.57199 3.55808 10.1893 3.55808 10.1893C4.12847 11.1683 5.04758 10.8917 5.41735 10.7214C5.47011 10.3063 5.63926 10.0189 5.81885 9.85934C4.40314 9.71031 2.91364 9.15691 2.91364 6.68768C2.91364 5.98525 3.16703 5.41055 3.56853 4.9636C3.50518 4.80399 3.28327 4.14401 3.63201 3.26068C3.63201 3.26068 4.17078 3.09036 5.38561 3.92053C5.90572 3.77982 6.4421 3.70824 6.98091 3.70763C7.51968 3.70763 8.06891 3.78221 8.57607 3.92053C9.79103 3.09036 10.3298 3.26068 10.3298 3.26068C10.6785 4.14401 10.4565 4.80399 10.3932 4.9636C10.8052 5.41055 11.0482 5.98525 11.0482 6.68768C11.0482 9.15691 9.55867 9.6996 8.13238 9.85934C8.36487 10.0615 8.56549 10.4446 8.56549 11.0513C8.56549 11.9133 8.55504 12.6052 8.55504 12.818C8.55504 12.9883 8.67129 13.1906 8.98815 13.1267C11.5236 12.2751 13.3513 9.86992 13.3513 7.02832C13.3617 3.47344 10.4988 0.599976 6.98091 0.599976Z"
fill="currentColor"
/>
</svg>
{:else}
<Icon name="branch" />
{/if}
{$base$?.branchName}
</div>
</div>
</a>
</a>
{/if}
<style lang="postcss">
.base-branch-card {
@ -88,6 +122,10 @@
.trunk-label {
color: var(--clr-theme-scale-ntrl-0);
}
.trunk-label-collapsed {
color: var(--clr-theme-scale-ntrl-0);
transform: rotate(180deg);
}
.row_1 {
display: flex;
gap: var(--space-6);
@ -100,4 +138,21 @@
gap: var(--space-4);
color: var(--clr-theme-scale-ntrl-40);
}
/* COLLAPSABLE NAV */
.base-branch-card-collapsed {
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: space-between;
padding: var(--space-10) var(--space-8);
border-radius: var(--radius-m);
gap: var(--space-8);
transition: background-color var(--transition-fast);
}
.base-branch-card-collapsed:not(.selected):hover,
.base-branch-card-collapsed:not(.selected):focus,
.selected {
background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
}
</style>

View File

@ -1,12 +1,54 @@
<script lang="ts">
import { page } from '$app/stores';
import type { Persisted } from '$lib/persisted/persisted';
import type { BranchController } from '$lib/vbranches/branchController';
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
import UpdateBaseButton from './UpdateBaseButton.svelte';
export let href: string;
export let domain: string;
export let branchController: BranchController;
export let baseBranchService: BaseBranchService;
export let isNavCollapsed: Persisted<boolean>;
$: base$ = baseBranchService.base$;
$: selected = $page.url.href.includes(href);
</script>
<a {href} class="domain-button text-base-14 text-semibold" class:selected>
<slot />
<a
{href}
class="domain-button text-base-14 text-semibold"
class:collapsed={$isNavCollapsed}
class:selected
>
{#if domain === 'workspace'}
<div class="icon">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 6.64C0 4.17295 0 2.93942 0.525474 2.01817C0.880399 1.39592 1.39592 0.880399 2.01817 0.525474C2.93942 0 4.17295 0 6.64 0H9.36C11.8271 0 13.0606 0 13.9818 0.525474C14.6041 0.880399 15.1196 1.39592 15.4745 2.01817C16 2.93942 16 4.17295 16 6.64V9.36C16 11.8271 16 13.0606 15.4745 13.9818C15.1196 14.6041 14.6041 15.1196 13.9818 15.4745C13.0606 16 11.8271 16 9.36 16H6.64C4.17295 16 2.93942 16 2.01817 15.4745C1.39592 15.1196 0.880399 14.6041 0.525474 13.9818C0 13.0606 0 11.8271 0 9.36V6.64Z"
fill="#48B0AA"
/>
<rect x="2" y="3" width="6" height="10" rx="2" fill="#D9F3F2" />
<rect opacity="0.7" x="10" y="3" width="4" height="10" rx="2" fill="#D9F3F2" />
</svg>
</div>
<span class="text-base-13 text-semibold" class:collapsed-txt={$isNavCollapsed}>
Workspace
</span>
{#if ($base$?.behind || 0) > 0 && !$isNavCollapsed}
<UpdateBaseButton {branchController} />
{/if}
{:else}
<slot />
{/if}
</a>
<style lang="postcss">
@ -20,6 +62,25 @@
height: var(--space-36);
transition: background-color var(--transition-fast);
}
.collapsed {
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: space-between;
padding: var(--space-10) var(--space-8);
border-radius: var(--radius-m);
gap: var(--space-8);
transition: background-color var(--transition-fast);
height: initial;
}
.collapsed-txt {
transform: rotate(180deg);
flex-shrink: 0;
}
.icon {
flex-shrink: 0;
}
.domain-button:not(.selected):hover,
.domain-button:not(.selected):focus,

View File

@ -4,12 +4,14 @@
import Link from '$lib/components/Link.svelte';
import * as events from '$lib/utils/events';
import type { User } from '$lib/backend/cloud';
import type { Persisted } from '$lib/persisted/persisted';
export let user: User | undefined;
export let projectId: string | undefined;
export let isNavCollapsed: Persisted<boolean>;
</script>
<div class="footer" style:border-color="var(--clr-theme-container-outline-light)">
<div class="footer" class:collapsed={$isNavCollapsed}>
<div class="left-btns">
<IconButton
icon="mail"
@ -20,7 +22,7 @@
<IconButton icon="settings" help="Project settings" />
</Link>
</div>
<AccountLink {user} />
<AccountLink {user} {isNavCollapsed} />
</div>
<style lang="postcss">
@ -30,6 +32,10 @@
justify-content: space-between;
padding: var(--space-12);
border-top: 1px solid var(--clr-theme-container-outline-light);
border-color: var(--clr-theme-container-outline-light);
}
.collapsed {
padding: 0;
}
.left-btns {
display: flex;

View File

@ -4,9 +4,11 @@
import Footer from './Footer.svelte';
import ProjectSelector from './ProjectSelector.svelte';
import UpdateBaseButton from './UpdateBaseButton.svelte';
import BaseBranchCard from '$lib/components/BaseBranchCard.svelte';
import Resizer from '$lib/components/Resizer.svelte';
import { persisted } from '$lib/persisted/persisted';
import BaseBranchCard from './BaseBranchCard.svelte';
import Resizer from './Resizer.svelte';
import Button from './Button.svelte';
import { navCollapsed } from '$lib/config/config';
import { persisted, type Persisted } from '$lib/persisted/persisted';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
import { getContext } from 'svelte';
import type { User } from '$lib/backend/cloud';
@ -30,58 +32,93 @@
'defaulTrayWidth_ ' + project.id
);
$: base$ = baseBranchService.base$;
let viewport: HTMLDivElement;
const foldNav = () => {
console.log('BeforeSet: ' + $isNavCollapsed);
$isNavCollapsed = true;
console.log('AfterSet: ' + $isNavCollapsed);
};
const unfoldNav = () => {
$isNavCollapsed = false;
};
$: isNavCollapsed = navCollapsed();
</script>
<div
class="navigation relative flex w-80 shrink-0 flex-col border-r"
style:width={$defaultTrayWidthRem ? $defaultTrayWidthRem + 'rem' : null}
bind:this={viewport}
role="menu"
tabindex="0"
>
<div class="drag-region" data-tauri-drag-region></div>
<div class="domains">
<ProjectSelector {project} {projectService} />
<div class="flex flex-col gap-1">
<BaseBranchCard {project} {baseBranchService} {githubService} />
<DomainButton href={`/${project.id}/board`}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 6.64C0 4.17295 0 2.93942 0.525474 2.01817C0.880399 1.39592 1.39592 0.880399 2.01817 0.525474C2.93942 0 4.17295 0 6.64 0H9.36C11.8271 0 13.0606 0 13.9818 0.525474C14.6041 0.880399 15.1196 1.39592 15.4745 2.01817C16 2.93942 16 4.17295 16 6.64V9.36C16 11.8271 16 13.0606 15.4745 13.9818C15.1196 14.6041 14.6041 15.1196 13.9818 15.4745C13.0606 16 11.8271 16 9.36 16H6.64C4.17295 16 2.93942 16 2.01817 15.4745C1.39592 15.1196 0.880399 14.6041 0.525474 13.9818C0 13.0606 0 11.8271 0 9.36V6.64Z"
fill="#48B0AA"
/>
<rect x="2" y="3" width="6" height="10" rx="2" fill="#D9F3F2" />
<rect opacity="0.7" x="10" y="3" width="4" height="10" rx="2" fill="#D9F3F2" />
</svg>
<span>Workspace</span>
{#if ($base$?.behind || 0) > 0}
<UpdateBaseButton {branchController} />
{/if}
</DomainButton>
{#if $isNavCollapsed}
<div class="collapsed-nav-wrapper">
<div class="card collapsed-nav">
<Button
icon="unfold-lane"
kind="outlined"
color="neutral"
help="Collapse Nav"
on:click={unfoldNav}
/>
<div class="collapsed-nav__info">
<h3 class="collapsed-nav__label text-base-13 text-bold">
{project?.title}
</h3>
<DomainButton
href={`/${project.id}/board`}
domain="workspace"
{branchController}
{baseBranchService}
{isNavCollapsed}
></DomainButton>
<BaseBranchCard {project} {baseBranchService} {githubService} {isNavCollapsed} />
</div>
<div class="collapsed-nav__footer">
<Footer {user} projectId={project.id} {isNavCollapsed} />
</div>
</div>
</div>
<Branches projectId={project.id} {branchService} {githubService} />
<Footer {user} projectId={project.id} />
{:else}
<div
class="navigation relative flex w-80 shrink-0 flex-col border-r"
style:width={$defaultTrayWidthRem ? $defaultTrayWidthRem + 'rem' : null}
bind:this={viewport}
role="menu"
tabindex="0"
>
<div class="drag-region" data-tauri-drag-region></div>
<div class="hide-nav-button">
<Button
icon="fold-lane"
kind="outlined"
color="neutral"
help="Collapse Nav"
align="flex-end"
on:click={foldNav}
/>
</div>
<div class="domains">
<ProjectSelector {project} {projectService} />
<div class="flex flex-col gap-1">
<BaseBranchCard {project} {baseBranchService} {githubService} {isNavCollapsed} />
<DomainButton
href={`/${project.id}/board`}
domain="workspace"
{branchController}
{baseBranchService}
{isNavCollapsed}
></DomainButton>
</div>
</div>
<Branches projectId={project.id} {branchService} {githubService} />
<Footer {user} projectId={project.id} {isNavCollapsed} />
<Resizer
{viewport}
direction="right"
minWidth={320}
on:width={(e) => {
$defaultTrayWidthRem = e.detail / (16 * $userSettings.zoom);
}}
/>
</div>
<Resizer
{viewport}
direction="right"
minWidth={320}
on:width={(e) => {
$defaultTrayWidthRem = e.detail / (16 * $userSettings.zoom);
}}
/>
</div>
{/if}
<style lang="postcss">
.navigation {
@ -99,4 +136,51 @@
padding-left: var(--space-12);
padding-right: var(--space-12);
}
.hide-nav-button {
align-self: flex-end;
margin-right: var(--space-12);
}
.collapsed-nav-wrapper {
padding: var(--space-12);
height: 100%;
border-right: 1px solid var(--clr-theme-container-outline-light);
}
.collapsed-nav {
display: flex;
flex-direction: column;
cursor: default;
user-select: none;
height: 100%;
gap: var(--space-8);
padding: var(--space-8) var(--space-8) var(--space-20);
&:focus-within {
outline: none;
}
}
.collapsed-nav__info {
flex: 1;
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: flex-end;
height: 100%;
writing-mode: vertical-rl;
gap: var(--space-8);
}
.collapsed-nav__label {
color: var(--clr-theme-scale-ntrl-0);
transform: rotate(180deg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: var(--space-8);
}
.collapsed-nav__footer {
align-self: flex-end;
writing-mode: vertical-rl;
gap: var(--space-8);
}
</style>

View File

@ -1,60 +1,65 @@
import { persisted, type Persisted } from '$lib/persisted/persisted';
export function projectHttpsWarningBannerDismissed(projectId: string): Persisted<boolean> {
const key = 'projectHttpsWarningBannerDismissed_';
return persisted(false, key + projectId);
const key = 'projectHttpsWarningBannerDismissed_';
return persisted(false, key + projectId);
}
export function projectMergeUpstreamWarningDismissed(projectId: string): Persisted<boolean> {
const key = 'projectMergeUpstreamWarningDismissed_';
return persisted(false, key + projectId);
const key = 'projectMergeUpstreamWarningDismissed_';
return persisted(false, key + projectId);
}
export function projectCommitGenerationExtraConcise(projectId: string): Persisted<boolean> {
const key = 'projectCommitGenerationExtraConcise_';
return persisted(false, key + projectId);
const key = 'projectCommitGenerationExtraConcise_';
return persisted(false, key + projectId);
}
export function projectCommitGenerationUseEmojis(projectId: string): Persisted<boolean> {
const key = 'projectCommitGenerationUseEmojis_';
return persisted(false, key + projectId);
const key = 'projectCommitGenerationUseEmojis_';
return persisted(false, key + projectId);
}
export enum ListPRsFilter {
All = 'ALL',
ExcludeBots = 'EXCLUDE_BOTS',
OnlyYours = 'ONLY_YOURS'
All = 'ALL',
ExcludeBots = 'EXCLUDE_BOTS',
OnlyYours = 'ONLY_YOURS'
}
export function projectPullRequestListingFilter(projectId: string): Persisted<string> {
const key = 'projectPullRequestListingFilter_';
return persisted(ListPRsFilter.All, key + projectId);
const key = 'projectPullRequestListingFilter_';
return persisted(ListPRsFilter.All, key + projectId);
}
export function projectAiGenEnabled(projectId: string): Persisted<boolean> {
const key = 'projectAiGenEnabled_';
return persisted(false, key + projectId);
const key = 'projectAiGenEnabled_';
return persisted(false, key + projectId);
}
export function projectAiGenAutoBranchNamingEnabled(projectId: string): Persisted<boolean> {
const key = 'projectAiGenAutoBranchNamingEnabled_';
return persisted(false, key + projectId);
const key = 'projectAiGenAutoBranchNamingEnabled_';
return persisted(false, key + projectId);
}
export function projectRunCommitHooks(projectId: string): Persisted<boolean> {
const key = 'projectRunCommitHooks_';
return persisted(false, key + projectId);
const key = 'projectRunCommitHooks_';
return persisted(false, key + projectId);
}
export function projectLaneCollapsed(projectId: string, laneId: string): Persisted<boolean> {
const key = 'projectLaneCollapsed_';
return persisted(false, key + projectId + '_' + laneId);
const key = 'projectLaneCollapsed_';
return persisted(false, key + projectId + '_' + laneId);
}
export function navCollapsed(): Persisted<boolean> {
const key = 'projectNavCollapsed_';
return persisted(false, key);
}
export function projectCurrentCommitMessage(
projectId: string,
branchId: string
projectId: string,
branchId: string
): Persisted<string> {
const key = 'projectCurrentCommitMessage_';
return persisted('', key + projectId + '_' + branchId);
const key = 'projectCurrentCommitMessage_';
return persisted('', key + projectId + '_' + branchId);
}