From 0194c6ed6a318fb1bab95d9c396e496830ee73cc Mon Sep 17 00:00:00 2001 From: svinod Date: Fri, 24 Apr 2026 10:11:43 +0200 Subject: [PATCH 1/3] feat: setup tools panel --- docs/workspace.md | 55 +++++++++------- nx2/blocks/browse/browse.css | 37 +++++++++++ nx2/blocks/browse/browse.js | 51 ++++++++++++++- nx2/blocks/browse/overrides.css | 6 ++ nx2/blocks/canvas/canvas.css | 16 ----- nx2/blocks/canvas/canvas.js | 82 +++++++++++------------- nx2/blocks/chat/chat.css | 61 ++++++++++++++++-- nx2/blocks/chat/chat.js | 36 +++++------ nx2/blocks/chat/constants.js | 2 +- nx2/blocks/tool-panel/tool-panel.css | 66 ++++++++++++++++++++ nx2/blocks/tool-panel/tool-panel.js | 93 +++++++++++++++++++++++++++- nx2/utils/panel.js | 40 ++++++++---- 12 files changed, 420 insertions(+), 125 deletions(-) create mode 100644 nx2/blocks/tool-panel/tool-panel.css diff --git a/docs/workspace.md b/docs/workspace.md index 54b90aa5..fdfc821b 100644 --- a/docs/workspace.md +++ b/docs/workspace.md @@ -16,8 +16,10 @@ The following sections highlight some principles in more detail. ### Repository Layout - Experience workspace work lives under **`nx2/`** (alongside the CDN-mapped `nx/` tree). - The nx2 scripts and utils provide URL/state helpers, auth, and block loading (`nx2/scripts/nx.js`, `nx2/utils/`). -- Feature code lives in **`nx2/blocks/`** — currently including **`canvas`** (the edit experience; not a separate edit block), **`chat`**, shell pieces (**`nav`**, **`sidenav`**, **`profile`**), **`fragment`** / **`dialog`**, and small helpers (**`action-button`**, **`canvas-actions`**). **`tool-panel`** is a placeholder block; tool UI is intended to ship inside loaded fragments. +- Feature code lives in **`nx2/blocks/`** — currently including **`canvas`** (the edit experience; not a separate edit block), **`chat`**, shell pieces (**`nav`**, **`sidenav`**, **`profile`**), **`fragment`** / **`dialog`**, and small helpers (**`action-button`**, **`canvas-actions`**). - Shared shell behavior for app-frame side regions is implemented in **`nx2/utils/panel.js`** (DOM panel chrome, resize, show/hide, persistence), not as a separate `blocks/panel` Lit shell. +- **`blocks/tool-panel`** provides the managed panel shell (`nx-tool-panel`): a picker to switch between consumers, a header-actions zone for first-party consumers, and a close button. Consumer content is lazy-loaded on first activation. Like chat, `nx-tool-panel` is position-agnostic — host blocks instantiate it and set its `consumers` array inside their own `getContent`. +- **`blocks/chat`** has no public entry-point wrapper — host blocks mount `nx-chat` directly inside their own `getContent`. - When **`blocks/shared`** (or equivalent) exists, it should contain small, reusable pieces that make no assumptions about where they are invoked from. - Root **`Utils`** contains helpers such as the extension SDK client and DA API wrappers (`daFetch.js`: origins + `daFetch`). @@ -49,7 +51,7 @@ nx2 ├── blocks │ ├── canvas (edit: nx-canvas-header, panel toggles, main editing layout) │ ├── chat -│ ├── tool-panel (placeholder; content from fragments) +│ ├── tool-panel (managed panel shell: picker, header-actions zone, consumer lifecycle) │ └── … (e.g. browse when added as its own block) │ └── utils @@ -65,31 +67,37 @@ Skills Lab — external app at da.live/apps/skills, linked from Chat ## Side panels (app-frame) - Panels are **`aside.panel`** elements with **`data-position="before"`** (to the left of `main`) or **`"after"`** (to the right). Width is stored on the element; **`setPanelsGrid()`** updates CSS grid template vars on `body` when panels are visible. -- **`openPanelWithFragment`** loads markup via **`loadPanelContent`** (fragment URLs or, for legacy paths, block modules), then **`showPanel`** mounts the shell and appends content into **`.panel-body`**. -- **`hidePanel` / `unhidePanel`** toggle visibility without removing the node; hidden panels are omitted from the grid. -- **`localStorage`** key **`nx-panels`** stores `{ before?, after? }` with width and fragment URL. **`restorePanels()`** is invoked from **`loadArea`** in **`nx2/scripts/nx.js`** when that key is present so panels return across reloads. +- **`hidePanel` / `showPanel`** toggle visibility without removing the node; hidden panels are omitted from the grid. (`unhidePanel` is a legacy alias for `showPanel`.) Any consumer can close its panel by dispatching an **`nx-panel-close`** event — the panel frame handles it. +- **`localStorage`** key **`nx-panels`** stores `{ before?, after? }` with width and (for fragment-based panels) fragment URL. **`restorePanels()`** is invoked from **`loadArea`** in **`nx2/scripts/nx.js`** for fragment-based panels; page blocks (e.g. canvas) restore their own typed panels directly on load. +- **`openPanel({ position, width, getContent })`** is the single entry point for opening a panel programmatically. The caller provides a `getContent` async function that returns the element to mount in the panel body. This means the caller is fully responsible for what goes inside — whether that is a headless component like `nx-chat` or a managed shell like `nx-tool-panel`. `openPanel` handles the show/create/skip-if-visible logic and nothing else. -### Panel header and custom actions +### Two panel types -When a panel is opened via `canvas.js`, **`addPanelHeader`** prepends a header bar (`.panel-header`) to `.panel-body` and fires an **`nx-panel-slot`** event on `.panel-body`: +The distinction between panel types is a **caller convention**, not a framework concept. -```js -panelBody.dispatchEvent(new CustomEvent('nx-panel-slot', { - detail: { slot: header.querySelector('.panel-header-custom') }, -})); -``` +**Headless** — `getContent` returns a component that owns its entire layout: header, actions, close button. Used when a single, known first-party component permanently occupies the panel + +**Managed** — `getContent` imports and instantiates `nx-tool-panel`, sets its `consumers` array, and returns it. `nx-tool-panel` then owns the header with a consumer picker, actions zone, and close button. Used when multiple consumers share a panel (e.g. tools and extensions). + +### Consumer contract (managed panels) -Fragment content loaded into a panel can listen for this event in `connectedCallback` to register buttons into the header's left-side container: +Each consumer is a descriptor object passed in the `consumers` array: ```js -// in connectedCallback -this.closest('.panel-body')?.addEventListener('nx-panel-slot', (e) => { - this._panelSlot = e.detail.slot; - this._mountActions(); // called once both slot and icons are ready -}, { once: true }); +{ + id: 'my-tool', // unique string key + label: 'My Tool', // shown in the picker + firstParty: true, // omit or false for third-party + load: async () => element, // called once on first activation; must return an HTMLElement +} ``` -Buttons appended to the slot should use `className = 'panel-header-action'` to pick up the shared button styling defined in `nx-panel-header.css`. The `[hidden]` attribute is respected — set `btn.hidden = true/false` to show/hide conditionally. +Each consumer registered with a managed panel declares whether it is **first-party** or not: + +- **First-party consumers** (`firstParty: true`) are authored by the workspace team, fully trusted, and may expose header actions by implementing a `getHeaderActions()` method on the element returned by `load()`. The method should return an `HTMLElement` (or `null`/`undefined` to add nothing). It is called each time the consumer becomes active. +- **Third-party / fragment consumers** are not authored by the team. They receive only the content area; they cannot add anything to the header. This is the default for extensions and external fragments. + +Consumer content is lazy-loaded on first activation and preserved across open/close cycles — switching consumers or hiding the panel does not reload content. --- @@ -107,8 +115,9 @@ The edit experience is implemented as the **`canvas`** block — there is not a - Owns the editing workspace: breadcrumbs, view mode (doc/wysiwyg/split), and layout state as those features land; composes editing-focused layouts around **`main`**. - Decorates the canvas region with **`nx-canvas-header`** (Lit toolbar: e.g. split icons for panel edges, undo/redo affordances). -- Listens for **`nx-canvas-toggle-panel`** (`detail.position`: **`before`** | **`after`**) and calls **`toggleCanvasPanel`** in **`canvas.js`**: show or hide the matching **`aside.panel`**, or **`openPanelWithFragment`** with the configured fragment URL (e.g. chat before main, tool panel after main). -- Side regions use the same panel model: **`before`** / **`after`** asides can host in-context browser, history, metadata, outline, etc., loaded as fragments or blocks through **`panel.js`** — toggled from the header chrome or other entry points as the product defines. +- Listens for panel open events from **`nx-canvas-header`** and opens the matching panel via **`panel.js`**. Owns a `CANVAS_PANELS` config object keyed by position — each entry carries a default `width` and a `getContent` callback. `openCanvasPanel(position)` does a config lookup and calls `openPanel` directly; no dependency on block-specific helpers like `openChatPanel`. +- Owns the panel configuration for its context: which positions are supported and what `getContent` each panel uses. Browse or other page-level blocks define their own panel configurations independently. +- Side regions use the same panel model: **`before`** / **`after`** asides can host in-context browser, history, metadata, outline, etc., registered as consumers through the managed panel — toggled from the header chrome or other entry points as the product defines. - Adopts **`canvas.css`** once on the document for light-DOM rules that apply outside the header shadow root. ### Browse Block @@ -120,7 +129,7 @@ The edit experience is implemented as the **`canvas`** block — there is not a - Owns conversation state, tool execution, agent communication, and context item accumulator - Consumes workspace context (org, site, path, view) as read-only - Runs in Browse and in the edit (`canvas`) view with the same UI; only view context sent to the backend changes -- In the app-frame experiment, the chat surface may be loaded as fragment content inside a **`before`** panel opened from the canvas header. +- Chat is position-agnostic — host blocks (canvas, browse) decide to mount it in the `before` panel via their own `getContent`. Once mounted, chat owns its full internal layout: header, context-sensitive actions, and the close control. ### Shared Block Provides shared functionality for Browse, the edit (`canvas`) experience, and Chat. Examples, non-exhaustive: @@ -157,4 +166,6 @@ Chat receives host-pushed context (URL-derived workspace state + accumulated ite **Configured extensions** — we did not author them. Absent by default, sandboxed, minimal contract only. +In the panel system this maps directly: core features are first-party consumers and may add actions to the managed panel header; configured extensions are third-party consumers and receive only the content area. + The API for third-party extensions is defined by the extensions SDK. diff --git a/nx2/blocks/browse/browse.css b/nx2/blocks/browse/browse.css index e4a7ad4e..7fe7a6aa 100644 --- a/nx2/blocks/browse/browse.css +++ b/nx2/blocks/browse/browse.css @@ -28,7 +28,44 @@ min-width: 0; } + .browse-bar { + display: flex; + align-items: center; + flex-shrink: 0; + height: 40px; + padding: 0 var(--s2-spacing-100); + + path { + fill: currentcolor; + } + } + + .browse-panel-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + border-radius: var(--s2-corner-radius-400); + background: transparent; + color: var(--s2-gray-800); + cursor: pointer; + + svg { + display: block; + width: 16px; + height: 16px; + flex-shrink: 0; + } + + &:hover { + background-color: var(--s2-gray-75); + } + } + .browse-hint { + margin: 0 var(--s2-spacing-300); padding: var(--s2-spacing-200); border-radius: var(--s2-corner-radius-300); background-color: var(--s2-gray-75); diff --git a/nx2/blocks/browse/browse.js b/nx2/blocks/browse/browse.js index 38b0735b..bd26a40e 100644 --- a/nx2/blocks/browse/browse.js +++ b/nx2/blocks/browse/browse.js @@ -1,5 +1,7 @@ import { LitElement, html, nothing } from 'da-lit'; import { loadStyle, hashChange } from '../../utils/utils.js'; +import { loadHrefSvg, ICONS_BASE } from '../../utils/svg.js'; +import { getPanelStore, openPanel } from '../../utils/panel.js'; import { listFolder } from './browse-api.js'; import { contextToPathContext, @@ -11,6 +13,7 @@ import '../shared/breadcrumb/breadcrumb.js'; import './list/list.js'; const styles = await loadStyle(import.meta.url); +const panelIcon = await loadHrefSvg(`${ICONS_BASE}S2_Icon_SplitLeft_20_N.svg`); const documentLayoutStyles = await loadStyle( new URL('overrides.css', import.meta.url).href, @@ -32,6 +35,14 @@ class NxBrowse extends LitElement { } } + _openPanel(position) { + this.dispatchEvent(new CustomEvent('nx-browse-open-panel', { + bubbles: true, + composed: true, + detail: { position }, + })); + } + connectedCallback() { super.connectedCallback(); this.shadowRoot.adoptedStyleSheets = [styles]; @@ -103,8 +114,21 @@ class NxBrowse extends LitElement { render() { const ctx = this._pathContext; + const bar = html` +
+ +
+ `; + if (!ctx) { return html` + ${bar}

Nothing to show here yet

@@ -117,7 +141,7 @@ class NxBrowse extends LitElement { const title = ctx.pathSegments.at(-1) ?? ''; if (!this._listError && this._items === undefined) { - return nothing; + return bar; } const header = html` @@ -131,6 +155,7 @@ class NxBrowse extends LitElement { if (this._listError) { return html` + ${bar} ${header}