feat(electrobun): groups, cloning, shortcuts, custom window — all 5 features
Groups Sidebar: - SQLite groups table (4 seeded: Development, Testing, DevOps, Research) - Left icon rail with emoji group icons, Ctrl+1-4 switching - Active group highlighted, projects filtered by group Project Cloning: - Clone button on project cards (fork icon) - git worktree add via Bun.spawn (array form, no shell strings) - 3-clone limit, branch name validation, pending-status pattern - Clone cards: WT badge + branch name + accent top border - Chain link SVG icons between linked clones in grid Keyboard Shortcuts: - keybinding-store.svelte.ts: 16 defaults across 4 categories - Two-scope: document capture + terminal focus guard - KeyboardSettings.svelte: search, click-to-capture, conflict detection - Per-binding reset + Reset All Custom Window: - titleBarStyle: "hidden" — no native title bar - Vertical "AGOR" text in left sidebar (writing-mode: vertical-rl) - Floating window controls badge (minimize/maximize/close) - Draggable region via -webkit-app-region: drag - Window frame persisted to SQLite (debounced 500ms) Window is resizable by default (Electrobun BrowserWindow).
This commit is contained in:
parent
5032021915
commit
a020f59cb4
14 changed files with 1741 additions and 189 deletions
|
|
@ -29,6 +29,14 @@
|
|||
contextPct?: number;
|
||||
burnRate?: number;
|
||||
blinkVisible?: boolean;
|
||||
/** Worktree branch name — set when this is a clone card. */
|
||||
worktreeBranch?: string;
|
||||
/** ID of parent project — set when this is a clone card. */
|
||||
cloneOf?: string;
|
||||
/** Max clones reached for this project. */
|
||||
clonesAtMax?: boolean;
|
||||
/** Callback when user requests cloning. */
|
||||
onClone?: (projectId: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -46,8 +54,34 @@
|
|||
contextPct = 0,
|
||||
burnRate = 0,
|
||||
blinkVisible = true,
|
||||
worktreeBranch,
|
||||
cloneOf,
|
||||
clonesAtMax = false,
|
||||
onClone,
|
||||
}: Props = $props();
|
||||
|
||||
// ── Clone dialog state ──────────────────────────────────────────
|
||||
let showCloneDialog = $state(false);
|
||||
let cloneBranchName = $state('');
|
||||
let cloneError = $state('');
|
||||
|
||||
const BRANCH_RE = /^[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
function openCloneDialog() {
|
||||
cloneBranchName = '';
|
||||
cloneError = '';
|
||||
showCloneDialog = true;
|
||||
}
|
||||
|
||||
function submitClone() {
|
||||
if (!BRANCH_RE.test(cloneBranchName)) {
|
||||
cloneError = 'Use only letters, numbers, /, _, -, .';
|
||||
return;
|
||||
}
|
||||
onClone?.(id);
|
||||
showCloneDialog = false;
|
||||
}
|
||||
|
||||
let activeTab = $state<ProjectTab>('model');
|
||||
// svelte-ignore state_referenced_locally
|
||||
const seedMessages = initialMessages.slice();
|
||||
|
|
@ -77,8 +111,9 @@
|
|||
|
||||
<article
|
||||
class="project-card"
|
||||
class:is-clone={!!cloneOf}
|
||||
style="--accent: {accent}"
|
||||
aria-label="Project: {name}"
|
||||
aria-label="Project: {name}{cloneOf ? ' (worktree clone)' : ''}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="project-header">
|
||||
|
|
@ -94,6 +129,12 @@
|
|||
<span class="project-name" title={name}>{name}</span>
|
||||
<span class="project-cwd" title={cwd}>{cwd}</span>
|
||||
|
||||
{#if worktreeBranch}
|
||||
<span class="wt-badge" title="Worktree branch: {worktreeBranch}">
|
||||
WT · {worktreeBranch}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="provider-badge" title="Provider: {provider}">{provider}</span>
|
||||
|
||||
{#if profile}
|
||||
|
|
@ -112,8 +153,56 @@
|
|||
{#if burnRate > 0}
|
||||
<span class="burn-badge" title="Burn rate">${burnRate.toFixed(2)}/hr</span>
|
||||
{/if}
|
||||
|
||||
<!-- Clone button (only on non-clone cards) -->
|
||||
{#if !cloneOf && onClone}
|
||||
<button
|
||||
class="clone-btn"
|
||||
onclick={openCloneDialog}
|
||||
disabled={clonesAtMax}
|
||||
title={clonesAtMax ? 'Maximum 3 clones reached' : 'Clone into git worktree'}
|
||||
aria-label="Clone project into worktree"
|
||||
>
|
||||
<!-- Fork / branch SVG icon -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="6" y1="3" x2="6" y2="15"/>
|
||||
<circle cx="18" cy="6" r="3"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<path d="M18 9a9 9 0 0 1-9 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<!-- Clone dialog (inline, shown above tab bar) -->
|
||||
{#if showCloneDialog}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="clone-dialog"
|
||||
role="dialog"
|
||||
aria-label="Create worktree clone"
|
||||
onkeydown={(e) => { if (e.key === 'Escape') showCloneDialog = false; }}
|
||||
>
|
||||
<span class="clone-dialog-label">Branch name</span>
|
||||
<input
|
||||
class="clone-dialog-input"
|
||||
type="text"
|
||||
placeholder="feature/my-branch"
|
||||
bind:value={cloneBranchName}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') submitClone(); }}
|
||||
autofocus
|
||||
aria-label="New branch name for worktree"
|
||||
/>
|
||||
{#if cloneError}
|
||||
<span class="clone-dialog-error">{cloneError}</span>
|
||||
{/if}
|
||||
<div class="clone-dialog-actions">
|
||||
<button class="clone-dialog-cancel" onclick={() => showCloneDialog = false}>Cancel</button>
|
||||
<button class="clone-dialog-submit" onclick={submitClone}>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Project tab bar -->
|
||||
<div class="tab-bar" role="tablist" aria-label="{name} tabs">
|
||||
{#each ALL_TABS as tab}
|
||||
|
|
@ -276,6 +365,15 @@
|
|||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Worktree clone: accent top border instead of left stripe */
|
||||
.project-card.is-clone {
|
||||
border-top: 2px solid var(--accent, var(--ctp-mauve));
|
||||
}
|
||||
|
||||
.project-card.is-clone::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.project-header {
|
||||
height: 2.5rem;
|
||||
|
|
@ -367,6 +465,131 @@
|
|||
color: var(--ctp-peach);
|
||||
}
|
||||
|
||||
/* Worktree badge */
|
||||
.wt-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 0.1rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
background: color-mix(in srgb, var(--accent, var(--ctp-mauve)) 20%, transparent);
|
||||
color: var(--accent, var(--ctp-mauve));
|
||||
border: 1px solid color-mix(in srgb, var(--accent, var(--ctp-mauve)) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Clone button */
|
||||
.clone-btn {
|
||||
flex-shrink: 0;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ctp-overlay1);
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.clone-btn:hover:not(:disabled) {
|
||||
background: var(--ctp-surface0);
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
.clone-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.clone-btn svg {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
/* Inline clone dialog */
|
||||
.clone-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--ctp-mantle);
|
||||
border-bottom: 1px solid var(--ctp-surface0);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.clone-dialog-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ctp-subtext0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.clone-dialog-input {
|
||||
flex: 1;
|
||||
min-width: 8rem;
|
||||
height: 1.625rem;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--ctp-text);
|
||||
font-family: var(--term-font-family);
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.375rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clone-dialog-input:focus { border-color: var(--ctp-mauve); }
|
||||
|
||||
.clone-dialog-error {
|
||||
width: 100%;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--ctp-red);
|
||||
}
|
||||
|
||||
.clone-dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.clone-dialog-cancel,
|
||||
.clone-dialog-submit {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--ui-font-family);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, border-color 0.1s;
|
||||
}
|
||||
|
||||
.clone-dialog-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
color: var(--ctp-subtext0);
|
||||
}
|
||||
|
||||
.clone-dialog-cancel:hover {
|
||||
background: var(--ctp-surface0);
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
.clone-dialog-submit {
|
||||
background: color-mix(in srgb, var(--ctp-mauve) 20%, transparent);
|
||||
border: 1px solid var(--ctp-mauve);
|
||||
color: var(--ctp-mauve);
|
||||
}
|
||||
|
||||
.clone-dialog-submit:hover {
|
||||
background: color-mix(in srgb, var(--ctp-mauve) 35%, transparent);
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.tab-bar {
|
||||
height: 2rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue