feat(website): full-bleed editor + unified sidebar with Seiten/Einfügen/Block tabs

Two usability fixes for the website editor — the preview was cramped
between two sidebars inside the default max-w-7xl layout shell.

Layout:
- (app) layout: detect the editor route and skip the max-w-7xl clamp
  + horizontal padding, so the editor gets the full viewport width

Editor shell:
- Replace the two fixed sidebars (16rem left + 20rem right = 36rem) with
  one 18rem tabbed sidebar on the right — nets ~18rem (~288px) of extra
  canvas room on a 1440px display
- Tabs: Seiten (site meta + PageList), Einfügen (InsertPalette), Block
  (BlockInspector with the move/delete controls)
- Selecting a block auto-switches to the Block tab (via untrack-guarded
  $effect so changing the tab manually doesn't fight the selection)
- Switching pages resets selection + returns to the Seiten tab
- Empty-page hint points to the Einfügen tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-23 23:03:19 +02:00
parent 1198d01263
commit aa7909076c
2 changed files with 156 additions and 55 deletions

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import {
useAllSites,
useAllPages,
@ -30,8 +31,11 @@
const sitePages = $derived(pagesForSite(pages.value, props.siteId));
const pageBlocks = $derived(blocksForPage(blocks.value, props.pageId));
type SidebarTab = 'pages' | 'insert' | 'block';
let selectedBlockId = $state<string | null>(null);
let showSettings = $state(false);
let activeTab = $state<SidebarTab>('pages');
const selectedBlock = $derived(
selectedBlockId ? (pageBlocks.find((b) => b.id === selectedBlockId) ?? null) : null
@ -47,7 +51,21 @@
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
props.pageId;
selectedBlockId = null;
untrack(() => {
selectedBlockId = null;
activeTab = 'pages';
});
});
// Auto-switch to Block tab whenever a block is selected — the user's
// next action is almost always to edit its props. Untracked so this
// doesn't loop when `activeTab` itself changes.
$effect(() => {
if (selectedBlockId) {
untrack(() => {
activeTab = 'block';
});
}
});
async function addBlock(type: string) {
@ -62,37 +80,11 @@
{/if}
<div class="wb-editor">
<aside class="wb-editor__left">
{#if site}
<div class="wb-editor__site-meta">
<div class="wb-editor__site-row">
<div class="wb-editor__site-id">
<p class="wb-editor__site-name">{site.name}</p>
<p class="wb-editor__site-slug">/s/{site.slug}</p>
</div>
<button
class="wb-editor__settings-btn"
onclick={() => (showSettings = true)}
title="Website-Einstellungen"
>
</button>
</div>
</div>
{/if}
<PageList siteId={props.siteId} pages={sitePages} activePageId={props.pageId} />
<div class="wb-editor__palette">
<InsertPalette onInsert={addBlock} />
</div>
</aside>
<main class="wb-editor__center">
{#if pageBlocks.length === 0}
<div class="wb-editor__empty">
<h3>Leere Seite</h3>
<p>Füge links einen Block ein, um loszulegen.</p>
<p>Öffne den Tab <strong>Einfügen</strong> rechts, um den ersten Block zu setzen.</p>
</div>
{:else}
<div class="wb-editor__preview">
@ -106,18 +98,79 @@
{/if}
</main>
<aside class="wb-editor__right">
{#if selectedBlock}
<BlockInspector
block={selectedBlock}
siblings={selectedSiblings}
onDeleted={() => (selectedBlockId = null)}
/>
{:else}
<p class="wb-editor__inspector-empty">
Wähle einen Block in der Vorschau, um ihn zu bearbeiten.
</p>
{/if}
<aside class="wb-editor__sidebar">
<div class="wb-tabs" role="tablist" aria-label="Editor-Panels">
<button
class="wb-tab"
class:wb-tab--active={activeTab === 'pages'}
role="tab"
aria-selected={activeTab === 'pages'}
onclick={() => (activeTab = 'pages')}
>
Seiten
</button>
<button
class="wb-tab"
class:wb-tab--active={activeTab === 'insert'}
role="tab"
aria-selected={activeTab === 'insert'}
onclick={() => (activeTab = 'insert')}
>
Einfügen
</button>
<button
class="wb-tab"
class:wb-tab--active={activeTab === 'block'}
role="tab"
aria-selected={activeTab === 'block'}
onclick={() => (activeTab = 'block')}
>
Block
</button>
</div>
<div class="wb-sidebar__body">
{#if activeTab === 'pages'}
<div class="wb-sidebar__pane">
{#if site}
<div class="wb-editor__site-meta">
<div class="wb-editor__site-row">
<div class="wb-editor__site-id">
<p class="wb-editor__site-name">{site.name}</p>
<p class="wb-editor__site-slug">/s/{site.slug}</p>
</div>
<button
class="wb-editor__settings-btn"
onclick={() => (showSettings = true)}
title="Website-Einstellungen"
>
</button>
</div>
</div>
{/if}
<PageList siteId={props.siteId} pages={sitePages} activePageId={props.pageId} />
</div>
{:else if activeTab === 'insert'}
<div class="wb-sidebar__pane">
<InsertPalette onInsert={addBlock} />
</div>
{:else}
<div class="wb-sidebar__pane">
{#if selectedBlock}
<BlockInspector
block={selectedBlock}
siblings={selectedSiblings}
onDeleted={() => (selectedBlockId = null)}
/>
{:else}
<p class="wb-editor__inspector-empty">
Wähle einen Block in der Vorschau, um ihn zu bearbeiten.
</p>
{/if}
</div>
{/if}
</div>
</aside>
</div>
</div>
@ -134,22 +187,17 @@
}
.wb-editor {
display: grid;
grid-template-columns: 16rem 1fr 20rem;
grid-template-columns: 1fr 18rem;
gap: 1px;
flex: 1 1 auto;
min-height: 0;
background: rgba(255, 255, 255, 0.06);
}
.wb-editor__left,
.wb-editor__right {
.wb-editor__sidebar {
background: rgb(15, 18, 24);
padding: 1rem;
overflow-y: auto;
}
.wb-editor__left {
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 0;
}
.wb-editor__center {
background: rgb(10, 12, 16);
@ -178,6 +226,52 @@
margin: 0;
font-size: 0.875rem;
}
/* Tabs */
.wb-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
flex: 0 0 auto;
}
.wb-tab {
flex: 1 1 0;
padding: 0.625rem 0.5rem;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: inherit;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
opacity: 0.6;
transition:
opacity 0.15s,
border-color 0.15s,
background 0.15s;
}
.wb-tab:hover {
opacity: 0.9;
background: rgba(255, 255, 255, 0.03);
}
.wb-tab--active {
opacity: 1;
border-bottom-color: rgba(99, 102, 241, 0.9);
}
.wb-sidebar__body {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
}
.wb-sidebar__pane {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
/* Site meta (inside Pages tab) */
.wb-editor__site-meta {
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
@ -219,12 +313,10 @@
opacity: 0.55;
font-family: ui-monospace, monospace;
}
.wb-editor__palette {
margin-top: auto;
}
.wb-editor__inspector-empty {
font-size: 0.8125rem;
opacity: 0.5;
margin: 0;
}
@media (max-width: 960px) {
@ -232,9 +324,8 @@
grid-template-columns: 1fr;
grid-auto-rows: min-content;
}
.wb-editor__left,
.wb-editor__right {
max-height: 50vh;
.wb-editor__sidebar {
max-height: 60vh;
}
}
</style>

View file

@ -118,6 +118,12 @@
// is already inside the flow.
let isOnboarding = $derived($page.url.pathname.startsWith('/onboarding'));
// Full-bleed routes skip the max-w-7xl clamp + horizontal padding so
// the module can use the full viewport width. Currently used by the
// website editor — a canvas-style tool where the centre preview
// shouldn't fight two sidebars inside a 1280px container.
let isFullBleedRoute = $derived(/^\/website\/[^/]+\/edit\/[^/]+/.test($page.url.pathname));
// ── App switcher ────────────────────────────────────────
// Prefer the active Space's tier for gating — falls back to the user
// tier only during the bootstrap window where no space has loaded.
@ -1052,7 +1058,11 @@
8}px; --bottom-chrome-height: {bottomChromeHeight}px; --workbench-reserved-y: 1.5rem;"
class="pt-2"
>
<div class="mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8">
<div
class={isFullBleedRoute
? 'w-full'
: 'mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8'}
>
{#if routeBlocked && routeAppId}
<RouteTierGate
appName={routeAppId.name}