Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 71 additions & 0 deletions src/lib/layout/alertStack.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts">
import { setContext, onMount, onDestroy } from 'svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import { afterNavigate } from '$app/navigation';
import { bannerSpacing } from './headerAlert.svelte';

// Signal to child HeaderAlert components that layout is managed here
setContext('isInAlertStack', true);

let container: HTMLElement | null = null;
let resizeObserver: ResizeObserver;

function setNavigationHeight() {
const alertHeight = container ? container.getBoundingClientRect().height : 0;

if (alertHeight) {
bannerSpacing.set(`${alertHeight}px`);
} else {
bannerSpacing.set(null);
}

const header: HTMLHeadingElement = document.querySelector('main > header');
const sidebar: HTMLElement = document.querySelector('main nav');
const contentSection: HTMLElement = document.querySelector('main .main-content');

if (header) {
header.style.top = `${alertHeight}px`;
}

if (sidebar) {
const headerHeight = header?.getBoundingClientRect().height ?? 0;
const topOffset = alertHeight + ($isTabletViewport ? 0 : headerHeight);
sidebar.style.top = `${topOffset}px`;
sidebar.style.height = `calc(100vh - ${topOffset}px)`;
}

if (contentSection) {
contentSection.style.paddingBlockStart = `${alertHeight}px`;
}
}

onMount(() => {
if (container) {
resizeObserver = new ResizeObserver(setNavigationHeight);
resizeObserver.observe(container);
}
});

onDestroy(() => {
container = null;
setNavigationHeight();
if (resizeObserver) {
resizeObserver.disconnect();
}
});

afterNavigate(() => setNavigationHeight());
</script>

<div bind:this={container} class="alert-stack">
<slot />
</div>

<style>
.alert-stack {
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
</style>
33 changes: 23 additions & 10 deletions src/lib/layout/headerAlert.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</script>

<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { getContext, onDestroy, onMount } from 'svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import { createEventDispatcher } from 'svelte';
import { Button } from '$lib/elements/forms';
Expand All @@ -17,6 +17,9 @@
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
export let dismissible = false;

// When inside an AlertStack, layout management is handled by the stack wrapper
const isInStack: boolean = getContext('isInAlertStack') ?? false;

/**
* This is needed because when the
* slot's height gets changed, sometimes the update doesn't come through!
Expand All @@ -36,7 +39,7 @@
// for sidebar and sub-navigation!
bannerSpacing.set(`${alertHeight}px`);
} else {
bannerSpacing.set(undefined);
bannerSpacing.set(null);
}

if (header) {
Expand All @@ -57,28 +60,33 @@
}

onMount(() => {
if (container) {
if (!isInStack && container) {
resizeObserver = new ResizeObserver(setNavigationHeight);
resizeObserver.observe(container);
}
});

onDestroy(() => {
container = null;
setNavigationHeight();

// remove observer!
if (resizeObserver) {
resizeObserver.disconnect();
if (!isInStack) {
container = null;
setNavigationHeight();

// remove observer!
if (resizeObserver) {
resizeObserver.disconnect();
}
}
});

afterNavigate(() => setNavigationHeight());
afterNavigate(() => {
if (!isInStack) setNavigationHeight();
});
</script>

<section
bind:this={container}
class="alert is-action is-action-and-top-sticky u-sep-block-end"
class:in-stack={isInStack}
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
Expand Down Expand Up @@ -125,6 +133,11 @@
z-index: 100;
}

.alert.in-stack {
position: static;
z-index: unset;
}

.alert-content {
gap: 0.25rem;
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { default as Activity } from './activity.svelte';
export { default as Progress } from './progress.svelte';
export { default as GridHeader } from './gridHeader.svelte';
export { default as ContainerHeader } from './containerHeader.svelte';
export { default as AlertStack } from './alertStack.svelte';
export { default as HeaderAlert } from './headerAlert.svelte';
export { default as ContainerButton } from './containerButton.svelte';
export { default as WizardSecondaryContainer } from './wizardSecondaryContainer.svelte';
Expand Down
27 changes: 23 additions & 4 deletions src/lib/layout/shell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import { Navbar, Sidebar } from '$lib/components';
import { isNewWizardStatusOpen, wizard } from '$lib/stores/wizard';
import { activeHeaderAlert } from '$routes/(console)/store';
import AlertStack from './alertStack.svelte';
import ImpersonationBanner from '$lib/components/impersonation/banner.svelte';
import {
impersonationRevision,
readImpersonationTargetUserId
} from '$lib/appwrite/impersonation';
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import { showSubNavigation, showOnboardingAnimation } from '$lib/stores/layout';
Expand All @@ -28,6 +34,13 @@

$: activeProject = selectedProject && page.params.project ? selectedProject : null;

let isImpersonating = !!readImpersonationTargetUserId();
$: {
void $impersonationRevision;
isImpersonating = !!readImpersonationTargetUserId();
}
$: hasAnyAlert = isImpersonating || !!$activeHeaderAlert?.show;

// variables
let yOnMenuOpen: number;
let showAccountMenu = false;
Expand Down Expand Up @@ -214,16 +227,22 @@

<svelte:body use:style={$bodyStyle} />

{#if $activeHeaderAlert?.show && !$isNewWizardStatusOpen}
<svelte:component this={$activeHeaderAlert.component} />
{#if hasAnyAlert && !$isNewWizardStatusOpen}
<AlertStack>
<!-- ImpersonationBanner self-manages its own visibility via its internal {#if isImpersonating} guard -->
<ImpersonationBanner />
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
</AlertStack>
{/if}

<main
class:has-alert={$activeHeaderAlert?.show}
class:has-alert={hasAnyAlert}
class:is-open={$showSubNavigation}
class:is-sidebar-open={$isSidebarOpen}
class:u-hide={$wizard.show || $wizard.cover}
class:is-fixed-layout={$activeHeaderAlert?.show}
class:is-fixed-layout={hasAnyAlert}
class:no-header={!showHeader || $showOnboardingAnimation}
style:--p-side-size={sideSize}>
{#if showHeader && !$showOnboardingAnimation}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ export async function checkPaymentAuthorizationRequired(org: Models.Organization
importance: 8
});
}
activeHeaderAlert.set(headerAlert.get());
activeHeaderAlert.set(headerAlert.getExcluding('impersonation'));

actionRequiredInvoices.set(invoices);
}
Expand Down
36 changes: 18 additions & 18 deletions src/lib/stores/headerAlert.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Component } from 'svelte';
import { writable } from 'svelte/store';
import { get as getStore, writable } from 'svelte/store';

export type HeaderAlert = {
id: string;
Expand All @@ -13,13 +13,11 @@ export type HeaderAlertStore = {
};

function createHeaderAlertStore() {
const { subscribe, update, set } = writable<HeaderAlertStore>({
components: []
});
const store = writable<HeaderAlertStore>({ components: [] });
const { subscribe, update } = store;

return {
subscribe,
set,
add: (component: HeaderAlert) => {
update((n) => {
if (n.components.some((c) => c.id === component.id)) return n;
Expand All @@ -42,19 +40,21 @@ function createHeaderAlertStore() {
},
get: (): HeaderAlert => {
// return highest importance visible component
let component = {
id: '',
show: false,
component: null,
importance: 0
};
update((n) => {
n.components.forEach((c) => {
if (c.show && c.importance > component.importance) {
component = c;
}
});
return n;
let component = { id: '', show: false, component: null, importance: 0 };
getStore(store).components.forEach((c) => {
if (c.show && c.importance > component.importance) {
component = c;
}
});
return component as HeaderAlert;
},
getExcluding: (excludeId: string): HeaderAlert => {
// return highest importance visible component, excluding a specific id
let component = { id: '', show: false, component: null, importance: 0 };
getStore(store).components.forEach((c) => {
if (c.show && c.id !== excludeId && c.importance > component.importance) {
component = c;
}
});
return component as HeaderAlert;
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(console)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
calculateTrialDay(org);
}
}
$activeHeaderAlert = headerAlert.get();
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
}
}
Expand All @@ -318,7 +318,7 @@
$registerSearchers(orgSearcher, projectsSearcher);
afterUpdate(() => {
$activeHeaderAlert = headerAlert.get();
$activeHeaderAlert = headerAlert.getExcluding('impersonation');
});
</script>

Expand Down
Loading