generated from nhcarrigan/template
e45a1a1c98
## Summary - Add CodeMirror 6 editor with syntax highlighting for 40+ languages - Add file browser sidebar with collapsible directory tree navigation - Add multi-tab support with dirty state indicators and close buttons - Add keyboard shortcuts (Ctrl+E toggle, Ctrl+B file browser, Ctrl+S save, Ctrl+W close tab) - Add editor toggle button to status bar (disabled when not connected) - Editor automatically uses current session's working directory - Add Tauri backend commands for file operations (list_directory, read_file_content, write_file_content) ## Test Plan - [ ] Connect to a session and verify the editor toggle button becomes enabled - [ ] Press Ctrl+E to open the editor and verify file tree shows the session's CWD - [ ] Navigate directories and open files to verify syntax highlighting works - [ ] Edit a file and verify the dirty indicator (*) appears - [ ] Save with Ctrl+S and verify the dirty indicator disappears - [ ] Open multiple files and verify tab switching works - [ ] Close tabs with Ctrl+W or the X button - [ ] Disconnect and verify the editor automatically closes - [ ] Verify keyboard shortcuts are documented in the shortcuts modal Closes #72 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #79 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
171 lines
3.8 KiB
Svelte
171 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import { editorStore } from "$lib/stores/editor";
|
|
|
|
const tabs = editorStore.tabs;
|
|
const activeTabId = editorStore.activeTabId;
|
|
|
|
function handleTabClick(tabId: string) {
|
|
editorStore.setActiveTab(tabId);
|
|
}
|
|
|
|
function handleCloseTab(event: MouseEvent, tabId: string) {
|
|
event.stopPropagation();
|
|
editorStore.closeTab(tabId);
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent, tabId: string) {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
editorStore.setActiveTab(tabId);
|
|
}
|
|
}
|
|
|
|
function handleCloseKeydown(event: KeyboardEvent, tabId: string) {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
editorStore.closeTab(tabId);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="editor-tabs">
|
|
{#if $tabs.length === 0}
|
|
<div class="no-tabs">No files open</div>
|
|
{:else}
|
|
<div class="tabs-container" role="tablist">
|
|
{#each $tabs as tab (tab.id)}
|
|
<div
|
|
class="tab"
|
|
class:active={tab.id === $activeTabId}
|
|
class:dirty={tab.isDirty}
|
|
role="tab"
|
|
tabindex="0"
|
|
aria-selected={tab.id === $activeTabId}
|
|
on:click={() => handleTabClick(tab.id)}
|
|
on:keydown={(e) => handleKeydown(e, tab.id)}
|
|
title={tab.filePath}
|
|
>
|
|
<span class="tab-name">
|
|
{tab.fileName}
|
|
{#if tab.isDirty}
|
|
<span class="dirty-indicator">*</span>
|
|
{/if}
|
|
</span>
|
|
<button
|
|
class="close-button"
|
|
on:click={(e) => handleCloseTab(e, tab.id)}
|
|
on:keydown={(e) => handleCloseKeydown(e, tab.id)}
|
|
title="Close tab"
|
|
aria-label="Close {tab.fileName}"
|
|
>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.editor-tabs {
|
|
display: flex;
|
|
align-items: center;
|
|
background-color: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
min-height: 36px;
|
|
}
|
|
|
|
.no-tabs {
|
|
padding: 8px 12px;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.tabs-container {
|
|
display: flex;
|
|
overflow-x: auto;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
.tab {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
background: transparent;
|
|
border: none;
|
|
border-right: 1px solid var(--border-color);
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.tab:hover {
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab:focus {
|
|
outline: 1px solid var(--accent-primary);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
.tab.active {
|
|
background-color: var(--bg-terminal);
|
|
color: var(--text-primary);
|
|
border-bottom: 2px solid var(--accent-primary);
|
|
margin-bottom: -1px;
|
|
}
|
|
|
|
.tab-name {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.dirty-indicator {
|
|
color: var(--accent-primary);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.close-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2px;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.tab:hover .close-button,
|
|
.tab.active .close-button {
|
|
opacity: 1;
|
|
}
|
|
|
|
.close-button:hover {
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab.dirty .close-button {
|
|
opacity: 1;
|
|
}
|
|
</style>
|