diff --git a/source/components/console-panel/console-panel.css b/source/components/console-panel/console-panel.css new file mode 100644 index 0000000..f707304 --- /dev/null +++ b/source/components/console-panel/console-panel.css @@ -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; } diff --git a/source/components/console-panel/console-panel.ts b/source/components/console-panel/console-panel.ts new file mode 100644 index 0000000..2614dc1 --- /dev/null +++ b/source/components/console-panel/console-panel.ts @@ -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 = ` + + Console + + + + `; + + 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 = `${ hh }:${ mm }:${ ss }`; + ( 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 ); diff --git a/source/components/editor-shell/editor-shell.css b/source/components/editor-shell/editor-shell.css index cedba32..ea2a4f2 100644 --- a/source/components/editor-shell/editor-shell.css +++ b/source/components/editor-shell/editor-shell.css @@ -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; diff --git a/source/components/editor-shell/editor-shell.ts b/source/components/editor-shell/editor-shell.ts index 3f544be..c105b7c 100644 --- a/source/components/editor-shell/editor-shell.ts +++ b/source/components/editor-shell/editor-shell.ts @@ -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 {
${projectName} +
@@ -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 | 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)' ); diff --git a/source/components/file-tree-panel/file-tree-panel.css b/source/components/file-tree-panel/file-tree-panel.css index 21a82ac..8769bda 100644 --- a/source/components/file-tree-panel/file-tree-panel.css +++ b/source/components/file-tree-panel/file-tree-panel.css @@ -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 { diff --git a/source/components/file-tree-panel/file-tree-panel.ts b/source/components/file-tree-panel/file-tree-panel.ts index b66cd2e..86f0d4c 100644 --- a/source/components/file-tree-panel/file-tree-panel.ts +++ b/source/components/file-tree-panel/file-tree-panel.ts @@ -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(); + 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 ) : '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 = - { - '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 = + { + '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 = + { + 'page-editor-panel': [ 'code-panel' ], + 'rojo-settings-panel': [ 'rojo-chat-panel' ], + }; + + async _openFileDefault( path: string ): Promise + { + 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 + { + 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 { 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('') + ''; } - _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); diff --git a/source/components/page-editor-panel/page-editor-panel.css b/source/components/page-editor-panel/page-editor-panel.css index bfe05d8..7652935 100644 --- a/source/components/page-editor-panel/page-editor-panel.css +++ b/source/components/page-editor-panel/page-editor-panel.css @@ -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 ─────────────────────────────────────────────────────────────── */ diff --git a/source/components/page-editor-panel/page-editor-panel.ts b/source/components/page-editor-panel/page-editor-panel.ts index eda5490..b7b4ab0 100644 --- a/source/components/page-editor-panel/page-editor-panel.ts +++ b/source/components/page-editor-panel/page-editor-panel.ts @@ -211,39 +211,36 @@ class PageEditorPanel extends HTMLElement this.className = 'page-editor-panel'; this.innerHTML = ` -
+
+ + + + +
-
-
- - - - +
+
+ ${ PAGE_BLOCK_REGISTRY.map( b => ` +
+
${ b.preview ?? `${ b.name }` }
+
${ b.name }
+
+ ` ).join( '' ) }
-
-
- ${ PAGE_BLOCK_REGISTRY.map( b => ` -
-
${ b.preview ?? `${ b.name }` }
-
${ b.name }
-
- ` ).join( '' ) } -
-
- -
Open a .page file from the file tree
-
+ +
Open a .page file from the file tree
+ `; this._iframe = this.querySelector( 'iframe' ); diff --git a/source/components/tab-container/tab-container.ts b/source/components/tab-container/tab-container.ts index 8adcb5d..7b43ba0 100644 --- a/source/components/tab-container/tab-container.ts +++ b/source/components/tab-container/tab-container.ts @@ -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' }, ]; diff --git a/source/editor/Editor.ts b/source/editor/Editor.ts index c055771..bc5a625 100644 --- a/source/editor/Editor.ts +++ b/source/editor/Editor.ts @@ -83,14 +83,7 @@ export class Editor async openDocumentIn( filePath: string, panelElement: HTMLElement ): Promise { - 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 ) ) { diff --git a/source/editor/EditorConsole.ts b/source/editor/EditorConsole.ts new file mode 100644 index 0000000..34e96eb --- /dev/null +++ b/source/editor/EditorConsole.ts @@ -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 = new EventSlot(); + readonly onHover: EventSlot = 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 ); + } +} diff --git a/source/icons/directory.svg b/source/icons/directory.svg index c81553a..d3878de 100644 --- a/source/icons/directory.svg +++ b/source/icons/directory.svg @@ -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"> + 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" /> diff --git a/source/pages/editor.html b/source/pages/editor.html index 1461e2d..587ad4a 100644 --- a/source/pages/editor.html +++ b/source/pages/editor.html @@ -14,6 +14,7 @@ + + + + + + + +

INFO/CONSOLE Update

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.
+
+ +

UI

- The header gets an info element, right aligned ( or on portrait an overlay)
- There will be a new panel, named console logs
+
+

Info Element Functionality

- 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)
- Display info on hover for other elements

+
+

Console Logs Functionality

- Show the last 500 console messages of the editor

+
+ + + + \ No newline at end of file diff --git a/workspace/history/2026/07-July/31-Friday/small-updates.page b/workspace/history/2026/07-July/31-Friday/small-updates.page new file mode 100644 index 0000000..5088b2e --- /dev/null +++ b/workspace/history/2026/07-July/31-Friday/small-updates.page @@ -0,0 +1,65 @@ + + + + New Page + + + + + + + +

Small Updates

Several updates for improving the UX
+
+ +

File Tree Panel

Directory focusing

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.

Creating File/Direction Structure Reload 

When a file or directory is created it closes all directories, that's weird. It's maybe a refresh bug.

Files Open Context Menu  

Files need a menu entry to open them in other than the connected editor. For example a "index.page"  should have:
Open  > 
   Default (Page Editor)
   Code Editor

While a "mc-joe.rojo" should have:
Open > 
    Default (Rojo Settings)
    Rojo Chat

+
+

Page Editor

Toolbar Rearrangement

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.

+
+ + + + \ No newline at end of file diff --git a/workspace/history/index.html b/workspace/history/index.html index 9fa4e73..5453822 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -20,12 +20,17 @@

2026 — July

-

Wednesday, 30 July 2026

+

Friday, 31 July 2026

+

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.

+
+ +
+

Thursday, 30 July 2026

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.

-

Friday, 25 July 2026

+

Friday, 24 July 2026

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.

diff --git a/workspace/outline/index.html b/workspace/outline/index.html index af1a50f..7bc3c17 100644 --- a/workspace/outline/index.html +++ b/workspace/outline/index.html @@ -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 ... if longer). Files with registered + alternative editors show an Open > submenu. Directories show + As Root Directory to set a sub-root without double-clicking. + Open-directory state is preserved across tree refreshes.

Tab container context menu: @@ -206,6 +211,26 @@ static readonly type string; use implementsInterface(el, Def) for runtime checks.

+

+ EditorConsole (source/editor/EditorConsole.ts) is a + standalone singleton — separate from Editor — that holds a capped ring of + 500 ConsoleMessage objects (text, type: + 'info'|'error'|'hint', timestamp) and dispatches them via + onMessage: EventSlot. editor-shell bridges + Editor.onFileTypeUnknown to EditorConsole and shows the + latest message in a .es-info header element (opacity 0→1, auto-hides after + 5 s; portrait: fixed bottom bar). The console-panel tab renders the full + message log using custom elements (conp-header, conp-list, + conp-entry, conp-time, conp-text) and is + available from the tab container Add > menu. +

+

+ openDocumentIn derives editorTag from + panelElement.tagName.toLowerCase() — not from + FileEditorRegistry. 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. +

@@ -293,9 +318,10 @@ via a CSS media query. -

Sidebar modes

+

Toolbar modes

- Two icon buttons on the left edge of the panel switch between modes: + Two icon buttons in .pep-toolbar (pushed to the right by a + .pep-toolbar-sep spacer) switch between modes:

  • Blocks mode — a horizontal scrollable list of block