feat(v2): add tiling layout, sidebar controls, and keyboard shortcuts
- TilingGrid: dynamic CSS Grid with auto-preset based on pane count - Layout presets: 1-col, 2-col, 3-col, 2x2, master-stack - PaneContainer: close button, status indicator, focus highlight - SessionList: new terminal button, layout preset selector, pane list - Layout store: pane CRUD, focus management, grid template generation - Keyboard: Ctrl+N new terminal, Ctrl+1-4 focus pane by index
This commit is contained in:
parent
bb0e9283fc
commit
bfd4021909
5 changed files with 399 additions and 41 deletions
|
|
@ -1,6 +1,39 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import SessionList from './lib/components/Sidebar/SessionList.svelte';
|
import SessionList from './lib/components/Sidebar/SessionList.svelte';
|
||||||
import TilingGrid from './lib/components/Layout/TilingGrid.svelte';
|
import TilingGrid from './lib/components/Layout/TilingGrid.svelte';
|
||||||
|
import { addPane, focusPaneByIndex, getPanes } from './lib/stores/layout';
|
||||||
|
|
||||||
|
function newTerminal() {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const num = getPanes().length + 1;
|
||||||
|
addPane({
|
||||||
|
id,
|
||||||
|
type: 'terminal',
|
||||||
|
title: `Terminal ${num}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
// Ctrl+N — new terminal
|
||||||
|
if (e.ctrlKey && !e.shiftKey && e.key === 'n') {
|
||||||
|
e.preventDefault();
|
||||||
|
newTerminal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+1-4 — focus pane by index
|
||||||
|
if (e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '4') {
|
||||||
|
e.preventDefault();
|
||||||
|
focusPaneByIndex(parseInt(e.key) - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeydown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,25 @@
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
status?: 'idle' | 'running' | 'error' | 'done';
|
||||||
|
onClose?: () => void;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title, children }: Props = $props();
|
let { title, status = 'idle', onClose, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pane-container">
|
<div class="pane-container">
|
||||||
<div class="pane-header">
|
<div class="pane-header">
|
||||||
<span class="pane-title">{title}</span>
|
<span class="pane-title">{title}</span>
|
||||||
|
<div class="pane-controls">
|
||||||
|
{#if status !== 'idle'}
|
||||||
|
<span class="status {status}">{status}</span>
|
||||||
|
{/if}
|
||||||
|
{#if onClose}
|
||||||
|
<button class="close-btn" onclick={onClose} title="Close pane">×</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-content">
|
<div class="pane-content">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
@ -26,12 +36,14 @@
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-header {
|
.pane-header {
|
||||||
height: var(--pane-header-height);
|
height: var(--pane-header-height);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
@ -47,8 +59,39 @@
|
||||||
text-overflow: ellipsis;
|
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); }
|
||||||
|
|
||||||
|
.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 {
|
.pane-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,84 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PaneContainer from './PaneContainer.svelte';
|
import PaneContainer from './PaneContainer.svelte';
|
||||||
|
import TerminalPane from '../Terminal/TerminalPane.svelte';
|
||||||
|
import {
|
||||||
|
getPanes,
|
||||||
|
getGridTemplate,
|
||||||
|
getPaneGridArea,
|
||||||
|
focusPane,
|
||||||
|
removePane,
|
||||||
|
} from '../../stores/layout';
|
||||||
|
|
||||||
// Phase 2: dynamic pane management, resize, presets
|
let gridTemplate = $derived(getGridTemplate());
|
||||||
// For now: single empty pane as placeholder
|
let panes = $derived(getPanes());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="tiling-grid">
|
<div
|
||||||
<PaneContainer title="Welcome">
|
class="tiling-grid"
|
||||||
<div class="welcome">
|
style:grid-template-columns={gridTemplate.columns}
|
||||||
|
style:grid-template-rows={gridTemplate.rows}
|
||||||
|
>
|
||||||
|
{#if panes.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
<h1>BTerminal v2</h1>
|
<h1>BTerminal v2</h1>
|
||||||
<p>Claude Agent Mission Control</p>
|
<p>Claude Agent Mission Control</p>
|
||||||
<div class="status">
|
<p class="hint">Press <kbd>Ctrl+N</kbd> to open a terminal</p>
|
||||||
<span class="badge">Phase 1 — Scaffold</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</PaneContainer>
|
{: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={() => removePane(pane.id)}
|
||||||
|
>
|
||||||
|
{#if pane.type === 'terminal'}
|
||||||
|
<TerminalPane
|
||||||
|
shell={pane.shell}
|
||||||
|
cwd={pane.cwd}
|
||||||
|
args={pane.args}
|
||||||
|
onExit={() => removePane(pane.id)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder">
|
||||||
|
<p>{pane.type} pane — coming in Phase 3/4</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</PaneContainer>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tiling-grid {
|
.tiling-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
gap: var(--pane-gap);
|
gap: var(--pane-gap);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: var(--pane-gap);
|
padding: var(--pane-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -37,22 +88,33 @@
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome h1 {
|
.empty-state h1 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome p {
|
.empty-state p { font-size: 14px; }
|
||||||
font-size: 14px;
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ctp-overlay0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
kbd {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
color: var(--accent);
|
padding: 2px 6px;
|
||||||
padding: 4px 12px;
|
border-radius: 3px;
|
||||||
border-radius: var(--border-radius);
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,127 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Phase 4: session CRUD, groups, types
|
import {
|
||||||
|
getPanes,
|
||||||
|
addPane,
|
||||||
|
focusPane,
|
||||||
|
removePane,
|
||||||
|
getActivePreset,
|
||||||
|
setPreset,
|
||||||
|
type LayoutPreset,
|
||||||
|
} from '../../stores/layout';
|
||||||
|
|
||||||
|
let panes = $derived(getPanes());
|
||||||
|
let preset = $derived(getActivePreset());
|
||||||
|
|
||||||
|
const presets: LayoutPreset[] = ['1-col', '2-col', '3-col', '2x2', 'master-stack'];
|
||||||
|
|
||||||
|
function newTerminal() {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const num = panes.length + 1;
|
||||||
|
addPane({
|
||||||
|
id,
|
||||||
|
type: 'terminal',
|
||||||
|
title: `Terminal ${num}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="session-list">
|
<div class="session-list">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Sessions</h2>
|
<h2>Sessions</h2>
|
||||||
|
<button class="new-btn" onclick={newTerminal} title="New terminal (Ctrl+N)">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-state">
|
|
||||||
<p>No sessions yet.</p>
|
<div class="layout-presets">
|
||||||
<p class="hint">Phase 2 will add terminal sessions.</p>
|
{#each presets as p}
|
||||||
|
<button
|
||||||
|
class="preset-btn"
|
||||||
|
class:active={preset === p}
|
||||||
|
onclick={() => setPreset(p)}
|
||||||
|
title={p}
|
||||||
|
>{p}</button>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if panes.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No sessions yet.</p>
|
||||||
|
<p class="hint">Click + or press Ctrl+N</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="pane-list">
|
||||||
|
{#each panes as pane (pane.id)}
|
||||||
|
<li class="pane-item" class:focused={pane.focused}>
|
||||||
|
<button class="pane-btn" onclick={() => focusPane(pane.id)}>
|
||||||
|
<span class="pane-icon">{pane.type === 'terminal' ? '>' : '#'}</span>
|
||||||
|
<span class="pane-name">{pane.title}</span>
|
||||||
|
</button>
|
||||||
|
<button class="remove-btn" onclick={() => removePane(pane.id)}>×</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.session-list {
|
.session-list {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h2 {
|
.header h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 12px;
|
}
|
||||||
|
|
||||||
|
.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: 16px;
|
||||||
|
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 {
|
.empty-state {
|
||||||
|
|
@ -36,4 +136,62 @@
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--ctp-overlay0);
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,90 @@
|
||||||
// Layout state management — Svelte 5 runes
|
|
||||||
// Phase 2: pane positions, resize, presets
|
|
||||||
|
|
||||||
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
|
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
|
||||||
|
|
||||||
export interface PaneState {
|
export type PaneType = 'terminal' | 'agent' | 'markdown' | 'empty';
|
||||||
|
|
||||||
|
export interface Pane {
|
||||||
id: string;
|
id: string;
|
||||||
sessionId: string;
|
type: PaneType;
|
||||||
row: number;
|
title: string;
|
||||||
col: number;
|
shell?: string;
|
||||||
rowSpan: number;
|
cwd?: string;
|
||||||
colSpan: number;
|
args?: string[];
|
||||||
|
focused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let panes = $state<Pane[]>([]);
|
||||||
let activePreset = $state<LayoutPreset>('1-col');
|
let activePreset = $state<LayoutPreset>('1-col');
|
||||||
let panes = $state<PaneState[]>([]);
|
let focusedPaneId = $state<string | null>(null);
|
||||||
|
|
||||||
export function getActivePreset() {
|
export function getPanes(): Pane[] {
|
||||||
|
return panes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActivePreset(): LayoutPreset {
|
||||||
return activePreset;
|
return activePreset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPreset(preset: LayoutPreset) {
|
export function getFocusedPaneId(): string | null {
|
||||||
|
return focusedPaneId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPane(pane: Omit<Pane, 'focused'>): void {
|
||||||
|
panes.push({ ...pane, focused: false });
|
||||||
|
focusPane(pane.id);
|
||||||
|
autoPreset();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePane(id: string): void {
|
||||||
|
panes = panes.filter(p => p.id !== id);
|
||||||
|
if (focusedPaneId === id) {
|
||||||
|
focusedPaneId = panes.length > 0 ? panes[0].id : null;
|
||||||
|
}
|
||||||
|
autoPreset();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusPane(id: string): void {
|
||||||
|
focusedPaneId = id;
|
||||||
|
panes = panes.map(p => ({ ...p, focused: p.id === id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusPaneByIndex(index: number): void {
|
||||||
|
if (index >= 0 && index < panes.length) {
|
||||||
|
focusPane(panes[index].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPreset(preset: LayoutPreset): void {
|
||||||
activePreset = preset;
|
activePreset = preset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPanes() {
|
function autoPreset(): void {
|
||||||
return panes;
|
const count = panes.length;
|
||||||
|
if (count <= 1) activePreset = '1-col';
|
||||||
|
else if (count === 2) activePreset = '2-col';
|
||||||
|
else if (count === 3) activePreset = 'master-stack';
|
||||||
|
else activePreset = '2x2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSS grid-template for current preset */
|
||||||
|
export function getGridTemplate(): { columns: string; rows: string } {
|
||||||
|
switch (activePreset) {
|
||||||
|
case '1-col':
|
||||||
|
return { columns: '1fr', rows: '1fr' };
|
||||||
|
case '2-col':
|
||||||
|
return { columns: '1fr 1fr', rows: '1fr' };
|
||||||
|
case '3-col':
|
||||||
|
return { columns: '1fr 1fr 1fr', rows: '1fr' };
|
||||||
|
case '2x2':
|
||||||
|
return { columns: '1fr 1fr', rows: '1fr 1fr' };
|
||||||
|
case 'master-stack':
|
||||||
|
return { columns: '2fr 1fr', rows: '1fr 1fr' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For master-stack: first pane spans full height */
|
||||||
|
export function getPaneGridArea(index: number): string | undefined {
|
||||||
|
if (activePreset === 'master-stack' && index === 0) {
|
||||||
|
return '1 / 1 / 3 / 2';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue