refactor(v3): remove dead v2 components and empty directories
Delete 7 components no longer used in v3 Mission Control (~1,836 lines): TilingGrid, PaneContainer, PaneHeader, SessionList, SshSessionList, SshDialog, SettingsDialog. Empty directories (Layout/, Sidebar/, Settings/, SSH/) removed.
This commit is contained in:
parent
e0056f811f
commit
160712de50
7 changed files with 0 additions and 1836 deletions
|
|
@ -1,113 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
status?: 'idle' | 'running' | 'error' | 'done';
|
|
||||||
onClose?: () => void;
|
|
||||||
onDetach?: () => void;
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { title, status = 'idle', onClose, onDetach, children }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="pane-container">
|
|
||||||
<div class="pane-header">
|
|
||||||
<span class="pane-title">{title}</span>
|
|
||||||
<div class="pane-controls">
|
|
||||||
{#if status !== 'idle'}
|
|
||||||
<span class="status {status}">{status}</span>
|
|
||||||
{/if}
|
|
||||||
{#if onDetach}
|
|
||||||
<button class="detach-btn" onclick={onDetach} title="Pop out to new window">↗</button>
|
|
||||||
{/if}
|
|
||||||
{#if onClose}
|
|
||||||
<button class="close-btn" onclick={onClose} title="Close pane">×</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pane-content">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.pane-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-header {
|
|
||||||
height: var(--pane-header-height);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 10px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.running { color: var(--ctp-blue); }
|
|
||||||
.status.error { color: var(--ctp-red); }
|
|
||||||
.status.done { color: var(--ctp-green); }
|
|
||||||
|
|
||||||
.detach-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 2px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detach-btn:hover { color: var(--ctp-blue); }
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 2px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
color: var(--ctp-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
status?: 'idle' | 'running' | 'error' | 'done';
|
|
||||||
}
|
|
||||||
|
|
||||||
let { title, status = 'idle' }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="pane-header">
|
|
||||||
<span class="pane-title">{title}</span>
|
|
||||||
{#if status !== 'idle'}
|
|
||||||
<span class="status-indicator {status}">{status}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.pane-header {
|
|
||||||
height: var(--pane-header-height);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 10px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.running { color: var(--ctp-blue); }
|
|
||||||
.status-indicator.error { color: var(--ctp-red); }
|
|
||||||
.status-indicator.done { color: var(--ctp-green); }
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import PaneContainer from './PaneContainer.svelte';
|
|
||||||
import TerminalPane from '../Terminal/TerminalPane.svelte';
|
|
||||||
import AgentPane from '../Agent/AgentPane.svelte';
|
|
||||||
import MarkdownPane from '../Markdown/MarkdownPane.svelte';
|
|
||||||
import ContextPane from '../Context/ContextPane.svelte';
|
|
||||||
import {
|
|
||||||
getPanes,
|
|
||||||
getGridTemplate,
|
|
||||||
getPaneGridArea,
|
|
||||||
getActivePreset,
|
|
||||||
focusPane,
|
|
||||||
removePane,
|
|
||||||
} from '../../stores/layout.svelte';
|
|
||||||
import { detachPane } from '../../utils/detach';
|
|
||||||
import { isDetachedMode } from '../../utils/detach';
|
|
||||||
import { stopAgent } from '../../adapters/agent-bridge';
|
|
||||||
import { getAgentSession } from '../../stores/agents.svelte';
|
|
||||||
|
|
||||||
let gridTemplate = $derived(getGridTemplate());
|
|
||||||
let panes = $derived(getPanes());
|
|
||||||
let detached = isDetachedMode();
|
|
||||||
|
|
||||||
// Custom column/row sizes (overrides preset when user drags)
|
|
||||||
let customColumns = $state<string | null>(null);
|
|
||||||
let customRows = $state<string | null>(null);
|
|
||||||
let gridEl: HTMLDivElement | undefined = $state();
|
|
||||||
|
|
||||||
// Reset custom sizes when preset changes
|
|
||||||
let prevPreset = '';
|
|
||||||
$effect(() => {
|
|
||||||
const p = getActivePreset();
|
|
||||||
if (prevPreset && p !== prevPreset) {
|
|
||||||
customColumns = null;
|
|
||||||
customRows = null;
|
|
||||||
}
|
|
||||||
prevPreset = p;
|
|
||||||
});
|
|
||||||
|
|
||||||
let columns = $derived(customColumns ?? gridTemplate.columns);
|
|
||||||
let rows = $derived(customRows ?? gridTemplate.rows);
|
|
||||||
|
|
||||||
// Determine splitter positions based on preset
|
|
||||||
let colCount = $derived(gridTemplate.columns.split(' ').length);
|
|
||||||
let rowCount = $derived(gridTemplate.rows.split(' ').length);
|
|
||||||
|
|
||||||
function handleDetach(pane: typeof panes[0]) {
|
|
||||||
detachPane(pane);
|
|
||||||
removePane(pane.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drag resize logic
|
|
||||||
let dragging = $state(false);
|
|
||||||
|
|
||||||
function getColSplitterX(ci: number): number {
|
|
||||||
if (!gridEl) return 0;
|
|
||||||
const rect = gridEl.getBoundingClientRect();
|
|
||||||
const cols = columns.split(' ').map(s => parseFloat(s));
|
|
||||||
const total = cols.reduce((a, b) => a + b, 0);
|
|
||||||
let frac = 0;
|
|
||||||
for (let i = 0; i <= ci; i++) frac += cols[i] / total;
|
|
||||||
return rect.left + frac * rect.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRowSplitterY(ri: number): number {
|
|
||||||
if (!gridEl) return 0;
|
|
||||||
const rect = gridEl.getBoundingClientRect();
|
|
||||||
const rws = rows.split(' ').map(s => parseFloat(s));
|
|
||||||
const total = rws.reduce((a, b) => a + b, 0);
|
|
||||||
let frac = 0;
|
|
||||||
for (let i = 0; i <= ri; i++) frac += rws[i] / total;
|
|
||||||
return rect.top + frac * rect.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startColDrag(colIndex: number, e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
dragging = true;
|
|
||||||
if (!gridEl) return;
|
|
||||||
|
|
||||||
const rect = gridEl.getBoundingClientRect();
|
|
||||||
const totalWidth = rect.width;
|
|
||||||
|
|
||||||
function onMove(ev: MouseEvent) {
|
|
||||||
const relX = ev.clientX - rect.left;
|
|
||||||
const ratio = Math.max(0.1, Math.min(0.9, relX / totalWidth));
|
|
||||||
if (colCount === 2) {
|
|
||||||
customColumns = `${ratio}fr ${1 - ratio}fr`;
|
|
||||||
} else if (colCount === 3) {
|
|
||||||
if (colIndex === 0) {
|
|
||||||
const remaining = 1 - ratio;
|
|
||||||
customColumns = `${ratio}fr ${remaining / 2}fr ${remaining / 2}fr`;
|
|
||||||
} else {
|
|
||||||
// Get current first column ratio or default
|
|
||||||
const parts = (customColumns ?? gridTemplate.columns).split(' ');
|
|
||||||
const first = parseFloat(parts[0]) / parts.reduce((s, p) => s + parseFloat(p), 0);
|
|
||||||
const relRatio = (relX / totalWidth - first) / (1 - first);
|
|
||||||
const adj = Math.max(0.1, Math.min(0.9, relRatio));
|
|
||||||
customColumns = `${first}fr ${(1 - first) * adj}fr ${(1 - first) * (1 - adj)}fr`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUp() {
|
|
||||||
dragging = false;
|
|
||||||
window.removeEventListener('mousemove', onMove);
|
|
||||||
window.removeEventListener('mouseup', onUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', onMove);
|
|
||||||
window.addEventListener('mouseup', onUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRowDrag(_rowIndex: number, e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
dragging = true;
|
|
||||||
if (!gridEl) return;
|
|
||||||
|
|
||||||
const rect = gridEl.getBoundingClientRect();
|
|
||||||
const totalHeight = rect.height;
|
|
||||||
|
|
||||||
function onMove(ev: MouseEvent) {
|
|
||||||
const relY = ev.clientY - rect.top;
|
|
||||||
const ratio = Math.max(0.1, Math.min(0.9, relY / totalHeight));
|
|
||||||
if (rowCount === 2) {
|
|
||||||
customRows = `${ratio}fr ${1 - ratio}fr`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUp() {
|
|
||||||
dragging = false;
|
|
||||||
window.removeEventListener('mousemove', onMove);
|
|
||||||
window.removeEventListener('mouseup', onUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', onMove);
|
|
||||||
window.addEventListener('mouseup', onUp);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="tiling-grid"
|
|
||||||
class:dragging
|
|
||||||
bind:this={gridEl}
|
|
||||||
style:grid-template-columns={columns}
|
|
||||||
style:grid-template-rows={rows}
|
|
||||||
>
|
|
||||||
{#if panes.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
<h1>BTerminal v2</h1>
|
|
||||||
<p>Claude Agent Mission Control</p>
|
|
||||||
<p class="hint">Press <kbd>Ctrl+N</kbd> to open a terminal</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{#each panes as pane, i (pane.id)}
|
|
||||||
{@const gridArea = getPaneGridArea(i)}
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div
|
|
||||||
class="pane-slot"
|
|
||||||
class:focused={pane.focused}
|
|
||||||
style:grid-area={gridArea}
|
|
||||||
onclick={() => focusPane(pane.id)}
|
|
||||||
>
|
|
||||||
<PaneContainer
|
|
||||||
title={pane.title}
|
|
||||||
status={pane.focused ? 'running' : 'idle'}
|
|
||||||
onClose={() => {
|
|
||||||
if (pane.type === 'agent') {
|
|
||||||
const s = getAgentSession(pane.id);
|
|
||||||
if (s?.status === 'running' || s?.status === 'starting') {
|
|
||||||
stopAgent(pane.id).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
removePane(pane.id);
|
|
||||||
}}
|
|
||||||
onDetach={detached ? undefined : () => handleDetach(pane)}
|
|
||||||
>
|
|
||||||
{#if pane.type === 'terminal'}
|
|
||||||
<TerminalPane
|
|
||||||
shell={pane.shell}
|
|
||||||
cwd={pane.cwd}
|
|
||||||
args={pane.args}
|
|
||||||
onExit={() => removePane(pane.id)}
|
|
||||||
/>
|
|
||||||
{:else if pane.type === 'agent'}
|
|
||||||
<AgentPane
|
|
||||||
sessionId={pane.id}
|
|
||||||
cwd={pane.cwd}
|
|
||||||
onExit={() => removePane(pane.id)}
|
|
||||||
/>
|
|
||||||
{:else if pane.type === 'ssh'}
|
|
||||||
<TerminalPane
|
|
||||||
shell={pane.shell}
|
|
||||||
cwd={pane.cwd}
|
|
||||||
args={pane.args}
|
|
||||||
onExit={() => removePane(pane.id)}
|
|
||||||
/>
|
|
||||||
{:else if pane.type === 'context'}
|
|
||||||
<ContextPane onExit={() => removePane(pane.id)} />
|
|
||||||
{:else if pane.type === 'markdown'}
|
|
||||||
<MarkdownPane
|
|
||||||
paneId={pane.id}
|
|
||||||
filePath={pane.cwd ?? ''}
|
|
||||||
onExit={() => removePane(pane.id)}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="placeholder">
|
|
||||||
<p>{pane.type} pane — coming soon</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</PaneContainer>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Splitter overlays (outside grid to avoid layout interference) -->
|
|
||||||
{#if panes.length > 1 && gridEl}
|
|
||||||
{#each { length: colCount - 1 } as _, ci}
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div
|
|
||||||
class="splitter splitter-col"
|
|
||||||
style:left="{gridEl ? getColSplitterX(ci) : 0}px"
|
|
||||||
style:top="{gridEl?.getBoundingClientRect().top ?? 0}px"
|
|
||||||
style:height="{gridEl?.getBoundingClientRect().height ?? 0}px"
|
|
||||||
onmousedown={(e) => startColDrag(ci, e)}
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
{#each { length: rowCount - 1 } as _, ri}
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div
|
|
||||||
class="splitter splitter-row"
|
|
||||||
style:top="{gridEl ? getRowSplitterY(ri) : 0}px"
|
|
||||||
style:left="{gridEl?.getBoundingClientRect().left ?? 0}px"
|
|
||||||
style:width="{gridEl?.getBoundingClientRect().width ?? 0}px"
|
|
||||||
onmousedown={(e) => startRowDrag(ri, e)}
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.tiling-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--pane-gap);
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--pane-gap);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiling-grid.dragging {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-slot {
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-slot.focused {
|
|
||||||
outline: 1px solid var(--accent);
|
|
||||||
outline-offset: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p { font-size: 14px; }
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitter) {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 100;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitter:hover), :global(.splitter:active) {
|
|
||||||
background: var(--accent);
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitter-col) {
|
|
||||||
width: 6px;
|
|
||||||
margin-left: -3px;
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitter-row) {
|
|
||||||
height: 6px;
|
|
||||||
margin-top: -3px;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { notify } from '../../stores/notifications.svelte';
|
|
||||||
import { saveSshSession, type SshSession } from '../../adapters/ssh-bridge';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
editSession?: SshSession;
|
|
||||||
onClose: () => void;
|
|
||||||
onSaved: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { open, editSession, onClose, onSaved }: Props = $props();
|
|
||||||
|
|
||||||
let name = $state('');
|
|
||||||
let host = $state('');
|
|
||||||
let port = $state(22);
|
|
||||||
let username = $state('');
|
|
||||||
let keyFile = $state('');
|
|
||||||
let folder = $state('');
|
|
||||||
|
|
||||||
let validationError = $state('');
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (open && editSession) {
|
|
||||||
name = editSession.name;
|
|
||||||
host = editSession.host;
|
|
||||||
port = editSession.port;
|
|
||||||
username = editSession.username;
|
|
||||||
keyFile = editSession.key_file;
|
|
||||||
folder = editSession.folder;
|
|
||||||
} else if (open) {
|
|
||||||
name = '';
|
|
||||||
host = '';
|
|
||||||
port = 22;
|
|
||||||
username = '';
|
|
||||||
keyFile = '';
|
|
||||||
folder = '';
|
|
||||||
}
|
|
||||||
validationError = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function validate(): boolean {
|
|
||||||
if (!name.trim()) {
|
|
||||||
validationError = 'Name is required';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!host.trim()) {
|
|
||||||
validationError = 'Host is required';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!username.trim()) {
|
|
||||||
validationError = 'Username is required';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (port < 1 || port > 65535) {
|
|
||||||
validationError = 'Port must be between 1 and 65535';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
validationError = '';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
if (!validate()) return;
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const session: SshSession = {
|
|
||||||
id: editSession?.id ?? crypto.randomUUID(),
|
|
||||||
name: name.trim(),
|
|
||||||
host: host.trim(),
|
|
||||||
port,
|
|
||||||
username: username.trim(),
|
|
||||||
key_file: keyFile.trim(),
|
|
||||||
folder: folder.trim(),
|
|
||||||
color: editSession?.color ?? '#89b4fa',
|
|
||||||
created_at: editSession?.created_at ?? now,
|
|
||||||
last_used_at: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await saveSshSession(session);
|
|
||||||
notify('success', `SSH session "${session.name}" saved`);
|
|
||||||
onSaved();
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Failed to save SSH session: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Escape') onClose();
|
|
||||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSave();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div class="overlay" onkeydown={handleKeydown}>
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div class="backdrop" onclick={onClose}></div>
|
|
||||||
<div class="dialog" role="dialog" aria-label="SSH Session">
|
|
||||||
<div class="dialog-header">
|
|
||||||
<h2>{editSession ? 'Edit' : 'New'} SSH Session</h2>
|
|
||||||
<button class="close-btn" onclick={onClose}>×</button>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
{#if validationError}
|
|
||||||
<div class="validation-error">{validationError}</div>
|
|
||||||
{/if}
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Name</span>
|
|
||||||
<input type="text" bind:value={name} placeholder="My Server" />
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Host</span>
|
|
||||||
<input type="text" bind:value={host} placeholder="192.168.1.100 or server.example.com" />
|
|
||||||
</label>
|
|
||||||
<div class="field-row">
|
|
||||||
<label class="field" style="flex: 1;">
|
|
||||||
<span class="field-label">Username</span>
|
|
||||||
<input type="text" bind:value={username} placeholder="root" />
|
|
||||||
</label>
|
|
||||||
<label class="field" style="width: 100px;">
|
|
||||||
<span class="field-label">Port</span>
|
|
||||||
<input type="number" bind:value={port} min="1" max="65535" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">SSH Key (optional)</span>
|
|
||||||
<input type="text" bind:value={keyFile} placeholder="~/.ssh/id_ed25519" />
|
|
||||||
<span class="field-hint">Leave empty to use default key or password auth</span>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Folder (optional)</span>
|
|
||||||
<input type="text" bind:value={folder} placeholder="Group name for organizing" />
|
|
||||||
<span class="field-hint">Sessions with the same folder are grouped together</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<button class="btn-cancel" onclick={onClose}>Cancel</button>
|
|
||||||
<button class="btn-save" onclick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 9000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
position: relative;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 420px;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.dialog-body {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-error {
|
|
||||||
background: rgba(243, 139, 168, 0.1);
|
|
||||||
border: 1px solid var(--ctp-red);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--ctp-red);
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel, .btn-save {
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.btn-save {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--ctp-crust);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save:hover { opacity: 0.9; }
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { listSshSessions, deleteSshSession, type SshSession } from '../../adapters/ssh-bridge';
|
|
||||||
import { addPane } from '../../stores/layout.svelte';
|
|
||||||
import { notify } from '../../stores/notifications.svelte';
|
|
||||||
import SshDialog from './SshDialog.svelte';
|
|
||||||
|
|
||||||
let sessions = $state<SshSession[]>([]);
|
|
||||||
let dialogOpen = $state(false);
|
|
||||||
let editingSession = $state<SshSession | undefined>(undefined);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
loadSessions();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadSessions() {
|
|
||||||
try {
|
|
||||||
sessions = await listSshSessions();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load SSH sessions:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openNewDialog() {
|
|
||||||
editingSession = undefined;
|
|
||||||
dialogOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDialog(session: SshSession) {
|
|
||||||
editingSession = session;
|
|
||||||
dialogOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectSsh(session: SshSession) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const args: string[] = [];
|
|
||||||
|
|
||||||
// Build ssh command arguments
|
|
||||||
args.push('-p', String(session.port));
|
|
||||||
if (session.key_file) {
|
|
||||||
args.push('-i', session.key_file);
|
|
||||||
}
|
|
||||||
args.push(`${session.username}@${session.host}`);
|
|
||||||
|
|
||||||
addPane({
|
|
||||||
id,
|
|
||||||
type: 'ssh',
|
|
||||||
title: `SSH: ${session.name}`,
|
|
||||||
shell: '/usr/bin/ssh',
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(session: SshSession, event: MouseEvent) {
|
|
||||||
event.stopPropagation();
|
|
||||||
try {
|
|
||||||
await deleteSshSession(session.id);
|
|
||||||
sessions = sessions.filter(s => s.id !== session.id);
|
|
||||||
notify('success', `Deleted SSH session "${session.name}"`);
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Failed to delete SSH session: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group sessions by folder
|
|
||||||
let grouped = $derived(() => {
|
|
||||||
const groups = new Map<string, SshSession[]>();
|
|
||||||
for (const s of sessions) {
|
|
||||||
const folder = s.folder || '';
|
|
||||||
if (!groups.has(folder)) groups.set(folder, []);
|
|
||||||
groups.get(folder)!.push(s);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="ssh-sessions">
|
|
||||||
<div class="ssh-header">
|
|
||||||
<h3>SSH</h3>
|
|
||||||
<button class="new-btn" onclick={openNewDialog} title="Add SSH session">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if sessions.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
<p>No SSH sessions.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{@const groups = grouped()}
|
|
||||||
{#each [...groups.entries()] as [folder, folderSessions] (folder)}
|
|
||||||
{#if folder}
|
|
||||||
<div class="folder-label">{folder}</div>
|
|
||||||
{/if}
|
|
||||||
<ul class="ssh-list">
|
|
||||||
{#each folderSessions as session (session.id)}
|
|
||||||
<li class="ssh-item">
|
|
||||||
<button class="ssh-btn" onclick={() => connectSsh(session)} title="Connect to {session.host}">
|
|
||||||
<span class="ssh-color" style:background={session.color}></span>
|
|
||||||
<span class="ssh-info">
|
|
||||||
<span class="ssh-name">{session.name}</span>
|
|
||||||
<span class="ssh-host">{session.username}@{session.host}:{session.port}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button class="edit-btn" onclick={() => openEditDialog(session)} title="Edit">E</button>
|
|
||||||
<button class="remove-btn" onclick={(e) => handleDelete(session, e)} title="Delete">×</button>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SshDialog
|
|
||||||
open={dialogOpen}
|
|
||||||
editSession={editingSession}
|
|
||||||
onClose={() => { dialogOpen = false; }}
|
|
||||||
onSaved={loadSessions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.ssh-sessions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-header h3 {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text-primary);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn:hover {
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-label {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
padding: 4px 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-list {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-item:hover {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-btn {
|
|
||||||
flex: 1;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-btn:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.ssh-color {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-name {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-host {
|
|
||||||
font-size: 9px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 2px 3px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssh-item:hover .edit-btn { opacity: 1; }
|
|
||||||
.ssh-item:hover .remove-btn { opacity: 1; }
|
|
||||||
.edit-btn:hover { color: var(--ctp-yellow); }
|
|
||||||
.remove-btn:hover { color: var(--ctp-red); }
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { getSetting, setSetting } from '../../adapters/settings-bridge';
|
|
||||||
import { notify } from '../../stores/notifications.svelte';
|
|
||||||
import { getCurrentFlavor, setFlavor } from '../../stores/theme.svelte';
|
|
||||||
import { ALL_FLAVORS, FLAVOR_LABELS, type CatppuccinFlavor } from '../../styles/themes';
|
|
||||||
import {
|
|
||||||
getMachines,
|
|
||||||
addMachine,
|
|
||||||
removeMachine,
|
|
||||||
connectMachine,
|
|
||||||
disconnectMachine,
|
|
||||||
loadMachines,
|
|
||||||
} from '../../stores/machines.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { open, onClose }: Props = $props();
|
|
||||||
|
|
||||||
let defaultShell = $state('');
|
|
||||||
let defaultCwd = $state('');
|
|
||||||
let maxPanes = $state('4');
|
|
||||||
let themeFlavor = $state<CatppuccinFlavor>('mocha');
|
|
||||||
|
|
||||||
// Machine form state
|
|
||||||
let newMachineLabel = $state('');
|
|
||||||
let newMachineUrl = $state('');
|
|
||||||
let newMachineToken = $state('');
|
|
||||||
let newMachineAutoConnect = $state(false);
|
|
||||||
|
|
||||||
let remoteMachines = $derived(getMachines());
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
defaultShell = (await getSetting('default_shell')) ?? '';
|
|
||||||
defaultCwd = (await getSetting('default_cwd')) ?? '';
|
|
||||||
maxPanes = (await getSetting('max_panes')) ?? '4';
|
|
||||||
themeFlavor = getCurrentFlavor();
|
|
||||||
await loadMachines();
|
|
||||||
} catch {
|
|
||||||
// Use defaults
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
try {
|
|
||||||
if (defaultShell) await setSetting('default_shell', defaultShell);
|
|
||||||
if (defaultCwd) await setSetting('default_cwd', defaultCwd);
|
|
||||||
await setSetting('max_panes', maxPanes);
|
|
||||||
await setFlavor(themeFlavor);
|
|
||||||
notify('success', 'Settings saved');
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Failed to save settings: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAddMachine() {
|
|
||||||
if (!newMachineLabel || !newMachineUrl || !newMachineToken) {
|
|
||||||
notify('error', 'Label, URL, and token are required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await addMachine({
|
|
||||||
label: newMachineLabel,
|
|
||||||
url: newMachineUrl,
|
|
||||||
token: newMachineToken,
|
|
||||||
auto_connect: newMachineAutoConnect,
|
|
||||||
});
|
|
||||||
newMachineLabel = '';
|
|
||||||
newMachineUrl = '';
|
|
||||||
newMachineToken = '';
|
|
||||||
newMachineAutoConnect = false;
|
|
||||||
notify('success', 'Machine added');
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Failed to add machine: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRemoveMachine(id: string) {
|
|
||||||
try {
|
|
||||||
await removeMachine(id);
|
|
||||||
notify('success', 'Machine removed');
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Failed to remove machine: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggleConnection(id: string, status: string) {
|
|
||||||
try {
|
|
||||||
if (status === 'connected') {
|
|
||||||
await disconnectMachine(id);
|
|
||||||
} else {
|
|
||||||
await connectMachine(id);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
notify('error', `Connection error: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Escape') onClose();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div class="overlay" onkeydown={handleKeydown}>
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div class="backdrop" onclick={onClose}></div>
|
|
||||||
<div class="dialog" role="dialog" aria-label="Settings">
|
|
||||||
<div class="dialog-header">
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<button class="close-btn" onclick={onClose}>×</button>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Default Shell</span>
|
|
||||||
<input type="text" bind:value={defaultShell} placeholder="$SHELL (auto-detect)" />
|
|
||||||
<span class="field-hint">Leave empty to use system default</span>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Default Working Directory</span>
|
|
||||||
<input type="text" bind:value={defaultCwd} placeholder="$HOME" />
|
|
||||||
<span class="field-hint">Leave empty for home directory</span>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Max Panes</span>
|
|
||||||
<input type="number" bind:value={maxPanes} min="1" max="8" />
|
|
||||||
<span class="field-hint">Maximum simultaneous panes (1-8)</span>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Theme</span>
|
|
||||||
<select bind:value={themeFlavor}>
|
|
||||||
{#each ALL_FLAVORS as flavor}
|
|
||||||
<option value={flavor}>{FLAVOR_LABELS[flavor]}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<span class="field-hint">Catppuccin color scheme. New terminals use the updated theme.</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
|
||||||
<h3 class="section-title">Remote Machines</h3>
|
|
||||||
|
|
||||||
{#if remoteMachines.length > 0}
|
|
||||||
<div class="machine-list">
|
|
||||||
{#each remoteMachines as machine (machine.id)}
|
|
||||||
<div class="machine-item">
|
|
||||||
<div class="machine-info">
|
|
||||||
<span class="machine-label">{machine.label}</span>
|
|
||||||
<span class="machine-url">{machine.url}</span>
|
|
||||||
<span class="machine-status" class:connected={machine.status === 'connected'} class:error={machine.status === 'error'}>
|
|
||||||
{machine.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="machine-actions">
|
|
||||||
<button
|
|
||||||
class="machine-btn"
|
|
||||||
onclick={() => handleToggleConnection(machine.id, machine.status)}
|
|
||||||
>
|
|
||||||
{machine.status === 'connected' ? 'Disconnect' : 'Connect'}
|
|
||||||
</button>
|
|
||||||
<button class="machine-btn machine-btn-danger" onclick={() => handleRemoveMachine(machine.id)}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<p class="field-hint">No remote machines configured.</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="add-machine-form">
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Label</span>
|
|
||||||
<input type="text" bind:value={newMachineLabel} placeholder="devbox" />
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">URL</span>
|
|
||||||
<input type="text" bind:value={newMachineUrl} placeholder="wss://host:9750" />
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span class="field-label">Token</span>
|
|
||||||
<input type="password" bind:value={newMachineToken} placeholder="auth token" />
|
|
||||||
</label>
|
|
||||||
<label class="field-checkbox">
|
|
||||||
<input type="checkbox" bind:checked={newMachineAutoConnect} />
|
|
||||||
<span>Auto-connect on startup</span>
|
|
||||||
</label>
|
|
||||||
<button class="btn-save" onclick={handleAddMachine}>Add Machine</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<button class="btn-cancel" onclick={onClose}>Cancel</button>
|
|
||||||
<button class="btn-save" onclick={handleSave}>Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 9000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
position: relative;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 400px;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.dialog-body {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input, .field select {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input:focus, .field select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel, .btn-save {
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.btn-save {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--ctp-crust);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save:hover { opacity: 0.9; }
|
|
||||||
|
|
||||||
.section-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: var(--border);
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-url {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-status {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--ctp-overlay1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-status.connected {
|
|
||||||
color: var(--ctp-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-status.error {
|
|
||||||
color: var(--ctp-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-btn {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-btn:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-btn-danger:hover {
|
|
||||||
color: var(--ctp-red);
|
|
||||||
border-color: var(--ctp-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-machine-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-checkbox input[type="checkbox"] {
|
|
||||||
accent-color: var(--accent);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
getPanes,
|
|
||||||
addPane,
|
|
||||||
focusPane,
|
|
||||||
removePane,
|
|
||||||
getActivePreset,
|
|
||||||
setPreset,
|
|
||||||
setPaneGroup,
|
|
||||||
type LayoutPreset,
|
|
||||||
type Pane,
|
|
||||||
} from '../../stores/layout.svelte';
|
|
||||||
import { getMachines } from '../../stores/machines.svelte';
|
|
||||||
import SshSessionList from '../SSH/SshSessionList.svelte';
|
|
||||||
|
|
||||||
let panes = $derived(getPanes());
|
|
||||||
let preset = $derived(getActivePreset());
|
|
||||||
let machines = $derived(getMachines());
|
|
||||||
|
|
||||||
// Build machine label lookup
|
|
||||||
let machineLabels = $derived.by(() => {
|
|
||||||
const map = new Map<string, string>();
|
|
||||||
for (const m of machines) {
|
|
||||||
map.set(m.id, `${m.label} (${m.status})`);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
let grouped = $derived.by(() => {
|
|
||||||
const groups = new Map<string, Pane[]>();
|
|
||||||
for (const pane of panes) {
|
|
||||||
// Remote panes auto-group by machine label; local panes use explicit group
|
|
||||||
const g = pane.remoteMachineId
|
|
||||||
? machineLabels.get(pane.remoteMachineId) ?? `Remote ${pane.remoteMachineId.slice(0, 8)}`
|
|
||||||
: (pane.group || '');
|
|
||||||
if (!groups.has(g)) groups.set(g, []);
|
|
||||||
groups.get(g)!.push(pane);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
});
|
|
||||||
|
|
||||||
let collapsed = $state<Set<string>>(new Set());
|
|
||||||
|
|
||||||
function toggleGroup(name: string) {
|
|
||||||
if (collapsed.has(name)) {
|
|
||||||
collapsed = new Set([...collapsed].filter(g => g !== name));
|
|
||||||
} else {
|
|
||||||
collapsed = new Set([...collapsed, name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGroup(paneId: string) {
|
|
||||||
const current = panes.find(p => p.id === paneId)?.group || '';
|
|
||||||
const name = prompt('Group name (empty to ungroup):', current);
|
|
||||||
if (name !== null) {
|
|
||||||
setPaneGroup(paneId, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function paneIcon(type: string, title: string): string {
|
|
||||||
if (type === 'agent' && title.startsWith('Sub: ')) return '↳';
|
|
||||||
switch (type) {
|
|
||||||
case 'terminal': return '>';
|
|
||||||
case 'agent': return '*';
|
|
||||||
case 'markdown': return 'M';
|
|
||||||
case 'ssh': return '@';
|
|
||||||
case 'context': return 'C';
|
|
||||||
default: return '#';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const presets: LayoutPreset[] = ['1-col', '2-col', '3-col', '2x2', 'master-stack'];
|
|
||||||
|
|
||||||
function newTerminal() {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const num = panes.filter(p => p.type === 'terminal').length + 1;
|
|
||||||
addPane({
|
|
||||||
id,
|
|
||||||
type: 'terminal',
|
|
||||||
title: `Terminal ${num}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function newAgent() {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const num = panes.filter(p => p.type === 'agent').length + 1;
|
|
||||||
addPane({
|
|
||||||
id,
|
|
||||||
type: 'agent',
|
|
||||||
title: `Agent ${num}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openContext() {
|
|
||||||
const existing = panes.find(p => p.type === 'context');
|
|
||||||
if (existing) {
|
|
||||||
focusPane(existing.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
addPane({
|
|
||||||
id,
|
|
||||||
type: 'context',
|
|
||||||
title: 'Context',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileInputEl: HTMLInputElement | undefined = $state();
|
|
||||||
|
|
||||||
function openMarkdown() {
|
|
||||||
fileInputEl?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileSelect(e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement;
|
|
||||||
const file = input.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
// Tauri file paths from input elements include the full path
|
|
||||||
const path = (file as any).path ?? file.name;
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
addPane({
|
|
||||||
id,
|
|
||||||
type: 'markdown',
|
|
||||||
title: file.name,
|
|
||||||
cwd: path,
|
|
||||||
});
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="session-list">
|
|
||||||
<div class="header">
|
|
||||||
<h2>Sessions</h2>
|
|
||||||
<div class="header-buttons">
|
|
||||||
<button class="new-btn" onclick={openContext} title="Context manager">C</button>
|
|
||||||
<button class="new-btn" onclick={openMarkdown} title="Open markdown file">M</button>
|
|
||||||
<button class="new-btn" onclick={newAgent} title="New agent (Ctrl+Shift+N)">A</button>
|
|
||||||
<button class="new-btn" onclick={newTerminal} title="New terminal (Ctrl+N)">+</button>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
bind:this={fileInputEl}
|
|
||||||
type="file"
|
|
||||||
accept=".md,.markdown,.txt"
|
|
||||||
onchange={handleFileSelect}
|
|
||||||
style="display: none;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layout-presets">
|
|
||||||
{#each presets as p}
|
|
||||||
<button
|
|
||||||
class="preset-btn"
|
|
||||||
class:active={preset === p}
|
|
||||||
onclick={() => setPreset(p)}
|
|
||||||
title={p}
|
|
||||||
>{p}</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if panes.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
<p>No sessions yet.</p>
|
|
||||||
<p class="hint">Ctrl+N terminal / Ctrl+Shift+N agent</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<ul class="pane-list">
|
|
||||||
{#snippet paneItem(pane: Pane)}
|
|
||||||
<li class="pane-item" class:focused={pane.focused}>
|
|
||||||
<button class="pane-btn" onclick={() => focusPane(pane.id)} oncontextmenu={(e) => { e.preventDefault(); setGroup(pane.id); }}>
|
|
||||||
<span class="pane-icon">{paneIcon(pane.type, pane.title)}</span>
|
|
||||||
<span class="pane-name">{pane.title}</span>
|
|
||||||
</button>
|
|
||||||
<button class="remove-btn" onclick={() => removePane(pane.id)}>×</button>
|
|
||||||
</li>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#if grouped.has('')}
|
|
||||||
{#each grouped.get('')! as pane (pane.id)}
|
|
||||||
{@render paneItem(pane)}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#each [...grouped.entries()].filter(([k]) => k !== '') as [groupName, groupPanes] (groupName)}
|
|
||||||
<li class="group-header" onclick={() => toggleGroup(groupName)}>
|
|
||||||
<span class="group-arrow">{collapsed.has(groupName) ? '\u25B6' : '\u25BC'}</span>
|
|
||||||
<span>{groupName}</span>
|
|
||||||
<span class="group-count">{groupPanes.length}</span>
|
|
||||||
</li>
|
|
||||||
{#if !collapsed.has(groupName)}
|
|
||||||
{#each groupPanes as pane (pane.id)}
|
|
||||||
{@render paneItem(pane)}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
<SshSessionList />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
background: var(--border);
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list {
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text-primary);
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-btn:hover {
|
|
||||||
background: var(--bg-surface-hover);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-presets {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 9px;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn:hover { color: var(--text-primary); }
|
|
||||||
.preset-btn.active {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--ctp-crust);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--ctp-overlay1);
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-header:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-arrow {
|
|
||||||
font-size: 8px;
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-count {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 9px;
|
|
||||||
color: var(--ctp-overlay0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-list {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-item.focused {
|
|
||||||
background: var(--bg-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-btn {
|
|
||||||
flex: 1;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-btn:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.pane-icon {
|
|
||||||
color: var(--ctp-green);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane-item:hover .remove-btn { opacity: 1; }
|
|
||||||
.remove-btn:hover { color: var(--ctp-red); }
|
|
||||||
</style>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue