Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions WORKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,44 @@ 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.
- 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.
43 changes: 43 additions & 0 deletions nx2/blocks/action-button/action-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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;
}

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() };
}

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();
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);
}
166 changes: 166 additions & 0 deletions nx2/blocks/panel/panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { LitElement, html } from 'da-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-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);
}

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`
<div class="panel-shell">
<div class="panel-body">
<slot></slot>
</div>
</div>
<button
type="button"
class="panel-resize-handle panel-resize-handle-${edge}"
aria-label="Resize panel"
@pointerdown=${this._resizePointerDown}
></button>
`;
}
}

customElements.define('nx-panel', NXPanel);

function createPanel({ width, beforeMain }) {
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 = '400px', 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 nx = createPanel({ width: '400px', beforeMain: false });
if (fragment && nx) {
nx.replaceChildren();
while (fragment.firstChild) {
nx.appendChild(fragment.firstChild);
}
}

block.remove();
setPanelsGrid();
}
23 changes: 16 additions & 7 deletions nx2/scripts/nx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions nx2/scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const locales = {

const linkBlocks = [
{ fragment: '/fragments/' },
{ 'action-button': '/tools/widgets/panel' },
];

const imsClientId = 'nexter';
Expand Down
Loading
Loading