diff --git a/WORKLOG.md b/WORKLOG.md index 244bc69d..dfdbd9a2 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -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. diff --git a/nx2/blocks/action-button/action-button.js b/nx2/blocks/action-button/action-button.js new file mode 100644 index 00000000..fc961ba0 --- /dev/null +++ b/nx2/blocks/action-button/action-button.js @@ -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); +} diff --git a/nx2/blocks/panel/panel.js b/nx2/blocks/panel/panel.js new file mode 100644 index 00000000..d662ca7f --- /dev/null +++ b/nx2/blocks/panel/panel.js @@ -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` +
+
+ +
+
+ + `; + } +} + +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(); +} diff --git a/nx2/scripts/nx.js b/nx2/scripts/nx.js index c4ed0754..2310ca8a 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'; @@ -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; @@ -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 90fdd144..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,31 +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); - overflow-y: scroll; - max-height: 100%; - } - } } h1 { font-size: var(--s2-heading-size-xxl); } @@ -434,23 +415,198 @@ 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) { - html.spectrum-edge { - body.app-frame { + :root { + --app-frame-areas: "header header" var(--s2-nav-height) "sidenav main" 1fr; + --app-frame-columns: var(--s2-nav-width) 1fr; + } + + html:has(meta[content="app-frame"]) { + body { grid-template: "header header" var(--s2-nav-height) "sidenav main" 1fr / var(--s2-nav-width) 1fr; - height: 100dvh; + grid-template: var(--app-frame-areas) / var(--app-frame-columns); + height: 100vh; nav { display: unset; } main { + display: grid; margin: 0 12px 0 0; - border-radius: 32px 32px 0 0; - overflow-y: scroll; + border-radius: var(--s2-corner-radius-800) var(--s2-corner-radius-800) 0 0; max-height: 100%; + overflow-y: auto; + + } + + &:has(aside.panel)::before { + display: none; + pointer-events: none; + } + + 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..7091aec5 --- /dev/null +++ b/nx2/utils/panel.js @@ -0,0 +1,194 @@ +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 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(); + return w ? `min(${w}, 40vw)` : 'minmax(0, auto)'; + }; + + 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) { + 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 }); + } + } +}