feat(#332): Dashboard Command/Ctrl-drag tile move, text selection, shared cell-detail - #337
Conversation
…ared cell-detail
Replace the Dashboard's native whole-card HTML5 drag with a modifier-gated
pointer gesture and wire table/log cells into the shared Workbench cell-detail
drawer — no Dashboard-specific viewer.
Tile reorder (dashboard.ts): a move starts only on primary-button + Command/Ctrl
+ a 4px movement threshold, matching the schema graph's interaction model. A
plain (unmodified) drag never reorders and never calls preventDefault, so text
selection works inside Markdown/text, table, and logs tiles. Hit-testing is a
pure core/tile-reorder.ts helper over rects captured at drag-start (happy-dom's
elementFromPoint always returns null). A completed move dispatches the existing
atomic move-tile command exactly once and preserves canonical dashboard.tiles[]
order; a cancelled gesture (pointercancel / window blur / Escape) changes
nothing, and the click a completed move synthesizes on its origin card is
swallowed so it never activates a cell. While Command/Ctrl is held the grid
shows a grab affordance (.dash-grid.modkey); an active move dims the moving card
and outlines the drop target. Only one gesture runs at a time, and a flow-engine
KPI tile's unplaced card is skipped from hit-testing.
Cell detail: the Dashboard table/logs onCell (was a no-op) now calls the shared
openCellDetail (results.ts) in the Dashboard's own document, in both edit and
read-only modes — passing the source column name/type and the raw (untruncated)
value. core/logs.ts logRowDisplay is widened to carry {name,type,raw,display}
per field so each source-backed log field (time, level, message, each extra) is
independently clickable and keyboard-operable (Enter/Space); a selection gesture
ending on a field never opens the drawer. DashboardApp gains prefs for the
shared drawer's resize persist.
Read-only Dashboards never enable tile movement. Pure logic (hit-test, row
shaping) stays in core/ at 100%; per-file coverage gate satisfied.
Reconciles the #286 "pointer-drag-only" note in roadmap #68.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Owner UX rework of the #332 tile-move gesture. The modifier-only pointer drag (with only a faint drop-target outline) is replaced by a direct-manipulation model: - A move now starts from the top-left GRIP with no modifier, or from the tile body with ⌘/Ctrl held (the schema-graph modifier model). A plain body drag still selects text. - On the Grid Tiles (grafana-grid) engine the dragged tile lifts (position:fixed + transform follow) with a same-size placeholder holding its grid slot, and the sibling tiles reflow live (FLIP, honoring prefers-reduced-motion) to open the gap. The move commits only when the dragged tile overlaps a destination slot by >=2/3 of its own area (resolveOverlapInsertIndex, core/tile-reorder.ts) and otherwise snaps back. The dragged tile takes the slot it overlaps, and the placeholder previews at the exact final index the move-tile splice commits to (no forward-drag off-by-one). The flow engine keeps the simpler point hit-test drag (its KPI band has no grid slot to reflow into). - Snap-back / restore is synchronous and independent of the signature-gated grid reconcile (a snap-back leaves currentDoc unchanged, so the next publish would otherwise skip the DOM rebuild); restoreDrag() removes the placeholder, clears the float styles, restores the tile's grid height, clears sibling FLIP transforms, and force-invalidates lastGridSig. Pure geometry (overlap resolve, FLIP delta) lives in core/tile-reorder.ts at 100%; the DOM wrapper is unit-tested under happy-dom (float/placeholder state, grip vs ⌘/Ctrl vs plain-body gating, forward/backward commit, snap-back + Escape/cancel restore, reduced-motion). Reconciles the #286 note in roadmap #68. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
|
Follow-up (commit b613f78): reworked the tile-move gesture per owner feedback — the modifier-only drag with a drop-target outline is replaced by grip-drag (no modifier) / ⌘-Ctrl body-drag + live grafana-grid reflow (dragged tile lifts and follows, siblings shift to open a gap) with a ≥2/3-overlap commit, else snap back. Snap-back restore is synchronous (independent of the signature-gated reconcile). Flow engine keeps the simpler point-hit-test drag. An adversarial review caught a forward-drag preview/commit off-by-one (placeholder previewed one slot earlier than the splice landed) — fixed so the placeholder previews the exact committed slot, with a regression test. Gate + build green; CHANGELOG and #68 reconciled. |
Two owner-requested refinements on top of the grip-drag/live-reflow rework: - **Commit threshold measures the TARGET slot, not the dragged tile.** `resolveOverlapInsertIndex` now commits when the dragged tile covers >=2/3 of the destination slot's area (was >=2/3 of the dragged tile's own area). This is the correct behavior when tiles differ in span/height — a small tile dropped inside a big slot no longer commits just because it's fully contained; it must cover 2/3 of the slot. Zero-area candidate slots are skipped. - **Markdown cell values get a rendered preview in the cell-detail drawer.** When a table/log cell value looks like Markdown (new pure `looksLikeMarkdown` detector in `core/cell.ts` — headings, lists, blockquotes, fenced code, thematic breaks, links; conservative, HTML checked first), the drawer opens on a Rendered<->Source toggle (mirroring the existing HTML path) and renders via the docs pane's own top-quality `renderDocMarkdown` viewer (`ui/doc-markdown- view.ts` over `core/doc-markdown.ts`) — bounded, fail-closed (no innerHTML, links via `defaultDocLinkPolicy`, no CodeViewer seam so fences fall back to plain <pre>). Plain non-markup text still shows as source only. The HTML/MD toggle is refactored into one shared `mountToggle` helper. No e2e harness imports results.ts/renderDashboard, so the new marked-backed doc-markdown import adds no unbundled-harness bare-import dependency. Pure logic (overlap resolve, looksLikeMarkdown) stays 100%-covered in core/; results.ts and dashboard.ts hold their gates. CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
|
Two more refinements (commit 25cb573), all CI green (test/e2e ×2/bundle/docker/size):
Verified live on otel (build 25cb573): a Markdown cell renders # Heading→h4 + list + blockquote with a working Source toggle; no e2e harness imports results.ts, so the new marked-backed import adds no raw-ESM bare-import dependency. Coverage gates hold (core/cell.ts + core/tile-reorder.ts 100%; results.ts/dashboard.ts within gate). |
…preview; retire markdown-lite The cell-detail Markdown preview shipped for table/log cell VALUES, but the Dashboard's Markdown lives in a Text tile — clicking it did nothing. Per owner follow-up, the Text (Markdown) tile now: - **renders inline through the shared, top-quality doc viewer** (`renderDocMarkdown` over `core/doc-markdown.ts`) instead of the former lightweight markdown-lite renderer — so the app has ONE Markdown paradigm. `renderPanelMarkdown` (panels.ts) wraps it in the existing `.md-view` container (keeps the dashboard tile-containment CSS) and uses a panel-appropriate link policy (any absolute http(s) URL passes; scheme-less/other schemes render as literal text — NOT the docs pane's `defaultDocLinkPolicy`, which would rewrite to clickhouse.com/docs). No CodeViewer seam (a Text panel mounts no CM6) so SQL fences fall back to a plain <pre>; images/raw HTML/rejected links stay literal (fail-closed). - **is click / Enter-Space openable into the shared cell-detail drawer** showing the full Markdown (resizable, over the same viewer) — useful when the authored content overflows the tile. A drag-select inside the tile, or a click on an inner link, never opens it. Works in edit and read-only modes. `core/markdown-lite.ts` (+ its test) is deleted — it was imported only by panels.ts and superseded by the doc viewer. `panels.renderMarkdown` is replaced by `renderPanelMarkdown`; the workbench Text-panel preview goes through the same path. doc-markdown-view's header comment updated to note it is now the app's single Markdown renderer. Gate green (cell.ts/tile-reorder.ts 100%; panels.ts lines 100; dashboard.ts/ results.ts within gate); build + arch clean. No e2e harness imports panels.ts/ results.ts, so the doc-markdown (marked) graph reaches no raw-ESM harness. CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
|
Fixed the reported issue (clicking the Markdown tile did nothing) — commit c4553c6, all CI green (test/e2e ×2/bundle/docker/size):
Verified live on otel (c4553c6): the "About this demo" tile renders |
What & why
Closes #332.
Improves Dashboard interaction consistency and data inspection, interaction-wiring only — no Dashboard schema, query execution, panel format, or persistence semantics change.
1. Command/Ctrl-drag tile movement. The native whole-card HTML5
draggable(and itsdragstart/dragover/dropwiring) is removed. Tile reorder is now a modifier-gated pointer gesture that matches the schema graph (#55): a move starts only onprimary button + Command/Ctrl + a 4px movement threshold. A plain drag never reorders and neverpreventDefaults an unmodified pointerdown, so text now selects and copies inside Markdown/text, table, and logs tiles. Hit-testing is a purecore/tile-reorder.tshelper over rects captured at drag-start (happy-dom'selementFromPointalways returnsnull, so hit-testing can't be a live DOM query). A completed move dispatches the existing atomicmove-tilecommand exactly once and preserves canonicaldashboard.tiles[]order; a cancelled gesture (pointercancel / window blur / Escape) changes nothing; the click a completed move synthesizes on its origin card is swallowed so it never activates a cell. While Command/Ctrl is held the grid shows a grab affordance (.dash-grid.modkey); an active move dims the moving card and outlines the drop target. Read-only Dashboards never wire movement.2. Text selection inside tiles. Falls out of (1): no native drag, and
user-selectis disabled only for the duration of an active modifier-drag. Edit chrome (grip/delete/resize) stays non-selectable.3–4. Shared cell-detail for table and logs tiles. The Dashboard
onCell(previously a no-op) now calls the shared WorkbenchopenCellDetail(results.ts) in the Dashboard's own document — in both edit and read-only modes — with the source columnname/typeand the raw (untruncated) value. No Dashboard-specific viewer is constructed.core/logs.tslogRowDisplayis widened to{name,type,raw,display}per field so each source-backed log field (time, level, message, and each extra) is independently clickable and keyboard-operable (Enter/Space); a selection gesture ending on a field never opens the drawer.DashboardAppgainsprefsfor the shared drawer's resize-persist seam.Layering
src/core/: newtile-reorder.ts(threshold + rect hit-test) and the widenedlogs.tsrow-shaping, both 100% covered.src/ui/dashboard.tsstays a thin adapter over the pure helpers; the library-free pointer wiring is fully unit-tested under happy-dom.Review
A self-review pass caught and fixed an inverted click-suppression (a same-tile ⌘-drag would have spuriously opened cell-detail) and a vacuous suppression test; both are now covered. Added guards against overlapping gestures and a flow-KPI tile's unplaced card being a phantom drop target.
Checklist
npm testpasses (per-file coverage gate)npm run buildsucceeds (single-filedist/sql.html)src/core/, DOM insrc/ui/CHANGELOG.md([Unreleased]) updatedTest plan
🤖 Generated with Claude Code