2026-07-06 17:28:59 +00:00
|
|
|
import { Editor } from '../../editor/Editor.js';
|
|
|
|
|
|
|
|
|
|
// ── Layout helpers ────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-25 20:50:14 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
let tcCounter = 0;
|
|
|
|
|
function nextTcId(): string { return `tc-${++tcCounter}`; }
|
|
|
|
|
|
|
|
|
|
function makeResizeHandle(direction: 'v' | 'h'): HTMLElement {
|
|
|
|
|
const h = document.createElement('div');
|
|
|
|
|
h.className = direction === 'v' ? 'es-v-handle' : 'es-h-handle';
|
|
|
|
|
h.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
h.setPointerCapture(e.pointerId);
|
|
|
|
|
const prev = h.previousElementSibling as HTMLElement | null;
|
|
|
|
|
const next = h.nextElementSibling as HTMLElement | null;
|
|
|
|
|
if (!prev || !next) return;
|
|
|
|
|
const startPos = direction === 'v' ? e.clientX : e.clientY;
|
|
|
|
|
const startPrev = direction === 'v' ? prev.offsetWidth : prev.offsetHeight;
|
|
|
|
|
const startNext = direction === 'v' ? next.offsetWidth : next.offsetHeight;
|
|
|
|
|
const total = startPrev + startNext;
|
|
|
|
|
|
|
|
|
|
const onMove = (ev: PointerEvent) => {
|
|
|
|
|
const delta = (direction === 'v' ? ev.clientX : ev.clientY) - startPos;
|
|
|
|
|
const newPrev = Math.max(60, Math.min(total - 60, startPrev + delta));
|
|
|
|
|
prev.style.flexBasis = `${newPrev}px`;
|
|
|
|
|
next.style.flexBasis = `${total - newPrev}px`;
|
|
|
|
|
prev.style.flex = `0 0 ${newPrev}px`;
|
|
|
|
|
next.style.flex = `0 0 ${total - newPrev}px`;
|
|
|
|
|
};
|
|
|
|
|
h.addEventListener('pointermove', onMove);
|
|
|
|
|
h.addEventListener('pointerup', () => h.removeEventListener('pointermove', onMove), { once: true });
|
|
|
|
|
});
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeSection(): HTMLElement {
|
|
|
|
|
const sec = document.createElement('div');
|
|
|
|
|
sec.className = 'es-section';
|
|
|
|
|
const tc = document.createElement('tab-container') as HTMLElement;
|
|
|
|
|
tc.id = nextTcId();
|
|
|
|
|
sec.appendChild(tc);
|
|
|
|
|
return sec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makePanelInner(): HTMLElement {
|
|
|
|
|
const inner = document.createElement('div');
|
|
|
|
|
inner.className = 'es-sections';
|
|
|
|
|
const sec = makeSection();
|
|
|
|
|
inner.appendChild(sec);
|
|
|
|
|
return inner;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── EditorShell ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class EditorShell extends HTMLElement {
|
|
|
|
|
private activePortraitPanel: string = 'center';
|
|
|
|
|
private _deviceId: string = '';
|
|
|
|
|
private _saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
|
|
async connectedCallback(): Promise<void> {
|
Editor: auth redirect, targeted file open, Godot file types
- editor-shell: redirect to / if not authenticated on load
- file-tree: open file in first clean matching panel, create new panel if all dirty
- Editor: add openDocumentIn() with targetElement for precise panel targeting
- panels: skip onDocumentOpened if targeted at a different panel
- FileEditorRegistry: add Godot extensions (gd, gdshader, gdshaderinc, gdinclude, glsl, tscn, tres, cs, gdextension)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 20:22:42 +00:00
|
|
|
const authRes = await fetch( '/api/auth/me' );
|
|
|
|
|
if ( !authRes.ok ) { location.href = '/'; return; }
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
const params = new URLSearchParams(location.search);
|
|
|
|
|
const projectId = params.get('project') ?? '';
|
2026-07-30 20:02:14 +00:00
|
|
|
const localRoot = params.get('localRoot') ?? '';
|
|
|
|
|
const remoteProject = params.get('remoteProject') ?? '';
|
2026-07-06 17:28:59 +00:00
|
|
|
const projectName = params.get('name') ?? 'Project';
|
|
|
|
|
Editor.get().projectId = projectId;
|
2026-07-30 20:02:14 +00:00
|
|
|
Editor.get().localRoot = localRoot;
|
|
|
|
|
Editor.get().remoteProject = remoteProject;
|
2026-07-06 17:28:59 +00:00
|
|
|
Editor.get().projectName = projectName;
|
|
|
|
|
|
|
|
|
|
this.innerHTML = `
|
|
|
|
|
<div class="es-header">
|
2026-07-14 20:27:03 +00:00
|
|
|
<a class="es-back" href="/">←</a>
|
2026-07-06 17:28:59 +00:00
|
|
|
<span class="es-title">${projectName}</span>
|
|
|
|
|
<div class="es-portrait-btns">
|
|
|
|
|
<button class="es-pb-btn" data-panel="left">⊟</button>
|
|
|
|
|
<button class="es-pb-btn active" data-panel="center">⊡</button>
|
|
|
|
|
<button class="es-pb-btn" data-panel="right">⊞</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="es-workspace">
|
|
|
|
|
<div class="es-panel" data-panel="left">${makePanelInner().outerHTML}</div>
|
|
|
|
|
<div class="es-v-handle"></div>
|
|
|
|
|
<div class="es-panel" data-panel="center">${makePanelInner().outerHTML}</div>
|
|
|
|
|
<div class="es-v-handle"></div>
|
|
|
|
|
<div class="es-panel" data-panel="right">${makePanelInner().outerHTML}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
this.setupMainHandles();
|
|
|
|
|
this.setupPortrait();
|
|
|
|
|
this.setupSplitListener();
|
|
|
|
|
this.setupResizeHandler();
|
|
|
|
|
|
|
|
|
|
await Promise.all( [
|
|
|
|
|
customElements.whenDefined( 'tab-container' ),
|
|
|
|
|
customElements.whenDefined( 'file-tree-panel' ),
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
customElements.whenDefined( 'page-editor-panel' ),
|
2026-07-12 11:05:53 +00:00
|
|
|
customElements.whenDefined( 'code-panel' ),
|
2026-07-06 17:28:59 +00:00
|
|
|
this._loadLayout(),
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
this.initDefaultLayout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initDefaultLayout(): void {
|
|
|
|
|
const leftTc = this.querySelector('[data-panel="left"] tab-container') as any;
|
|
|
|
|
const centerTc = this.querySelector('[data-panel="center"] tab-container') as any;
|
|
|
|
|
|
|
|
|
|
leftTc?.addTab({ id: 'file-tree', label: 'Files', panelType: 'file-tree' }, () => {
|
|
|
|
|
return document.createElement('file-tree-panel');
|
|
|
|
|
});
|
|
|
|
|
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
centerTc?.addTab({ id: 'page-editor', label: 'Page', panelType: 'page-editor' }, () => {
|
|
|
|
|
return document.createElement('page-editor-panel');
|
2026-07-06 17:28:59 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupMainHandles(): void {
|
|
|
|
|
const workspace = this.querySelector('.es-workspace')!;
|
|
|
|
|
workspace.querySelectorAll(':scope > .es-v-handle').forEach(h => {
|
|
|
|
|
const handle = h as HTMLElement;
|
|
|
|
|
handle.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handle.setPointerCapture(e.pointerId);
|
|
|
|
|
const prev = handle.previousElementSibling as HTMLElement;
|
|
|
|
|
const next = handle.nextElementSibling as HTMLElement;
|
|
|
|
|
const start = e.clientX;
|
|
|
|
|
const startPrev = prev.offsetWidth;
|
|
|
|
|
const startNext = next.offsetWidth;
|
|
|
|
|
const total = startPrev + startNext;
|
|
|
|
|
|
|
|
|
|
const onMove = (ev: PointerEvent) => {
|
|
|
|
|
const delta = ev.clientX - start;
|
|
|
|
|
const np = Math.max(80, Math.min(total - 80, startPrev + delta));
|
|
|
|
|
prev.style.flex = `0 0 ${np}px`;
|
|
|
|
|
next.style.flex = `0 0 ${total - np}px`;
|
|
|
|
|
};
|
|
|
|
|
handle.addEventListener('pointermove', onMove);
|
|
|
|
|
handle.addEventListener('pointerup', () => {
|
|
|
|
|
handle.removeEventListener('pointermove', onMove);
|
|
|
|
|
this._scheduleLayoutSave();
|
|
|
|
|
}, { once: true });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.querySelectorAll('.es-sections').forEach(sections => {
|
|
|
|
|
this.observeNewHandles(sections as HTMLElement);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupSplitListener(): void {
|
|
|
|
|
this.addEventListener('tab-container:split', (e: Event) => {
|
2026-07-25 20:50:14 +00:00
|
|
|
const { containerId, direction } = (e as CustomEvent).detail as { containerId: string; direction: 'horizontal' | 'vertical' };
|
2026-07-06 17:28:59 +00:00
|
|
|
const tc = document.getElementById(containerId);
|
|
|
|
|
if (!tc) return;
|
|
|
|
|
const section = tc.closest('.es-section') as HTMLElement | null;
|
2026-07-25 20:50:14 +00:00
|
|
|
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();
|
|
|
|
|
}
|
2026-07-06 17:28:59 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.addEventListener('tab-container:add-panel', (e: Event) => {
|
|
|
|
|
const { containerId, panelType, tag, label } = (e as CustomEvent).detail as { containerId: string; panelType: string; tag: string; label: string };
|
|
|
|
|
const tc = document.getElementById(containerId) as any;
|
|
|
|
|
if (!tc) return;
|
|
|
|
|
const id = panelType + '-' + Math.random().toString(36).slice(2);
|
|
|
|
|
tc.addTab({ id, label: label ?? panelType, panelType }, () => document.createElement(tag));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _getDeviceId(): string
|
|
|
|
|
{
|
|
|
|
|
if ( this._deviceId ) return this._deviceId;
|
|
|
|
|
let id = localStorage.getItem( 'roject:deviceId' );
|
|
|
|
|
if ( !id )
|
|
|
|
|
{
|
|
|
|
|
id = Math.random().toString( 36 ).slice( 2 ) + Math.random().toString( 36 ).slice( 2 );
|
|
|
|
|
localStorage.setItem( 'roject:deviceId', id );
|
|
|
|
|
}
|
|
|
|
|
this._deviceId = id;
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _loadLayout(): Promise<void>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
const res = await fetch( `/api/layout?deviceId=${this._getDeviceId()}` );
|
|
|
|
|
if ( !res.ok ) return;
|
|
|
|
|
const layout = await res.json();
|
|
|
|
|
if ( !layout ) return;
|
|
|
|
|
|
|
|
|
|
if ( layout.panels )
|
|
|
|
|
{
|
|
|
|
|
const workspace = this.querySelector( '.es-workspace' ) as HTMLElement;
|
|
|
|
|
for ( const [ panel, flex ] of Object.entries( layout.panels ) )
|
|
|
|
|
{
|
|
|
|
|
if ( flex )
|
|
|
|
|
{
|
|
|
|
|
const el = workspace.querySelector( `[data-panel="${panel}"]` ) as HTMLElement;
|
|
|
|
|
if ( el ) el.style.flex = flex as string;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( layout.activePortraitPanel )
|
|
|
|
|
{
|
|
|
|
|
this.activePortraitPanel = layout.activePortraitPanel;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _scheduleLayoutSave(): void
|
|
|
|
|
{
|
|
|
|
|
if ( this._saveTimer ) clearTimeout( this._saveTimer );
|
|
|
|
|
this._saveTimer = setTimeout( () => this._saveLayout(), 800 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _saveLayout(): Promise<void>
|
|
|
|
|
{
|
|
|
|
|
const workspace = this.querySelector( '.es-workspace' ) as HTMLElement;
|
|
|
|
|
const panels: Record<string, string | null> = {};
|
|
|
|
|
for ( const p of [ 'left', 'center', 'right' ] )
|
|
|
|
|
{
|
|
|
|
|
const el = workspace.querySelector( `[data-panel="${p}"]` ) as HTMLElement;
|
|
|
|
|
panels[ p ] = el?.style.flex || null;
|
|
|
|
|
}
|
|
|
|
|
const layout = { panels, activePortraitPanel: this.activePortraitPanel };
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await fetch( `/api/layout?deviceId=${this._getDeviceId()}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify( layout ),
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
catch {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupResizeHandler(): void
|
|
|
|
|
{
|
|
|
|
|
const workspace = this.querySelector( '.es-workspace' ) as HTMLElement;
|
2026-07-25 20:50:14 +00:00
|
|
|
|
|
|
|
|
const workspaceObs = new ResizeObserver( () =>
|
2026-07-06 17:28:59 +00:00
|
|
|
{
|
|
|
|
|
this._redistributeFlex( workspace, '.es-panel' );
|
2026-07-25 20:50:14 +00:00
|
|
|
} );
|
|
|
|
|
workspaceObs.observe( workspace );
|
|
|
|
|
|
|
|
|
|
const sectionsObs = new ResizeObserver( ( entries ) =>
|
|
|
|
|
{
|
|
|
|
|
for ( const entry of entries )
|
2026-07-06 17:28:59 +00:00
|
|
|
{
|
2026-07-25 20:50:14 +00:00
|
|
|
this._redistributeFlex( entry.target as HTMLElement, '.es-section' );
|
|
|
|
|
}
|
2026-07-06 17:28:59 +00:00
|
|
|
} );
|
2026-07-25 20:50:14 +00:00
|
|
|
workspace.querySelectorAll( '.es-sections' ).forEach( s => sectionsObs.observe( s ) );
|
2026-07-06 17:28:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _redistributeFlex( container: HTMLElement, childSelector: string ): void
|
|
|
|
|
{
|
|
|
|
|
const children = Array.from( container.querySelectorAll( `:scope > ${childSelector}` ) ) as HTMLElement[];
|
|
|
|
|
if ( children.length < 2 ) return;
|
|
|
|
|
if ( ! children.some( c => c.style.flex ) ) return;
|
|
|
|
|
|
|
|
|
|
const totalPanel = children.reduce( ( sum, c ) => sum + c.offsetWidth, 0 );
|
|
|
|
|
if ( totalPanel === 0 ) return;
|
|
|
|
|
|
|
|
|
|
const handles = Array.from( container.children ).filter(
|
|
|
|
|
c => ! ( c as HTMLElement ).matches( childSelector )
|
|
|
|
|
) as HTMLElement[];
|
|
|
|
|
const handleTotal = handles.reduce( ( sum, h ) => sum + ( h as HTMLElement ).offsetWidth, 0 );
|
|
|
|
|
const available = container.clientWidth - handleTotal;
|
|
|
|
|
|
|
|
|
|
children.forEach( c =>
|
|
|
|
|
{
|
|
|
|
|
const ratio = c.offsetWidth / totalPanel;
|
|
|
|
|
c.style.flex = `0 0 ${Math.round( ratio * available )}px`;
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private observeNewHandles(sections: HTMLElement): void {
|
|
|
|
|
const observer = new MutationObserver(() => {
|
|
|
|
|
sections.querySelectorAll('.es-h-handle:not([data-bound])').forEach(h => {
|
|
|
|
|
(h as HTMLElement).dataset.bound = '1';
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
observer.observe(sections, { childList: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupPortrait(): void {
|
|
|
|
|
const btns = this.querySelector( '.es-portrait-btns' ) as HTMLElement;
|
|
|
|
|
const mq = window.matchMedia( '(orientation: portrait)' );
|
|
|
|
|
|
|
|
|
|
const apply = ( portrait: boolean ) => {
|
|
|
|
|
this.classList.toggle( 'portrait', portrait );
|
2026-07-25 20:50:14 +00:00
|
|
|
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 = ''; } );
|
|
|
|
|
}
|
2026-07-06 17:28:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
btns.querySelectorAll( '.es-pb-btn' ).forEach( btn => {
|
|
|
|
|
btn.addEventListener( 'click', () => {
|
|
|
|
|
const panel = ( btn as HTMLElement ).dataset.panel!;
|
|
|
|
|
this.activePortraitPanel = panel;
|
|
|
|
|
btns.querySelectorAll( '.es-pb-btn' ).forEach( b => b.classList.remove( 'active' ) );
|
|
|
|
|
btn.classList.add( 'active' );
|
|
|
|
|
this.showPortraitPanel( panel );
|
|
|
|
|
this._scheduleLayoutSave();
|
|
|
|
|
} );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
mq.addEventListener( 'change', e => apply( e.matches ) );
|
|
|
|
|
apply( mq.matches );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private showPortraitPanel(panelId: string): void {
|
|
|
|
|
this.querySelectorAll('.es-panel').forEach(p => {
|
|
|
|
|
(p as HTMLElement).style.display = (p as HTMLElement).dataset.panel === panelId ? '' : 'none';
|
|
|
|
|
});
|
|
|
|
|
this.querySelectorAll('.es-v-handle').forEach(h => { (h as HTMLElement).style.display = 'none'; });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('editor-shell', EditorShell);
|