Rework safe area insets with CSS variables and per-element spacers

Replace container-level env(safe-area-inset-*) inline padding with
--safe-top/--safe-bottom CSS variables applied per-element: drawer,
task list panel, detail panels, FAB, settings overlay, error banner,
and setup screen. Wrap task list in {#key app.activeListId} to force
re-render on list switch (co-located with FAB safe-bottom fix).
This commit is contained in:
Tristan Michael 2026-04-05 19:10:37 -07:00
parent 501f991f2c
commit ca52ed9fee
4 changed files with 28 additions and 5 deletions

View file

@ -19,18 +19,27 @@
class="relative h-full w-full overflow-hidden bg-surface-light text-text-light dark:bg-surface-dark dark:text-text-dark"
class:rounded-xl={isLinux}
class:linux-window-border={isLinux}
style="container-type: inline-size{isMobile ? '; padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)' : ''}"
style="container-type: inline-size"
>
{#if app.error}
<div
class="absolute top-0 left-0 right-0 z-50 flex items-center justify-between bg-danger px-4 py-2 text-sm text-white"
style="top: env(safe-area-inset-top)"
>
<span>{app.error}</span>
<button onclick={() => app.clearError()} class="ml-2 font-bold"></button>
</div>
{/if}
{#if app.screen === "missing"}
{#if app.initialSync}
<div class="flex h-full flex-col items-center justify-center gap-4">
<svg class="h-8 w-8 animate-spin text-primary" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
</svg>
<p class="text-sm text-text-secondary-light dark:text-text-secondary-dark">Syncing workspace&hellip;</p>
</div>
{:else if app.screen === "missing"}
<div class="flex h-full items-center justify-center p-6">
<div class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark">
<h1 class="mb-1 text-2xl font-bold">Workspace Not Found</h1>

View file

@ -36,6 +36,12 @@ body {
background: transparent;
}
/* Safe area CSS variable — content elements opt into this, overlays don't */
:root {
--safe-top: env(safe-area-inset-top);
--safe-bottom: env(safe-area-inset-bottom);
}
.linux-window-border {
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 0 2px rgba(0, 0, 0, 0.1);

View file

@ -221,6 +221,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="flex h-full flex-col" onmousedown={handleDrag}>
<div class="shrink-0" style="height: var(--safe-top)"></div>
<!-- Title bar area with window controls -->
<header class="flex h-11 shrink-0 items-center justify-between px-2">
<div>

View file

@ -215,6 +215,7 @@
>
<!-- Drawer panel -->
<div class="flex h-full shrink-0 flex-col bg-surface-light dark:bg-surface-dark" style="width: 80cqi">
<div class="shrink-0" style="height: var(--safe-top)"></div>
<!-- Drawer header: workspace switcher + settings -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
@ -327,7 +328,7 @@
</div>
<!-- Drawer footer: sync status -->
<div class="shrink-0 px-4 py-2.5">
<div class="shrink-0 px-4 py-2.5" style="padding-bottom: max(0.625rem, var(--safe-bottom))">
{#if app.isWebdav}
<div class="flex items-center gap-2">
<!-- Status dot -->
@ -374,6 +375,7 @@
>
<!-- Sub-panel: Task list -->
<div class="relative flex h-full w-1/3 flex-col">
<div class="shrink-0" style="height: var(--safe-top)"></div>
<!-- Header / drag region -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<header
@ -495,6 +497,7 @@
<!-- Task list -->
<main class="flex-1 overflow-y-auto">
{#key app.activeListId}
{#if app.lists.length === 0}
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
<p class="text-lg font-medium opacity-60">No lists yet</p>
@ -558,11 +561,13 @@
{/if}
{/if}
{/if}
{/key}
</main>
<!-- FAB button -->
<div
class="pointer-events-none absolute bottom-6 left-0 right-0 z-20 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer || taskStack.length > 0 ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
class="pointer-events-none absolute left-0 right-0 z-20 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer || taskStack.length > 0 ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
style="bottom: max(1.5rem, var(--safe-bottom))"
>
<button
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
@ -578,6 +583,7 @@
<!-- Sub-panel: Task detail -->
<div class="relative flex h-full w-1/3 flex-col bg-surface-light dark:bg-surface-dark">
<div class="shrink-0" style="height: var(--safe-top)"></div>
{#if parentTask}
{#key parentTask.id}
<TaskDetailView task={parentTask} onback={closeDetail} onopen={pushTask} />
@ -587,6 +593,7 @@
<!-- Sub-panel: Subtask detail -->
<div class="relative flex h-full w-1/3 flex-col bg-surface-light dark:bg-surface-dark">
<div class="shrink-0" style="height: var(--safe-top)"></div>
{#if subtaskDetail}
{#key subtaskDetail.id}
<TaskDetailView task={subtaskDetail} onback={closeDetail} />
@ -603,7 +610,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute inset-0 z-50 flex transition-opacity duration-200 {showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
style="padding: 4%"
style="padding: 4%; padding-top: max(4%, env(safe-area-inset-top)); padding-bottom: max(4%, env(safe-area-inset-bottom))"
>
<!-- Backdrop -->
<div