Files
Mermix/src/App.svelte
Aleksey Shakhmatov e1b5f31f87 feat(ui): toggle the diagram-list sidebar
Add a ☰ toolbar button (and ⌘/Ctrl+B shortcut) to show/hide the left sidebar
for a wider editing/preview area. The preview re-fits when the sidebar — or the
Optimize panel — toggles, so the diagram stays centred.
2026-05-22 21:50:06 +03:00

247 lines
6.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { fly } from 'svelte/transition';
import { store } from './lib/store.svelte';
import StartScreen from './lib/components/StartScreen.svelte';
import Sidebar from './lib/components/Sidebar.svelte';
import Toolbar from './lib/components/Toolbar.svelte';
import Editor from './lib/components/Editor.svelte';
import Preview from './lib/components/Preview.svelte';
import GitPanel from './lib/components/GitPanel.svelte';
import OptimizePanel from './lib/components/OptimizePanel.svelte';
import Toasts from './lib/components/Toasts.svelte';
// Editor/preview split ratio (fraction taken by the editor).
let ratio = $state(0.42);
let splitting = $state(false);
let splitHost = $state<HTMLDivElement>();
function startSplit(e: MouseEvent) {
splitting = true;
e.preventDefault();
}
function moveSplit(e: MouseEvent) {
if (!splitting || !splitHost) return;
const rect = splitHost.getBoundingClientRect();
ratio = Math.min(0.8, Math.max(0.2, (e.clientX - rect.left) / rect.width));
}
function endSplit() {
splitting = false;
}
function onKey(e: KeyboardEvent) {
if (store.view !== 'editor') return;
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
e.preventDefault();
void store.save();
}
// ⌘/Ctrl+B: toggle the diagram-list sidebar.
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'b') {
e.preventDefault();
store.showSidebar = !store.showSidebar;
}
// ⌘/Ctrl+E: jump into viewer mode and toggle the quick-edit drawer.
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'e') {
e.preventDefault();
if (store.layout !== 'preview') store.layout = 'preview';
store.editDrawer = !store.editDrawer;
}
if (e.key === 'Escape' && store.editDrawer) {
store.editDrawer = false;
}
}
</script>
<svelte:window onkeydown={onKey} onmousemove={moveSplit} onmouseup={endSplit} />
{#if store.view === 'start'}
<StartScreen />
{:else}
<div class="app">
<Toolbar />
<div class="body">
{#if store.showSidebar}
<Sidebar />
{/if}
{#if store.activeId}
<div class="split" bind:this={splitHost} class:splitting>
{#if store.layout !== 'preview'}
<div
class="pane"
style={store.layout === 'split' ? `width: ${ratio * 100}%` : 'flex: 1'}
>
<Editor />
</div>
{/if}
{#if store.layout === 'split'}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="gutter"
role="separator"
aria-orientation="vertical"
tabindex="-1"
onmousedown={startSplit}
></div>
{/if}
{#if store.layout !== 'code'}
<div class="pane preview-pane">
<Preview />
</div>
{/if}
{#if store.layout === 'preview'}
{#if store.editDrawer}
<div class="edit-drawer" transition:fly={{ x: -360, duration: 180 }}>
<div class="drawer-head">
<span class="drawer-title">{store.activeName}</span>
<div class="drawer-actions">
<button onclick={() => store.save()} disabled={!store.dirty}>Save</button>
<button class="ghost icon" title="Close (Esc)" onclick={() => (store.editDrawer = false)}>✕</button>
</div>
</div>
<div class="drawer-body">
<Editor />
</div>
</div>
{:else}
<button
class="fab"
title="Quick edit (⌘E)"
onclick={() => (store.editDrawer = true)}
>
✎ Edit
{#if store.dirty}<span class="fab-dot"></span>{/if}
</button>
{/if}
{/if}
</div>
{:else}
<div class="no-diagram">
<div class="muted">No diagram open.</div>
<div class="faint">Pick one from the sidebar, or create a new diagram with .</div>
</div>
{/if}
{#if store.showOptimize}
<OptimizePanel />
{/if}
{#if store.showGit}
<GitPanel />
{/if}
</div>
</div>
{/if}
<Toasts />
<style>
.app {
height: 100%;
display: flex;
flex-direction: column;
}
.body {
flex: 1;
display: flex;
min-height: 0;
}
.split {
flex: 1;
display: flex;
min-width: 0;
position: relative;
}
.edit-drawer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: clamp(360px, 42%, 640px);
display: flex;
flex-direction: column;
background: var(--bg-elev);
border-right: 1px solid var(--border-strong);
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.45);
z-index: 20;
}
.drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 8px 8px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.drawer-title {
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drawer-actions {
display: flex;
gap: 6px;
align-items: center;
}
.drawer-body {
flex: 1;
min-height: 0;
}
.fab {
position: absolute;
left: 16px;
bottom: 16px;
z-index: 25;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 16px;
border-radius: 999px;
background: var(--accent);
border-color: var(--accent);
color: #fff;
font-weight: 600;
box-shadow: 0 6px 22px rgba(124, 92, 255, 0.45);
}
.fab:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.fab-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--amber);
}
.split.splitting {
cursor: col-resize;
user-select: none;
}
.pane {
min-width: 0;
height: 100%;
overflow: hidden;
}
.preview-pane {
flex: 1;
}
.gutter {
width: 5px;
flex-shrink: 0;
background: var(--border);
cursor: col-resize;
transition: background 0.12s ease;
}
.gutter:hover {
background: var(--accent);
}
.no-diagram {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
text-align: center;
}
</style>