Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 202 lines 4.8 kB view raw
1/** 2 * Split View — side-by-side document pane management. 3 * 4 * Pure logic module: pane state, layout calculations, focus tracking. 5 * DOM rendering handled by the editor entry points. 6 */ 7 8export type SplitDirection = 'horizontal' | 'vertical'; 9 10export interface Pane { 11 id: string; 12 docId: string; 13 docType: 'doc' | 'sheet'; 14 /** Percentage of total space (0-100) */ 15 size: number; 16} 17 18export interface SplitState { 19 direction: SplitDirection; 20 panes: Pane[]; 21 focusedPaneId: string | null; 22} 23 24let _paneCounter = 0; 25 26/** 27 * Generate a unique pane ID. 28 */ 29export function generatePaneId(): string { 30 return `pane-${++_paneCounter}`; 31} 32 33/** 34 * Reset pane counter (for testing). 35 */ 36export function resetPaneCounter(): void { 37 _paneCounter = 0; 38} 39 40/** 41 * Create an initial single-pane state. 42 */ 43export function createSplitState( 44 docId: string, 45 docType: 'doc' | 'sheet', 46): SplitState { 47 const id = generatePaneId(); 48 return { 49 direction: 'horizontal', 50 panes: [{ id, docId, docType, size: 100 }], 51 focusedPaneId: id, 52 }; 53} 54 55/** 56 * Add a new pane by splitting the focused pane. 57 * Returns updated state or null if max panes reached. 58 */ 59export function addPane( 60 state: SplitState, 61 docId: string, 62 docType: 'doc' | 'sheet', 63 maxPanes = 4, 64): SplitState | null { 65 if (state.panes.length >= maxPanes) return null; 66 67 const newId = generatePaneId(); 68 const newCount = state.panes.length + 1; 69 const equalSize = Math.floor(100 / newCount); 70 const remainder = 100 - equalSize * newCount; 71 72 const panes: Pane[] = state.panes.map((p, i) => ({ 73 ...p, 74 size: equalSize + (i === 0 ? remainder : 0), 75 })); 76 77 panes.push({ id: newId, docId, docType, size: equalSize }); 78 79 return { 80 ...state, 81 panes, 82 focusedPaneId: newId, 83 }; 84} 85 86/** 87 * Remove a pane by ID. Redistributes space to remaining panes. 88 * Returns null if only one pane remains (can't remove the last one). 89 */ 90export function removePane( 91 state: SplitState, 92 paneId: string, 93): SplitState | null { 94 if (state.panes.length <= 1) return null; 95 96 const removed = state.panes.find(p => p.id === paneId); 97 if (!removed) return state; 98 99 const remaining = state.panes.filter(p => p.id !== paneId); 100 const redistributed = removed.size / remaining.length; 101 102 const panes = remaining.map(p => ({ 103 ...p, 104 size: p.size + redistributed, 105 })); 106 107 // If we removed the focused pane, focus the first remaining 108 const focusedPaneId = 109 state.focusedPaneId === paneId 110 ? panes[0].id 111 : state.focusedPaneId; 112 113 return { ...state, panes, focusedPaneId }; 114} 115 116/** 117 * Set focus to a specific pane. 118 */ 119export function focusPane( 120 state: SplitState, 121 paneId: string, 122): SplitState { 123 const exists = state.panes.some(p => p.id === paneId); 124 if (!exists) return state; 125 return { ...state, focusedPaneId: paneId }; 126} 127 128/** 129 * Toggle split direction between horizontal and vertical. 130 */ 131export function toggleDirection(state: SplitState): SplitState { 132 return { 133 ...state, 134 direction: state.direction === 'horizontal' ? 'vertical' : 'horizontal', 135 }; 136} 137 138/** 139 * Resize a pane. Adjusts the next pane to compensate. 140 * newSize is the desired percentage for the target pane. 141 */ 142export function resizePane( 143 state: SplitState, 144 paneId: string, 145 newSize: number, 146): SplitState { 147 const idx = state.panes.findIndex(p => p.id === paneId); 148 if (idx === -1 || idx >= state.panes.length - 1) return state; 149 150 const minSize = 10; 151 const currentPane = state.panes[idx]; 152 const nextPane = state.panes[idx + 1]; 153 const combinedSize = currentPane.size + nextPane.size; 154 155 // Clamp to valid range 156 const clamped = Math.max(minSize, Math.min(combinedSize - minSize, newSize)); 157 const nextSize = combinedSize - clamped; 158 159 const panes = state.panes.map((p, i) => { 160 if (i === idx) return { ...p, size: clamped }; 161 if (i === idx + 1) return { ...p, size: nextSize }; 162 return p; 163 }); 164 165 return { ...state, panes }; 166} 167 168/** 169 * Swap the document in a pane. 170 */ 171export function swapPaneDoc( 172 state: SplitState, 173 paneId: string, 174 docId: string, 175 docType: 'doc' | 'sheet', 176): SplitState { 177 const panes = state.panes.map(p => 178 p.id === paneId ? { ...p, docId, docType } : p, 179 ); 180 return { ...state, panes }; 181} 182 183/** 184 * Get the focused pane, or null if none. 185 */ 186export function getFocusedPane(state: SplitState): Pane | null { 187 return state.panes.find(p => p.id === state.focusedPaneId) || null; 188} 189 190/** 191 * Check if split view is active (more than one pane). 192 */ 193export function isSplitActive(state: SplitState): boolean { 194 return state.panes.length > 1; 195} 196 197/** 198 * Compute CSS grid template from pane sizes. 199 */ 200export function computeGridTemplate(state: SplitState): string { 201 return state.panes.map(p => `${p.size}%`).join(' '); 202}