Files
Mermix/src/lib/components/GitPanel.svelte
Aleksey Shakhmatov 29bf6438b3 feat(frontend): Svelte editor, live preview and Git panel
- CodeMirror 6 editor with a debounced Mermaid 11 preview (zoom, pan,
  auto fit-to-view and inline error reporting)
- project start screen with recent projects, sidebar diagram explorer,
  toolbar and a Git panel (status, commit, history, branches)
- SVG/PNG export, per-project theme switching, toasts
- rune-based central store orchestrating all backend calls
- view modes (Code / Split / Preview) plus a viewer mode with a
  slide-in quick-edit drawer (Cmd/Ctrl+E)
2026-05-22 16:27:28 +03:00

269 lines
6.3 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 { store } from '../store.svelte';
import Modal from './Modal.svelte';
let message = $state('');
let newBranch = $state<string | null>(null);
const hasChanges = $derived(store.changes.length > 0 || store.dirty);
async function commit() {
const msg = message.trim();
if (!msg) return;
await store.commit(msg);
message = '';
}
async function onBranchChange(e: Event) {
const name = (e.target as HTMLSelectElement).value;
if (name && name !== store.branch) await store.checkoutBranch(name);
}
async function createBranch() {
const name = (newBranch ?? '').trim();
if (!name) return;
await store.createBranch(name);
await store.checkoutBranch(name);
newBranch = null;
}
function ago(iso: string): string {
if (!iso) return '';
const then = new Date(iso).getTime();
const s = Math.max(0, Math.floor((Date.now() - then) / 1000));
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
const d = Math.floor(h / 24);
if (d < 30) return `${d}d ago`;
return new Date(iso).toLocaleDateString();
}
</script>
<aside class="git">
<div class="branch-row">
<span class="branch-icon" title="Current branch"></span>
<select value={store.branch} onchange={onBranchChange} title="Switch branch">
{#each store.branches as b (b.name)}
<option value={b.name}>{b.name}</option>
{/each}
{#if store.branches.length === 0}
<option value={store.branch}>{store.branch}</option>
{/if}
</select>
<button class="ghost icon" title="New branch" onclick={() => (newBranch = '')}></button>
</div>
<div class="commit-box">
<div class="changes">
{#if hasChanges}
<span class="count">{store.changes.length || (store.dirty ? 1 : 0)} change(s)</span>
<ul>
{#each store.changes as c (c.path)}
<li><span class="badge {c.status}">{c.status[0].toUpperCase()}</span>{c.path}</li>
{/each}
{#if store.changes.length === 0 && store.dirty}
<li class="faint">unsaved edits in current diagram</li>
{/if}
</ul>
{:else}
<span class="clean faint">✓ Working tree clean</span>
{/if}
</div>
<textarea
bind:value={message}
rows="2"
placeholder="Commit message…"
onkeydown={(e) => (e.metaKey || e.ctrlKey) && e.key === 'Enter' && commit()}
></textarea>
<button class="primary" onclick={commit} disabled={!message.trim() || !hasChanges}>
Commit
</button>
</div>
<div class="hist-head">History</div>
<div class="history">
{#each store.history as c (c.id)}
<div class="commit" title={`${c.message}\n${c.author} <${c.email}>`}>
<code class="sha">{c.short_id}</code>
<div class="meta">
<div class="msg">{c.message}</div>
<div class="sub faint">{c.author} · {ago(c.time)}</div>
</div>
</div>
{/each}
{#if store.history.length === 0}
<div class="empty faint">No commits yet.</div>
{/if}
</div>
</aside>
{#if newBranch !== null}
<Modal title="New branch" onClose={() => (newBranch = null)}>
<label class="fld">
<span>Branch name</span>
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
bind:value={newBranch}
placeholder="e.g. feature/new-layout"
onkeydown={(e) => e.key === 'Enter' && createBranch()}
/>
</label>
<div class="actions">
<button onclick={() => (newBranch = null)}>Cancel</button>
<button class="primary" onclick={createBranch} disabled={!newBranch.trim()}>
Create &amp; switch
</button>
</div>
</Modal>
{/if}
<style>
.git {
width: 280px;
flex-shrink: 0;
background: var(--bg-elev);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.branch-row {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.branch-icon {
color: var(--accent);
}
.commit-box {
padding: 12px;
border-bottom: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 8px;
}
.changes {
font-size: 12px;
}
.count {
color: var(--amber);
font-weight: 600;
}
.changes ul {
list-style: none;
margin: 6px 0 0;
padding: 0;
max-height: 120px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 3px;
}
.changes li {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--text-dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge {
display: inline-grid;
place-items: center;
width: 16px;
height: 16px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.badge.added {
background: var(--green);
}
.badge.modified {
background: var(--amber);
}
.badge.deleted {
background: var(--red);
}
.badge.renamed,
.badge.changed {
background: var(--accent);
}
textarea {
resize: vertical;
font-family: var(--mono);
font-size: 12px;
}
.hist-head {
padding: 12px 12px 6px;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-faint);
}
.history {
flex: 1;
overflow-y: auto;
padding: 0 8px 12px;
}
.commit {
display: flex;
gap: 8px;
padding: 8px;
border-radius: var(--radius-sm);
}
.commit:hover {
background: var(--bg-elev-2);
}
.sha {
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
flex-shrink: 0;
padding-top: 1px;
}
.meta {
overflow: hidden;
}
.msg {
font-size: 12.5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sub {
font-size: 11px;
margin-top: 2px;
}
.empty {
padding: 16px;
text-align: center;
font-size: 12px;
}
.fld {
display: flex;
flex-direction: column;
gap: 6px;
}
.fld span {
font-size: 12px;
color: var(--text-dim);
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>