import { css, html, LitElement } from "lit"; import { property, state } from "lit/decorators.js"; const DEFAULT_SIDEBAR_WIDTH = 320; const MIN_SIDEBAR_WIDTH = 200; const MAX_SIDEBAR_WIDTH = 600; export class XOmniLayout extends LitElement { static override styles = css` :host { display: grid; grid-template-rows: 1fr; height: 100%; width: 100%; overflow: hidden; transition: grid-template-columns 0.3s ease; position: relative; } /* Desktop: sidebar displaces content */ @media (min-width: 769px) { :host { grid-template-columns: 1fr; } :host([sidebar-open]) { grid-template-columns: 1fr var(--sidebar-width, 320px); } } /* Mobile: sidebar overlays content */ @media (max-width: 768px) { :host { grid-template-columns: 1fr; } } .main { grid-column: 1; grid-row: 1; position: relative; overflow: auto; } .sidebar-container { position: relative; overflow: hidden; display: none; } /* Desktop: grid positioning */ @media (min-width: 769px) { .sidebar-container.visible { display: block; grid-column: 2; grid-row: 1; } } /* Mobile: overlay positioning within content area */ @media (max-width: 768px) { .sidebar-container.visible { display: block; position: absolute; top: 0; right: 0; bottom: 0; width: 320px; z-index: 100; box-shadow: -4px 0 8px rgba(0, 0, 0, 0.1); } } .sidebar { height: 100%; width: 100%; background-color: white; border-left: var(--border-width, 2px) solid var(--border-color, #000); overflow: auto; display: flex; flex-direction: column; } .resize-handle { position: absolute; left: 0; top: 0; bottom: 0; width: 8px; cursor: col-resize; background: transparent; z-index: 10; user-select: none; touch-action: none; } .resize-handle:hover, .resize-handle.dragging { background: var(--border-color, #000); opacity: 0.2; } /* Hide resize handle on mobile */ @media (max-width: 768px) { .resize-handle { display: none; } } .sidebar-content { flex: 1; min-height: 0; padding: 1rem; box-sizing: border-box; } .fab { position: fixed; bottom: 24px; right: 24px; z-index: 1000; } `; @property({ type: Boolean }) sidebarOpen = false; @state() private hasSidebarContent = false; @state() private sidebarWidth = DEFAULT_SIDEBAR_WIDTH; @state() private isResizing = false; private resizeStartX = 0; private resizeStartWidth = 0; override firstUpdated() { const slot = this.shadowRoot?.querySelector( 'slot[name="sidebar"]', ) as HTMLSlotElement | null; if (slot) { this.#updateSidebarContent(slot); slot.addEventListener("slotchange", () => { this.#updateSidebarContent(slot); }); } } override updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has("sidebarWidth")) { this.style.setProperty("--sidebar-width", `${this.sidebarWidth}px`); } // Only show sidebar grid column when both open AND has content if ( changedProperties.has("sidebarOpen") || changedProperties.has("hasSidebarContent") ) { const shouldExpand = this.sidebarOpen && this.hasSidebarContent; if (shouldExpand) { this.setAttribute("sidebar-open", ""); } else { this.removeAttribute("sidebar-open"); } } } #updateSidebarContent(slot: HTMLSlotElement) { const nodes = slot.assignedNodes({ flatten: true }); const hasContent = nodes.some((node) => { if (node.nodeType === Node.ELEMENT_NODE) return true; if (node.nodeType === Node.TEXT_NODE) { return (node.textContent ?? "").trim().length > 0; } return false; }); this.hasSidebarContent = hasContent; } private handleResizeStart = (e: PointerEvent) => { e.preventDefault(); this.isResizing = true; this.resizeStartX = e.clientX; this.resizeStartWidth = this.sidebarWidth; const handle = e.target as HTMLElement; handle.setPointerCapture(e.pointerId); handle.classList.add("dragging"); document.addEventListener("pointermove", this.handleResizeMove); document.addEventListener("pointerup", this.handleResizeEnd); }; private handleResizeMove = (e: PointerEvent) => { if (!this.isResizing) return; const delta = this.resizeStartX - e.clientX; const newWidth = Math.min( MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, this.resizeStartWidth + delta), ); this.sidebarWidth = newWidth; }; private handleResizeEnd = (_e: PointerEvent) => { if (!this.isResizing) return; this.isResizing = false; const handle = this.shadowRoot?.querySelector( ".resize-handle", ) as HTMLElement; if (handle) { handle.classList.remove("dragging"); } document.removeEventListener("pointermove", this.handleResizeMove); document.removeEventListener("pointerup", this.handleResizeEnd); }; override render() { const showSidebar = this.hasSidebarContent && this.sidebarOpen; return html`
`; } } globalThis.customElements.define("x-omni-layout", XOmniLayout);