Files
Mermix/src/App.svelte
Aleksey Shakhmatov bbc12ee4be feat(optimize): Diagram Doctor panel to declutter tangled flowcharts
Add an Optimize side panel that analyses the active flowchart and applies
one-click, reversible source rewrites to make dense graphs readable.

- optimize.ts: pure, label-aware flowchart parser + metrics, a 0-100
  readability score, and transforms — ELK layered layout, node/rank
  spacing + curved edges, de-emphasising cross-cutting hub edges
  (event bus / audit / cost-style fan-ins), duplicate-edge removal,
  direction toggle, and a non-destructive focus variant.
- mermaid.ts: register @mermaid-js/layout-elk so `layout: elk` renders.
- OptimizePanel.svelte + Toolbar toggle; panels are mutually exclusive.
- Preview: render the focus variant when a node is spotlighted, with a
  clear-focus pill.
- store/Editor: applySource() bumps a revision so programmatic rewrites
  re-sync CodeMirror and stay undoable with the editor's history.
2026-05-22 20:11:49 +03:00

240 lines
6.2 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+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">
<Sidebar />
{#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>