history: tab container updates, EditorPanel interfaces, editor-shell bug fixes, workspace docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3e25a419ca
commit
dba0fdca1e
|
|
@ -1,3 +1,3 @@
|
|||
# Roject
|
||||
|
||||
See **[workspace/outline/index.html](workspace/outline/index.html)** for the full developer documentation — project outline, tech stack, coding conventions, and repeatable actions.
|
||||
See **[workspace/index.html](workspace/index.html)** for the full developer documentation — project outline, tech stack, coding conventions, and repeatable actions.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanelDefinition, FileEditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
import { csharpMode } from './CSharpMode.js';
|
||||
|
||||
|
|
@ -9,7 +10,9 @@ CodeMirror.defineMode( 'rokojori-cs', () => csharpMode.cmMode );
|
|||
|
||||
class CodePanel extends HTMLElement
|
||||
{
|
||||
__interfaces__ = [ EditorPanelDefinition.type, FileEditorPanelDefinition.type ];
|
||||
currentPath: string | null = null;
|
||||
_dirty = false;
|
||||
_pinned: boolean = false;
|
||||
_cm: any = null;
|
||||
_initialized = false;
|
||||
|
|
@ -132,8 +135,14 @@ class CodePanel extends HTMLElement
|
|||
this.dispatchEvent( new CustomEvent( 'panel:label-change', { bubbles: true, detail: { label: '📝 ' + name } } ) );
|
||||
}
|
||||
|
||||
hasUnsavedChanges(): boolean
|
||||
{
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
_updateButtons( dirty: boolean ): void
|
||||
{
|
||||
this._dirty = dirty;
|
||||
( this.querySelector( '.cp-save' ) as HTMLButtonElement ).disabled = ! dirty;
|
||||
( this.querySelector( '.cp-undo' ) as HTMLButtonElement ).disabled = this._cm.historySize().undo < 1;
|
||||
( this.querySelector( '.cp-redo' ) as HTMLButtonElement ).disabled = this._cm.historySize().redo < 1;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,21 @@ import { Editor } from '../../editor/Editor.js';
|
|||
|
||||
// ── Layout helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function removeAdjacentHandle( el: HTMLElement, handleClass: string ): void
|
||||
{
|
||||
const next = el.nextElementSibling as HTMLElement | null;
|
||||
if ( next && next.classList.contains( handleClass ) )
|
||||
{
|
||||
next.remove();
|
||||
return;
|
||||
}
|
||||
const prev = el.previousElementSibling as HTMLElement | null;
|
||||
if ( prev && prev.classList.contains( handleClass ) )
|
||||
{
|
||||
prev.remove();
|
||||
}
|
||||
}
|
||||
|
||||
let tcCounter = 0;
|
||||
function nextTcId(): string { return `tc-${++tcCounter}`; }
|
||||
|
||||
|
|
@ -151,16 +166,51 @@ class EditorShell extends HTMLElement {
|
|||
|
||||
private setupSplitListener(): void {
|
||||
this.addEventListener('tab-container:split', (e: Event) => {
|
||||
const { containerId } = (e as CustomEvent).detail as { containerId: string };
|
||||
const { containerId, direction } = (e as CustomEvent).detail as { containerId: string; direction: 'horizontal' | 'vertical' };
|
||||
const tc = document.getElementById(containerId);
|
||||
if (!tc) return;
|
||||
const section = tc.closest('.es-section') as HTMLElement | null;
|
||||
const sections = tc.closest('.es-sections') as HTMLElement | null;
|
||||
if (!section || !sections) return;
|
||||
if (!section) return;
|
||||
|
||||
if ( 'vertical' === direction )
|
||||
{
|
||||
const newTc = document.createElement('tab-container') as HTMLElement;
|
||||
newTc.id = nextTcId();
|
||||
const handle = makeResizeHandle('h');
|
||||
section.appendChild(handle);
|
||||
section.appendChild(newTc);
|
||||
}
|
||||
else
|
||||
{
|
||||
const sections = section.closest('.es-sections') as HTMLElement | null;
|
||||
if (!sections) return;
|
||||
const newSec = makeSection();
|
||||
const handle = makeResizeHandle('v');
|
||||
sections.insertBefore(handle, section.nextSibling);
|
||||
sections.insertBefore(newSec, handle.nextSibling);
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener('tab-container:close-container', (e: Event) => {
|
||||
const { containerId } = (e as CustomEvent).detail as { containerId: string };
|
||||
const tc = document.getElementById(containerId) as HTMLElement | null;
|
||||
if (!tc) return;
|
||||
const section = tc.closest('.es-section') as HTMLElement | null;
|
||||
if (!section) return;
|
||||
|
||||
const tcsInSection = section.querySelectorAll(':scope > tab-container');
|
||||
if ( tcsInSection.length > 1 )
|
||||
{
|
||||
removeAdjacentHandle(tc, 'es-h-handle');
|
||||
tc.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
const sections = section.closest('.es-sections') as HTMLElement | null;
|
||||
if (!sections) return;
|
||||
removeAdjacentHandle(section, 'es-v-handle');
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener('tab-container:add-panel', (e: Event) => {
|
||||
|
|
@ -245,15 +295,21 @@ class EditorShell extends HTMLElement {
|
|||
private setupResizeHandler(): void
|
||||
{
|
||||
const workspace = this.querySelector( '.es-workspace' ) as HTMLElement;
|
||||
const obs = new ResizeObserver( () =>
|
||||
|
||||
const workspaceObs = new ResizeObserver( () =>
|
||||
{
|
||||
this._redistributeFlex( workspace, '.es-panel' );
|
||||
workspace.querySelectorAll( '.es-sections' ).forEach( container =>
|
||||
} );
|
||||
workspaceObs.observe( workspace );
|
||||
|
||||
const sectionsObs = new ResizeObserver( ( entries ) =>
|
||||
{
|
||||
this._redistributeFlex( container as HTMLElement, '.es-section' );
|
||||
for ( const entry of entries )
|
||||
{
|
||||
this._redistributeFlex( entry.target as HTMLElement, '.es-section' );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
obs.observe( workspace );
|
||||
workspace.querySelectorAll( '.es-sections' ).forEach( s => sectionsObs.observe( s ) );
|
||||
}
|
||||
|
||||
private _redistributeFlex( container: HTMLElement, childSelector: string ): void
|
||||
|
|
@ -293,7 +349,15 @@ class EditorShell extends HTMLElement {
|
|||
|
||||
const apply = ( portrait: boolean ) => {
|
||||
this.classList.toggle( 'portrait', portrait );
|
||||
if ( portrait ) this.showPortraitPanel( this.activePortraitPanel );
|
||||
if ( portrait )
|
||||
{
|
||||
this.showPortraitPanel( this.activePortraitPanel );
|
||||
}
|
||||
else
|
||||
{
|
||||
this.querySelectorAll( '.es-panel' ).forEach( p => { ( p as HTMLElement ).style.display = ''; } );
|
||||
this.querySelectorAll( '.es-v-handle' ).forEach( h => { ( h as HTMLElement ).style.display = ''; } );
|
||||
}
|
||||
};
|
||||
|
||||
btns.querySelectorAll( '.es-pb-btn' ).forEach( btn => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuEntry, ContextMenuReadOnlyEntry, ContextMenuSeparator } from '../context-menu/context-menu.js';
|
||||
import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js';
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ interface FileNode {
|
|||
}
|
||||
|
||||
class FileTreePanel extends HTMLElement {
|
||||
__interfaces__ = [ EditorPanelDefinition.type ];
|
||||
selectedPath: string | null = null;
|
||||
_rootPath: string = '';
|
||||
_initialized = false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanelDefinition, FileEditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
|
||||
// ── Block registry ────────────────────────────────────────────────────────────
|
||||
|
|
@ -191,7 +192,9 @@ function wrapSelection( doc: Document, range: Range, tagName: string, attributes
|
|||
|
||||
class PageEditorPanel extends HTMLElement
|
||||
{
|
||||
__interfaces__ = [ EditorPanelDefinition.type, FileEditorPanelDefinition.type ];
|
||||
currentPath: string | null = null;
|
||||
_dirty = false;
|
||||
_pinned: boolean = false;
|
||||
_iframe: HTMLIFrameElement | null = null;
|
||||
_undoStack: string[] = [];
|
||||
|
|
@ -477,8 +480,14 @@ class PageEditorPanel extends HTMLElement
|
|||
dir.add( new ContextMenuReadOnlyEntry( dir, this.currentPath ? `Editing: ${ this.currentPath }` : 'No document open' ) );
|
||||
}
|
||||
|
||||
hasUnsavedChanges(): boolean
|
||||
{
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
_updateButtons( dirty: boolean ): void
|
||||
{
|
||||
this._dirty = dirty;
|
||||
( this.querySelector( '.pep-save' ) as HTMLButtonElement ).disabled = ! dirty;
|
||||
( this.querySelector( '.pep-undo' ) as HTMLButtonElement ).disabled = this._undoStack.length < 2;
|
||||
( this.querySelector( '.pep-redo' ) as HTMLButtonElement ).disabled = this._redoStack.length === 0;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
|
||||
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
||||
|
|
@ -39,6 +40,7 @@ function parentDir( filePath: string ): string
|
|||
|
||||
class RojoChatPanel extends HTMLElement
|
||||
{
|
||||
__interfaces__ = [ EditorPanelDefinition.type ];
|
||||
_initialized = false;
|
||||
_conversationId = '';
|
||||
_sending = false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
|
||||
const NS_INKSCAPE = 'http://www.inkscape.org/namespaces/inkscape';
|
||||
|
|
@ -111,6 +112,7 @@ async function loadPortraitSvg(): Promise<string>
|
|||
|
||||
class RojoSettingsPanel extends HTMLElement
|
||||
{
|
||||
__interfaces__ = [ EditorPanelDefinition.type ];
|
||||
currentPath: string | null = null;
|
||||
_initialized = false;
|
||||
_ignoreChange = false;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorPanel, EditorPanelDefinition, FileEditorPanel, FileEditorPanelDefinition, implementsInterface } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuEntry, ContextMenuSeparator } from '../context-menu/context-menu.js';
|
||||
import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js';
|
||||
|
||||
interface TabEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
panelType: string;
|
||||
element: HTMLElement;
|
||||
dirty: boolean;
|
||||
factory: () => HTMLElement;
|
||||
}
|
||||
|
||||
export interface EditorPanel extends HTMLElement {
|
||||
addContextMenuEntries(dir: ContextMenuDirectory): void;
|
||||
}
|
||||
export { EditorPanel };
|
||||
|
||||
class TabContainer extends HTMLElement {
|
||||
tabs: TabEntry[] = [];
|
||||
|
|
@ -54,17 +53,8 @@ class TabContainer extends HTMLElement {
|
|||
if ( tab ) { tab.label = ( e as CustomEvent ).detail.label; this.renderBar(); }
|
||||
} );
|
||||
|
||||
Editor.get().onDocumentDirty.addListener( ( e ) =>
|
||||
{
|
||||
const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path );
|
||||
if ( tab ) { tab.dirty = true; this.renderBar(); }
|
||||
} );
|
||||
|
||||
Editor.get().onDocumentSaved.addListener( ( e ) =>
|
||||
{
|
||||
const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path );
|
||||
if ( tab ) { tab.dirty = false; this.renderBar(); }
|
||||
} );
|
||||
Editor.get().onDocumentDirty.addListener( () => this.renderBar() );
|
||||
Editor.get().onDocumentSaved.addListener( () => this.renderBar() );
|
||||
}
|
||||
|
||||
openMenu(e: MouseEvent): void {
|
||||
|
|
@ -91,9 +81,26 @@ class TabContainer extends HTMLElement {
|
|||
root.add(addDir);
|
||||
|
||||
root.add(new ContextMenuEntry(root, 'Duplicate', () => this.duplicateActive()));
|
||||
root.add(new ContextMenuEntry(root, 'Split', () => {
|
||||
this.dispatchEvent(new CustomEvent('tab-container:split', { bubbles: true, detail: { containerId: this.id } }));
|
||||
|
||||
const splitDir = new ContextMenuDirectory(root, 'Split');
|
||||
splitDir.add(new ContextMenuEntry(splitDir, '↔ Horizontally', () => {
|
||||
this.dispatchEvent(new CustomEvent('tab-container:split', {
|
||||
bubbles: true,
|
||||
detail: { containerId: this.id, direction: 'horizontal' },
|
||||
}));
|
||||
}));
|
||||
splitDir.add(new ContextMenuEntry(splitDir, '↕ Vertically', () => {
|
||||
this.dispatchEvent(new CustomEvent('tab-container:split', {
|
||||
bubbles: true,
|
||||
detail: { containerId: this.id, direction: 'vertical' },
|
||||
}));
|
||||
}));
|
||||
root.add(splitDir);
|
||||
|
||||
const panelCount = this.closest('.es-panel')?.querySelectorAll('tab-container').length ?? 1;
|
||||
if (panelCount > 1) {
|
||||
root.add(new ContextMenuEntry(root, 'Close Container', () => this.closeContainer()));
|
||||
}
|
||||
|
||||
const activeTab = this.tabs.find(t => t.id === this.activeId);
|
||||
if (activeTab) {
|
||||
|
|
@ -110,6 +117,32 @@ class TabContainer extends HTMLElement {
|
|||
root.show(rect.left, rect.bottom);
|
||||
}
|
||||
|
||||
async closeContainer(): Promise<void>
|
||||
{
|
||||
const hasUnsaved = this.tabs.some( t =>
|
||||
implementsInterface( t.element, FileEditorPanelDefinition ) &&
|
||||
( t.element as FileEditorPanel ).hasUnsavedChanges()
|
||||
);
|
||||
|
||||
if ( hasUnsaved )
|
||||
{
|
||||
const confirmed = await showConfirmDialog( {
|
||||
title: 'Close Container',
|
||||
message: 'Some tabs have unsaved changes.',
|
||||
confirmLabel: 'Close Without Saving',
|
||||
cancelLabel: "Don't Close",
|
||||
danger: true,
|
||||
} );
|
||||
|
||||
if ( !confirmed ) return;
|
||||
}
|
||||
|
||||
this.dispatchEvent( new CustomEvent( 'tab-container:close-container', {
|
||||
bubbles: true,
|
||||
detail: { containerId: this.id },
|
||||
} ) );
|
||||
}
|
||||
|
||||
duplicateActive(): void
|
||||
{
|
||||
if ( ! this.activeId )
|
||||
|
|
@ -133,7 +166,6 @@ class TabContainer extends HTMLElement {
|
|||
if ( ! this.activeId )
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
this.extractTab( this.activeId );
|
||||
|
|
@ -144,7 +176,7 @@ class TabContainer extends HTMLElement {
|
|||
const element = factory();
|
||||
element.style.display = 'none';
|
||||
this.querySelector('.tc-content')!.appendChild(element);
|
||||
this.tabs.push({ ...config, element, dirty: false, factory });
|
||||
this.tabs.push({ ...config, element, factory });
|
||||
this.activateTab(config.id);
|
||||
}
|
||||
|
||||
|
|
@ -176,15 +208,25 @@ class TabContainer extends HTMLElement {
|
|||
|
||||
renderBar(): void {
|
||||
const bar = this.querySelector('.tc-tabs')!;
|
||||
bar.innerHTML = this.tabs.map(t => `
|
||||
bar.innerHTML = this.tabs.map(t => {
|
||||
const unsaved = implementsInterface( t.element, FileEditorPanelDefinition ) &&
|
||||
( t.element as FileEditorPanel ).hasUnsavedChanges();
|
||||
return `
|
||||
<div class="tc-tab${t.id === this.activeId ? ' active' : ''}" draggable="true"
|
||||
data-tab-id="${t.id}" data-source="${this.id}">
|
||||
${t.dirty ? '<span class="dirty-dot">●</span>' : ''}${t.label}
|
||||
${unsaved ? '<span class="dirty-dot">●</span>' : ''}${t.label}
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
bar.querySelectorAll('.tc-tab').forEach(el => {
|
||||
el.addEventListener('click', () => this.activateTab((el as HTMLElement).dataset.tabId!));
|
||||
el.addEventListener('mousedown', (e: Event) => {
|
||||
const me = e as MouseEvent;
|
||||
if ( me.button !== 1 ) return;
|
||||
me.preventDefault();
|
||||
this.extractTab( (el as HTMLElement).dataset.tabId! );
|
||||
});
|
||||
el.addEventListener('dragstart', (e: Event) => {
|
||||
const de = e as DragEvent;
|
||||
const tabId = (el as HTMLElement).dataset.tabId!;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { ContextMenuDirectory } from '../components/context-menu/context-menu.js';
|
||||
|
||||
export class EditorPanelDefinition
|
||||
{
|
||||
static readonly type = 'EditorPanel';
|
||||
}
|
||||
|
||||
export class FileEditorPanelDefinition
|
||||
{
|
||||
static readonly type = 'FileEditorPanel';
|
||||
}
|
||||
|
||||
export interface EditorPanel extends HTMLElement
|
||||
{
|
||||
__interfaces__: string[];
|
||||
addContextMenuEntries( dir: ContextMenuDirectory ): void;
|
||||
}
|
||||
|
||||
export interface FileEditorPanel extends EditorPanel
|
||||
{
|
||||
hasUnsavedChanges(): boolean;
|
||||
}
|
||||
|
||||
export function implementsInterface( el: any, def: { type: string } ): boolean
|
||||
{
|
||||
return Array.isArray( el.__interfaces__ ) && el.__interfaces__.includes( def.type );
|
||||
}
|
||||
|
|
@ -80,8 +80,8 @@
|
|||
<h3>Step 2 — Check CLAUDE.md</h3>
|
||||
<p>
|
||||
Open <code>CLAUDE.md</code> at the project root and verify it still just points
|
||||
to the outline. If it has drifted and contains duplicated or stale content,
|
||||
trim it back so it only references <code>workspace/outline/index.html</code>.
|
||||
to the workspace index. If it has drifted and contains duplicated or stale content,
|
||||
trim it back so it only references <code>workspace/index.html</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,57 @@
|
|||
from the shared library — not a plain function property or a custom event.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong>Structural markup — custom element names, not div.class-name</strong>
|
||||
<p>
|
||||
Never write <code><div class="es-header"></code> or similar. Use a
|
||||
hyphenated custom element name instead: <code><editor-shell-header></code>.
|
||||
The element does not need to be registered with <code>customElements.define</code>
|
||||
— it can be an anonymous container used purely as a CSS selector. This makes
|
||||
the DOM tree readable in DevTools without decoding class names, and keeps
|
||||
component markup self-documenting.
|
||||
</p>
|
||||
<pre><code>/* Wrong */
|
||||
<div class="es-header">...</div>
|
||||
|
||||
/* Right */
|
||||
<editor-shell-header>...</editor-shell-header></code></pre>
|
||||
<p>CSS uses the element name as the root selector:</p>
|
||||
<pre><code>editor-shell-header {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong>Web Component interfaces — Definition classes</strong>
|
||||
<p>
|
||||
Web Components must extend <code>HTMLElement</code> and cannot use class
|
||||
inheritance for shared contracts — interfaces are the justified exception to
|
||||
the "classes not interfaces" rule. Each interface has a companion Definition
|
||||
class with a <code>static readonly type</code> string, and every implementing
|
||||
class declares an <code>__interfaces__</code> member listing its Definition types.
|
||||
Use <code>implementsInterface(el, SomeDefinition)</code> for runtime checks —
|
||||
never spread raw string literals.
|
||||
</p>
|
||||
<pre><code>export class EditorPanelDefinition
|
||||
{
|
||||
static readonly type = 'EditorPanel';
|
||||
}
|
||||
|
||||
export interface EditorPanel extends HTMLElement
|
||||
{
|
||||
__interfaces__: string[];
|
||||
addContextMenuEntries( dir: ContextMenuDirectory ): void;
|
||||
}
|
||||
|
||||
// implementing class:
|
||||
__interfaces__ = [ EditorPanelDefinition.type ];
|
||||
|
||||
// runtime check:
|
||||
if ( implementsInterface( panel, EditorPanelDefinition ) ) { ... }</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,82 @@
|
|||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Tab container updates</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>Split submenu + vertical split</h3>
|
||||
<p>
|
||||
The single <em>Split</em> menu entry is replaced by a <em>Split ></em> submenu
|
||||
with <em>↔ Horizontally</em> (adds a new section side by side in <code>.es-sections</code>)
|
||||
and <em>↕ Vertically</em> (adds a second <code>tab-container</code> + horizontal
|
||||
resize handle inside the same <code>.es-section</code>). The
|
||||
<code>tab-container:split</code> event now carries a <code>direction</code> field.
|
||||
<code>.es-section</code> was already <code>flex-direction: column</code>, so no CSS
|
||||
changes were needed for vertical split.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Close Container</h3>
|
||||
<p>
|
||||
A <em>Close Container</em> context menu entry fires <code>tab-container:close-container</code>,
|
||||
handled by <code>editor-shell</code>: if the container is in a vertical split (multiple
|
||||
<code>tab-container</code> elements in the section), only that container and its adjacent
|
||||
handle are removed; otherwise the whole section and its adjacent <code>es-v-handle</code>
|
||||
are removed. The entry is hidden when the container is the last one in its
|
||||
<code>.es-panel</code> slot. If any tab has unsaved changes,
|
||||
<code>showConfirmDialog</code> prompts with <em>Don't Close</em> /
|
||||
<em>Close Without Saving</em> before dispatching the event.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Middle-mouse tab close</h3>
|
||||
<p>
|
||||
Tabs now close on middle-mouse click: a <code>mousedown</code> listener with
|
||||
<code>e.button === 1</code> calls <code>e.preventDefault()</code> (suppresses the
|
||||
browser scroll cursor) and extracts the tab immediately.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>EditorPanel / FileEditorPanel interface system</h3>
|
||||
<p>
|
||||
New file <code>source/editor/editor-panel.ts</code> defines the panel contract.
|
||||
<code>EditorPanel</code> requires <code>__interfaces__: string[]</code> and
|
||||
<code>addContextMenuEntries()</code>. <code>FileEditorPanel extends EditorPanel</code>
|
||||
adds <code>hasUnsavedChanges(): boolean</code>, replacing the old
|
||||
<code>TabEntry.dirty</code> flag — <code>renderBar()</code> now queries the panel
|
||||
live instead of caching a boolean.
|
||||
Each interface has a companion Definition class with <code>static readonly type</code>;
|
||||
<code>implementsInterface(el, Def)</code> is the single runtime check function.
|
||||
All five panels (<code>page-editor-panel</code>, <code>code-panel</code>,
|
||||
<code>file-tree-panel</code>, <code>rojo-settings-panel</code>,
|
||||
<code>rojo-chat-panel</code>) updated with <code>__interfaces__</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Bug fixes</h3>
|
||||
<p>
|
||||
<strong>Section resize:</strong> <code>setupResizeHandler</code> previously observed
|
||||
only the workspace element, so section redistribution never fired when a panel was
|
||||
dragged (workspace size doesn't change on panel drag). Fixed by observing each
|
||||
<code>.es-sections</code> element directly — when the panel narrows, its sections
|
||||
container narrows too and the observer redistributes sections correctly.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong>Portrait → landscape:</strong> <code>showPortraitPanel</code> set inline
|
||||
<code>display: none</code> on panels and handles, but returning to landscape only
|
||||
removed the <code>portrait</code> class — the inline styles persisted and kept panels
|
||||
hidden. Fixed by clearing <code>style.display</code> on all panels and
|
||||
<code>.es-v-handle</code> elements when <code>apply(false)</code> runs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Key decisions</h2>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
<section>
|
||||
<h2>2026 — July</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/25-Friday/index.html">Friday, 25 July 2026</a></h3>
|
||||
<p>Tab container: split submenu (horizontal/vertical), close container with unsaved-changes dialog, middle-mouse tab close. EditorPanel/FileEditorPanel interface system replacing TabEntry.dirty. Section resize and portrait→landscape bug fixes. TypeScript guide: custom element names + web component interface pattern. CLAUDE.md and workspace index corrected.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/18-Friday/index.html">Friday, 18 July 2026</a></h3>
|
||||
<p>CodeMirror syntax highlighting: vendor modes (clike, python, shell, yaml) + custom C# mode built on BrowserLexer + CodeMirrorLexerMode with dynamic keyword sets. File tree single-click smart open (focus existing, skip pinned). rokojori-auth login fix: removed broken refresh-session redirect. Electron local dev fixes: Bearer-before-cookie token extraction, JWT_CLOCK_TOLERANCE clock skew escape hatch, startup token refresh, quit-on-login race fix, credential persistence with remember-me. Mobile nav z-index fix.</p>
|
||||
|
|
|
|||
|
|
@ -85,9 +85,10 @@
|
|||
Check Boards for what is active right now.
|
||||
</p>
|
||||
<p style="margin-top: 0.75rem">
|
||||
<strong>Agents:</strong> read Outline → Guides → Reference before making changes.
|
||||
<strong>Agents:</strong> read Outline → Guides → Actions → Reference before making changes.
|
||||
Check Boards and the most recent History entry to understand the current context.
|
||||
Follow all coding conventions in the Guides — do not infer style from existing code alone.
|
||||
After completing work, run the Update Outline and Update History actions.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -185,6 +185,27 @@
|
|||
available unpinned non-dirty editor of the correct type is used; a new panel is
|
||||
created in the active section if none qualifies. Pinned panels are never overwritten.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong>Tab container context menu:</strong>
|
||||
<em>Add ></em> opens a panel-type submenu. <em>Duplicate</em> clones the active
|
||||
tab. <em>Split ></em> offers <em>↔ Horizontally</em> (new section side by side)
|
||||
and <em>↕ Vertically</em> (new tab-container stacked inside the same section).
|
||||
<em>Close Container</em> removes the container and its adjacent resize handle; it is
|
||||
hidden when the container is the last one in its slot. If any tab has unsaved changes,
|
||||
a confirm dialog (<em>Don't Close</em> / <em>Close Without Saving</em>) appears first.
|
||||
Tabs can also be closed by middle-mouse click.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong>Panel interfaces</strong> — <code>source/editor/editor-panel.ts</code> defines
|
||||
two interfaces for Web Component panels. <code>EditorPanel</code> (all panels) requires
|
||||
<code>__interfaces__: string[]</code> and <code>addContextMenuEntries()</code>.
|
||||
<code>FileEditorPanel extends EditorPanel</code> (file-editing panels only) adds
|
||||
<code>hasUnsavedChanges(): boolean</code>, replacing the old <code>TabEntry.dirty</code>
|
||||
flag. Each interface has a companion Definition class
|
||||
(<code>EditorPanelDefinition</code>, <code>FileEditorPanelDefinition</code>) with a
|
||||
<code>static readonly type</code> string; use <code>implementsInterface(el, Def)</code>
|
||||
for runtime checks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
|
|||
Loading…
Reference in New Issue