From 293be92a58da4894a180c2f8cb40853b4733b273 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Mon, 30 Mar 2026 17:45:18 +0200 Subject: [PATCH 01/13] wip --- nx2/styles/styles.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index 0fb4a7ba..e6850116 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -354,7 +354,6 @@ html:has(meta[content="edge-delivery"]) { main { grid-area: main; background-color: light-dark(#fff, #000); - overflow-y: scroll; max-height: 100%; } } @@ -431,7 +430,6 @@ html:has(meta[content="edge-delivery"]) { main { margin: 0 12px 0 0; border-radius: 32px 32px 0 0; - overflow-y: scroll; max-height: 100%; } } From f4426399b94919984f187046106dbb0d2bbd1f8b Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Mon, 30 Mar 2026 17:45:53 +0200 Subject: [PATCH 02/13] wip missing code --- nx2/blocks/canvas/canvas.css | 84 ++++++++++++++++++++++++++++++++++++ nx2/blocks/canvas/canvas.js | 74 +++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 nx2/blocks/canvas/canvas.css create mode 100644 nx2/blocks/canvas/canvas.js diff --git a/nx2/blocks/canvas/canvas.css b/nx2/blocks/canvas/canvas.css new file mode 100644 index 00000000..16acd30c --- /dev/null +++ b/nx2/blocks/canvas/canvas.css @@ -0,0 +1,84 @@ +:host { + display: block; + height: 100%; + color: var(--s2-gray-900); + + --canvas-chat-width: 360px; +} + +.canvas-shell { + display: grid; + grid-template-columns: minmax(0, 1fr); + padding: var(--s2-spacing-300); + height: calc(100vh - var(--s2-nav-height)); + box-sizing: border-box; + background: #f0f0f0; + overflow: hidden; +} + +.workspace-pane, +.chat-pane { + display: flex; + flex-direction: column; + min-height: 0; + background: var(--s2-gray-25); + border: 1px solid color-mix(in srgb, var(--s2-gray-200) 90%, transparent); + border-radius: 22px; + overflow: hidden; +} + +.panel-divider { + display: none; +} + +.chat-pane { + margin-bottom: 12px; +} + +.workspace-pane { + border-radius: 22px 22px 0 0; + max-height: 100%; +} + +.workspace-pane-body, +.chat-pane-body { + min-height: 0; + overflow: auto; +} + +.workspace-surface, +.chat-surface { + min-height: calc(100% + 1px); +} + +.workspace-surface { + min-height: 200vh; +} + +@media (width >= 900px) { + .canvas-shell { + grid-template-columns: minmax(0, 1fr) 10px minmax(320px, var(--canvas-chat-width)); + align-items: stretch; + padding: 0; + } + + .panel-divider { + display: flex; + align-items: center; + justify-content: center; + border-radius: 999px; + cursor: ew-resize; + touch-action: none; + } + + .panel-divider-handle { + width: 6px; + height: 52px; + border-radius: 999px; + background: var(--s2-gray-500); + } +} + +:host(.is-resizing) { + user-select: none; +} diff --git a/nx2/blocks/canvas/canvas.js b/nx2/blocks/canvas/canvas.js new file mode 100644 index 00000000..99850f96 --- /dev/null +++ b/nx2/blocks/canvas/canvas.js @@ -0,0 +1,74 @@ +import { LitElement, html } from 'lit'; +import { loadStyle } from '../../utils/utils.js'; + +const style = await loadStyle(import.meta.url); + +class NxCanvas extends LitElement { + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [style]; + } + + handleResizeStart(event) { + if (window.innerWidth < 900) return; + this._dragging = true; + event.currentTarget.setPointerCapture(event.pointerId); + this.classList.add('is-resizing'); + } + + handleResizeMove(event) { + if (!this._dragging) return; + + const shell = this.shadowRoot.querySelector('.canvas-shell'); + const { left, right } = shell.getBoundingClientRect(); + const width = Math.round(right - event.clientX); + const minWidth = 320; + const maxWidth = Math.min(560, Math.round((right - left) * 0.45)); + const nextWidth = Math.max(minWidth, Math.min(maxWidth, width)); + + this.style.setProperty('--canvas-chat-width', `${nextWidth}px`); + } + + handleResizeEnd(event) { + if (!this._dragging) return; + this._dragging = false; + event.currentTarget.releasePointerCapture(event.pointerId); + this.classList.remove('is-resizing'); + } + + render() { + return html` +
+
+
+ +
+
+ + + + +
+ `; + } +} + +customElements.define('nx-canvas', NxCanvas); + +export default function init(block) { + const canvas = document.createElement('nx-canvas'); + block.replaceChildren(canvas); +} From 012d000b88aa67d71a21f48cbc32f8906a56fd8a Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Wed, 1 Apr 2026 21:46:55 +0200 Subject: [PATCH 03/13] wip panels --- nx2/scripts/panels.js | 196 +++++++++++++++++++++++++++++++++++++++++ nx2/scripts/scripts.js | 16 ++++ nx2/styles/styles.css | 66 +++++++++++++- 3 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 nx2/scripts/panels.js diff --git a/nx2/scripts/panels.js b/nx2/scripts/panels.js new file mode 100644 index 00000000..6cda74b9 --- /dev/null +++ b/nx2/scripts/panels.js @@ -0,0 +1,196 @@ +function getPanels(el = document.body) { + const beforeMain = []; + const afterMain = []; + + let pastMain = false; + el.querySelectorAll('aside.panel, main').forEach((panel) => { + if (panel.tagName === 'MAIN') { + pastMain = true; + return; + } + + if (pastMain) { + afterMain.push(panel); + } else { + beforeMain.push(panel); + } + }); + + return { beforeMain, afterMain }; +} + +function panelGridName(prefix, index) { + return `${prefix}${index}`; +} + +function panelColumnTrack(panel) { + const w = panel.dataset.width?.trim(); + return w || 'minmax(0, auto)'; +} + +const PANEL_WIDTH_MIN = 120; +const PANEL_WIDTH_MAX = 1600; + +function clearPanelResizeHandles(root) { + root.querySelectorAll('aside.panel .panel-resize-handle').forEach((h) => h.remove()); +} + +function parsePanelWidthPx(panel) { + const w = panel.dataset.width?.trim(); + if (w && /^\d+(\.\d+)?px$/i.test(w)) { + return parseFloat(w); + } + return panel.getBoundingClientRect().width; +} + +function clampPanelWidth(px) { + return Math.max(PANEL_WIDTH_MIN, Math.min(PANEL_WIDTH_MAX, Math.round(px))); +} + +function applyPanelWidthPx(panel, px) { + panel.dataset.width = `${clampPanelWidth(px)}px`; +} + +/** Updates grid template and panel grid areas only (no resize handles). */ +function refreshAppFrameGrid() { + const { body } = document; + if (!body.classList.contains('app-frame')) return; + + const { beforeMain, afterMain } = getPanels(body); + + if (!beforeMain.length && !afterMain.length) { + body.style.removeProperty('--app-frame-areas'); + body.style.removeProperty('--app-frame-columns'); + body.querySelectorAll(':scope > aside.panel').forEach((el) => { + el.style.removeProperty('grid-area'); + }); + return; + } + + const colCount = 1 + beforeMain.length + 1 + afterMain.length; + const headerRow = Array(colCount).fill('header').join(' '); + const contentRow = [ + 'sidenav', + ...beforeMain.map((_, i) => panelGridName('nx-pb-', i)), + 'main', + ...afterMain.map((_, i) => panelGridName('nx-pa-', i)), + ].join(' '); + + beforeMain.forEach((el, i) => { + el.style.gridArea = panelGridName('nx-pb-', i); + }); + afterMain.forEach((el, i) => { + el.style.gridArea = panelGridName('nx-pa-', i); + }); + + const areas = `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`; + const columns = [ + 'var(--s2-nav-width)', + ...beforeMain.map((el) => panelColumnTrack(el)), + '1fr', + ...afterMain.map((el) => panelColumnTrack(el)), + ].join(' '); + + body.style.setProperty('--app-frame-areas', areas); + body.style.setProperty('--app-frame-columns', columns); +} + +function bindPanelResize(panel, { deltaSign }) { + const onPointerDown = (downEvent) => { + if (downEvent.button !== 0) return; + const handle = downEvent.currentTarget; + handle.setPointerCapture(downEvent.pointerId); + const startX = downEvent.clientX; + const startW = parsePanelWidthPx(panel); + const prevUserSelect = document.body.style.userSelect; + document.body.style.userSelect = 'none'; + + const onPointerMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + applyPanelWidthPx(panel, startW + deltaSign * dx); + refreshAppFrameGrid(); + }; + + const onPointerUp = (upEvent) => { + handle.releasePointerCapture(upEvent.pointerId); + document.body.style.userSelect = prevUserSelect; + handle.removeEventListener('pointermove', onPointerMove); + handle.removeEventListener('pointerup', onPointerUp); + handle.removeEventListener('pointercancel', onPointerUp); + }; + + handle.addEventListener('pointermove', onPointerMove); + handle.addEventListener('pointerup', onPointerUp); + handle.addEventListener('pointercancel', onPointerUp); + }; + + return onPointerDown; +} + +function syncPanelResizeHandles(beforeMain, afterMain) { + beforeMain.forEach((panel) => { + const trail = document.createElement('button'); + trail.type = 'button'; + trail.className = 'panel-resize-handle panel-resize-handle-trailing'; + trail.setAttribute('aria-label', 'Resize panel'); + trail.addEventListener('pointerdown', bindPanelResize(panel, { deltaSign: 1 })); + panel.append(trail); + }); + + afterMain.forEach((panel) => { + const lead = document.createElement('button'); + lead.type = 'button'; + lead.className = 'panel-resize-handle panel-resize-handle-leading'; + lead.setAttribute('aria-label', 'Resize panel'); + lead.addEventListener('pointerdown', bindPanelResize(panel, { deltaSign: -1 })); + panel.append(lead); + }); +} + +export function setPanelsGrid() { + const { body } = document; + clearPanelResizeHandles(body); + refreshAppFrameGrid(); + + if (!body.classList.contains('app-frame')) return; + + const { beforeMain, afterMain } = getPanels(body); + if (beforeMain.length || afterMain.length) { + syncPanelResizeHandles(beforeMain, afterMain); + } +} + +export function showPanel(name, { width = '200px', beforeMain = false } = {}) { + const existing = document.querySelector(`aside.panel.${name}`); + if (existing) { + existing.style.display = 'block'; + existing.dataset.width = width; + setPanelsGrid(); + return existing; + } + + const panel = document.createElement('aside'); + panel.classList.add('panel', name); + panel.dataset.width = width; + + const main = document.querySelector('main'); + + if (beforeMain) { + main.before(panel); + } else { + main.after(panel); + } + + panel.innerHTML = `

${name}

`; + setPanelsGrid(); + + return panel; +} + +export function closePanel(name) { + const panel = document.querySelector(`aside.panel.${name}`); + if (panel) { + panel.style.display = 'none'; + setPanelsGrid(); + } +} diff --git a/nx2/scripts/scripts.js b/nx2/scripts/scripts.js index 6a2dfdcd..98a19a4b 100644 --- a/nx2/scripts/scripts.js +++ b/nx2/scripts/scripts.js @@ -11,6 +11,7 @@ */ import { loadArea, setConfig } from './nx.js'; +import { closePanel, showPanel } from './panels.js'; const hostnames = ['nx.live']; @@ -51,5 +52,20 @@ const conf = { export async function loadPage() { await setConfig(conf); await loadArea(); + + showPanel('test', { width: '300px' }); + showPanel('test2', { width: '200px', beforeMain: true }); + + const button = document.createElement('button'); + button.innerText = 'Toggle Panel'; + button.addEventListener('click', () => { + const panel = document.querySelector('aside.panel.test'); + if (panel && panel.style.display === 'block') { + closePanel('test'); + } else { + showPanel('test', { width: '300px' }); + } + }); + document.querySelector('main').appendChild(button); } await loadPage(); diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index e6850116..9321ff8e 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -416,11 +416,14 @@ html:has(meta[content="edge-delivery"]) { } @media (width >= 600px) { + :root { + --app-frame-areas: "header header" var(--s2-nav-height) "sidenav main" 1fr; + --app-frame-columns: var(--s2-nav-width) 1fr; + } + html.spectrum-eco { body.app-frame { - grid-template: - "header header" var(--s2-nav-height) - "sidenav main" 1fr / var(--s2-nav-width) 1fr; + grid-template: var(--app-frame-areas) / var(--app-frame-columns); height: 100dvh; nav { @@ -432,6 +435,63 @@ html:has(meta[content="edge-delivery"]) { border-radius: 32px 32px 0 0; max-height: 100%; } + + aside.panel { + --panel-bottom-margin: 12px; + + position: relative; + height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); + background-color: light-dark(#fff, #000); + border-radius: 32px; + margin: 0 12px var(--panel-bottom-margin) 0; + } + + aside.panel .panel-resize-handle { + position: absolute; + top: 50%; + translate: 0 -50%; + z-index: 2; + box-sizing: border-box; + width: 12px; + height: 56px; + padding: 0; + margin: 0; + border: none; + cursor: ew-resize; + touch-action: none; + background: transparent; + color: inherit; + } + + aside.panel .panel-resize-handle::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + translate: -50% -50%; + width: 4px; + height: 48px; + border-radius: 999px; + background-color: var(--s2-gray-800); + } + + aside.panel .panel-resize-handle:hover::before, + aside.panel .panel-resize-handle:focus-visible::before { + background-color: var(--s2-gray-900); + } + + aside.panel .panel-resize-handle:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; + } + + aside.panel .panel-resize-handle-trailing { + right: -12px; + } + + aside.panel .panel-resize-handle-leading { + left: -12px; + } } } } From 3b108def110f5464c5c622d6685ecba93da3bbe4 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 12:27:24 +0200 Subject: [PATCH 04/13] refactor panels as block --- WORKLOG.md | 7 ++ nx2/blocks/panel/panel.css | 82 ++++++++++++++++ nx2/blocks/panel/panel.js | 181 ++++++++++++++++++++++++++++++++++ nx2/scripts/panels.js | 196 ------------------------------------- nx2/scripts/scripts.js | 16 --- nx2/styles/styles.css | 57 ----------- 6 files changed, 270 insertions(+), 269 deletions(-) create mode 100644 nx2/blocks/panel/panel.css create mode 100644 nx2/blocks/panel/panel.js delete mode 100644 nx2/scripts/panels.js diff --git a/WORKLOG.md b/WORKLOG.md index 244bc69d..c7ae2d9f 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -34,3 +34,10 @@ Decided to wrap nav and sidenav in semantic HTML elements: ### README.md updated - Added "Context" section linking to AGENTS.md and WORKLOG.md with descriptions. + +## 2026-04-02 + +### nx2 `blocks/panel/` (app-frame side panels) +- Added `panel.js`: Lit `nx-panel` (shadow shell, default slot, resize handle in shadow), `createPanel` / `showPanel` (`{ width, beforeMain }`), `setPanelsGrid` for app-frame column/area CSS vars. Shell is `aside.panel` with `data-position` before/after main; `createPanel` / `showPanel` return the `nx-panel` element. Empty `aside` after removing `nx-panel` is dropped in `disconnectedCallback`. +- `decorate(block)`: if the block has an anchor → `loadFragment(a.href)` → `createPanel`, move fragment children onto `nx-panel` with DOM APIs, remove the block. +- Styling split: `styles.css` keeps app-frame grid (`--app-frame-*`, `body.app-frame` row); `panel.css` holds panel surface and resize affordance. diff --git a/nx2/blocks/panel/panel.css b/nx2/blocks/panel/panel.css new file mode 100644 index 00000000..091b7206 --- /dev/null +++ b/nx2/blocks/panel/panel.css @@ -0,0 +1,82 @@ +:host { + --panel-bottom-margin: 12px; + + display: block; + position: relative; + box-sizing: border-box; + height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); + margin: 0 12px var(--panel-bottom-margin) 0; + background-color: light-dark(#fff, #000); + border-radius: 32px; +} + +.panel-shell { + display: flex; + flex-direction: column; + box-sizing: border-box; + min-height: 0; + height: 100%; + padding: 0 12px; +} + +.panel-body { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + display: flex; + flex-direction: column; +} + +.panel-body slot::slotted(h2) { + flex-shrink: 0; + margin: 12px 0 8px; + font-size: var(--s2-heading-size-xl, 1.25rem); + font-weight: var(--s2-heading-font-weight, 700); +} + +.panel-resize-handle { + position: absolute; + top: 50%; + translate: 0 -50%; + z-index: 2; + box-sizing: border-box; + width: 12px; + height: 56px; + padding: 0; + margin: 0; + border: none; + cursor: ew-resize; + touch-action: none; + background: transparent; + color: inherit; +} + +.panel-resize-handle::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + translate: -50% -50%; + width: 4px; + height: 48px; + border-radius: 999px; + background-color: var(--s2-gray-600); +} + +.panel-resize-handle:hover::before, +.panel-resize-handle:focus-visible::before { + background-color: var(--s2-gray-900); +} + +.panel-resize-handle:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; +} + +.panel-resize-handle-trailing { + right: -12px; +} + +.panel-resize-handle-leading { + left: -12px; +} diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js new file mode 100644 index 00000000..4e7203c6 --- /dev/null +++ b/nx2/blocks/panel/panel.js @@ -0,0 +1,181 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { LitElement, html } from 'lit'; + +import { loadFragment } from '../fragment/fragment.js'; +import { loadStyle } from '../../utils/utils.js'; + +const style = await loadStyle(import.meta.url); + +// Computes css variables to define grid areas +export function setPanelsGrid() { + const { body } = document; + if (!body.classList.contains('app-frame')) return; + + const beforeMain = [...body.querySelectorAll('aside.panel[data-position="before"]')]; + const afterMain = [...body.querySelectorAll('aside.panel[data-position="after"]')]; + + beforeMain.forEach((el, i) => { el.style.gridArea = `nx-pb-${i}`; }); + afterMain.forEach((el, i) => { el.style.gridArea = `nx-pa-${i}`; }); + + const colCount = 1 + beforeMain.length + 1 + afterMain.length; + const headerRow = Array(colCount).fill('header').join(' '); + const contentRow = [ + 'sidenav', + ...beforeMain.map((_, i) => `nx-pb-${i}`), + 'main', + ...afterMain.map((_, i) => `nx-pa-${i}`), + ].join(' '); + + const track = (el) => { + const w = el.dataset.width?.trim(); + return w ? `min(${w}, 40vw)` : 'minmax(0, auto)'; + }; + const columns = [ + 'var(--s2-nav-width)', + ...beforeMain.map(track), + '1fr', + ...afterMain.map(track), + ].join(' '); + + body.style.setProperty('--app-frame-areas', `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`); + body.style.setProperty('--app-frame-columns', columns); +} + +const PANEL_WIDTH_MIN = 120; +const PANEL_WIDTH_MAX = () => Math.min(1600, window.innerWidth * 0.4); + +function parsePanelWidth(aside) { + const w = aside.dataset.width?.trim(); + if (w && /^\d+(\.\d+)?px$/i.test(w)) return parseFloat(w); + return aside.getBoundingClientRect().width; +} + +function applyPanelWidth(aside, px) { + aside.dataset.width = `${Math.max(PANEL_WIDTH_MIN, Math.min(PANEL_WIDTH_MAX(), Math.round(px)))}px`; +} + +class NXPanel extends LitElement { + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [style]; + this._panelAside = this.closest('aside.panel'); + } + + disconnectedCallback() { + super.disconnectedCallback(); + const aside = this._panelAside; + this._panelAside = undefined; + if (aside?.isConnected && aside.childElementCount === 0) { + aside.remove(); + } + setPanelsGrid(); + } + + _resizePointerDown(downEvent) { + const aside = this.closest('aside.panel'); + if (!aside || downEvent.button !== 0) return; + const deltaSign = aside.dataset.position === 'before' ? 1 : -1; + + const handle = downEvent.currentTarget; + handle.setPointerCapture(downEvent.pointerId); + const startX = downEvent.clientX; + const startW = parsePanelWidth(aside); + const prevUserSelect = document.body.style.userSelect; + document.body.style.userSelect = 'none'; + + const onPointerMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + applyPanelWidth(aside, startW + deltaSign * dx); + setPanelsGrid(); + }; + + const onPointerUp = (upEvent) => { + handle.releasePointerCapture(upEvent.pointerId); + document.body.style.userSelect = prevUserSelect; + handle.removeEventListener('pointermove', onPointerMove); + handle.removeEventListener('pointerup', onPointerUp); + handle.removeEventListener('pointercancel', onPointerUp); + }; + + handle.addEventListener('pointermove', onPointerMove); + handle.addEventListener('pointerup', onPointerUp); + handle.addEventListener('pointercancel', onPointerUp); + } + + render() { + const aside = this.closest('aside.panel'); + const edge = aside?.dataset.position === 'before' ? 'trailing' : 'leading'; + + return html` +
+
+ +
+
+ + `; + } +} + +customElements.define('nx-panel', NXPanel); + +export function createPanel({ width = '200px', beforeMain = false } = {}) { + const aside = document.createElement('aside'); + aside.classList.add('panel'); + aside.dataset.width = width; + aside.dataset.position = beforeMain ? 'before' : 'after'; + + const nx = document.createElement('nx-panel'); + aside.append(nx); + + if (beforeMain) { + document.querySelector('main').before(aside); + } else { + document.querySelector('main').after(aside); + } + + return nx; +} + +export function showPanel({ width = '200px', beforeMain = false } = {}) { + const nx = createPanel({ width, beforeMain }); + setPanelsGrid(); + return nx; +} + +export default async function decorate(block) { + const a = block.querySelector('a'); + if (!a) return; + + const fragment = await loadFragment(a.href); + + const beforeMain = block.dataset.beforeMain === 'true'; + const width = block.dataset.width?.trim() || '200px'; + + const nx = createPanel({ width, beforeMain }); + if (fragment && nx) { + nx.replaceChildren(); + while (fragment.firstChild) { + nx.appendChild(fragment.firstChild); + } + } + + block.remove(); + setPanelsGrid(); +} diff --git a/nx2/scripts/panels.js b/nx2/scripts/panels.js deleted file mode 100644 index 6cda74b9..00000000 --- a/nx2/scripts/panels.js +++ /dev/null @@ -1,196 +0,0 @@ -function getPanels(el = document.body) { - const beforeMain = []; - const afterMain = []; - - let pastMain = false; - el.querySelectorAll('aside.panel, main').forEach((panel) => { - if (panel.tagName === 'MAIN') { - pastMain = true; - return; - } - - if (pastMain) { - afterMain.push(panel); - } else { - beforeMain.push(panel); - } - }); - - return { beforeMain, afterMain }; -} - -function panelGridName(prefix, index) { - return `${prefix}${index}`; -} - -function panelColumnTrack(panel) { - const w = panel.dataset.width?.trim(); - return w || 'minmax(0, auto)'; -} - -const PANEL_WIDTH_MIN = 120; -const PANEL_WIDTH_MAX = 1600; - -function clearPanelResizeHandles(root) { - root.querySelectorAll('aside.panel .panel-resize-handle').forEach((h) => h.remove()); -} - -function parsePanelWidthPx(panel) { - const w = panel.dataset.width?.trim(); - if (w && /^\d+(\.\d+)?px$/i.test(w)) { - return parseFloat(w); - } - return panel.getBoundingClientRect().width; -} - -function clampPanelWidth(px) { - return Math.max(PANEL_WIDTH_MIN, Math.min(PANEL_WIDTH_MAX, Math.round(px))); -} - -function applyPanelWidthPx(panel, px) { - panel.dataset.width = `${clampPanelWidth(px)}px`; -} - -/** Updates grid template and panel grid areas only (no resize handles). */ -function refreshAppFrameGrid() { - const { body } = document; - if (!body.classList.contains('app-frame')) return; - - const { beforeMain, afterMain } = getPanels(body); - - if (!beforeMain.length && !afterMain.length) { - body.style.removeProperty('--app-frame-areas'); - body.style.removeProperty('--app-frame-columns'); - body.querySelectorAll(':scope > aside.panel').forEach((el) => { - el.style.removeProperty('grid-area'); - }); - return; - } - - const colCount = 1 + beforeMain.length + 1 + afterMain.length; - const headerRow = Array(colCount).fill('header').join(' '); - const contentRow = [ - 'sidenav', - ...beforeMain.map((_, i) => panelGridName('nx-pb-', i)), - 'main', - ...afterMain.map((_, i) => panelGridName('nx-pa-', i)), - ].join(' '); - - beforeMain.forEach((el, i) => { - el.style.gridArea = panelGridName('nx-pb-', i); - }); - afterMain.forEach((el, i) => { - el.style.gridArea = panelGridName('nx-pa-', i); - }); - - const areas = `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`; - const columns = [ - 'var(--s2-nav-width)', - ...beforeMain.map((el) => panelColumnTrack(el)), - '1fr', - ...afterMain.map((el) => panelColumnTrack(el)), - ].join(' '); - - body.style.setProperty('--app-frame-areas', areas); - body.style.setProperty('--app-frame-columns', columns); -} - -function bindPanelResize(panel, { deltaSign }) { - const onPointerDown = (downEvent) => { - if (downEvent.button !== 0) return; - const handle = downEvent.currentTarget; - handle.setPointerCapture(downEvent.pointerId); - const startX = downEvent.clientX; - const startW = parsePanelWidthPx(panel); - const prevUserSelect = document.body.style.userSelect; - document.body.style.userSelect = 'none'; - - const onPointerMove = (moveEvent) => { - const dx = moveEvent.clientX - startX; - applyPanelWidthPx(panel, startW + deltaSign * dx); - refreshAppFrameGrid(); - }; - - const onPointerUp = (upEvent) => { - handle.releasePointerCapture(upEvent.pointerId); - document.body.style.userSelect = prevUserSelect; - handle.removeEventListener('pointermove', onPointerMove); - handle.removeEventListener('pointerup', onPointerUp); - handle.removeEventListener('pointercancel', onPointerUp); - }; - - handle.addEventListener('pointermove', onPointerMove); - handle.addEventListener('pointerup', onPointerUp); - handle.addEventListener('pointercancel', onPointerUp); - }; - - return onPointerDown; -} - -function syncPanelResizeHandles(beforeMain, afterMain) { - beforeMain.forEach((panel) => { - const trail = document.createElement('button'); - trail.type = 'button'; - trail.className = 'panel-resize-handle panel-resize-handle-trailing'; - trail.setAttribute('aria-label', 'Resize panel'); - trail.addEventListener('pointerdown', bindPanelResize(panel, { deltaSign: 1 })); - panel.append(trail); - }); - - afterMain.forEach((panel) => { - const lead = document.createElement('button'); - lead.type = 'button'; - lead.className = 'panel-resize-handle panel-resize-handle-leading'; - lead.setAttribute('aria-label', 'Resize panel'); - lead.addEventListener('pointerdown', bindPanelResize(panel, { deltaSign: -1 })); - panel.append(lead); - }); -} - -export function setPanelsGrid() { - const { body } = document; - clearPanelResizeHandles(body); - refreshAppFrameGrid(); - - if (!body.classList.contains('app-frame')) return; - - const { beforeMain, afterMain } = getPanels(body); - if (beforeMain.length || afterMain.length) { - syncPanelResizeHandles(beforeMain, afterMain); - } -} - -export function showPanel(name, { width = '200px', beforeMain = false } = {}) { - const existing = document.querySelector(`aside.panel.${name}`); - if (existing) { - existing.style.display = 'block'; - existing.dataset.width = width; - setPanelsGrid(); - return existing; - } - - const panel = document.createElement('aside'); - panel.classList.add('panel', name); - panel.dataset.width = width; - - const main = document.querySelector('main'); - - if (beforeMain) { - main.before(panel); - } else { - main.after(panel); - } - - panel.innerHTML = `

${name}

`; - setPanelsGrid(); - - return panel; -} - -export function closePanel(name) { - const panel = document.querySelector(`aside.panel.${name}`); - if (panel) { - panel.style.display = 'none'; - setPanelsGrid(); - } -} diff --git a/nx2/scripts/scripts.js b/nx2/scripts/scripts.js index 98a19a4b..6a2dfdcd 100644 --- a/nx2/scripts/scripts.js +++ b/nx2/scripts/scripts.js @@ -11,7 +11,6 @@ */ import { loadArea, setConfig } from './nx.js'; -import { closePanel, showPanel } from './panels.js'; const hostnames = ['nx.live']; @@ -52,20 +51,5 @@ const conf = { export async function loadPage() { await setConfig(conf); await loadArea(); - - showPanel('test', { width: '300px' }); - showPanel('test2', { width: '200px', beforeMain: true }); - - const button = document.createElement('button'); - button.innerText = 'Toggle Panel'; - button.addEventListener('click', () => { - const panel = document.querySelector('aside.panel.test'); - if (panel && panel.style.display === 'block') { - closePanel('test'); - } else { - showPanel('test', { width: '300px' }); - } - }); - document.querySelector('main').appendChild(button); } await loadPage(); diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index 9321ff8e..90fcf694 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -435,63 +435,6 @@ html:has(meta[content="edge-delivery"]) { border-radius: 32px 32px 0 0; max-height: 100%; } - - aside.panel { - --panel-bottom-margin: 12px; - - position: relative; - height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); - background-color: light-dark(#fff, #000); - border-radius: 32px; - margin: 0 12px var(--panel-bottom-margin) 0; - } - - aside.panel .panel-resize-handle { - position: absolute; - top: 50%; - translate: 0 -50%; - z-index: 2; - box-sizing: border-box; - width: 12px; - height: 56px; - padding: 0; - margin: 0; - border: none; - cursor: ew-resize; - touch-action: none; - background: transparent; - color: inherit; - } - - aside.panel .panel-resize-handle::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - translate: -50% -50%; - width: 4px; - height: 48px; - border-radius: 999px; - background-color: var(--s2-gray-800); - } - - aside.panel .panel-resize-handle:hover::before, - aside.panel .panel-resize-handle:focus-visible::before { - background-color: var(--s2-gray-900); - } - - aside.panel .panel-resize-handle:focus-visible { - outline: 2px solid var(--s2-blue-800); - outline-offset: 2px; - } - - aside.panel .panel-resize-handle-trailing { - right: -12px; - } - - aside.panel .panel-resize-handle-leading { - left: -12px; - } } } } From 84e84998295f361c3974dbe62b37bba6e23b090b Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 14:27:33 +0200 Subject: [PATCH 05/13] cleanup --- nx2/blocks/panel/panel.js | 28 ++++++++-------------------- nx2/styles/styles.css | 1 + 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js index 4e7203c6..e42a3ed5 100644 --- a/nx2/blocks/panel/panel.js +++ b/nx2/blocks/panel/panel.js @@ -1,15 +1,3 @@ -/* - * Copyright 2026 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - import { LitElement, html } from 'lit'; import { loadFragment } from '../fragment/fragment.js'; @@ -25,27 +13,27 @@ export function setPanelsGrid() { const beforeMain = [...body.querySelectorAll('aside.panel[data-position="before"]')]; const afterMain = [...body.querySelectorAll('aside.panel[data-position="after"]')]; - beforeMain.forEach((el, i) => { el.style.gridArea = `nx-pb-${i}`; }); - afterMain.forEach((el, i) => { el.style.gridArea = `nx-pa-${i}`; }); + beforeMain.forEach((el, i) => { el.style.gridArea = `nx-panel-before-${i}`; }); + afterMain.forEach((el, i) => { el.style.gridArea = `nx-panel-after-${i}`; }); const colCount = 1 + beforeMain.length + 1 + afterMain.length; const headerRow = Array(colCount).fill('header').join(' '); const contentRow = [ 'sidenav', - ...beforeMain.map((_, i) => `nx-pb-${i}`), + ...beforeMain.map((_, i) => `nx-panel-before-${i}`), 'main', - ...afterMain.map((_, i) => `nx-pa-${i}`), + ...afterMain.map((_, i) => `nx-panel-after-${i}`), ].join(' '); - const track = (el) => { + const getWidth = (el) => { const w = el.dataset.width?.trim(); return w ? `min(${w}, 40vw)` : 'minmax(0, auto)'; }; const columns = [ 'var(--s2-nav-width)', - ...beforeMain.map(track), + ...beforeMain.map(getWidth), '1fr', - ...afterMain.map(track), + ...afterMain.map(getWidth), ].join(' '); body.style.setProperty('--app-frame-areas', `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`); @@ -135,7 +123,7 @@ class NXPanel extends LitElement { customElements.define('nx-panel', NXPanel); -export function createPanel({ width = '200px', beforeMain = false } = {}) { +function createPanel({ width, beforeMain }) { const aside = document.createElement('aside'); aside.classList.add('panel'); aside.dataset.width = width; diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index 90fcf694..1a7c876a 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -434,6 +434,7 @@ html:has(meta[content="edge-delivery"]) { margin: 0 12px 0 0; border-radius: 32px 32px 0 0; max-height: 100%; + overflow-y: auto; } } } From 99a1b67e0d461766aa5062895910f27017d493e0 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 15:43:59 +0200 Subject: [PATCH 06/13] implement mobile styles --- WORKLOG.md | 1 + nx2/blocks/panel/panel.css | 23 +++++++++++++++++++---- nx2/blocks/panel/panel.js | 4 ++-- nx2/styles/styles.css | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/WORKLOG.md b/WORKLOG.md index c7ae2d9f..57d45f9c 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -41,3 +41,4 @@ Decided to wrap nav and sidenav in semantic HTML elements: - Added `panel.js`: Lit `nx-panel` (shadow shell, default slot, resize handle in shadow), `createPanel` / `showPanel` (`{ width, beforeMain }`), `setPanelsGrid` for app-frame column/area CSS vars. Shell is `aside.panel` with `data-position` before/after main; `createPanel` / `showPanel` return the `nx-panel` element. Empty `aside` after removing `nx-panel` is dropped in `disconnectedCallback`. - `decorate(block)`: if the block has an anchor → `loadFragment(a.href)` → `createPanel`, move fragment children onto `nx-panel` with DOM APIs, remove the block. - Styling split: `styles.css` keeps app-frame grid (`--app-frame-*`, `body.app-frame` row); `panel.css` holds panel surface and resize affordance. +- Mobile-first: default `body.app-frame` uses fixed panel insets + `:has(aside.panel)::before` scrim; `@media (width >= 600px)` restores grid layout and clears modal positioning. `setPanelsGrid` always sets `--app-frame-*` (only applied at 600px+). diff --git a/nx2/blocks/panel/panel.css b/nx2/blocks/panel/panel.css index 091b7206..caccb62d 100644 --- a/nx2/blocks/panel/panel.css +++ b/nx2/blocks/panel/panel.css @@ -4,10 +4,11 @@ display: block; position: relative; box-sizing: border-box; - height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); - margin: 0 12px var(--panel-bottom-margin) 0; + height: 100%; + max-height: 100%; + margin: 0; + border-radius: 24px; background-color: light-dark(#fff, #000); - border-radius: 32px; } .panel-shell { @@ -17,12 +18,12 @@ min-height: 0; height: 100%; padding: 0 12px; + overflow-y: auto;; } .panel-body { flex: 1 1 auto; min-height: 0; - overflow: auto; display: flex; flex-direction: column; } @@ -35,6 +36,7 @@ } .panel-resize-handle { + display: none; position: absolute; top: 50%; translate: 0 -50%; @@ -80,3 +82,16 @@ .panel-resize-handle-leading { left: -12px; } + +@media (width >= 600px) { + :host { + height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); + max-height: none; + margin: 0 12px var(--panel-bottom-margin) 0; + border-radius: 32px; + } + + .panel-resize-handle { + display: block; + } +} diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js index e42a3ed5..af2fdc40 100644 --- a/nx2/blocks/panel/panel.js +++ b/nx2/blocks/panel/panel.js @@ -141,7 +141,7 @@ function createPanel({ width, beforeMain }) { return nx; } -export function showPanel({ width = '200px', beforeMain = false } = {}) { +export function showPanel({ width = '400px', beforeMain = false } = {}) { const nx = createPanel({ width, beforeMain }); setPanelsGrid(); return nx; @@ -154,7 +154,7 @@ export default async function decorate(block) { const fragment = await loadFragment(a.href); const beforeMain = block.dataset.beforeMain === 'true'; - const width = block.dataset.width?.trim() || '200px'; + const width = block.dataset.width?.trim() || '400px'; const nx = createPanel({ width, beforeMain }); if (fragment && nx) { diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index 1a7c876a..6595e0da 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -356,6 +356,25 @@ html:has(meta[content="edge-delivery"]) { background-color: light-dark(#fff, #000); max-height: 100%; } + + &:has(aside.panel)::before { + content: ''; + position: fixed; + inset: 0; + z-index: 50; + background-color: light-dark(rgb(0 0 0 / 40%), rgb(0 0 0 / 55%)); + pointer-events: auto; + } + + aside.panel { + position: fixed; + inset: calc(var(--s2-nav-height) + 12px) 12px 12px; + z-index: 70; + width: auto; + height: auto; + margin: 0; + max-height: none; + } } } @@ -436,6 +455,15 @@ html:has(meta[content="edge-delivery"]) { max-height: 100%; overflow-y: auto; } + + &:has(aside.panel)::before { + display: none; + pointer-events: none; + } + + aside.panel { + position: static; + } } } } From 93088709897bf5a3e1e607edb2111bf968da8747 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 15:47:03 +0200 Subject: [PATCH 07/13] simplify --- nx2/blocks/panel/panel.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js index af2fdc40..51518a0e 100644 --- a/nx2/blocks/panel/panel.js +++ b/nx2/blocks/panel/panel.js @@ -153,10 +153,7 @@ export default async function decorate(block) { const fragment = await loadFragment(a.href); - const beforeMain = block.dataset.beforeMain === 'true'; - const width = block.dataset.width?.trim() || '400px'; - - const nx = createPanel({ width, beforeMain }); + const nx = createPanel({ width: '400px', beforeMain: false }); if (fragment && nx) { nx.replaceChildren(); while (fragment.firstChild) { From 995938786111e1482cc9063fe6cf735f152fce59 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 15:57:57 +0200 Subject: [PATCH 08/13] remove canvas file --- nx2/blocks/canvas/canvas.css | 84 ------------------------------------ nx2/blocks/canvas/canvas.js | 74 ------------------------------- 2 files changed, 158 deletions(-) delete mode 100644 nx2/blocks/canvas/canvas.css delete mode 100644 nx2/blocks/canvas/canvas.js diff --git a/nx2/blocks/canvas/canvas.css b/nx2/blocks/canvas/canvas.css deleted file mode 100644 index 16acd30c..00000000 --- a/nx2/blocks/canvas/canvas.css +++ /dev/null @@ -1,84 +0,0 @@ -:host { - display: block; - height: 100%; - color: var(--s2-gray-900); - - --canvas-chat-width: 360px; -} - -.canvas-shell { - display: grid; - grid-template-columns: minmax(0, 1fr); - padding: var(--s2-spacing-300); - height: calc(100vh - var(--s2-nav-height)); - box-sizing: border-box; - background: #f0f0f0; - overflow: hidden; -} - -.workspace-pane, -.chat-pane { - display: flex; - flex-direction: column; - min-height: 0; - background: var(--s2-gray-25); - border: 1px solid color-mix(in srgb, var(--s2-gray-200) 90%, transparent); - border-radius: 22px; - overflow: hidden; -} - -.panel-divider { - display: none; -} - -.chat-pane { - margin-bottom: 12px; -} - -.workspace-pane { - border-radius: 22px 22px 0 0; - max-height: 100%; -} - -.workspace-pane-body, -.chat-pane-body { - min-height: 0; - overflow: auto; -} - -.workspace-surface, -.chat-surface { - min-height: calc(100% + 1px); -} - -.workspace-surface { - min-height: 200vh; -} - -@media (width >= 900px) { - .canvas-shell { - grid-template-columns: minmax(0, 1fr) 10px minmax(320px, var(--canvas-chat-width)); - align-items: stretch; - padding: 0; - } - - .panel-divider { - display: flex; - align-items: center; - justify-content: center; - border-radius: 999px; - cursor: ew-resize; - touch-action: none; - } - - .panel-divider-handle { - width: 6px; - height: 52px; - border-radius: 999px; - background: var(--s2-gray-500); - } -} - -:host(.is-resizing) { - user-select: none; -} diff --git a/nx2/blocks/canvas/canvas.js b/nx2/blocks/canvas/canvas.js deleted file mode 100644 index 99850f96..00000000 --- a/nx2/blocks/canvas/canvas.js +++ /dev/null @@ -1,74 +0,0 @@ -import { LitElement, html } from 'lit'; -import { loadStyle } from '../../utils/utils.js'; - -const style = await loadStyle(import.meta.url); - -class NxCanvas extends LitElement { - connectedCallback() { - super.connectedCallback(); - this.shadowRoot.adoptedStyleSheets = [style]; - } - - handleResizeStart(event) { - if (window.innerWidth < 900) return; - this._dragging = true; - event.currentTarget.setPointerCapture(event.pointerId); - this.classList.add('is-resizing'); - } - - handleResizeMove(event) { - if (!this._dragging) return; - - const shell = this.shadowRoot.querySelector('.canvas-shell'); - const { left, right } = shell.getBoundingClientRect(); - const width = Math.round(right - event.clientX); - const minWidth = 320; - const maxWidth = Math.min(560, Math.round((right - left) * 0.45)); - const nextWidth = Math.max(minWidth, Math.min(maxWidth, width)); - - this.style.setProperty('--canvas-chat-width', `${nextWidth}px`); - } - - handleResizeEnd(event) { - if (!this._dragging) return; - this._dragging = false; - event.currentTarget.releasePointerCapture(event.pointerId); - this.classList.remove('is-resizing'); - } - - render() { - return html` -
-
-
- -
-
- - - - -
- `; - } -} - -customElements.define('nx-canvas', NxCanvas); - -export default function init(block) { - const canvas = document.createElement('nx-canvas'); - block.replaceChildren(canvas); -} From faddd62ef9455f97278d010bc84b21cfecc6e5d0 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 16:04:27 +0200 Subject: [PATCH 09/13] fix lit import --- nx2/blocks/panel/panel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js index 51518a0e..d662ca7f 100644 --- a/nx2/blocks/panel/panel.js +++ b/nx2/blocks/panel/panel.js @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html } from 'da-lit'; import { loadFragment } from '../fragment/fragment.js'; import { loadStyle } from '../../utils/utils.js'; From 3423bdbcffb63ccd59b923ed16f99d148c9c681d Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 17:49:50 +0200 Subject: [PATCH 10/13] fix rounded corners --- nx2/blocks/panel/panel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/nx2/blocks/panel/panel.css b/nx2/blocks/panel/panel.css index caccb62d..7e7183ba 100644 --- a/nx2/blocks/panel/panel.css +++ b/nx2/blocks/panel/panel.css @@ -9,6 +9,7 @@ margin: 0; border-radius: 24px; background-color: light-dark(#fff, #000); + overflow: hidden; } .panel-shell { From 369eda48ffdc8969a529f6ad50429df40680bcb7 Mon Sep 17 00:00:00 2001 From: Hannes Hertach Date: Thu, 2 Apr 2026 18:04:05 +0200 Subject: [PATCH 11/13] fix hiding resize --- nx2/blocks/panel/panel.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nx2/blocks/panel/panel.css b/nx2/blocks/panel/panel.css index 7e7183ba..97044099 100644 --- a/nx2/blocks/panel/panel.css +++ b/nx2/blocks/panel/panel.css @@ -9,7 +9,6 @@ margin: 0; border-radius: 24px; background-color: light-dark(#fff, #000); - overflow: hidden; } .panel-shell { @@ -18,8 +17,8 @@ box-sizing: border-box; min-height: 0; height: 100%; - padding: 0 12px; - overflow-y: auto;; + border-radius: inherit; + overflow: hidden; } .panel-body { @@ -27,6 +26,7 @@ min-height: 0; display: flex; flex-direction: column; + overflow-y: auto; } .panel-body slot::slotted(h2) { From f384ff4dd36ae1c502e463a10c2056ff2afe538b Mon Sep 17 00:00:00 2001 From: Chris Millar Date: Mon, 6 Apr 2026 11:33:16 -0600 Subject: [PATCH 12/13] Reduce scope of PR into ew-panels --- WORKLOG.md | 33 ++++ nx2/blocks/action-button/action-button.js | 30 +++ nx2/blocks/panel/panel.css | 98 ---------- nx2/scripts/nx.js | 19 +- nx2/scripts/scripts.js | 1 + nx2/styles/styles.css | 228 +++++++++++++++++----- nx2/utils/panel.js | 190 ++++++++++++++++++ 7 files changed, 445 insertions(+), 154 deletions(-) create mode 100644 nx2/blocks/action-button/action-button.js delete mode 100644 nx2/blocks/panel/panel.css create mode 100644 nx2/utils/panel.js diff --git a/WORKLOG.md b/WORKLOG.md index 57d45f9c..dfdbd9a2 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -42,3 +42,36 @@ Decided to wrap nav and sidenav in semantic HTML elements: - `decorate(block)`: if the block has an anchor → `loadFragment(a.href)` → `createPanel`, move fragment children onto `nx-panel` with DOM APIs, remove the block. - Styling split: `styles.css` keeps app-frame grid (`--app-frame-*`, `body.app-frame` row); `panel.css` holds panel surface and resize affordance. - Mobile-first: default `body.app-frame` uses fixed panel insets + `:has(aside.panel)::before` scrim; `@media (width >= 600px)` restores grid layout and clears modal positioning. `setPanelsGrid` always sets `--app-frame-*` (only applied at 600px+). + +## 2026-04-03 + +### utils.js rewrite — multi-environment DA service config +- Replaced stub `DA_ORIGIN`/`daFetch` exports with real environment-aware origins for DA services (admin, collab, content, preview, etc.). +- `getEnv(key, envs)` resolves origin per service: checks query param → localStorage → default (stage for dev/stage, prod for prod). +- Removed `HashController` reactive controller; sidenav no longer uses it. +- `parseWindowPath` now returns `null` for missing/invalid hashes and strips trailing `/index` from hash. + +### New api.js — extracted API layer +- `daFetch` handles auth token injection, checks URL against `ALLOWED_TOKEN` origins before attaching bearer. +- `ping`, `source`, `list`, `signout` — thin wrappers for DA/AEM endpoints. +- Profile block now imports `signout` from api.js instead of inlining the fetch. + +### CSS: class selectors → meta-content selectors +- Spectrum Edge and app-frame layouts no longer rely on JS adding classes (`spectrum-edge`, `app-frame`). +- Replaced with `html:has(meta[content="edge-delivery"])` and `html:has(meta[content="app-frame"])` — pure CSS, no JS decoration needed. +- Removed `spectrum-edge` class addition from `decorateDoc` in nx.js. +- App-frame grid extracted to its own top-level rule block. + +### profile.js — handleScheme simplification +- Color scheme toggle simplified: remove both classes, add the toggled one. No intermediate object. + +### AGENTS.md — "parse, don't validate" convention +- Added to JS conventions section. Core idea: push validation to the boundary where data enters, return `null` or a well-formed result — no ambiguous middle ground. Downstream code trusts the shape without re-checking. +- Codifies the distinct meaning of `null` (absent), `undefined` (not yet loaded), and `''` (explicitly cleared). +- `parseWindowPath` is the canonical example: returns a clean `{ view, org, site, path }` or `null`. +## 2026-04-04 + +### Panel-aware default-content max-width +- When either side panel is visible (`aside.panel:not([hidden])`), `.default-content` inside `main` now uses `max-width: 83.4%` instead of the fixed `--se-grid-container-width` value. +- Uses sibling selectors: `main:has(~ aside.panel:not([hidden]))` for panels after main, `aside.panel:not([hidden]) ~ main` for panels before main. +- The fixed `1200px` media query (`@media (width >= 1440px)`) remains for the no-panel case. diff --git a/nx2/blocks/action-button/action-button.js b/nx2/blocks/action-button/action-button.js new file mode 100644 index 00000000..7560d50a --- /dev/null +++ b/nx2/blocks/action-button/action-button.js @@ -0,0 +1,30 @@ +import { loadFragment } from '../fragment/fragment.js'; +import { showPanel, hidePanel, unhidePanel } from '../../utils/panel.js'; + +const FRAGMENT_PATHS = { + before: '/nx/fragments/before-panel', + after: '/nx/fragments/after-panel', +}; + +export default async function decorate(a) { + const { hash } = new URL(a.href); + if (hash !== '#_before' && hash !== '#_after') return; + const beforeMain = hash === '#_before'; + const position = beforeMain ? 'before' : 'after'; + + a.addEventListener('click', async (e) => { + e.preventDefault(); + const existing = document.querySelector(`aside.panel[data-position="${position}"]`); + if (existing) { + if (existing.hidden) { + unhidePanel(existing); + } else { + hidePanel(existing); + } + return; + } + const path = FRAGMENT_PATHS[position]; + const content = await loadFragment(path); + if (content) showPanel({ width: '400px', beforeMain, content, fragment: path }); + }); +} diff --git a/nx2/blocks/panel/panel.css b/nx2/blocks/panel/panel.css deleted file mode 100644 index 97044099..00000000 --- a/nx2/blocks/panel/panel.css +++ /dev/null @@ -1,98 +0,0 @@ -:host { - --panel-bottom-margin: 12px; - - display: block; - position: relative; - box-sizing: border-box; - height: 100%; - max-height: 100%; - margin: 0; - border-radius: 24px; - background-color: light-dark(#fff, #000); -} - -.panel-shell { - display: flex; - flex-direction: column; - box-sizing: border-box; - min-height: 0; - height: 100%; - border-radius: inherit; - overflow: hidden; -} - -.panel-body { - flex: 1 1 auto; - min-height: 0; - display: flex; - flex-direction: column; - overflow-y: auto; -} - -.panel-body slot::slotted(h2) { - flex-shrink: 0; - margin: 12px 0 8px; - font-size: var(--s2-heading-size-xl, 1.25rem); - font-weight: var(--s2-heading-font-weight, 700); -} - -.panel-resize-handle { - display: none; - position: absolute; - top: 50%; - translate: 0 -50%; - z-index: 2; - box-sizing: border-box; - width: 12px; - height: 56px; - padding: 0; - margin: 0; - border: none; - cursor: ew-resize; - touch-action: none; - background: transparent; - color: inherit; -} - -.panel-resize-handle::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - translate: -50% -50%; - width: 4px; - height: 48px; - border-radius: 999px; - background-color: var(--s2-gray-600); -} - -.panel-resize-handle:hover::before, -.panel-resize-handle:focus-visible::before { - background-color: var(--s2-gray-900); -} - -.panel-resize-handle:focus-visible { - outline: 2px solid var(--s2-blue-800); - outline-offset: 2px; -} - -.panel-resize-handle-trailing { - right: -12px; -} - -.panel-resize-handle-leading { - left: -12px; -} - -@media (width >= 600px) { - :host { - height: calc(100dvh - var(--s2-nav-height) - var(--panel-bottom-margin)); - max-height: none; - margin: 0 12px var(--panel-bottom-margin) 0; - border-radius: 32px; - } - - .panel-resize-handle { - display: block; - } -} diff --git a/nx2/scripts/nx.js b/nx2/scripts/nx.js index c4ed0754..2b3ae3ee 100644 --- a/nx2/scripts/nx.js +++ b/nx2/scripts/nx.js @@ -24,7 +24,7 @@ export function getLocale(locales) { return { key, ...locales[key] }; } -const env = (() => { +export const env = (() => { const { host } = window.location; if (host.endsWith('.aem.live')) return 'prod'; if (!['--', 'local'].some((check) => host.includes(check))) return 'prod'; @@ -248,10 +248,14 @@ function loadSession() { document.body.classList.add('session'); } -function decorateDoc() { - decorateNav(); +async function decorateDoc() { + // Fast track IMS if returning from sign in + if (window.location.hash.startsWith('#old_hash')) { + const { loadIms } = await import('../utils/ims.js'); + await loadIms(); + } - document.documentElement.classList.add('spectrum-edge'); + decorateNav(); const template = getMetadata('template'); if (template) document.body.classList.add(template); @@ -261,12 +265,17 @@ function decorateDoc() { const pageId = window.location.hash?.replace('#', ''); if (pageId) localStorage.setItem('lazyhash', pageId); + + if (localStorage.getItem('nx-panels')) { + const { restorePanels } = await import('../utils/panel.js'); + await restorePanels(); + } } export async function loadArea({ area } = { area: document }) { const isDoc = area === document; const isSession = sessionStorage.getItem('session'); - if (isDoc) decorateDoc(); + if (isDoc) await decorateDoc(); await decoratePlaceholders(area, isDoc); decoratePictures(area); const { decorateArea } = getConfig(); diff --git a/nx2/scripts/scripts.js b/nx2/scripts/scripts.js index 6a2dfdcd..8f069f3e 100644 --- a/nx2/scripts/scripts.js +++ b/nx2/scripts/scripts.js @@ -22,6 +22,7 @@ const locales = { const linkBlocks = [ { fragment: '/fragments/' }, + { 'action-button': '/tools/widgets/panel' }, ]; const imsClientId = 'nexter'; diff --git a/nx2/styles/styles.css b/nx2/styles/styles.css index 1d578cd0..e4dd3c9d 100644 --- a/nx2/styles/styles.css +++ b/nx2/styles/styles.css @@ -308,8 +308,10 @@ } } -/* --- Edge Delivery-specific decoration hiding --- */ +/* --- Spectrum Edge pre-decoration hiding --- */ html:has(meta[content="edge-delivery"]) { + display: block; + main > div, div[data-status] { display: none; } @@ -322,11 +324,16 @@ html:has(meta[content="edge-delivery"]) { margin: 0 auto; } } + + main:has(~ aside.panel:not([hidden])) .default-content, + aside.panel:not([hidden]) ~ main .default-content { + max-width: 83.4%; + } } -/* --- Spectrum Eco default styles --- */ +/* --- Spectrum Edge default styles --- */ @layer spectrum-edge { - html.spectrum-edge { + html:has(meta[content="edge-delivery"]) { body { margin: 0; background-color: light-dark(#fff, #000); @@ -343,7 +350,6 @@ html:has(meta[content="edge-delivery"]) { display: none; } - /* Don't let fonts load unless there's a session */ &.session { font-family: var(--s2-font-family); } @@ -351,49 +357,6 @@ html:has(meta[content="edge-delivery"]) { &.no-header { --s2-nav-height: 0; } - - /* Everything for the app frame */ - &.app-frame { - display: grid; - height: 100dvh; - grid-template: - "header" var(--s2-nav-height) - "main" 1fr; - background-color: light-dark(rgb(240 240 240), rgb(17 17 17)); - - header { - grid-area: header; - } - - nav { - grid-area: sidenav; - } - - main { - grid-area: main; - background-color: light-dark(#fff, #000); - max-height: 100%; - } - - &:has(aside.panel)::before { - content: ''; - position: fixed; - inset: 0; - z-index: 50; - background-color: light-dark(rgb(0 0 0 / 40%), rgb(0 0 0 / 55%)); - pointer-events: auto; - } - - aside.panel { - position: fixed; - inset: calc(var(--s2-nav-height) + 12px) 12px 12px; - z-index: 70; - width: auto; - height: auto; - margin: 0; - max-height: none; - } - } } h1 { font-size: var(--s2-heading-size-xxl); } @@ -452,26 +415,178 @@ html:has(meta[content="edge-delivery"]) { } } + html:has(meta[content="app-frame"]) { + body { + display: grid; + height: 100vh; + grid-template: + "header" var(--s2-nav-height) + "main" 1fr; + background-color: light-dark(rgb(240 240 240), rgb(17 17 17)); + + header { + grid-area: header; + } + + nav { + grid-area: sidenav; + } + + main { + grid-area: main; + background-color: light-dark(#fff, #000); + max-height: 100%; + } + + .section { + &.container { + .default-content, .block-content { + max-width: var(--section-container-width); + margin-inline: auto; + } + } + } + + &:has(aside.panel)::before { + content: ''; + position: fixed; + inset: 0; + z-index: 50; + background-color: light-dark(rgb(0 0 0 / 40%), rgb(0 0 0 / 55%)); + pointer-events: auto; + } + + aside.panel { + position: fixed; + inset: calc(var(--s2-nav-height) + 12px) 12px 12px; + z-index: 70; + width: auto; + height: auto; + margin: 0; + max-height: none; + + .panel-wrapper { + --panel-bottom-margin: 12px; + + display: block; + position: relative; + box-sizing: border-box; + height: 100%; + max-height: 100%; + margin: 0; + border-radius: 24px; + background-color: light-dark(#fff, #000); + + .panel-shell { + display: flex; + flex-direction: column; + box-sizing: border-box; + min-height: 0; + height: 100%; + border-radius: inherit; + overflow: hidden; + } + + .panel-body { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow-y: auto; + } + + .panel-body h2 { + flex-shrink: 0; + margin: 12px 0 8px; + font-size: var(--s2-heading-size-xl, 1.25rem); + font-weight: var(--s2-heading-font-weight, 700); + } + + .panel-resize-handle { + display: none; + position: absolute; + top: 50%; + translate: 0 -50%; + z-index: 2; + box-sizing: border-box; + width: 12px; + height: 56px; + padding: 0; + margin: 0; + border: none; + cursor: ew-resize; + touch-action: none; + background: transparent; + color: inherit; + } + + .panel-resize-handle::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + translate: -50% -50%; + width: 4px; + height: 48px; + border-radius: 999px; + background-color: var(--s2-gray-600); + } + + .panel-resize-handle:hover::before, + .panel-resize-handle:focus-visible::before { + background-color: var(--s2-gray-900); + } + + .panel-resize-handle:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; + } + + .panel-resize-handle-trailing { + right: -12px; + } + + .panel-resize-handle-leading { + left: -12px; + } + } + } + + footer { + display: none; + } + } + + helix-sidekick, aem-sidekick { + display: none !important; + } + } + @media (width >= 600px) { :root { --app-frame-areas: "header header" var(--s2-nav-height) "sidenav main" 1fr; --app-frame-columns: var(--s2-nav-width) 1fr; } - html.spectrum-edge { - body.app-frame { + html:has(meta[content="app-frame"]) { + body { + grid-template: + "header header" var(--s2-nav-height) + "sidenav main" 1fr / var(--s2-nav-width) 1fr; grid-template: var(--app-frame-areas) / var(--app-frame-columns); - height: 100dvh; + height: 100vh; nav { display: unset; } main { + display: grid; margin: 0 12px 0 0; - border-radius: 32px 32px 0 0; + border-radius: var(--s2-corner-radius-800) var(--s2-corner-radius-800) 0 0; max-height: 100%; overflow-y: auto; + } &:has(aside.panel)::before { @@ -481,6 +596,17 @@ html:has(meta[content="edge-delivery"]) { aside.panel { position: static; + + .panel-wrapper { + height: calc(100vh - var(--s2-nav-height) - var(--panel-bottom-margin)); + max-height: none; + margin: 0 12px var(--panel-bottom-margin) 0; + border-radius: var(--s2-corner-radius-800); + + .panel-resize-handle { + display: block; + } + } } } } diff --git a/nx2/utils/panel.js b/nx2/utils/panel.js new file mode 100644 index 00000000..d3183b69 --- /dev/null +++ b/nx2/utils/panel.js @@ -0,0 +1,190 @@ +import { getMetadata } from '../scripts/nx.js'; + +const PANEL_WIDTH_MIN = 120; +const PANEL_WIDTH_MAX = () => Math.min(1600, window.innerWidth * 0.4); + +function parsePanelWidth(aside) { + const w = aside.dataset.width?.trim(); + if (w && /^\d+(\.\d+)?px$/i.test(w)) return parseFloat(w); + return aside.getBoundingClientRect().width; +} + +function applyPanelWidth(aside, px) { + const clamped = `${Math.max(PANEL_WIDTH_MIN, Math.min(PANEL_WIDTH_MAX(), Math.round(px)))}px`; + aside.dataset.width = clamped; + aside.style.width = clamped; +} + +const PANEL_STORAGE_KEY = 'nx-panels'; + +function getPanelStore() { + try { + return JSON.parse(localStorage.getItem(PANEL_STORAGE_KEY)) || {}; + } catch { + return {}; + } +} + +function savePanelState(position, { width, fragment }) { + const store = getPanelStore(); + store[position] = { width, fragment }; + localStorage.setItem(PANEL_STORAGE_KEY, JSON.stringify(store)); +} + +function removePanelState(position) { + const store = getPanelStore(); + delete store[position]; + localStorage.setItem(PANEL_STORAGE_KEY, JSON.stringify(store)); +} + +export function setPanelsGrid() { + const { body } = document; + if (getMetadata('template') !== 'app-frame') return; + + const beforeMain = [...body.querySelectorAll('aside.panel[data-position="before"]:not([hidden])')]; + const afterMain = [...body.querySelectorAll('aside.panel[data-position="after"]:not([hidden])')]; + + beforeMain.forEach((el, i) => { el.style.gridArea = `nx-panel-before-${i}`; }); + afterMain.forEach((el, i) => { el.style.gridArea = `nx-panel-after-${i}`; }); + + const colCount = 1 + beforeMain.length + 1 + afterMain.length; + const headerRow = Array(colCount).fill('header').join(' '); + const contentRow = [ + 'sidenav', + ...beforeMain.map((_, i) => `nx-panel-before-${i}`), + 'main', + ...afterMain.map((_, i) => `nx-panel-after-${i}`), + ].join(' '); + + const getWidth = (el) => { + const w = el.dataset.width?.trim(); + return w ? `min(${w}, 40vw)` : 'minmax(0, auto)'; + }; + const columns = [ + 'var(--s2-nav-width)', + ...beforeMain.map(getWidth), + '1fr', + ...afterMain.map(getWidth), + ].join(' '); + + body.style.setProperty('--app-frame-areas', `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`); + body.style.setProperty('--app-frame-columns', columns); +} + +function resizePointerDown(downEvent) { + const handle = downEvent.currentTarget; + const aside = handle.closest('aside.panel'); + if (!aside || downEvent.button !== 0) return; + const deltaSign = aside.dataset.position === 'before' ? 1 : -1; + + handle.setPointerCapture(downEvent.pointerId); + const startX = downEvent.clientX; + const startW = parsePanelWidth(aside); + const prevUserSelect = document.body.style.userSelect; + document.body.style.userSelect = 'none'; + + const onPointerMove = (moveEvent) => { + const dx = moveEvent.clientX - startX; + applyPanelWidth(aside, startW + deltaSign * dx); + setPanelsGrid(); + }; + + const onPointerUp = (upEvent) => { + handle.releasePointerCapture(upEvent.pointerId); + document.body.style.userSelect = prevUserSelect; + handle.removeEventListener('pointermove', onPointerMove); + handle.removeEventListener('pointerup', onPointerUp); + handle.removeEventListener('pointercancel', onPointerUp); + savePanelState(aside.dataset.position, { + width: aside.dataset.width, + fragment: aside.dataset.fragment, + }); + }; + + handle.addEventListener('pointermove', onPointerMove); + handle.addEventListener('pointerup', onPointerUp); + handle.addEventListener('pointercancel', onPointerUp); +} + +function buildPanelDOM(aside) { + const edge = aside.dataset.position === 'before' ? 'trailing' : 'leading'; + + const wrapper = document.createElement('div'); + wrapper.className = 'panel-wrapper'; + + const shell = document.createElement('div'); + shell.className = 'panel-shell'; + + const body = document.createElement('div'); + body.className = 'panel-body'; + + const handle = document.createElement('button'); + handle.type = 'button'; + handle.className = `panel-resize-handle panel-resize-handle-${edge}`; + handle.setAttribute('aria-label', 'Resize panel'); + handle.addEventListener('pointerdown', resizePointerDown); + + shell.append(body); + wrapper.append(shell, handle); + aside.append(wrapper); +} + +export function createPanel({ width = '400px', beforeMain = false, content, fragment } = {}) { + const aside = document.createElement('aside'); + aside.classList.add('panel'); + aside.dataset.width = width; + aside.style.width = width; + const position = beforeMain ? 'before' : 'after'; + aside.dataset.position = position; + if (fragment) aside.dataset.fragment = fragment; + + buildPanelDOM(aside); + + if (content) aside.querySelector('.panel-body').append(content); + + savePanelState(position, { width, fragment }); + + if (beforeMain) { + document.querySelector('main').before(aside); + } else { + document.querySelector('main').after(aside); + } + + return aside; +} + +export function hidePanel(aside) { + removePanelState(aside.dataset.position); + aside.hidden = true; + setPanelsGrid(); +} + +export function unhidePanel(aside) { + aside.hidden = false; + savePanelState(aside.dataset.position, { + width: aside.dataset.width, + fragment: aside.dataset.fragment, + }); + setPanelsGrid(); +} + +export { getPanelStore }; + +export function showPanel(opts) { + const aside = createPanel(opts); + setPanelsGrid(); + return aside; +} + +export async function restorePanels() { + const panels = getPanelStore(); + if (!panels.before && !panels.after) return; + const { loadFragment } = await import('../blocks/fragment/fragment.js'); + for (const [position, { width, fragment }] of Object.entries(panels)) { + const content = await loadFragment(fragment); + if (content) { + const beforeMain = position === 'before'; + showPanel({ width, beforeMain, content, fragment }); + } + } +} From a07f07dc90095db57c6cdf334299d09b5aa4545e Mon Sep 17 00:00:00 2001 From: Chris Millar Date: Tue, 7 Apr 2026 07:30:12 -0600 Subject: [PATCH 13/13] PR feedback --- nx2/blocks/action-button/action-button.js | 59 ++++++++++++++--------- nx2/scripts/nx.js | 4 +- nx2/utils/panel.js | 52 +++++++++++--------- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/nx2/blocks/action-button/action-button.js b/nx2/blocks/action-button/action-button.js index 7560d50a..fc961ba0 100644 --- a/nx2/blocks/action-button/action-button.js +++ b/nx2/blocks/action-button/action-button.js @@ -1,30 +1,43 @@ -import { loadFragment } from '../fragment/fragment.js'; -import { showPanel, hidePanel, unhidePanel } from '../../utils/panel.js'; +async function togglePanel(position) { + const existing = document.querySelector(`aside.panel[data-position="${position}"]`); + if (!existing) return false; + const { hidePanel, unhidePanel } = await import('../../utils/panel.js'); + if (existing.hidden) unhidePanel(existing); + else hidePanel(existing); + return true; +} -const FRAGMENT_PATHS = { - before: '/nx/fragments/before-panel', - after: '/nx/fragments/after-panel', -}; +async function loadPanelContent(value) { + if (value.includes('/fragments/')) { + const { loadFragment } = await import('../fragment/fragment.js'); + return { content: await loadFragment(value), fragment: value }; + } + const mod = await import(`../../../nx/blocks/${value}/${value}.js`); + return { content: await mod.getPanel() }; +} -export default async function decorate(a) { - const { hash } = new URL(a.href); - if (hash !== '#_before' && hash !== '#_after') return; - const beforeMain = hash === '#_before'; - const position = beforeMain ? 'before' : 'after'; +function decoratePanel(a, hash) { + const match = hash.match(/^#_(before|after)=(.+)$/); + if (!match) return; + const [, position, value] = match; + const beforeMain = position === 'before'; a.addEventListener('click', async (e) => { e.preventDefault(); - const existing = document.querySelector(`aside.panel[data-position="${position}"]`); - if (existing) { - if (existing.hidden) { - unhidePanel(existing); - } else { - hidePanel(existing); - } - return; - } - const path = FRAGMENT_PATHS[position]; - const content = await loadFragment(path); - if (content) showPanel({ width: '400px', beforeMain, content, fragment: path }); + if (await togglePanel(position)) return; + const { content, fragment } = await loadPanelContent(value); + if (!content) return; + const { showPanel } = await import('../../utils/panel.js'); + showPanel({ width: '400px', beforeMain, content, fragment }); }); } + +const ACTIONS = [ + { pathname: '/tools/widgets/panel', handler: decoratePanel }, +]; + +export default async function decorate(a) { + const action = ACTIONS.find((entry) => entry.pathname === a.pathname); + if (!action) return; + action.handler(a, a.hash); +} diff --git a/nx2/scripts/nx.js b/nx2/scripts/nx.js index 2b3ae3ee..2310ca8a 100644 --- a/nx2/scripts/nx.js +++ b/nx2/scripts/nx.js @@ -141,10 +141,10 @@ export function decorateLink(config, a) { const { dnb } = decorateHash(a, url); if (!dnb) { - const { href, hash } = a; + const { pathname, hash } = a; const found = config.linkBlocks.some((pattern) => { const key = Object.keys(pattern)[0]; - if (!href.includes(pattern[key])) return false; + if (!pathname.includes(pattern[key])) return false; const blockName = key === 'fragment' && hash ? 'dialog' : key; a.classList.add(blockName, 'auto-block'); return true; diff --git a/nx2/utils/panel.js b/nx2/utils/panel.js index d3183b69..7091aec5 100644 --- a/nx2/utils/panel.js +++ b/nx2/utils/panel.js @@ -41,34 +41,38 @@ export function setPanelsGrid() { const { body } = document; if (getMetadata('template') !== 'app-frame') return; - const beforeMain = [...body.querySelectorAll('aside.panel[data-position="before"]:not([hidden])')]; - const afterMain = [...body.querySelectorAll('aside.panel[data-position="after"]:not([hidden])')]; - - beforeMain.forEach((el, i) => { el.style.gridArea = `nx-panel-before-${i}`; }); - afterMain.forEach((el, i) => { el.style.gridArea = `nx-panel-after-${i}`; }); - - const colCount = 1 + beforeMain.length + 1 + afterMain.length; - const headerRow = Array(colCount).fill('header').join(' '); - const contentRow = [ - 'sidenav', - ...beforeMain.map((_, i) => `nx-panel-before-${i}`), - 'main', - ...afterMain.map((_, i) => `nx-panel-after-${i}`), - ].join(' '); + const before = body.querySelector('aside.panel[data-position="before"]:not([hidden])'); + const after = body.querySelector('aside.panel[data-position="after"]:not([hidden])'); const getWidth = (el) => { - const w = el.dataset.width?.trim(); + const w = el?.dataset.width?.trim(); return w ? `min(${w}, 40vw)` : 'minmax(0, auto)'; }; - const columns = [ - 'var(--s2-nav-width)', - ...beforeMain.map(getWidth), - '1fr', - ...afterMain.map(getWidth), - ].join(' '); - - body.style.setProperty('--app-frame-areas', `"${headerRow}" var(--s2-nav-height) "${contentRow}" 1fr`); - body.style.setProperty('--app-frame-columns', columns); + + const header = ['header']; + const content = ['sidenav']; + const columns = ['var(--s2-nav-width)']; + + if (before) { + before.style.gridArea = 'panel-before'; + header.push('header'); + content.push('panel-before'); + columns.push(getWidth(before)); + } + + header.push('header'); + content.push('main'); + columns.push('1fr'); + + if (after) { + after.style.gridArea = 'panel-after'; + header.push('header'); + content.push('panel-after'); + columns.push(getWidth(after)); + } + + body.style.setProperty('--app-frame-areas', `"${header.join(' ')}" var(--s2-nav-height) "${content.join(' ')}" 1fr`); + body.style.setProperty('--app-frame-columns', columns.join(' ')); } function resizePointerDown(downEvent) {