From 58febd4e6555fde9a3ed1f8d13c4a6a9d7082742 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 1 Sep 2026 19:58:31 +0530 Subject: [PATCH 1/2] feat(console): add a top banner for the new Console Uses GradientBanner, the same component newDevUpgradePro uses, registered through headerAlert at importance 1 so it sits in the promo tier and can never outrank a payment or usage warning. Dismissal is keyed on the alert id in localStorage, matching newDevUpgradePro. Also makes GradientBanner stack-aware. It is position:fixed and offsets the shell itself, but AlertStack is fixed too and measures its children to do the same job. Inside a stack the banner measured as 0, so the stack reset the header offset the banner had just set and the banner covered the navigation until the next resize. It now stands down when a stack is in charge, mirroring headerAlert.svelte. This also fixes the same first-paint overlap for newDevUpgradePro and enterpriseTrial. The CTA carries utm_medium=banner so this surface can be compared against the promo card. --- .../components/billing/gradientBanner.svelte | 18 +++++++- src/lib/components/newConsoleBanner.svelte | 44 +++++++++++++++++++ src/routes/(console)/+layout.svelte | 14 ++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/newConsoleBanner.svelte diff --git a/src/lib/components/billing/gradientBanner.svelte b/src/lib/components/billing/gradientBanner.svelte index 7f9b62a644..228e1f6542 100644 --- a/src/lib/components/billing/gradientBanner.svelte +++ b/src/lib/components/billing/gradientBanner.svelte @@ -6,10 +6,18 @@ import { isTabletViewport } from '$lib/stores/viewport'; import PinkBackground from '$lib/images/pink-background.svg'; import { bannerSpacing } from '$lib/layout/headerAlert.svelte'; - import { createEventDispatcher, onMount, onDestroy } from 'svelte'; + import { createEventDispatcher, getContext, onMount, onDestroy } from 'svelte'; export let variant: 'gradient' | 'image' = 'gradient'; + /** + * An AlertStack parent is itself fixed and measures its children to offset the shell. This + * banner is position:fixed, so inside a stack it measures as 0 and the stack resets the + * header offset we just set, leaving the banner covering the navigation until the next + * resize. Mirror headerAlert.svelte and stand down when the stack is in charge. + */ + const isInStack: boolean = getContext('isInAlertStack') ?? false; + let container: HTMLElement; const dispatch = createEventDispatcher(); @@ -20,6 +28,8 @@ }); const setNavigationHeight = () => { + if (isInStack) return; + const alertHeight = container?.getBoundingClientRect()?.height || 0; const { header, sidebar, content } = queryLayoutElements(); const headerHeight = header?.getBoundingClientRect().height || 0; @@ -51,6 +61,7 @@
{#if variant === 'gradient'}
@@ -92,6 +103,11 @@ position: fixed; padding: 0.8rem; + &.in-stack { + position: relative; + z-index: unset; + } + &.darker { background: var(--bgcolor-neutral-default); } diff --git a/src/lib/components/newConsoleBanner.svelte b/src/lib/components/newConsoleBanner.svelte new file mode 100644 index 0000000000..87c9a783f0 --- /dev/null +++ b/src/lib/components/newConsoleBanner.svelte @@ -0,0 +1,44 @@ + + +{#if show} + + + + Introducing the new Appwrite Console, rebuilt from the ground up. + + + + + +{/if} diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index b0f4267981..f9ed943d87 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -45,6 +45,7 @@ import { UsageRates } from '$lib/components/billing'; import { canSeeProjects } from '$lib/stores/roles'; import { BottomModalAlert } from '$lib/components'; + import NewConsoleBanner from '$lib/components/newConsoleBanner.svelte'; import { isSmallViewport } from '$lib/stores/viewport'; import { IconAnnotation, @@ -325,6 +326,19 @@ $registerSearchers(orgSearcher, projectsSearcher); + onMount(() => { + // Same shape as newDevUpgradePro: promo tier (importance 1) so it can never outrank a + // payment or usage warning, dismissal keyed on the alert id in localStorage. + if (isCloud && !localStorage.getItem('newConsoleBanner')) { + headerAlert.add({ + id: 'newConsoleBanner', + component: NewConsoleBanner, + show: true, + importance: 1 + }); + } + }); + $: (void $headerAlert, ($activeHeaderAlert = headerAlert.getExcluding('impersonation'))); From 4fe48d75d916f887dce582591d90273413285c15 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 1 Sep 2026 20:05:46 +0530 Subject: [PATCH 2/2] fix(console): release the header alert slot when the banner is dismissed handleClose only hid the local component, leaving the store entry visible. headerAlert.get() compares importance and breaks ties by registration order, so the dismissed entry kept winning the importance-1 tie and rendered an empty slot, hiding newDevUpgradePro and the backup policy alert until a reload. Now calls headerAlert.updateShow(id, false) so activeHeaderAlert recomputes and the next eligible promo can take the slot. Verified: after dismissal the AlertStack unmounts and the header offset returns to 0 rather than leaving an empty banner mounted. Also routes NewConsoleBanner through the components barrel, per AGENTS.md. --- src/lib/components/index.ts | 1 + src/lib/components/newConsoleBanner.svelte | 54 +++++++++++----------- src/routes/(console)/+layout.svelte | 3 +- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 09b4d248c3..84a530f097 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -76,6 +76,7 @@ export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte'; export { default as ImagePreview } from './imagePreview.svelte'; export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte'; export { default as BottomModalAlert } from './bottomModalAlert.svelte'; +export { default as NewConsoleBanner } from './newConsoleBanner.svelte'; export { default as Navbar } from './navbar.svelte'; export { default as Breadcrumbs } from './breadcrumbs.svelte'; export { default as Sidebar } from './sidebar.svelte'; diff --git a/src/lib/components/newConsoleBanner.svelte b/src/lib/components/newConsoleBanner.svelte index 87c9a783f0..82857f99c3 100644 --- a/src/lib/components/newConsoleBanner.svelte +++ b/src/lib/components/newConsoleBanner.svelte @@ -3,6 +3,7 @@ import { Button } from '$lib/elements/forms'; import { Layout, Typography } from '@appwrite.io/pink-svelte'; import { isSmallViewport } from '$lib/stores/viewport'; + import { headerAlert } from '$lib/stores/headerAlert'; import { activeHeaderAlert } from '$routes/(console)/store'; import GradientBanner from './billing/gradientBanner.svelte'; @@ -10,35 +11,36 @@ const href = 'https://appwrite.io/?utm_source=old-console&utm_medium=banner&utm_campaign=new-console'; - let show = true; - function handleClose() { - show = false; - localStorage.setItem($activeHeaderAlert.id, new Date().getTime().toString()); + const { id } = $activeHeaderAlert; + localStorage.setItem(id, new Date().getTime().toString()); trackEvent('close_new_console_banner', { source: 'new_console_banner' }); + + // Clear the store entry, not just this component. headerAlert.get() picks the highest + // importance and breaks ties by registration order, so an entry left visible keeps + // winning the importance-1 tie and hides the next eligible promo behind an empty slot. + headerAlert.updateShow(id, false); } -{#if show} - - - - Introducing the new Appwrite Console, rebuilt from the ground up. - + + + + Introducing the new Appwrite Console, rebuilt from the ground up. + - - - -{/if} + + + diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index f9ed943d87..2cd9e62be6 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -44,8 +44,7 @@ import { headerAlert } from '$lib/stores/headerAlert'; import { UsageRates } from '$lib/components/billing'; import { canSeeProjects } from '$lib/stores/roles'; - import { BottomModalAlert } from '$lib/components'; - import NewConsoleBanner from '$lib/components/newConsoleBanner.svelte'; + import { BottomModalAlert, NewConsoleBanner } from '$lib/components'; import { isSmallViewport } from '$lib/stores/viewport'; import { IconAnnotation,