Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/plugin-workspace-layout-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@tanstack/devtools': minor
---

feat: arrange plugin panes in splits and stacked tabs, drag and resize them, and raise the limit to eighteen

The Plugins destination is now a workspace instead of a fixed row. Panes can sit side by side, above and below each other, or stacked as tabs in one group, and the arrangement is a tree that persists across reloads along with each pane's size and which tab is selected. Up to eighteen plugins can be open, up from three, because a stacked tab costs no space.

Drag a pane's tab onto the edge of another pane to split it, onto its middle to stack, or onto another tab bar to move it there. Drag the gutter between two panes to resize: one grows by exactly what the other loses, and neither can shrink below a readable minimum. Where the panel is too short to split without leaving an unreadable cell, the same drop becomes a stacked tab rather than being refused. The tab being carried follows the cursor, and a highlight shows where it will land.

The Plugins strip now lists only the plugins that are _not_ open, so each plugin has exactly one control: its strip entry while closed, its pane tab once open. Entries can be dragged out of the strip to place a pane exactly where you want it instead of appending it, including onto an empty workspace, where it takes the whole area. The strip folds itself away once everything is open and returns when a plugin closes.

Every one of those actions has a keyboard equivalent, because the pointer gestures are suppressed while the panel is detached into a picture-in-picture window. `Enter` picks a pane up, the arrow keys choose where it goes, `Enter` drops it and `Escape` puts it back; gutters take arrow keys, `Shift`-arrow and `Home`/`End`, the same pattern the whole-panel resizer already used. Picking up and dropping is announced to screen readers.

For plugin authors, two guarantees are now explicit. A pane's mount node is never removed from the document while the plugin is open, whatever the user does to the layout, so an `<iframe>` will not reload and a `<canvas>` will not lose its context. And `destroy` is called exactly once, when the plugin closes, before the node is detached — not when a pane is moved, resized, or hidden by navigating to another destination.

`state.activePlugins` in `localStorage` is superseded by `state.layout`. Existing state is migrated on first read, reopening as a single group in the order it recorded, and an arrangement that cannot be read is repaired rather than throwing: unknown plugin ids are dropped, empty groups close up, and a wholly unusable entry falls back to reopening whatever plugins it can still identify.
1 change: 1 addition & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{ "label": "Installation", "to": "installation" },
{ "label": "Configuration", "to": "configuration" },
{ "label": "Plugin Configuration", "to": "plugin-configuration" },
{ "label": "Plugin Workspace", "to": "plugin-workspace" },
{ "label": "Vite Plugin", "to": "vite-plugin" },
{ "label": "Production", "to": "production" }
],
Expand Down
107 changes: 107 additions & 0 deletions docs/plugin-workspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: Plugin workspace
id: plugin-workspace
---

The Plugins destination arranges open plugins in a workspace you can rearrange:
panes side by side, stacked on top of each other, or grouped as tabs. Up to
eighteen plugins can be open at once.

## Opening a plugin

The Plugins strip lists the plugins that are **not** open. Once a plugin has a
pane, its entry leaves the strip — the pane's own tab is where you select and
close it, so there is only ever one control for it.

- **Click** an entry to open it beside the others, sharing the space equally.
- **Drag** an entry down into the workspace to choose where it lands, using the
same zones as below. Dropping onto an empty workspace gives it the whole area.

The strip folds itself away when everything is open, and comes back on its own the
moment a plugin closes and returns to it.

## Arranging panes

Each pane has a tab above it. **Click** a tab to bring that pane to the front of
its group; **drag** it to rearrange:

| Drop it on | Result |
| --- | --- |
| the left or right quarter of a pane | the pane splits into a column, and the tab takes the new side |
| the top or bottom quarter of a pane | the pane splits into a row |
| the middle of a pane | the tab joins that pane as a stacked tab |
| another pane's tab bar | the tab moves into that group, at the position you drop it |

A highlight shows where the pane will land before you let go, and the tab you are
carrying follows the cursor so it is clear which pane is moving.

Dropping on a tab bar always means "put it in this group" rather than splitting the
top edge, so the two gestures never compete for the same few pixels.

### When a pane is too small to split

The devtools panel is short, so splitting a small pane would leave both halves
unreadable. When there is not enough room, the same drop becomes a **stacked tab**
instead of being refused. Stacking costs no space, so every plugin stays usable in
a short panel. The highlight tells you which is about to happen.

At the default panel height a vertical split has no room at all, so drops on a top
or bottom edge stack instead. Drag the panel taller first if you want rows.

## Resizing

Drag the gutter between two panes. One pane grows by exactly what the other loses,
and neither can shrink past a readable minimum.

## Keyboard

Everything above works without a pointer. This matters in a detached
picture-in-picture window, where the pointer gestures are deliberately turned off
(the same way the panel's own resize handle is) but the keyboard still works.

**Moving a pane** — focus a tab, then:

| Key | Action |
| --- | --- |
| `Enter` or `Space` | pick the pane up, or drop it if already held |
| `Arrow` keys | choose where it goes — a neighbouring pane, or a side of its own pane to split |
| `Escape` | put it back |

**Resizing** — focus a gutter, then:

| Key | Action |
| --- | --- |
| `Arrow` keys | move the gutter |
| `Shift` + `Arrow` | move it in larger steps |
| `Home` / `End` | push it as far as it will go |

Picking a pane up and putting it down is announced to screen readers.

## What persists

The arrangement is saved to `localStorage` and restored on reload, including which
tab is selected in each group and the size of every pane.

If a plugin is no longer registered, it is dropped from the saved arrangement and
the panes around it close up. If the saved arrangement cannot be read at all, the
devtools recover by reopening whichever plugins it can identify as a single group,
rather than refusing to open.

State saved by an older version, which recorded only *which* plugins were open,
reopens as one group in that order. This happens once and is then saved in the new
form.

## Notes for plugin authors

Panes are never re-parented. A plugin's mount node stays a child of the same
element for as long as the plugin is open, whatever you do to the layout, so an
`<iframe>` will not reload and a `<canvas>` will not lose its context when the user
drags panes around.

`destroy` is called when the plugin closes, once, and before its mount node is
removed — so the node is still there if you need to clean up inside it. Moving,
splitting, resizing and switching destinations do not call it. Navigating to
Marketplace, SEO or Settings leaves your pane mounted and hidden.

`render` is called again when the theme or the panel's open state changes, as
documented in [plugin lifecycle](./plugin-lifecycle.md).
Loading
Loading