From acacd6cabf5ecb7e348c33726d8080b3f0308db5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 7 Aug 2026 15:46:34 +0200 Subject: [PATCH 1/4] feat(devtools): add the plugin workspace layout tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace layout becomes a tree of splits and tab groups so plugins can be arranged in rows, columns and stacks instead of one equal-width flex row. This commit is the maths only: no UI is wired up and no dependency is added yet. Everything in `layout-tree.ts` is pure and imports nothing. That is deliberate. jsdom has no layout engine, so `getBoundingClientRect` returns zeros, and rect maths verified through the DOM would only be verifying its own mocks. Keeping it here makes it exhaustively testable — 67 cases in 58ms — and keeps the layout logic out of the components. Every returned tree upholds the same invariants: a group has at least one tab, a split has at least two children, sizes match the child count and sum to 1, the active index names a real tab, and a plugin id appears at most once. `prune` restores them bottom up after any edit, so closing a tab can collapse an emptied group, unwrap a single-child split, and flatten a same-direction nested split without the callers knowing. `repairLayout` cannot throw. A malformed layout is a data problem, the same as the unknown plugin ids that are already pruned on load, and it must not stop the panel from opening; storage *access* errors still propagate. It prunes unknown and duplicated ids, renormalises sizes, clamps the active index, and falls back to salvaging whatever plugin ids it can find from an unrecognisable shape so a bad write costs the arrangement but not the open plugins. The hostile-input test caught a real stack overflow on a self-referencing object, so reads are depth capped and the salvage walk tracks visited objects. Design and the decisions behind it, including the measured bundle cost of each neodrag primitive, are in docs/superpowers/specs/2026-08-07-plugin-layout-tree-design.md. --- .../2026-08-07-plugin-layout-tree-design.md | 227 ++++++ .../devtools/src/utils/layout-tree.test.ts | 656 ++++++++++++++++++ packages/devtools/src/utils/layout-tree.ts | 588 ++++++++++++++++ 3 files changed, 1471 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-07-plugin-layout-tree-design.md create mode 100644 packages/devtools/src/utils/layout-tree.test.ts create mode 100644 packages/devtools/src/utils/layout-tree.ts diff --git a/docs/superpowers/specs/2026-08-07-plugin-layout-tree-design.md b/docs/superpowers/specs/2026-08-07-plugin-layout-tree-design.md new file mode 100644 index 00000000..db3067a9 --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-plugin-layout-tree-design.md @@ -0,0 +1,227 @@ +# Plugin Workspace Layout Tree, Drag and Drop, and Resizing + +- **Date:** 2026-08-07 +- **Status:** Design agreed, not yet implemented +- **Scope:** `@tanstack/devtools`, `@tanstack/devtools-e2e`, and the e2e apps +- **Depends on:** PR #492 (`codex/tanstack-devtools-workbench`) merged first +- **Supersedes:** the non-goal in `2026-07-31-tanstack-devtools-branding-design.md` that reads "No per-pane splitter or per-pane resizing is added. Plugin panes remain equal-width." + +## Context + +The Plugins destination shows active plugin panes in one flex row. Each pane is `flex: 1 1 0px`, separators are decorative, and `MAX_ACTIVE_PLUGINS` is 3. Pane order comes from `state.activePlugins`, which is the only record of which plugins are open. + +Users want to open up to 9 plugins, arrange them in splits and stacks, resize them, and rearrange them by dragging. `@neodrag/solid` supplies the interaction layer. + +Two properties of the existing code constrain every decision below. + +**Plugins own their DOM.** `plugin.render(mount, props)` hands a mount node to third-party code. `examples/react/basic/src/setup.tsx:89` registers a plugin whose whole body is `