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="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:rounded-xl={isLinux}
class:linux-window-border={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} {#if app.error}
<div <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" 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> <span>{app.error}</span>
<button onclick={() => app.clearError()} class="ml-2 font-bold"></button> <button onclick={() => app.clearError()} class="ml-2 font-bold"></button>
</div> </div>
{/if} {/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="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"> <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> <h1 class="mb-1 text-2xl font-bold">Workspace Not Found</h1>

View file

@ -36,6 +36,12 @@ body {
background: transparent; 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 { .linux-window-border {
border: 1px solid rgba(0, 0, 0, 0.15); 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); 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 --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="flex h-full flex-col" onmousedown={handleDrag}> <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 --> <!-- Title bar area with window controls -->
<header class="flex h-11 shrink-0 items-center justify-between px-2"> <header class="flex h-11 shrink-0 items-center justify-between px-2">
<div> <div>

View file

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