Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile Nav #2197

Merged
merged 22 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/lib/components/bottom-nav-links.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import NavigationItem from '$lib/holocene/navigation/navigation-item.svelte';
import type { NavLinkListItem } from '$lib/types/global';

export let open = false;
export let linkList: NavLinkListItem[];
</script>

{#if open}
<div class="flex h-full flex-col justify-start gap-4">
{#each [...linkList].reverse() as item}
Alex-Tideman marked this conversation as resolved.
Show resolved Hide resolved
{#if item.divider}
<hr class="border-subtle" />
{/if}
<NavigationItem
link={item.href}
label={item.label}
icon={item.icon}
tooltip={item.tooltip || item.label}
external={item?.external}
animate={item?.animate}
/>
{/each}
</div>
{/if}
40 changes: 40 additions & 0 deletions src/lib/components/bottom-nav-namespaces.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script lang="ts">
import Input from '$lib/holocene/input/input.svelte';
import type { NamespaceListItem } from '$lib/types/global';

export let open = false;
export let namespaceList: NamespaceListItem[] = [];

let search = '';

$: namespaces = (
search
? namespaceList.filter(({ namespace }) => namespace.includes(search))
: namespaceList
).sort((a, b) => a.namespace.localeCompare(b.namespace));
</script>

{#if open}
<div
class="relative flex h-full flex-col items-center gap-4 overflow-auto px-4 py-8"
>
<Input
id="namespace-search"
type="search"
label="Namespace search"
labelHidden
autoFocus
placeholder="Search"
class="sticky top-0 w-full"
bind:value={search}
/>
{#each namespaces as { namespace, onClick }}
<button
class="w-full text-left"
on:click|preventDefault|stopPropagation={() => onClick(namespace)}
>
{namespace}
</button>
{/each}
</div>
{/if}
88 changes: 88 additions & 0 deletions src/lib/components/bottom-nav-settings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import { onDestroy } from 'svelte';

import DataEncoderSettings from '$lib/components/data-encoder-settings.svelte';
import TimezoneSelect from '$lib/components/timezone-select.svelte';
import NavigationButton from '$lib/holocene/navigation/navigation-button.svelte';
import { translate } from '$lib/i18n/translate';
import { authUser } from '$lib/stores/auth-user';
import { dataEncoder } from '$lib/stores/data-encoder';
import { labsMode } from '$lib/stores/labs-mode';
import { goto } from '$lib/svelte-mocks/app/navigation';
import { useDarkMode } from '$lib/utilities/dark-mode';

import { viewDataEncoderSettings } from './data-encoder-settings.svelte';

export let open = false;
export let logout: () => void;
export let userEmaiLink = '';

$: labsHoverText = `${translate('common.labs')} ${
$labsMode
? `${translate('common.on')} - ${translate('common.experimental')}`
: translate('common.off')
}`;
$: labsText = `${translate('common.labs')} ${
$labsMode ? translate('common.on') : translate('common.off')
}`;

$: hasCodecServer = $dataEncoder?.endpoint;

const onCodecServerClick = () => {
$viewDataEncoderSettings = !$viewDataEncoderSettings;
};

onDestroy(() => {
$viewDataEncoderSettings = false;
});
</script>

{#if open}
<div class="flex h-full flex-col justify-start gap-4">
<TimezoneSelect position="left" />
{#if $dataEncoder.hasError}
<p class="text-red-400">{translate('data-encoder.codec-server-error')}</p>
{/if}
<NavigationButton
onClick={onCodecServerClick}
tooltip={translate('data-encoder.codec-server')}
label={translate('data-encoder.codec-server')}
icon={hasCodecServer ? 'transcoder-on' : 'transcoder-off'}
/>
<DataEncoderSettings />
<div class="border-b-2 border-subtle" />
<NavigationButton
onClick={() => ($useDarkMode = !$useDarkMode)}
tooltip={$useDarkMode
? translate('common.night')
: translate('common.day')}
label={$useDarkMode ? translate('common.night') : translate('common.day')}
icon={$useDarkMode ? 'moon' : 'sun'}
/>
<NavigationButton
onClick={() => ($labsMode = !$labsMode)}
tooltip={labsHoverText}
label={labsText}
icon="labs"
active={$labsMode}
data-testid="labs-mode-button"
/>
{#if $authUser.accessToken}
<div class="border-b-2 border-subtle" />
<NavigationButton
onClick={() => userEmaiLink && goto(userEmaiLink)}
Alex-Tideman marked this conversation as resolved.
Show resolved Hide resolved
tooltip={$authUser.email}
label={$authUser.email}
data-testid="email"
/>

<NavigationButton
onClick={logout}
tooltip={translate('common.log-out')}
label={translate('common.log-out')}
icon="exit"
data-testid="log-out"
/>
{/if}
</div>
{/if}
193 changes: 193 additions & 0 deletions src/lib/components/bottom-nav.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script lang="ts">
import { slide } from 'svelte/transition';

import { twMerge as merge } from 'tailwind-merge';

import { beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';

import Button from '$lib/holocene/button.svelte';
import Icon from '$lib/holocene/icon/icon.svelte';
import Logo from '$lib/holocene/logo.svelte';
import { translate } from '$lib/i18n/translate';
import { authUser } from '$lib/stores/auth-user';
import { dataEncoder } from '$lib/stores/data-encoder';
import { lastUsedNamespace } from '$lib/stores/namespaces';
import type { NamespaceListItem, NavLinkListItem } from '$lib/types/global';
import { routeForNamespace } from '$lib/utilities/route-for';

import BottomNavLinks from './bottom-nav-links.svelte';
import BottomNavNamespaces from './bottom-nav-namespaces.svelte';
import BottomNavSettings from './bottom-nav-settings.svelte';

export let logout: () => void;
export let namespaceList: NamespaceListItem[] = [];
export let userEmaiLink = '';
export let linkList: NavLinkListItem[];
export let isCloud = false;

let viewLinks = false;
let viewNamespaces = false;
let viewSettings = false;

$: namespace = $page.params.namespace || $lastUsedNamespace;
$: pathNameSplit = $page.url.pathname.split('/');
$: showNamespaceSpecificNav =
namespace &&
(pathNameSplit.includes('workflows') ||
pathNameSplit.includes('schedules') ||
pathNameSplit.includes('batch-operations') ||
pathNameSplit.includes('task-queues') ||
pathNameSplit.includes('import'));
$: namespaceExists = namespaceList.some(
(namespaceListItem) => namespaceListItem.namespace === namespace,
);

let showProfilePic = true;

function fixImage() {
showProfilePic = false;
}

const onLinksClick = () => {
viewSettings = false;
viewNamespaces = false;
viewLinks = !viewLinks;
};

const onNamespaceClick = () => {
viewLinks = false;
viewNamespaces = !viewNamespaces;
viewSettings = false;
};

const onSettingsClick = () => {
viewLinks = false;
viewNamespaces = false;
viewSettings = !viewSettings;
};

beforeNavigate(() => {
viewLinks = false;
viewSettings = false;
viewNamespaces = false;
});

$: menuIsOpen = viewLinks || viewNamespaces || viewSettings;

const truncateNamespace = (namespace: string) => {
if (namespace.length > 16) {
return `${namespace.slice(0, 8)}...${namespace.slice(-8)}`;
}
return namespace;
};
</script>

{#if menuIsOpen}
<div
class="group surface-primary fixed top-0 z-50 h-[calc(100%-64px)] w-full overflow-auto p-4 md:hidden"
data-nav="open"
in:slide
out:slide
>
<BottomNavLinks open={viewLinks} {linkList} />
<BottomNavNamespaces open={viewNamespaces} {namespaceList} />
<BottomNavSettings open={viewSettings} {logout} {userEmaiLink} />
</div>
{/if}
<nav
class={merge(
'fixed bottom-0 z-40 flex h-[64px] w-full flex-row items-center justify-between gap-5 px-4 py-2 transition-colors md:hidden',
isCloud
? 'bg-gradient-to-b from-indigo-600 to-indigo-900 text-off-white focus-visible:[&_[role=button]]:ring-success focus-visible:[&_a]:ring-success'
: 'surface-primary border-t border-subtle',
)}
data-testid="top-nav"
class:bg-red-400={$dataEncoder.hasError && showNamespaceSpecificNav}
aria-label={translate('common.main')}
>
<button
class="shadow-button"
class:active-shadow={!isCloud && viewLinks}
class:cloud-shadow-button={isCloud}
class:cloud-active-shadow={isCloud && viewLinks}
type="button"
on:click={onLinksClick}
>
<Logo height={32} width={32} class="m-1" />
</button>
<div class="namespace-wrapper">
<Button
variant="ghost"
leadingIcon="namespace-switcher"
size="xs"
class="grow"
on:click={onNamespaceClick}>{truncateNamespace(namespace)}</Button
>
<div class="ml-1 h-full w-1 border-l-2 border-subtle" />
<Button
variant="ghost"
size="xs"
href={routeForNamespace({ namespace })}
disabled={!namespaceExists}><Icon name="external-link" /></Button
>
</div>
<button
class="shadow-button"
class:active-shadow={!isCloud && viewSettings}
class:cloud-shadow-button={isCloud}
class:cloud-active-shadow={isCloud && viewSettings}
type="button"
class:rounded-md={$authUser.accessToken}
class:rounded-full={!$authUser.accessToken}
on:click={onSettingsClick}
>
{#if $authUser.accessToken}
<img
src={$authUser.picture}
alt={$authUser?.profile ?? translate('common.user-profile')}
class="w-[36px] min-w-[36px] cursor-pointer rounded-md"
on:error={fixImage}
class:hidden={!showProfilePic}
/>
<div
class="flex aspect-square w-[36px] min-w-[36px] items-center justify-center rounded-md bg-blue-200"
class:hidden={showProfilePic}
>
{#if $authUser?.name}
<div class="w-full text-center text-sm text-black">
{$authUser?.name.trim().charAt(0)}
</div>
{/if}
</div>
{:else}
<div
class="flex aspect-square items-center justify-center rounded-md p-1"
>
<Icon name="settings" height={32} width={32} />
</div>
{/if}
</button>
</nav>

<style lang="postcss">
.namespace-wrapper {
@apply surface-primary flex flex h-10 w-full grow flex-row items-center items-center rounded-lg border-2 border-subtle px-0.5 text-sm dark:focus-within:surface-primary focus-within:border-interactive focus-within:outline-none focus-within:ring-4 focus-within:ring-primary/70;
}

.shadow-button {
@apply relative select-none rounded-lg text-center align-middle text-xs font-medium uppercase shadow-md shadow-slate-900/40 transition-all dark:shadow-slate-300/60;
}

.active-shadow {
@apply shadow-lg shadow-slate-900/80 dark:shadow-slate-300/80;
}

.cloud-shadow-button {
@apply shadow-slate-300/60 dark:shadow-slate-300/60;
}

.cloud-active-shadow {
@apply shadow-lg shadow-slate-300/80 dark:shadow-slate-300/80;
}
</style>
4 changes: 1 addition & 3 deletions src/lib/components/data-encoder-settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import Accordion from '$lib/holocene/accordion.svelte';
import Button from '$lib/holocene/button.svelte';
import Link from '$lib/holocene/link.svelte';
import { clickoutside } from '$lib/holocene/outside-click';
import { translate } from '$lib/i18n/translate';
import {
codecEndpoint,
Expand Down Expand Up @@ -76,9 +75,8 @@

{#if $viewDataEncoderSettings}
<aside
use:clickoutside={() => ($viewDataEncoderSettings = false)}
in:fly={{ y: -50, delay: 0, duration: 500 }}
class="surface-primary relative flex h-[540px] w-full flex-col gap-6 overflow-auto border-b border-subtle p-12"
class="surface-primary relative flex h-[540px] w-full flex-col gap-6 overflow-auto border-b border-subtle p-4 md:p-12"
>
<div class="flex w-full flex-col gap-4 xl:w-1/2">
<div class="flex items-center justify-between space-x-2">
Expand Down
Loading
Loading