From c4342a5ba52abf53d3763fa13e3a07cb863c84ac Mon Sep 17 00:00:00 2001 From: kozmaadrian Date: Tue, 9 Jun 2026 14:57:16 +0200 Subject: [PATCH 1/2] feat(form): integrate sc into ew --- blocks/form/canvas-header/canvas-header.css | 63 +++++++++++++++++ blocks/form/canvas-header/canvas-header.js | 48 +++++++++++++ blocks/form/form.css | 30 ++++++++ blocks/form/form.js | 76 +++++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 blocks/form/canvas-header/canvas-header.css create mode 100644 blocks/form/canvas-header/canvas-header.js create mode 100644 blocks/form/form.css create mode 100644 blocks/form/form.js diff --git a/blocks/form/canvas-header/canvas-header.css b/blocks/form/canvas-header/canvas-header.css new file mode 100644 index 00000000..96a8c3cf --- /dev/null +++ b/blocks/form/canvas-header/canvas-header.css @@ -0,0 +1,63 @@ +:host { + display: block; + box-sizing: border-box; + font-family: var( + --s2-font-family, + adobe-clean, + 'Source Sans Pro', + 'Trebuchet MS', + sans-serif + ); + color: var(--s2-gray-800); +} + +.bar { + display: flex; + align-items: center; + gap: 4px; + box-sizing: border-box; + height: var(--canvas-header-height, 48px); + padding: 0 12px; + background-color: light-dark(#fff, var(--s2-gray-25)); + border-bottom: 1px solid var(--s2-gray-200); +} + +.group { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + min-width: 24px; + min-height: 24px; + padding: 0 4px; + margin: 0; + border: none; + border-radius: 8px; + color: var(--s2-gray-800); + background: transparent; + cursor: pointer; +} + +.icon-btn:hover { + background-color: var(--s2-gray-75); +} + +.icon-btn:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; +} + +.icon-btn svg.icon { + display: block; + flex-shrink: 0; + width: 16px; + height: 16px; + overflow: hidden; +} diff --git a/blocks/form/canvas-header/canvas-header.js b/blocks/form/canvas-header/canvas-header.js new file mode 100644 index 00000000..5a5967c4 --- /dev/null +++ b/blocks/form/canvas-header/canvas-header.js @@ -0,0 +1,48 @@ +import { LitElement, html } from 'da-lit'; + +import { getNx } from '../../../scripts/utils.js'; + +const { loadStyle } = await import(`${getNx()}/utils/utils.js`); + +const style = await loadStyle(import.meta.url); + +const ICONS = { splitLeft: '/img/icons/s2-icon-splitleft-20-n.svg' }; + +// The form's own header: a slim bar with the chat toggle. Emits +// `form-toggle-chat`; exposes the toggle as a part so form.css can hide it. +class CanvasHeader extends LitElement { + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [style]; + } + + _toggleChat() { + this.dispatchEvent( + new CustomEvent('form-toggle-chat', { bubbles: true, composed: true }), + ); + } + + _renderIcon(name) { + return html``; + } + + render() { + return html` +
+
+ +
+
+ `; + } +} + +customElements.define('canvas-header', CanvasHeader); diff --git a/blocks/form/form.css b/blocks/form/form.css new file mode 100644 index 00000000..c63de9b0 --- /dev/null +++ b/blocks/form/form.css @@ -0,0 +1,30 @@ +/* Form workspace: the nx2 form fills main; nx-chat is docked on the left. */ + +:root { + --canvas-header-height: 48px; + --form-content-top: 32px; + + /* Top gap for scrolled-to groups + the sticky nav. The header is outside the + scroll region, so this only covers the gap, not the header. Apply it here + only — padding the .form scroll container would misalign the two. */ + --nx-scroll-offset-top: var(--form-content-top); +} + +/* Nested scroll (like /canvas): the form scrolls inside a fixed-height region, + so the header — a sibling above .form — stays put without `sticky`. */ +.form { + box-sizing: border-box; + height: calc(100vh - var(--s2-nav-height) - var(--canvas-header-height)); + overflow-y: auto; +} + +/* Never override the chat's `display` here, or its flex layout collapses. */ +aside.panel[data-position='before'] nx-chat { + height: 100%; +} + +/* Hide the chat toggle while the chat is open (it has its own close). */ +html:has(aside.panel[data-position='before']:not([hidden])) + canvas-header::part(toggle-before) { + display: none; +} diff --git a/blocks/form/form.js b/blocks/form/form.js new file mode 100644 index 00000000..9917f8cb --- /dev/null +++ b/blocks/form/form.js @@ -0,0 +1,76 @@ +import { getNx } from '../../scripts/utils.js'; +import './canvas-header/canvas-header.js'; + +const { loadStyle } = await import(`${getNx()}/utils/utils.js`); +const { openPanel } = await import(`${getNx()}/utils/panel.js`); + +const style = await loadStyle(import.meta.url); +document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]; + +// Engage the app-frame panel grid even when the page didn't declare it. +function ensureAppFrame() { + let meta = document.head.querySelector('meta[name="template"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.name = 'template'; + document.head.append(meta); + } + meta.content = 'app-frame'; +} + +const WORKSPACE_NAV_PATH = '/fragments/exp-workspace/nav'; + +// Use the workspace nav (breadcrumb + actions), like canvas. Read by nav.js, +// which loads after this block decorates. Page nav-path wins. +function ensureNavPath() { + if (document.head.querySelector('meta[name="nav-path"]')) return; + const meta = document.createElement('meta'); + meta.name = 'nav-path'; + meta.content = WORKSPACE_NAV_PATH; + document.head.append(meta); +} + +// Open preview/publish at the structured-content renderer. +function ensurePreviewRenderer() { + let meta = document.head.querySelector('meta[name="ew-preview-renderer"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.name = 'ew-preview-renderer'; + document.head.append(meta); + } + meta.content = 'sc'; +} + +function openChatPanel() { + return openPanel({ + position: 'before', + width: '400px', + getContent: async () => { + await import(`${getNx()}/blocks/chat/chat.js`); + return document.createElement('nx-chat'); + }, + }); +} + +// Header sits before .form (outside its scroll region), so it stays put. +function installHeader(block) { + const header = document.createElement('canvas-header'); + header.addEventListener('form-toggle-chat', () => openChatPanel()); + block.before(header); + return header; +} + +// Requires `?nxver=2` so getNx() resolves to nx2 (where the form lives). The +// form and chat both read the `#/org/site/path` hash, so they stay in sync. +export default async function decorate(block) { + ensureAppFrame(); + ensureNavPath(); + ensurePreviewRenderer(); + + installHeader(block); + + const { default: decorateForm } = await import(`${getNx()}/blocks/form/form.js`); + await decorateForm(block); + + await openChatPanel(); +} From c3f7fc5c9dc53abad375f090399f40c75aade5da Mon Sep 17 00:00:00 2001 From: kozmaadrian Date: Fri, 12 Jun 2026 14:38:59 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(form):=20TEMP=20workaround=20?= =?UTF-8?q?=E2=80=94=20rewrite=20.nx-form=20to=20.form=20in=20decorateArea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/scripts.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/scripts.js b/scripts/scripts.js index 88af3df2..64d069b9 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -14,6 +14,11 @@ import { initIms } from '../blocks/shared/utils.js'; import { setNx, nxJS, nxCSS } from './utils.js'; export function decorateArea({ area = document } = {}) { + // Render any `nx-form` block as the `form` workspace (the nx2 form in the + // center with nx-chat docked on the left). Runs before sections/blocks are + // collected, so NX loads the rewritten block. + area.querySelectorAll('.nx-form').forEach((el) => el.classList.replace('nx-form', 'form')); + // Find all dark & light images const lcpImgs = [...area.querySelectorAll('[alt="light"], [alt="dark"]')].filter((img) => { const pic = img.parentElement;