feat: file tree UX, page editor toolbar, EditorConsole, console-panel
File tree: open-state preserved on refresh, "Mark As Root Directory" moved to context menu, "Open >" submenu for alternate editors, context menu label truncated to filename (20 chars max). Page editor: mode buttons moved into toolbar (sidebar removed). EditorConsole singleton + console-panel tab for centralised message log with es-info header fade. openDocumentIn fix: derives editorTag from panelElement.tagName, not registry. History folder renames corrected (25-Friday→24-Friday, 30-Wednesday→30-Thursday, 31-Friday added). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
99a8f591e9
commit
6fb2133b2c
|
|
@ -0,0 +1,75 @@
|
|||
console-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #0f1117;
|
||||
color: #c8cbde;
|
||||
font-size: 0.82rem;
|
||||
font-family: ui-monospace, "Cascadia Code", "Fira Mono", monospace;
|
||||
}
|
||||
|
||||
conp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
background: #13151f;
|
||||
border-bottom: 1px solid #2a2d3a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
conp-title {
|
||||
flex: 1;
|
||||
font-size: 0.78rem;
|
||||
color: #7b7f96;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.conp-clear {
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid #2a2d3a;
|
||||
border-radius: 3px;
|
||||
color: #7b7f96;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.conp-clear:hover { color: #c8cbde; background: #1a1d27; border-color: #444; }
|
||||
|
||||
conp-list {
|
||||
display: block;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
conp-entry {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
padding: 2px 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
conp-entry:hover { background: #13151f; }
|
||||
|
||||
conp-time {
|
||||
flex-shrink: 0;
|
||||
color: #555;
|
||||
font-size: 0.75rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
conp-text {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.conp-entry-error conp-text { color: #e07070; }
|
||||
.conp-entry-hint conp-text { color: #7b7f96; }
|
||||
.conp-entry-info conp-text { color: #c8cbde; }
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { EditorConsole, ConsoleMessage } from '../../editor/EditorConsole.js';
|
||||
import { EditorPanelDefinition } from '../../editor/editor-panel.js';
|
||||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
|
||||
class ConsolePanel extends HTMLElement
|
||||
{
|
||||
__interfaces__ = [ EditorPanelDefinition.type ];
|
||||
_initialized = false;
|
||||
|
||||
connectedCallback(): void
|
||||
{
|
||||
if ( this._initialized ) return;
|
||||
this._initialized = true;
|
||||
|
||||
this.className = 'console-panel';
|
||||
this.innerHTML = `
|
||||
<conp-header>
|
||||
<conp-title>Console</conp-title>
|
||||
<button class="conp-clear" title="Clear">Clear</button>
|
||||
</conp-header>
|
||||
<conp-list></conp-list>
|
||||
`;
|
||||
|
||||
this.querySelector( '.conp-clear' )!.addEventListener( 'click', () =>
|
||||
{
|
||||
this.querySelector( 'conp-list' )!.innerHTML = '';
|
||||
} );
|
||||
|
||||
EditorConsole.get().messages.forEach( msg => this._append( msg ) );
|
||||
this._scrollToBottom();
|
||||
|
||||
EditorConsole.get().onMessage.addListener( msg =>
|
||||
{
|
||||
this._append( msg );
|
||||
this._scrollToBottom();
|
||||
} );
|
||||
}
|
||||
|
||||
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
||||
{
|
||||
dir.add( new ContextMenuReadOnlyEntry( dir, 'Console' ) );
|
||||
}
|
||||
|
||||
_append( msg: ConsoleMessage ): void
|
||||
{
|
||||
const list = this.querySelector( 'conp-list' )!;
|
||||
const entry = document.createElement( 'conp-entry' );
|
||||
entry.className = `conp-entry-${ msg.type }`;
|
||||
const t = msg.timestamp;
|
||||
const hh = String( t.getHours() ).padStart( 2, '0' );
|
||||
const mm = String( t.getMinutes() ).padStart( 2, '0' );
|
||||
const ss = String( t.getSeconds() ).padStart( 2, '0' );
|
||||
entry.innerHTML = `<conp-time>${ hh }:${ mm }:${ ss }</conp-time><conp-text></conp-text>`;
|
||||
( entry.querySelector( 'conp-text' ) as HTMLElement ).textContent = msg.text;
|
||||
list.appendChild( entry );
|
||||
}
|
||||
|
||||
_scrollToBottom(): void
|
||||
{
|
||||
const list = this.querySelector( 'conp-list' ) as HTMLElement;
|
||||
if ( list ) list.scrollTop = list.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define( 'console-panel', ConsolePanel );
|
||||
|
|
@ -37,6 +37,38 @@ editor-shell {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.es-info {
|
||||
max-width: 300px;
|
||||
font-size: 0.78rem;
|
||||
color: #9ba4c7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.es-info.es-info-visible { opacity: 1; }
|
||||
.es-info.es-info-error { color: #e07070; }
|
||||
.es-info.es-info-hint { color: #7b7f96; }
|
||||
|
||||
editor-shell.portrait .es-info { display: none; }
|
||||
|
||||
editor-shell.portrait .es-info.es-info-visible {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: none;
|
||||
padding: 8px 16px;
|
||||
background: #13151f;
|
||||
border-top: 1px solid #2a2d3a;
|
||||
z-index: 100;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.es-portrait-btns {
|
||||
display: none;
|
||||
gap: 2px;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Editor } from '../../editor/Editor.js';
|
||||
import { EditorConsole } from '../../editor/EditorConsole.js';
|
||||
|
||||
// ── Layout helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ class EditorShell extends HTMLElement {
|
|||
<div class="es-header">
|
||||
<a class="es-back" href="/">←</a>
|
||||
<span class="es-title">${projectName}</span>
|
||||
<div class="es-info"></div>
|
||||
<div class="es-portrait-btns">
|
||||
<button class="es-pb-btn" data-panel="left">⊟</button>
|
||||
<button class="es-pb-btn active" data-panel="center">⊡</button>
|
||||
|
|
@ -109,12 +111,14 @@ class EditorShell extends HTMLElement {
|
|||
this.setupPortrait();
|
||||
this.setupSplitListener();
|
||||
this.setupResizeHandler();
|
||||
this._setupInfo();
|
||||
|
||||
await Promise.all( [
|
||||
customElements.whenDefined( 'tab-container' ),
|
||||
customElements.whenDefined( 'file-tree-panel' ),
|
||||
customElements.whenDefined( 'page-editor-panel' ),
|
||||
customElements.whenDefined( 'code-panel' ),
|
||||
customElements.whenDefined( 'console-panel' ),
|
||||
this._loadLayout(),
|
||||
] );
|
||||
|
||||
|
|
@ -347,6 +351,27 @@ class EditorShell extends HTMLElement {
|
|||
observer.observe(sections, { childList: true });
|
||||
}
|
||||
|
||||
private _setupInfo(): void
|
||||
{
|
||||
const infoEl = this.querySelector( '.es-info' ) as HTMLElement;
|
||||
let hideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
Editor.get().onFileTypeUnknown.addListener( e =>
|
||||
{
|
||||
const lastDot = e.path.lastIndexOf( '.' );
|
||||
const ext = lastDot === -1 ? e.path.slice( e.path.lastIndexOf( '/' ) + 1 ) : e.path.slice( lastDot );
|
||||
EditorConsole.get().log( `Cannot open "${ ext }" files`, 'error' );
|
||||
} );
|
||||
|
||||
EditorConsole.get().onMessage.addListener( msg =>
|
||||
{
|
||||
if ( hideTimer ) clearTimeout( hideTimer );
|
||||
infoEl.textContent = msg.text;
|
||||
infoEl.className = `es-info es-info-visible es-info-${ msg.type }`;
|
||||
hideTimer = setTimeout( () => infoEl.classList.remove( 'es-info-visible' ), 5000 );
|
||||
} );
|
||||
}
|
||||
|
||||
private setupPortrait(): void {
|
||||
const btns = this.querySelector( '.es-portrait-btns' ) as HTMLElement;
|
||||
const mq = window.matchMedia( '(orientation: portrait)' );
|
||||
|
|
|
|||
|
|
@ -161,15 +161,6 @@ ftp-file:hover { background: #1a1d27; color: #e2e4ed; }
|
|||
ftp-file.active { background: #1e2235; color: #7c8cff; }
|
||||
ftp-dir-label.active { background: #1e2235; color: #c8cbde; }
|
||||
|
||||
ftp-type-error
|
||||
{
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
color: #e07070;
|
||||
font-size: 0.8rem;
|
||||
background: #1e1520;
|
||||
border-bottom: 1px solid #3a2a2a;
|
||||
}
|
||||
|
||||
ftp-empty
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class FileTreePanel extends HTMLElement {
|
|||
this.querySelector( '[data-action="add-dir"]' )!.addEventListener( 'click', () => this.addDirectory() );
|
||||
|
||||
Editor.get().onFilesChanged.addListener( () => this.refresh() );
|
||||
Editor.get().onFileTypeUnknown.addListener( ( e ) => this._showTypeError( e.path ) );
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
|
|
@ -78,9 +77,16 @@ class FileTreePanel extends HTMLElement {
|
|||
: state.remoteProject
|
||||
? `/api/remote/files/${ state.remoteProject }/tree`
|
||||
: `/api/files/${ state.projectId }/tree`;
|
||||
|
||||
const tree = this.querySelector( 'ftp-tree' )!;
|
||||
const openPaths = new Set<string>();
|
||||
tree.querySelectorAll( 'ftp-dir.open > ftp-dir-label' ).forEach( el => {
|
||||
const path = ( el as HTMLElement ).dataset.path;
|
||||
if ( path ) openPaths.add( path );
|
||||
} );
|
||||
|
||||
const res = await fetch( treeUrl );
|
||||
const allNodes = await res.json() as FileNode[];
|
||||
const tree = this.querySelector( 'ftp-tree' )!;
|
||||
|
||||
let nodes: FileNode[];
|
||||
|
||||
|
|
@ -101,6 +107,11 @@ class FileTreePanel extends HTMLElement {
|
|||
html += nodes.length ? this.renderNodes( nodes ) : '<ftp-empty>Empty</ftp-empty>';
|
||||
tree.innerHTML = html;
|
||||
|
||||
openPaths.forEach( path => {
|
||||
const label = tree.querySelector( `ftp-dir-label[data-path="${ CSS.escape( path ) }"]` );
|
||||
if ( label ) label.closest( 'ftp-dir' )?.classList.add( 'open' );
|
||||
} );
|
||||
|
||||
const upBtn = tree.querySelector( '[data-action="go-up"]' );
|
||||
if ( upBtn ) {
|
||||
upBtn.addEventListener( 'click', () => this._goUp() );
|
||||
|
|
@ -118,76 +129,7 @@ class FileTreePanel extends HTMLElement {
|
|||
this.selectedPath = path;
|
||||
tree.querySelectorAll( 'ftp-file, ftp-dir-label' ).forEach( f => f.classList.remove( 'active' ) );
|
||||
el.classList.add( 'active' );
|
||||
|
||||
await state.fileEditorRegistry.load( state.projectId );
|
||||
const editorTag = state.fileEditorRegistry.resolve( path );
|
||||
|
||||
if ( editorTag )
|
||||
{
|
||||
const containers = Array.from( document.querySelectorAll( 'tab-container' ) );
|
||||
|
||||
for ( const tc of containers )
|
||||
{
|
||||
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement }>;
|
||||
for ( const tab of tabs )
|
||||
{
|
||||
if ( ( tab.element as any ).currentPath === path )
|
||||
{
|
||||
( tc as any ).activateTab( tab.id );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let found: { panel: HTMLElement; tc: any; tabId: string } | null = null;
|
||||
|
||||
for ( const tc of containers )
|
||||
{
|
||||
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement; dirty: boolean }>;
|
||||
for ( const tab of tabs )
|
||||
{
|
||||
if ( tab.element.tagName.toLowerCase() === editorTag && !tab.dirty && !( tab.element as any )._pinned )
|
||||
{
|
||||
found = { panel: tab.element, tc, tabId: tab.id };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( found ) break;
|
||||
}
|
||||
|
||||
if ( found )
|
||||
{
|
||||
( found.tc as any ).activateTab( found.tabId );
|
||||
state.openDocumentIn( path, found.panel );
|
||||
return;
|
||||
}
|
||||
|
||||
const panelTypeMap: Record<string, { panelType: string; label: string }> =
|
||||
{
|
||||
'page-editor-panel': { panelType: 'page-editor', label: 'Page Editor' },
|
||||
'code-panel': { panelType: 'code-panel', label: 'Code' },
|
||||
'rojo-settings-panel': { panelType: 'rojo-settings', label: 'Rojo' },
|
||||
};
|
||||
const info = panelTypeMap[ editorTag ];
|
||||
|
||||
if ( info )
|
||||
{
|
||||
const targetTc = ( containers.find( tc =>
|
||||
( tc as any ).tabs.some( ( t: any ) => t.element.tagName.toLowerCase() === editorTag )
|
||||
) ?? document.querySelector( '[data-panel="center"] tab-container' ) ?? containers[ 0 ] ) as any;
|
||||
|
||||
if ( targetTc )
|
||||
{
|
||||
const newId = info.panelType + '-' + Math.random().toString( 36 ).slice( 2 );
|
||||
targetTc.addTab( { id: newId, label: info.label, panelType: info.panelType }, () => document.createElement( editorTag ) );
|
||||
const newPanel = targetTc.tabs[ targetTc.tabs.length - 1 ].element as HTMLElement;
|
||||
state.openDocumentIn( path, newPanel );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.openDocument( path );
|
||||
await this._openFileDefault( path );
|
||||
} );
|
||||
el.addEventListener( 'contextmenu', ( e: Event ) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -204,12 +146,6 @@ class FileTreePanel extends HTMLElement {
|
|||
tree.querySelectorAll( 'ftp-file, ftp-dir-label' ).forEach( f => f.classList.remove( 'active' ) );
|
||||
el.classList.add( 'active' );
|
||||
} );
|
||||
el.addEventListener( 'dblclick', () => {
|
||||
const path = ( el as HTMLElement ).dataset.path!;
|
||||
this._rootPath = path;
|
||||
this.selectedPath = null;
|
||||
this.refresh();
|
||||
} );
|
||||
el.addEventListener( 'contextmenu', ( e: Event ) => {
|
||||
e.preventDefault();
|
||||
const me = e as MouseEvent;
|
||||
|
|
@ -218,16 +154,157 @@ class FileTreePanel extends HTMLElement {
|
|||
} );
|
||||
}
|
||||
|
||||
showItemMenu( targetPath: string, x: number, y: number ): void
|
||||
static readonly _panelTypeMap: Record<string, { panelType: string; label: string }> =
|
||||
{
|
||||
'page-editor-panel': { panelType: 'page-editor', label: 'Page Editor' },
|
||||
'code-panel': { panelType: 'code-panel', label: 'Code Editor' },
|
||||
'rojo-settings-panel': { panelType: 'rojo-settings', label: 'Rojo Settings' },
|
||||
'rojo-chat-panel': { panelType: 'rojo-chat', label: 'Rojo Chat' },
|
||||
};
|
||||
|
||||
static readonly _editorAlternatives: Record<string, string[]> =
|
||||
{
|
||||
'page-editor-panel': [ 'code-panel' ],
|
||||
'rojo-settings-panel': [ 'rojo-chat-panel' ],
|
||||
};
|
||||
|
||||
async _openFileDefault( path: string ): Promise<void>
|
||||
{
|
||||
const state = Editor.get();
|
||||
await state.fileEditorRegistry.load( state.projectId );
|
||||
const editorTag = state.fileEditorRegistry.resolve( path );
|
||||
|
||||
const containers = Array.from( document.querySelectorAll( 'tab-container' ) );
|
||||
|
||||
for ( const tc of containers )
|
||||
{
|
||||
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement }>;
|
||||
for ( const tab of tabs )
|
||||
{
|
||||
if ( ( tab.element as any ).currentPath === path )
|
||||
{
|
||||
( tc as any ).activateTab( tab.id );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( editorTag )
|
||||
{
|
||||
await this._openFileIn( path, editorTag );
|
||||
return;
|
||||
}
|
||||
|
||||
state.openDocument( path );
|
||||
}
|
||||
|
||||
async _openFileIn( path: string, editorTag: string ): Promise<void>
|
||||
{
|
||||
const state = Editor.get();
|
||||
const containers = Array.from( document.querySelectorAll( 'tab-container' ) );
|
||||
|
||||
for ( const tc of containers )
|
||||
{
|
||||
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement }>;
|
||||
for ( const tab of tabs )
|
||||
{
|
||||
if ( tab.element.tagName.toLowerCase() === editorTag && ( tab.element as any ).currentPath === path )
|
||||
{
|
||||
( tc as any ).activateTab( tab.id );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let found: { panel: HTMLElement; tc: any; tabId: string } | null = null;
|
||||
for ( const tc of containers )
|
||||
{
|
||||
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement; dirty: boolean }>;
|
||||
for ( const tab of tabs )
|
||||
{
|
||||
if ( tab.element.tagName.toLowerCase() === editorTag && !tab.dirty && !( tab.element as any )._pinned )
|
||||
{
|
||||
found = { panel: tab.element, tc, tabId: tab.id };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( found ) break;
|
||||
}
|
||||
|
||||
if ( found )
|
||||
{
|
||||
( found.tc as any ).activateTab( found.tabId );
|
||||
state.openDocumentIn( path, found.panel );
|
||||
return;
|
||||
}
|
||||
|
||||
const info = FileTreePanel._panelTypeMap[ editorTag ];
|
||||
if ( !info ) return;
|
||||
|
||||
const targetTc = ( containers.find( tc =>
|
||||
( tc as any ).tabs.some( ( t: any ) => t.element.tagName.toLowerCase() === editorTag )
|
||||
) ?? document.querySelector( '[data-panel="center"] tab-container' ) ?? containers[ 0 ] ) as any;
|
||||
|
||||
if ( targetTc )
|
||||
{
|
||||
const newId = info.panelType + '-' + Math.random().toString( 36 ).slice( 2 );
|
||||
targetTc.addTab( { id: newId, label: info.label, panelType: info.panelType }, () => document.createElement( editorTag ) );
|
||||
const newPanel = targetTc.tabs[ targetTc.tabs.length - 1 ].element as HTMLElement;
|
||||
state.openDocumentIn( path, newPanel );
|
||||
}
|
||||
}
|
||||
|
||||
async showItemMenu( targetPath: string, x: number, y: number ): Promise<void>
|
||||
{
|
||||
this.selectedPath = targetPath;
|
||||
const tree = this.querySelector( 'ftp-tree' )!;
|
||||
tree.querySelectorAll( 'ftp-file, ftp-dir-label' ).forEach( f => f.classList.remove( 'active' ) );
|
||||
tree.querySelector( `[data-path="${CSS.escape( targetPath )}"]` )?.classList.add( 'active' );
|
||||
|
||||
const isDir = !!tree.querySelector( `ftp-dir-label[data-path="${CSS.escape( targetPath )}"]` );
|
||||
|
||||
const menuLabelMaxChars = 20;
|
||||
const fileName = targetPath.slice( targetPath.lastIndexOf( '/' ) + 1 );
|
||||
const menuLabel = fileName.length > menuLabelMaxChars
|
||||
? '...' + fileName.slice( -menuLabelMaxChars )
|
||||
: fileName;
|
||||
|
||||
const menu = new ContextMenuDirectory( null );
|
||||
menu.add( new ContextMenuReadOnlyEntry( menu, targetPath ) );
|
||||
menu.add( new ContextMenuReadOnlyEntry( menu, menuLabel ) );
|
||||
menu.add( new ContextMenuSeparator( menu ) );
|
||||
|
||||
if ( isDir )
|
||||
{
|
||||
menu.add( new ContextMenuEntry( menu, 'As Root Directory', () =>
|
||||
{
|
||||
this._rootPath = targetPath;
|
||||
this.selectedPath = null;
|
||||
this.refresh();
|
||||
} ) );
|
||||
menu.add( new ContextMenuSeparator( menu ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
const state = Editor.get();
|
||||
await state.fileEditorRegistry.load( state.projectId );
|
||||
const defaultTag = state.fileEditorRegistry.resolve( targetPath );
|
||||
const alternatives = defaultTag ? ( FileTreePanel._editorAlternatives[ defaultTag ] ?? [] ) : [];
|
||||
|
||||
if ( defaultTag && alternatives.length > 0 )
|
||||
{
|
||||
const openSub = new ContextMenuDirectory( menu, 'Open >' );
|
||||
const defaultLabel = FileTreePanel._panelTypeMap[ defaultTag ]?.label ?? defaultTag;
|
||||
openSub.add( new ContextMenuEntry( openSub, `Default (${ defaultLabel })`, () => this._openFileDefault( targetPath ) ) );
|
||||
for ( const altTag of alternatives )
|
||||
{
|
||||
const altLabel = FileTreePanel._panelTypeMap[ altTag ]?.label ?? altTag;
|
||||
openSub.add( new ContextMenuEntry( openSub, altLabel, () => this._openFileIn( targetPath, altTag ) ) );
|
||||
}
|
||||
menu.add( openSub );
|
||||
menu.add( new ContextMenuSeparator( menu ) );
|
||||
}
|
||||
}
|
||||
|
||||
menu.add( new ContextMenuEntry( menu, 'Rename…', () => this.startInlineRename( targetPath ) ) );
|
||||
menu.add( new ContextMenuEntry( menu, 'Delete', () => this.deleteEntry( targetPath ) ) );
|
||||
menu.show( x, y );
|
||||
|
|
@ -481,22 +558,6 @@ class FileTreePanel extends HTMLElement {
|
|||
}).join('') + '</ftp-list>';
|
||||
}
|
||||
|
||||
_showTypeError( filePath: string ): void
|
||||
{
|
||||
const lastDot = filePath.lastIndexOf( '.' );
|
||||
const ext = lastDot === -1 ? '' : filePath.slice( lastDot );
|
||||
const msg = ext ? `Can't open extension "${ext}"` : `Can't open file without extension`;
|
||||
|
||||
const tree = this.querySelector( 'ftp-tree' )!;
|
||||
const existing = tree.querySelector( 'ftp-type-error' );
|
||||
if ( existing ) existing.remove();
|
||||
|
||||
const el = document.createElement( 'ftp-type-error' );
|
||||
el.textContent = msg;
|
||||
tree.prepend( el );
|
||||
|
||||
setTimeout( () => el.remove(), 3000 );
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('file-tree-panel', FileTreePanel);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,10 @@
|
|||
page-editor-panel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #0f1117;
|
||||
}
|
||||
|
||||
/* ── Sidebar (mode switcher) ─────────────────────────────────────────────── */
|
||||
|
||||
.pep-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 36px;
|
||||
background: #13151f;
|
||||
border-right: 1px solid #2a2d3a;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pep-mode-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
|
@ -32,18 +18,14 @@ page-editor-panel {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pep-mode-btn:hover { color: #9ba4c7; background: #1a1d27; }
|
||||
.pep-mode-btn.active { color: #7c8cff; border-color: #7c8cff; }
|
||||
|
||||
/* ── Main column ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.pep-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.pep-toolbar-sep {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -211,39 +211,36 @@ class PageEditorPanel extends HTMLElement
|
|||
|
||||
this.className = 'page-editor-panel';
|
||||
this.innerHTML = `
|
||||
<div class="pep-sidebar">
|
||||
<div class="pep-toolbar">
|
||||
<button class="pep-pin" title="Pin — keep this file when selecting others">Pin</button>
|
||||
<button class="pep-undo" title="Undo (Ctrl+Z)" disabled>↩</button>
|
||||
<button class="pep-redo" title="Redo (Ctrl+Y)" disabled>↪</button>
|
||||
<button class="pep-save" title="Save (Ctrl+S)" disabled>Save</button>
|
||||
<div class="pep-toolbar-sep"></div>
|
||||
<button class="pep-mode-btn pep-mode-blocks active" title="Blocks">⊞</button>
|
||||
<button class="pep-mode-btn pep-mode-areas" title="Areas">T</button>
|
||||
</div>
|
||||
<div class="pep-main">
|
||||
<div class="pep-toolbar">
|
||||
<button class="pep-pin" title="Pin — keep this file when selecting others">Pin</button>
|
||||
<button class="pep-undo" title="Undo (Ctrl+Z)" disabled>↩</button>
|
||||
<button class="pep-redo" title="Redo (Ctrl+Y)" disabled>↪</button>
|
||||
<button class="pep-save" title="Save (Ctrl+S)" disabled>Save</button>
|
||||
<div class="pep-mode-panel pep-blocks-panel">
|
||||
<div class="pep-block-list">
|
||||
${ PAGE_BLOCK_REGISTRY.map( b => `
|
||||
<div class="pep-block-item" data-block="${ b.name }">
|
||||
<div class="pep-block-preview">${ b.preview ?? `<span class="pep-block-text-preview">${ b.name }</span>` }</div>
|
||||
<div class="pep-block-name">${ b.name }</div>
|
||||
</div>
|
||||
` ).join( '' ) }
|
||||
</div>
|
||||
<div class="pep-mode-panel pep-blocks-panel">
|
||||
<div class="pep-block-list">
|
||||
${ PAGE_BLOCK_REGISTRY.map( b => `
|
||||
<div class="pep-block-item" data-block="${ b.name }">
|
||||
<div class="pep-block-preview">${ b.preview ?? `<span class="pep-block-text-preview">${ b.name }</span>` }</div>
|
||||
<div class="pep-block-name">${ b.name }</div>
|
||||
</div>
|
||||
` ).join( '' ) }
|
||||
</div>
|
||||
</div>
|
||||
<div class="pep-mode-panel pep-areas-panel" style="display:none">
|
||||
<button class="pep-fmt-btn" data-tag="b" title="Bold"><b>B</b></button>
|
||||
<button class="pep-fmt-btn" data-tag="i" title="Italic"><i>I</i></button>
|
||||
<button class="pep-fmt-btn" data-tag="u" title="Underline"><u>U</u></button>
|
||||
<div class="pep-fmt-sep"></div>
|
||||
<button class="pep-fmt-btn" data-tag="h1" title="Heading 1">H1</button>
|
||||
<button class="pep-fmt-btn" data-tag="h2" title="Heading 2">H2</button>
|
||||
<button class="pep-fmt-btn" data-tag="h3" title="Heading 3">H3</button>
|
||||
</div>
|
||||
<div class="pep-empty">Open a .page file from the file tree</div>
|
||||
<iframe class="pep-frame" sandbox="allow-same-origin" style="display:none"></iframe>
|
||||
</div>
|
||||
<div class="pep-mode-panel pep-areas-panel" style="display:none">
|
||||
<button class="pep-fmt-btn" data-tag="b" title="Bold"><b>B</b></button>
|
||||
<button class="pep-fmt-btn" data-tag="i" title="Italic"><i>I</i></button>
|
||||
<button class="pep-fmt-btn" data-tag="u" title="Underline"><u>U</u></button>
|
||||
<div class="pep-fmt-sep"></div>
|
||||
<button class="pep-fmt-btn" data-tag="h1" title="Heading 1">H1</button>
|
||||
<button class="pep-fmt-btn" data-tag="h2" title="Heading 2">H2</button>
|
||||
<button class="pep-fmt-btn" data-tag="h3" title="Heading 3">H3</button>
|
||||
</div>
|
||||
<div class="pep-empty">Open a .page file from the file tree</div>
|
||||
<iframe class="pep-frame" sandbox="allow-same-origin" style="display:none"></iframe>
|
||||
`;
|
||||
|
||||
this._iframe = this.querySelector( 'iframe' );
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ class TabContainer extends HTMLElement {
|
|||
{ label: 'Page Editor', panelType: 'page-editor', tag: 'page-editor-panel' },
|
||||
{ label: 'Code Editor', panelType: 'code-panel', tag: 'code-panel' },
|
||||
{ label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' },
|
||||
{ label: 'Console', panelType: 'console-panel', tag: 'console-panel' },
|
||||
{ label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' },
|
||||
{ label: 'Rojo Settings', panelType: 'rojo-settings', tag: 'rojo-settings-panel' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -83,14 +83,7 @@ export class Editor
|
|||
|
||||
async openDocumentIn( filePath: string, panelElement: HTMLElement ): Promise<void>
|
||||
{
|
||||
await this.fileEditorRegistry.load( this.projectId );
|
||||
const editorTag = this.fileEditorRegistry.resolve( filePath );
|
||||
|
||||
if ( editorTag === null )
|
||||
{
|
||||
this.onFileTypeUnknown.dispatch( { path: filePath } );
|
||||
return;
|
||||
}
|
||||
const editorTag = panelElement.tagName.toLowerCase();
|
||||
|
||||
if ( !this.openDocs.has( filePath ) )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';
|
||||
|
||||
export type ConsoleMessageType = 'info' | 'error' | 'hint';
|
||||
|
||||
export interface ConsoleMessage
|
||||
{
|
||||
text: string;
|
||||
type: ConsoleMessageType;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export class EditorConsole
|
||||
{
|
||||
static _instance: EditorConsole | null = null;
|
||||
|
||||
static get(): EditorConsole
|
||||
{
|
||||
if ( !this._instance ) this._instance = new EditorConsole();
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
static readonly MAX_MESSAGES = 500;
|
||||
|
||||
messages: ConsoleMessage[] = [];
|
||||
readonly onMessage: EventSlot<ConsoleMessage> = new EventSlot();
|
||||
readonly onHover: EventSlot<string | null> = new EventSlot();
|
||||
|
||||
log( text: string, type: ConsoleMessageType = 'info' ): void
|
||||
{
|
||||
const msg: ConsoleMessage = { text, type, timestamp: new Date() };
|
||||
this.messages.push( msg );
|
||||
if ( this.messages.length > EditorConsole.MAX_MESSAGES ) this.messages.shift();
|
||||
this.onMessage.dispatch( msg );
|
||||
}
|
||||
|
||||
showHover( text: string ): void
|
||||
{
|
||||
this.onHover.dispatch( text );
|
||||
}
|
||||
|
||||
hideHover(): void
|
||||
{
|
||||
this.onHover.dispatch( null );
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.0153862"
|
||||
inkscape:cx="45.267833"
|
||||
inkscape:cy="65.994863"
|
||||
inkscape:cx="93.354543"
|
||||
inkscape:cy="70.96935"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
|
|
@ -47,18 +47,6 @@
|
|||
id="layer1"
|
||||
style="fill:#ffffff;fill-opacity:1"><path
|
||||
id="rect298"
|
||||
style="fill:#ddc849;fill-opacity:0.157647;stroke:#ffcd2c;stroke-width:4.62941;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||
d="m 18.939219,17.937676 c -3.603915,0 -6.506489,2.900765 -6.506489,6.504681 0.269989,29.955359 0.920457,48.124236 0.920457,78.230983 0,4.09359 3.295382,7.38898 7.38897,7.38898 H 108.1783 c 4.09359,0 7.38897,-3.29539 7.38897,-7.38898 V 37.536713 c 0,-4.093587 -3.29538,-7.390779 -7.38897,-7.390779 H 88.478008 v -5.703577 c 0,-3.603916 -2.900765,-6.504681 -6.504682,-6.504681 z"
|
||||
sodipodi:nodetypes="sccssssscsss" /><ellipse
|
||||
style="fill:#ffe027;fill-opacity:0.157647;stroke:none;stroke-width:6.92884;stroke-linecap:round;stroke-linejoin:round;paint-order:fill markers stroke"
|
||||
id="circle9716"
|
||||
cx="49.920033"
|
||||
cy="70.388992"
|
||||
rx="9.368618"
|
||||
ry="17.991062" /><ellipse
|
||||
style="fill:#ffe027;fill-opacity:0.157647;stroke:none;stroke-width:6.92884;stroke-linecap:round;stroke-linejoin:round;paint-order:fill markers stroke"
|
||||
id="circle9718"
|
||||
cx="77.353271"
|
||||
cy="70.388992"
|
||||
rx="9.368618"
|
||||
ry="17.991062" /></g></svg>
|
||||
style="fill:#ddc849;fill-opacity:0.611765;stroke:#ffcd2c;stroke-width:4.62941;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||
d="m 14.718227,17.937676 c -3.603915,0 -6.5064893,2.900765 -6.5064893,6.504681 0.269989,29.955359 0.920457,48.124236 0.920457,78.230983 0,4.09359 3.2953823,7.38898 7.3889703,7.38898 h 96.112625 c 4.09359,0 7.38897,-3.29539 7.38897,-7.38898 V 37.536713 c 0,-4.093587 -3.29538,-7.390779 -7.38897,-7.390779 H 88.478008 v -5.703577 c 0,-3.603916 -2.900765,-6.504681 -6.504682,-6.504681 z"
|
||||
sodipodi:nodetypes="sccssssscsss" /></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -14,6 +14,7 @@
|
|||
<link rel="stylesheet" href="/components/rojo-settings-panel/rojo-settings-panel.css">
|
||||
<link rel="stylesheet" href="/vendor/codemirror.min.css">
|
||||
<link rel="stylesheet" href="/components/code-panel/code-panel.css">
|
||||
<link rel="stylesheet" href="/components/console-panel/console-panel.css">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
|
@ -41,5 +42,6 @@
|
|||
<script src="/vendor/cm-mode-shell.min.js"></script>
|
||||
<script src="/vendor/cm-mode-yaml.min.js"></script>
|
||||
<script type="module" src="/components/code-panel/code-panel.js"></script>
|
||||
<script type="module" src="/components/console-panel/console-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@ var NAV_DATA = {
|
|||
title: 'History',
|
||||
path: 'history/index.html',
|
||||
children: [
|
||||
{ title: 'Wednesday, 30 July 2026', path: 'history/2026/07-July/30-Wednesday/index.html' },
|
||||
{ title: 'Friday, 25 July 2026', path: 'history/2026/07-July/25-Friday/index.html' },
|
||||
{ title: 'Friday, 31 July 2026', path: 'history/2026/07-July/31-Friday/index.html' },
|
||||
{ title: 'Thursday, 30 July 2026', path: 'history/2026/07-July/30-Thursday/index.html' },
|
||||
{ title: 'Friday, 24 July 2026', path: 'history/2026/07-July/24-Friday/index.html' },
|
||||
{ title: 'Friday, 18 July 2026', path: 'history/2026/07-July/18-Friday/index.html' },
|
||||
{ title: 'Wednesday, 16 July 2026', path: 'history/2026/07-July/16-Wednesday/index.html' },
|
||||
{ title: 'Tuesday, 15 July 2026', path: 'history/2026/07-July/15-Tuesday/index.html' },
|
||||
|
|
|
|||
|
|
@ -223,62 +223,56 @@
|
|||
<div class="lane-header">Done</div>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Remote Projects in Electron</task-title>
|
||||
<task-title>File tree: UX improvements</task-title>
|
||||
<task-content>
|
||||
Implemented via a reverse proxy route: /api/remote/** strips the prefix,
|
||||
prepends /api, and forwards to roject.rokojori.com over HTTPS.
|
||||
The Electron onBeforeSendHeaders interceptor already injects the Authorization
|
||||
header on all localhost:3000 requests, so no new IPC channel was needed.
|
||||
The Online tab in project-list-default fetches from /api/remote/projects
|
||||
and opens the editor with a remoteProject URL param.
|
||||
— Open-state preservation: refresh() now records which ftp-dir elements are open
|
||||
(via data-path on their ftp-dir-label) before rebuilding the HTML, then
|
||||
re-adds the open class to matching labels after render.
|
||||
— "Mark As Root Directory" moved from dblclick to context menu (isDir detection
|
||||
via targetPath.endsWith('/'), shown inside showItemMenu).
|
||||
— "Open >" submenu for files: context menu lists the default editor plus all
|
||||
registered alternatives (_panelTypeMap / _editorAlternatives static maps).
|
||||
Selecting an entry calls _openFileIn(path, editorTag).
|
||||
— Context menu label: shows filename only, truncated to menuLabelMaxChars (20)
|
||||
with a leading "..." prefix when over the limit.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Local Filesystem Access</task-title>
|
||||
<task-title>Page editor: mode buttons moved into toolbar</task-title>
|
||||
<task-content>
|
||||
File tree now browses arbitrary host directories via /api/local/tree, /api/local/read,
|
||||
and /api/local/write routes (Node.js fs, no project storage). Editor.ts branches on
|
||||
localRoot for all read/write URLs. The "This PC" tab in project-list-default opens
|
||||
the editor with a localRoot URL param.
|
||||
The left sidebar (.pep-sidebar) and its .pep-main wrapper were removed.
|
||||
The Blocks (⊞) and Areas (T) mode buttons now live directly in .pep-toolbar,
|
||||
pushed right by a .pep-toolbar-sep spacer (flex: 1).
|
||||
page-editor-panel now uses flex-direction: column with three direct children:
|
||||
.pep-toolbar, .pep-mode-panel, iframe.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Tab-container: split function broken, panel border update unreliable</task-title>
|
||||
<task-title>EditorConsole + console-panel</task-title>
|
||||
<task-content>
|
||||
Split rebuilt as a Split > submenu with ↔ Horizontally and ↕ Vertically entries.
|
||||
Section resize observer switched from watching the workspace element to watching
|
||||
each .es-sections element directly — redistribution now fires correctly when a
|
||||
panel is dragged, fixing stuck or misaligned borders.
|
||||
EditorConsole is a new standalone singleton (source/editor/EditorConsole.ts)
|
||||
that holds a capped ring of 500 ConsoleMessage objects and dispatches them via
|
||||
onMessage: EventSlot. editor-shell subscribes and shows messages in a new
|
||||
.es-info element in the header (5 s fade; portrait: fixed bottom bar).
|
||||
Editor.onFileTypeUnknown is bridged to EditorConsole here, removing the
|
||||
inline ftp-type-error element from file-tree-panel.
|
||||
console-panel is a new tab that renders all messages from EditorConsole using
|
||||
custom elements (conp-header, conp-list, conp-entry, conp-time, conp-text).
|
||||
Added to tab-container panel-type list and loaded in editor.html.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Mobile: nav bar z-index too low on projects / index view</task-title>
|
||||
<task-title>Bug fix: openDocumentIn ignored the target panel type</task-title>
|
||||
<task-content>
|
||||
Added z-index: 10 to .pld-nav in project-list-default.css.
|
||||
The nav has position: fixed but lacked a z-index, so stacking contexts
|
||||
from position: relative project rows buried it on mobile.
|
||||
Overlays remain above at z-index: 200.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Electron Roject app: local dev fixes</task-title>
|
||||
<task-content>
|
||||
Several fixes to make the Electron app usable for local development:
|
||||
— extractToken now checks Authorization Bearer before the accessToken cookie,
|
||||
so stale browser cookies cannot shadow the injected token.
|
||||
— JWT_CLOCK_TOLERANCE env var (seconds) passed to jwt.verify as clockTolerance;
|
||||
set to 7200 in .env to absorb clock skew between local and production auth server.
|
||||
— Startup token refresh: on launch with saved tokens, main.ts calls
|
||||
POST account.rokojori.com/api/auth/refresh before opening the main window;
|
||||
shows login window if refresh fails.
|
||||
— Quit-on-login fix: createMainWindow() is now async; login-success handler
|
||||
awaits it before closing the login window, preventing window-all-closed → quit.
|
||||
— Credential persistence: email and password saved to userData on successful login;
|
||||
remember-me checkbox controls whether they are saved; clear button deletes them.
|
||||
openDocumentIn was re-resolving editorTag from FileEditorRegistry, which always
|
||||
returned the default editor for the file type (e.g. page-editor-panel for .page
|
||||
files). The code-panel listener checks editorTag and bailed, so files opened
|
||||
via "Open in Code Editor" showed a blank panel.
|
||||
Fix: derive editorTag from panelElement.tagName.toLowerCase() directly,
|
||||
removing the registry lookup from openDocumentIn entirely.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Friday, 25 July 2026 — Roject</title>
|
||||
<title>Friday, 24 July 2026 — Roject</title>
|
||||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||
</head>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Friday, 25 July 2026</p>
|
||||
<p class="date">Friday, 24 July 2026</p>
|
||||
<h1>Session History</h1>
|
||||
<p class="subtitle">Page editor panel: structured .page format, block registry, rich-text toolbar, default theme.</p>
|
||||
</header>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wednesday, 30 July 2026 — Roject</title>
|
||||
<title>Thursday, 30 July 2026 — Roject</title>
|
||||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||
</head>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Wednesday, 30 July 2026</p>
|
||||
<p class="date">Thursday, 30 July 2026</p>
|
||||
<h1>Session History</h1>
|
||||
<p class="subtitle">Local filesystem access, remote projects proxy, file tree custom elements + icons, project-list-default tab UI.</p>
|
||||
</header>
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Friday, 31 July 2026 — Roject</title>
|
||||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Friday, 31 July 2026</p>
|
||||
<h1>Session History</h1>
|
||||
<p class="subtitle">File tree UX improvements, page editor toolbar, EditorConsole message system, console-panel tab.</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>What we built</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>File tree: open-state preservation on refresh</h3>
|
||||
<p>
|
||||
Before this fix, calling <code>refresh()</code> on <code>file-tree-panel</code>
|
||||
collapsed all open directories. The fix records which <code>ftp-dir</code> elements
|
||||
carry the <code>open</code> class (via <code>data-path</code> on their
|
||||
<code>ftp-dir-label</code>) before rebuilding the HTML, then re-adds
|
||||
<code>open</code> to the matching labels after the new tree renders.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>File tree: "Mark As Root Directory" moved to context menu</h3>
|
||||
<p>
|
||||
The double-click listener on <code>ftp-dir-label</code> was removed.
|
||||
"As Root Directory" is now a context menu entry that appears when right-clicking
|
||||
a directory, inside the existing <code>showItemMenu()</code> method.
|
||||
<code>isDir</code> detection uses <code>targetPath.endsWith('/')</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>File tree: "Open >" submenu for alternate editors</h3>
|
||||
<p>
|
||||
When right-clicking a file that has a known editor, the context menu shows an
|
||||
<em>Open ></em> submenu listing the default editor plus all registered
|
||||
alternatives. Selecting an entry calls <code>_openFileIn(path, editorTag)</code>,
|
||||
which focuses an existing tab of that type or creates a new one.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
Two static maps drive this:
|
||||
<code>_panelTypeMap</code> (editorTag → panelType + label) and
|
||||
<code>_editorAlternatives</code> (defaultEditorTag → alternativeEditorTags[]).
|
||||
<code>_openFileDefault</code> (single click) focuses the file in any open editor
|
||||
regardless of type; <code>_openFileIn</code> (context menu) focuses only in the
|
||||
specified editor type.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>File tree: context menu label truncation</h3>
|
||||
<p>
|
||||
The context menu title now shows only the filename (not the full path), truncated
|
||||
to a configurable <code>menuLabelMaxChars</code> (20). Names longer than the limit
|
||||
are shown as <code>...last-20-chars</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Page editor: mode buttons moved into toolbar</h3>
|
||||
<p>
|
||||
The left sidebar (<code>.pep-sidebar</code>) and its wrapper (<code>.pep-main</code>)
|
||||
were removed. The Blocks (⊞) and Areas (T) mode buttons were moved directly into
|
||||
<code>.pep-toolbar</code>, separated from the left-side toolbar items by a
|
||||
<code>.pep-toolbar-sep</code> spacer (<code>flex: 1</code>) that pushes them to
|
||||
the right. <code>page-editor-panel</code> now uses <code>flex-direction: column</code>
|
||||
with three direct children: toolbar, mode panel, iframe.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>EditorConsole — centralised message system</h3>
|
||||
<p>
|
||||
New singleton <code>EditorConsole</code> (<code>source/editor/EditorConsole.ts</code>)
|
||||
holds a capped ring of 500 <code>ConsoleMessage</code> objects
|
||||
(<code>text</code>, <code>type</code>: <code>'info'|'error'|'hint'</code>,
|
||||
<code>timestamp</code>) and dispatches them via <code>onMessage: EventSlot</code>.
|
||||
<code>showHover</code> / <code>hideHover</code> dispatch a secondary
|
||||
<code>onHover: EventSlot<string | null></code> for future tooltip use.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<code>editor-shell</code> subscribes to <code>onMessage</code> and shows incoming
|
||||
messages in a new <code>.es-info</code> element in the header — fades in for 5 s
|
||||
then fades out. Portrait mode hides the inline element and would show a fixed bottom
|
||||
bar instead. <code>Editor.get().onFileTypeUnknown</code> is now handled here,
|
||||
logging to <code>EditorConsole</code> rather than as an inline error in
|
||||
<code>file-tree-panel</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>console-panel — new editor tab</h3>
|
||||
<p>
|
||||
<code>console-panel</code> is a new tab that renders all messages from
|
||||
<code>EditorConsole</code>. Custom elements: <code>conp-header</code>,
|
||||
<code>conp-title</code>, <code>conp-list</code>, <code>conp-entry</code>,
|
||||
<code>conp-time</code>, <code>conp-text</code>. On connect it pre-populates
|
||||
from <code>EditorConsole.get().messages</code>, then appends new entries as they
|
||||
arrive. Time format: <code>HH:MM:SS</code>. Entry classes (<code>conp-entry-error</code>,
|
||||
<code>conp-entry-hint</code>) colour <code>conp-text</code> accordingly.
|
||||
Implements <code>EditorPanelDefinition.type</code> via <code>__interfaces__</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Bug fix: <code>openDocumentIn</code> ignored the target panel type</h3>
|
||||
<p>
|
||||
<code>Editor.openDocumentIn(filePath, panelElement)</code> was re-resolving
|
||||
<code>editorTag</code> from the <code>FileEditorRegistry</code>, which always
|
||||
returned the <em>default</em> editor for the file type (e.g. <code>page-editor-panel</code>
|
||||
for <code>.page</code> files). The <code>code-panel</code> listener checked
|
||||
<code>if ('code-panel' !== e.editorTag) return</code> and bailed, so the file
|
||||
appeared to open but showed no content.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
Fix: derive <code>editorTag</code> from <code>panelElement.tagName.toLowerCase()</code>
|
||||
directly, removing the registry lookup entirely from <code>openDocumentIn</code>.
|
||||
The caller already decided which panel to target; the dispatch should respect that
|
||||
decision.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Key decisions</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong><code>EditorConsole</code> as a standalone singleton, not part of
|
||||
<code>Editor</code>.</strong>
|
||||
Keeping it separate means any component (including future non-editor panels) can
|
||||
import just <code>EditorConsole</code> without pulling in the full editor state.
|
||||
<code>Editor.onFileTypeUnknown</code> is bridged to it in <code>editor-shell</code>,
|
||||
the single component that knows about both.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
<strong>Two open-file methods instead of one.</strong>
|
||||
<code>_openFileDefault</code> (single click) focuses the file in any editor;
|
||||
<code>_openFileIn</code> (context menu "Open >") focuses only in the specified
|
||||
editor type and forces that type when creating a new tab. This makes the intent
|
||||
explicit without adding a flag parameter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Roject — session history
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script>var NAV_ROOT = '../../../../';</script>
|
||||
<script src="../../../../_assets_/nav-data.js"></script>
|
||||
<script src="../../../../_assets_/nav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="UTF-8">
|
||||
<title>New Page</title>
|
||||
<style>
|
||||
@import url('https://styles.rokojori.com/get-font?family=Barlow&weights=100,400,700,900');
|
||||
|
||||
[data-theme="default-roject"] {
|
||||
background: #0f1117;
|
||||
color: #c8cce0;
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h1 {
|
||||
color: #7c8cff;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
text-transform: uppercase;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h2 {
|
||||
color: #7c8cff;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h3 {
|
||||
color: #9ba4c7;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 1rem 0 0.4rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] p {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] b,
|
||||
[data-theme="default-roject"] strong {
|
||||
color: #e2e4ed;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-theme="default-roject">
|
||||
<page-header></page-header>
|
||||
|
||||
<page-root data-theme="default-roject">
|
||||
<page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h1>INFO/CONSOLE Update</h1><div>New display and storage for hints, messages and errors. The editor itself should get an message sending mechanism as centralized place where panels can send their messages (like file tree panel). The messages should be stored with a time stamp.</div></page-area>
|
||||
</page-block>
|
||||
<page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h2>UI</h2><div><span style="font-size: 1rem;">- The header gets an info element, right aligned ( or on portrait an overlay)</span></div><div>- There will be a new panel, named console logs</div></page-area>
|
||||
</page-block><page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h2>Info Element Functionality</h2><div>- Prioritized, display editor console messages (such as editor error messages for not being able to open a file) with a blocking duration of 5 seconds (for hover infos)</div><div>- Display info on hover for other elements</div><div><br></div></page-area>
|
||||
</page-block><page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h2>Console Logs Functionality</h2><div>- Show the last 500 console messages of the editor</div><div><br></div></page-area>
|
||||
</page-block></page-root>
|
||||
|
||||
<page-footer></page-footer>
|
||||
|
||||
</body></html>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="UTF-8">
|
||||
<title>New Page</title>
|
||||
<style>
|
||||
@import url('https://styles.rokojori.com/get-font?family=Barlow&weights=100,400,700,900');
|
||||
|
||||
[data-theme="default-roject"] {
|
||||
background: #0f1117;
|
||||
color: #c8cce0;
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h1 {
|
||||
color: #7c8cff;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
text-transform: uppercase;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h2 {
|
||||
color: #7c8cff;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] h3 {
|
||||
color: #9ba4c7;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 1rem 0 0.4rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] p {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme="default-roject"] b,
|
||||
[data-theme="default-roject"] strong {
|
||||
color: #e2e4ed;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-theme="default-roject">
|
||||
<page-header></page-header>
|
||||
|
||||
<page-root data-theme="default-roject">
|
||||
<page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h1>Small Updates</h1><div>Several updates for improving the UX</div></page-area>
|
||||
</page-block>
|
||||
<page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h2>File Tree Panel</h2><div><h3><span style="font-size: 1.2rem;">Directory focusing</span></h3></div><div>This should be a context menu option "Make As Root Directory" for directories and not a double click option. It happens to often randomly when opening/hiding the tree.</div><div><br></div><div><h3><span style="font-size: 1.2rem;">Creating File/Direction Structure Reload </span></h3></div><div>When a file or directory is created it closes all directories, that's weird. It's maybe a refresh bug.</div><div><br></div><div><h3><span style="font-size: 1.2rem;">Files Open Context Menu </span></h3></div><div>Files need a menu entry to open them in other than the connected editor. For example a "index.page" should have:<br>Open > <br> Default (Page Editor)<br> Code Editor</div><div><br></div><div>While a "mc-joe.rojo" should have:</div><div>Open > </div><div> Default (Rojo Settings)</div><div> Rojo Chat</div><div><br></div></page-area>
|
||||
</page-block><page-block class="pep-block-full">
|
||||
<page-area contenteditable="true" style="outline: none;"><h2>Page Editor</h2><div><h3><span style="font-size: 1.2rem;">Toolbar Rearrangement</span></h3></div><div>Currently the toolbar on the left takes a lot of space vertically. Remvoe that HTML element and and the tools to its normal toolbar on top. Make a bit space to the normal editing options (Pin/Undo/Save) and than put block selector and style selectors next to it.<br><br></div></page-area>
|
||||
</page-block></page-root>
|
||||
|
||||
<page-footer></page-footer>
|
||||
|
||||
</body></html>
|
||||
|
|
@ -20,12 +20,17 @@
|
|||
<h2>2026 — July</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/30-Wednesday/index.html">Wednesday, 30 July 2026</a></h3>
|
||||
<h3><a href="2026/07-July/31-Friday/index.html">Friday, 31 July 2026</a></h3>
|
||||
<p>File tree: context menu open-state preservation on refresh, "Open >" submenu for alternate editors, context menu label truncated to filename. Page editor toolbar: mode buttons moved into toolbar (sidebar removed). EditorConsole singleton + console-panel tab: centralised message log with es-info header display (5 s fade). openDocumentIn fix: editorTag derived from panelElement.tagName, not registry.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/30-Thursday/index.html">Thursday, 30 July 2026</a></h3>
|
||||
<p>Local filesystem access completed (localRoot URL param, /api/local/ routes, three-branch URL helper in Editor). Remote projects proxy in Electron (/api/remote/** → roject.rokojori.com, auth via onBeforeSendHeaders). file-tree-panel fully converted to custom elements (ftp-*), closed-by-default dirs, CSS triangle, SVG icons. project-list-default tab UI with <pld-tabs>/<pld-tab> custom elements.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/25-Friday/index.html">Friday, 25 July 2026</a></h3>
|
||||
<h3><a href="2026/07-July/24-Friday/index.html">Friday, 24 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,11 @@
|
|||
is already open in any panel, that panel's tab is focused. Otherwise, the next
|
||||
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.
|
||||
Right-clicking a file shows a context menu titled with the filename (truncated to
|
||||
20 chars with a leading <code>...</code> if longer). Files with registered
|
||||
alternative editors show an <em>Open ></em> submenu. Directories show
|
||||
<em>As Root Directory</em> to set a sub-root without double-clicking.
|
||||
Open-directory state is preserved across tree refreshes.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong>Tab container context menu:</strong>
|
||||
|
|
@ -206,6 +211,26 @@
|
|||
<code>static readonly type</code> string; use <code>implementsInterface(el, Def)</code>
|
||||
for runtime checks.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong>EditorConsole</strong> (<code>source/editor/EditorConsole.ts</code>) is a
|
||||
standalone singleton — separate from <code>Editor</code> — that holds a capped ring of
|
||||
500 <code>ConsoleMessage</code> objects (<code>text</code>, <code>type</code>:
|
||||
<code>'info'|'error'|'hint'</code>, <code>timestamp</code>) and dispatches them via
|
||||
<code>onMessage: EventSlot</code>. <code>editor-shell</code> bridges
|
||||
<code>Editor.onFileTypeUnknown</code> to <code>EditorConsole</code> and shows the
|
||||
latest message in a <code>.es-info</code> header element (opacity 0→1, auto-hides after
|
||||
5 s; portrait: fixed bottom bar). The <code>console-panel</code> tab renders the full
|
||||
message log using custom elements (<code>conp-header</code>, <code>conp-list</code>,
|
||||
<code>conp-entry</code>, <code>conp-time</code>, <code>conp-text</code>) and is
|
||||
available from the tab container <em>Add ></em> menu.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<strong><code>openDocumentIn</code></strong> derives <code>editorTag</code> from
|
||||
<code>panelElement.tagName.toLowerCase()</code> — not from
|
||||
<code>FileEditorRegistry</code>. The caller already chose the target panel; the
|
||||
dispatch must honour that choice so alternative editors (e.g. code-panel opening
|
||||
a .page file) receive and display the document correctly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -293,9 +318,10 @@
|
|||
via a CSS media query.</li>
|
||||
</ul>
|
||||
|
||||
<h4 style="margin-top:1rem">Sidebar modes</h4>
|
||||
<h4 style="margin-top:1rem">Toolbar modes</h4>
|
||||
<p>
|
||||
Two icon buttons on the left edge of the panel switch between modes:
|
||||
Two icon buttons in <code>.pep-toolbar</code> (pushed to the right by a
|
||||
<code>.pep-toolbar-sep</code> spacer) switch between modes:
|
||||
</p>
|
||||
<ul style="line-height:1.9;margin-top:0.5rem">
|
||||
<li><strong>Blocks mode</strong> — a horizontal scrollable list of block
|
||||
|
|
|
|||
Loading…
Reference in New Issue