Skip to content

feat: detached-tab primitive; Pipeline gains a tab; Data Pane Expand (#100) - #107

Merged
BorisTyshkevich merged 5 commits into
mainfrom
feat/detached-tab-primitive-100
Jul 1, 2026
Merged

feat: detached-tab primitive; Pipeline gains a tab; Data Pane Expand (#100)#107
BorisTyshkevich merged 5 commits into
mainfrom
feat/detached-tab-primitive-100

Conversation

@BorisTyshkevich

@BorisTyshkevich BorisTyshkevich commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What & why

Closes #100.

Extracts the schema graph's real-tab/overlay-fallback logic (openInTab/openInOverlay/buildGraphPanel/mirrorTheme in src/ui/explain-graph.js) into a shared primitive, openInDetachedTab (src/ui/detached-view.js), used by three consumers:

  • Schema graph — existing tab/overlay behavior unchanged (verified line-by-line against the pre-existing ~57-test suite).
  • EXPLAIN Pipeline graph — gains real-tab support (previously overlay-only).
  • Data Pane (new) — an Expand button next to Copy in the results toolbar opens a snapshot of the current grid — with the full Table/JSON/Chart switcher, same as the inline pane — in a tab or the in-app overlay. It's a frozen snapshot: it does not update if the query is re-run afterward (live-sync would need a BroadcastChannel/postMessage bridge — real additional scope, not built speculatively here). The active view/sort/column-widths/chart-config are all local to the snapshot — switching them there never touches the live tab's own resultView/chart state, and vice versa (confirmed live: running a new query in the main tab doesn't blank or repaint the already-open snapshot).

Also fixes the issue's named blocker: renderGrid (src/ui/results.js) built its <table>/<thead>/<tbody>/<tr> via raw document.createElement instead of h(), which would build nodes in the wrong document when called inside a detached tab.

app.state.detachedView (a count, not a bool — so several views can be open at once) tracks how many detached views are currently open, per the issue's reactivity requirement.

Scope decision vs. the acceptance-bullet wording

The acceptance criteria's parenthetical says the Data Pane snapshot should support "sort/copy/export/row-limit." I shipped sort (free via renderGrid) and copy, but deliberately did not add Export or an interactive row-limit selector to the Data Pane:

  • Export already exists as a separate feature (the editor-toolbar Export button) that re-runs the live query and streams it to disk — it's about the query, not the rendered grid, so it doesn't fit a frozen snapshot's semantics, and wiring it into a second-window context risks a showSaveFilePicker user-activation mismatch that's out of scope to solve here.
  • Row-limit as an interactive re-fetch control contradicts "snapshot, not live-sync" — there's nothing to re-fetch against a frozen r. The existing truncation note (renderGrid's own cap param) carries over automatically.

Real bug fixed along the way

copyResult() resolved navigator.clipboard off the main window unconditionally. Calling it from a click that originated in a detached tab (a different top-level browsing context) could be rejected by the Clipboard API's focused-document permission model. Refactored into copySnapshot(r, targetDoc), resolving navigator from the target document's defaultView (verified live: granting clipboard-write on a fresh browser context, Copy succeeds from inside a real detached tab).

Found & fixed via /code-review (8 finder agents + self-verification)

  • The primitive originally appended its own ✕ close button to the title bar immediately after mount() returned — but the schema graph builds its own action cluster (theme/zoom/undo-redo) asynchronously, later, via render(). Appending the ✕ eagerly meant it ended up stranded next to the title instead of staying the rightmost control once the graph loaded. Fixed by threading a pre-wired closeBtn into mount() instead, so each caller places it wherever its own trailing actions cluster ends up (restores the original schema layout exactly).
  • The new Data Pane's overlay fallback had no Escape-to-close at all, unlike every sibling detached view. Added, with the same nested-drawer-first priority the schema graph uses (a .cd-backdrop cell-detail drawer closes on the first Escape, the pane on the second).
  • openInDetachedTab's try/catch originally wrapped mount()'s execution, not just the window-open attempt — a bug inside a caller's content-building code would have been misattributed as "popup blocked," producing a duplicate overlay plus an orphaned broken tab. Narrowed the try to only the window-open + cross-realm .document access.

Follow-up: full Table/JSON/Chart switcher in the Data Pane

A later commit adds the switcher the acceptance criteria didn't originally call for but was requested in review. renderChart gained optional tab/rerender/setChart/running overrides (all defaulting to today's main-pane behavior — app.activeTab()/renderResults(app)/app.chart/app.state.running.value, so the main pane's call site is unchanged) so the Data Pane can supply its own local chart-config holder, local repaint, and chart-instance slot instead. viewSwitcherTabs is extracted as a shared helper (second consumer — schema/pipeline don't use it, but the main toolbar and the Data Pane both build the identical Table/JSON/Chart tab strip).

Real bug found and fixed while wiring this up: Chart.js rendered nothing at all inside a detached tab — first a 0×0 canvas, then (after a partial fix) correctly laid-out axes/labels but no visible bars/points. Root cause: Chart.js's responsive sizing and resize-triggered relayout (getComputedStyle, ResizeObserver, window.devicePixelRatio, and a debounced update() gated on an "is this canvas attached" check) all read through APIs bound to the window Chart.js's own bundled module executes in — always the main window, even when the canvas it's drawing into belongs to a completely different document. renderChart now forces one explicit chart.resize(w, h) + chart.update('resize') off the canvas's own geometry (offsetWidth/offsetHeight — plain DOM properties, realm-agnostic, and already pre-html{zoom} per zoomScale's own doc comment) via a requestAnimationFrame scheduled on the canvas's own window, once it's actually attached. Verified against the live antalya cluster: bars/labels render correctly in the detached tab now; the main pane's chart (which never hit this cross-realm path) is unaffected.

Follow-up: detached tabs showed the browser's generic favicon

window.open('', '_blank') opens an about:blank tab, which ships no favicon either (same problem the stylesText seam already solves for CSS). Added a faviconHref seam (mirrors stylesText) and copy a matching <link rel=icon> into the child tab's <head> alongside the mirrored stylesheet, for all three consumers. Verified live: the Data Pane tab's favicon now matches the main page's exactly.

Verification

  • npm test: 1222/1222 passing, src/ui/detached-view.js at 100/100/100/100, results.js/explain-graph.js/app.js within their existing per-file floors.
  • npm run build: succeeds.
  • npm run test:e2e: 39/39 passing (chromium/firefox/webkit).
  • Manual verification against the live antalya ClickHouse cluster (26.3.10) via Playwright: Data Pane Expand opens a real tab with a working grid/Copy/cell-drawer, Escape priority behaves correctly, the Table/JSON/Chart switcher works (including the Chart.js cross-window fix — bars render), switching the Data Pane's chart type doesn't touch the live tab's own chart config, and running a new query in the main tab doesn't disturb an already-open snapshot. Pipeline Expand now opens in a real tab with working pan/zoom and no JS close button. (Schema-graph's live path wasn't re-driven — its trigger is a drag-and-drop gesture — relying on the existing test suite + direct code trace for that one.)

Reconciled

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency
  • README / CHANGELOG.md ([Unreleased]) updated
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG)

BorisTyshkevich and others added 5 commits July 1, 2026 11:44
…100)

Extract the schema graph's real-tab/overlay-fallback logic into a shared
openInDetachedTab helper (src/ui/detached-view.js), now used by the schema
graph, the EXPLAIN pipeline graph (gains real-tab support), and a new Expand
button next to Copy in the results toolbar that opens a sortable/resizable
snapshot of the current grid in a tab or overlay. Fixes renderGrid's raw
document.createElement calls (the known detached-tab-safety blocker) and a
cross-window clipboard/navigator resolution risk in copyResult/copySnapshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Give the detached Data Pane the same view switcher as the inline results
pane (Table/JSON/Chart), scoped locally so switching view or chart config
there never touches the live tab's own resultView/chart state. Extracted
viewSwitcherTabs as a shared helper (second consumer). renderChart now takes
optional tab/rerender/setChart/running overrides so a detached snapshot can
supply its own chart-config holder, local repaint, and chart-instance slot
instead of the main pane's app.activeTab()/renderResults()/app.chart.

Also fixes Chart.js rendering nothing when its canvas lives in a detached
tab: Chart.js's responsive sizing and resize-triggered relayout read layout
through APIs (getComputedStyle, ResizeObserver, window.devicePixelRatio)
bound to the window its own module runs in — always the main window, even
for a canvas in a different document. renderChart now forces an explicit
resize + a 'resize'-mode update off the canvas's own geometry (a plain DOM
method, realm-agnostic) once it's actually attached.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
about:blank ships no favicon, so every real browser tab opened via
openInDetachedTab (schema graph, pipeline, Data Pane) showed the browser's
generic default icon instead of the app's own. Add a faviconHref seam
(mirrors the stylesText seam) and copy a matching <link rel=icon> into the
child tab's <head> alongside the mirrored stylesheet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mitive-100

# Conflicts:
#	CHANGELOG.md
#	src/ui/app.js
#	tests/helpers/fake-app.js
@BorisTyshkevich
BorisTyshkevich merged commit 2ac3e44 into main Jul 1, 2026
6 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/detached-tab-primitive-100 branch August 6, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detached-tab pattern: extract shared primitive; add Data Pane Expand + convert Pipeline Expand

1 participant