mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
1198d01263
commit
aa7909076c
2 changed files with 156 additions and 55 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue