fix(mana/web): EncryptionIntroBanner sits inside the bottom-stack

The one-time encryption intro banner used its own position: fixed at
bottom: 1.5rem with z-index: 60, mounted at the root layout level.
That put it in a stacking context the QuickInputBar covered up — the
search bar visually sat ON TOP of the banner instead of below it,
making the privacy claim half-readable and the dismiss X impossible
to click.

Same fix the guest nudge got in c8ed58b7d: move into the bottom-stack
flex container in (app)/+layout.svelte and let the parent handle
positioning. The banner is now the FIRST child of the stack so it
renders above the guest nudge / QuickInputBar / TagStrip / PillNav
and stays in flow as the stack reflows when nav collapses.

- Removed `<EncryptionIntroBanner />` from root +layout.svelte (it
  doesn't belong above the (app) gate anyway since it self-checks
  isVaultUnlocked() which is always false outside auth context)
- Mounted inside `.bottom-stack` as the first `.bottom-stack-notification`
  child in (app)/+layout.svelte
- Stripped position: fixed / bottom / left / transform / max-width /
  z-index from the banner CSS — now an in-flow flex item with
  width: 100% (the wrapper centres + caps width via the existing
  bottom-stack-notification rules)
- Slide-up animation rewritten to use translateY only since the
  parent no longer transforms the banner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-08 18:08:35 +02:00
parent 09f81d77de
commit 2a437a5861
3 changed files with 27 additions and 13 deletions

View file

@ -16,9 +16,17 @@
"you understand and accept what's happening" social contract
that encryption-at-rest requires.
The component is mounted once at the root layout. It self-checks
the vault state on mount and via a small interval, so it can fire
even if the unlock happens asynchronously after the layout renders.
The component is mounted inside the bottom-stack of (app)/+layout.svelte
(NOT the root layout) so it shares the stack's reflow with the
QuickInputBar / TagStrip / PillNav and can't end up rendered
behind them. Earlier the banner used its own `position: fixed`
with z-index 60, which the QuickInputBar's higher stacking context
covered up — fix was to make positioning the parent's job.
It self-checks the vault state on mount and via a small interval,
so it can fire even if the unlock happens asynchronously after the
layout renders. Guests never see it because isVaultUnlocked()
returns false until a real master key is loaded.
NOTE: The flag uses a constant string instead of a STORAGE_KEYS
import because the central storage-keys file was removed in a
@ -112,13 +120,14 @@
{/if}
<style>
/* Positioning is the parent's job (.bottom-stack-notification in
(app)/+layout.svelte). The banner used to be position: fixed
with its own bottom + transform centring; that put it in a
stacking context the QuickInputBar covered. Now it's a normal
in-flow flex item and the bottom-stack handles where it goes. */
.banner {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
max-width: 32rem;
width: calc(100% - 2rem);
width: 100%;
display: flex;
align-items: flex-start;
gap: 0.875rem;
@ -128,18 +137,17 @@
border-left: 4px solid rgb(34, 197, 94);
border-radius: 0.75rem;
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.18);
z-index: 60;
animation: slide-up 250ms ease-out;
}
@keyframes slide-up {
from {
opacity: 0;
transform: translate(-50%, 1rem);
transform: translateY(0.75rem);
}
to {
opacity: 1;
transform: translate(-50%, 0);
transform: translateY(0);
}
}

View file

@ -7,6 +7,7 @@
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
import SessionWarning from '$lib/components/SessionWarning.svelte';
import EncryptionIntroBanner from '$lib/components/EncryptionIntroBanner.svelte';
import { locale, _ } from 'svelte-i18n';
import {
PillNavigation,
@ -445,6 +446,13 @@
<div class="min-h-screen bg-background">
<!-- Bottom Stack: all fixed-bottom elements in one flex container -->
<div class="bottom-stack" style:--bottom-chrome-height="{bottomChromeHeight}px">
<!-- One-time encryption intro — sits at the top of the stack so
it can't be obscured by the QuickInputBar / TagStrip / PillNav.
Self-gates on isVaultUnlocked() so guests never see it. -->
<div class="bottom-stack-notification">
<EncryptionIntroBanner />
</div>
<!-- Guest nudge — sits above the input bar, fades with the stack -->
{#if guestMode && guestMode.notifications.length > 0}
<div class="bottom-stack-notification">

View file

@ -10,7 +10,6 @@
import { installDataLayerListeners } from '$lib/data/data-layer-listeners';
import { getVaultClient, hasAnyEncryption } from '$lib/data/crypto';
import SuggestionToast from '$lib/components/SuggestionToast.svelte';
import EncryptionIntroBanner from '$lib/components/EncryptionIntroBanner.svelte';
import RecoveryCodeUnlockModal from '$lib/components/RecoveryCodeUnlockModal.svelte';
import SyncConflictToast from '$lib/components/SyncConflictToast.svelte';
import OfflineIndicator from '$lib/components/OfflineIndicator.svelte';
@ -111,7 +110,6 @@
<SyncConflictToast />
<OfflineIndicator />
<PwaUpdatePrompt />
<EncryptionIntroBanner />
<AuthRequiredModal />
{#if needsRecoveryCode}