so child hooks work. Stable. */
+ controller: LayoutController
+ getRootProps: & { ref?: React.Ref }>(props?: P) => P
+ tabsets: readonly TabsetState[] // changes only on structural changes
+ splitters: readonly SplitterState[]
+ tabIds: readonly string[]
+ dropIndicator: { target: DropTarget; props: SlotProps } | null
+ rootDropZones: readonly { side: Side; isActive: boolean; props: SlotProps }[] // empty unless dragging
+ actions: LayoutActions // stable
+}
+export function useDynamixLayout(options: UseDynamixLayoutOptions): UseDynamixLayoutResult
+export function DynamixLayoutProvider(props: { controller: LayoutController; children: ReactNode }): ReactElement
+
+export function useTabset(tabsetId: string): {
+ tabset: TabsetState; isRotated: boolean
+ getPanelProps: PropGetter; getTabBarProps: PropGetter // tablist role, drag source for the whole tabset
+ toggleMaximize(): void; toggleFold(): void
+}
+export function useTab(tabId: string): {
+ tab: TabState; isActive: boolean; isDragging: boolean
+ select(): void; close(): void
+ getTabProps: PropGetter // role=tab, aria-*, roving tabindex, pointer drag, keyboard
+ getTabContentProps: PropGetter // role=tabpanel, positioning ref
+}
+export function useSplitter(splitterId: string): {
+ splitter: SplitterState; isDragging: boolean; direction: Direction
+ value: number; min: number; max: number // px, for custom UI
+ getSplitterProps: PropGetter // role=separator, aria-*, pointer + keys
+}
+export function useDragState(): DragState | null
+```
+
+The brief's example, corrected so the child hooks have context:
+
+```tsx
+function MyLayout({ tabs }: { tabs: MyTab[] }) {
+ const { controller, getRootProps, tabsets, splitters, tabIds, dropIndicator } = useDynamixLayout({ tabs })
+ return (
+
+
+ {tabsets.map((ts) =>
)}
+ {tabIds.map((id) =>
)} {/* flat: never remounts */}
+ {splitters.map((s) =>
)}
+ {dropIndicator &&
}
+
+
+ )
+}
+```
+
+`useTabDrag` from the brief became `useDragState`. Drag *initiation* is already part of `getTabProps` and `getTabBarProps`, so a separate drag hook would be a second way to do the same thing. `useContainerRect` stays internal; `getRootProps` wires it.
+
+**Prop getters** merge rather than overwrite:
+- `ref`s are combined.
+- `className`s are joined.
+- `style` merges user style first, then positioning (`left/top/width/height/transform`) last.
+- Event handlers run the user's first; if the user calls `event.preventDefault()`, the internal handler is skipped (documented).
+
+A missing provider throws in development: `` `useTab` must be used inside or . ``
+
+### 3.7 Level 3 — state and actions anywhere
+
+```ts
+export function useLayoutState(selector: (snapshot: LayoutSnapshot) => T, isEqual?: (a: T, b: T) => boolean): T
+export function useLayoutActions(): LayoutActions // stable object
+export type LayoutActions = Pick
+```
+
+`useLayoutState` is built on `useSyncExternalStore` with a memoized selector: it caches by snapshot reference plus selector and checks `isEqual`, defaulting to `Object.is`. It's about 30 lines and needs no dependency. Because snapshots share structure (§2.5), `s.tabsets.get(id)?.activeTabId` re-renders only when that value changes; a test asserts the render counts (§10).
+
+**Stability guarantee** (documented in the API docs):
+
+| Value | Stable across renders? |
+|---|---|
+| `controller`, `actions`, every function in hook results, `select`/`close`/`toggle*`, `getXProps` | Yes, always |
+| `tabsets`, `splitters`, `tabIds` arrays | Change only when the structure changes (not on resize or drag frames) |
+| `TabsetState` / `SplitterState` / `TabState` objects | Change only when that entry changes |
+| `dropIndicator` | Changes when the target changes (not on every pointer move) |
+| Rects | Not exposed through React state; applied by the fast path |
+
+### 3.8 DOM fast path (`internal/rect-writer.ts`)
+
+It keeps v1's key performance property (R3): **pointer moves never re-render React.**
+
+1. Components register their element under `slot:id` (`panel`, `tabBar`, `tabContent`, `splitter`, `dropIndicator`, `toolbar`) through the ref in the prop getters.
+2. The writer subscribes to the engine. When `snapshot.rects` changes reference, it walks the registry and writes `left/top/width/height` (and `transform` for rotated strips) **only for rects whose object identity changed**.
+3. Visibility goes through attributes (`data-dx-hidden`, `data-dx-visible`) styled in CSS. Inline `display` is never written, for the reason in #83 (an inline `display` wipes the slot component's own value).
+4. Components *also* render positioning from the current snapshot at render time. That makes the first paint and SSR correct, and React's diff (props against props, not against the DOM) never fights the writer.
+
+The module is internal, documented in its header, and covered by a test that drags a splitter and asserts zero React commits.
+
+### 3.9 Pointer-based tab dragging (D7, replaces P7)
+
+- **Start.** `pointerdown` on a tab or tab bar, then movement past 4px for mouse/pen, or a **350ms long press** for touch so the tab bar can still be scrolled by panning. Then `setPointerCapture` and `engine.startDrag`. A click without movement selects the tab, as today.
+- **Move.** Moves are coalesced to one per frame with `createFrameScheduler`. The pointer is converted to root coordinates, and `engine.updateDrag(point, measurements)` is called. `measurements` (tab bar and label rects) is measured once at drag start and again if a tab bar scrolls.
+- **End.** `pointerup` → `engine.endDrag()` (performs the move, one `onLayoutChange`). **Escape** or `pointercancel` → `cancelDrag()`.
+- **Feedback.**
+ - The root gets `data-dx-dragging`, which keeps `.is-dragging`-style rules so iframes and editors inside tab content don't swallow events (R6).
+ - The cursor comes from CSS on the captured element; there are no `document.body.style` writes (P5).
+ - `DropIndicator` and `RootDropZone` render from `snapshot.drag`.
+- **Maximize.** As in #83, starting a drag restores maximize first, so every target is visible.
+- **Splitters** use the same module and `startDrag({ type: 'splitter' })`.
+- **Lost by this change:** native HTML5 drag events on the wrappers. Users who attached `onDragStart` and similar to custom `WrapTab*` components must move to `useDragState`. This is listed as a breaking change.
+
+### 3.10 Container measurement
+
+- A `ResizeObserver` on the root measures the content box minus `padding`. Updates are batched per animation frame, or throttled by `resizeThrottleMs`. This replaces `window.resize` (P6) and catches sidebars and flex parents.
+- The first measurement happens synchronously in an isomorphic layout effect (`getBoundingClientRect`), so there's no flash.
+- Rects are root-relative (D8), so page scroll and the layout's offset don't matter (B25).
+
+### 3.11 SSR, StrictMode, Next.js
+
+- **No `window`/`document` at import or during render.** Everything DOM-related lives in effects or event handlers. The core is pure.
+- **Deterministic first render.**
+ - Engine ids are deterministic (§2.6), and DOM ids come from `useId()` (`internal/dom-ids.ts` encodes tab ids into CSS-safe ids). This fixes B4 and B14.
+ - Before the first measurement the root carries `data-dx-measuring` (CSS `visibility: hidden` on its children). Server and client render the same markup, and the client measures before paint.
+- **`'use client'`.** The React build prepends `'use client';` to the entry chunk with a Rollup `output.banner`. `migrateLayoutFromV1` and the types come from core, which is server-safe.
+- **StrictMode.**
+ - Every effect returns a cleanup: RO disconnect, pointer listeners, frame cancel, keyboard listeners. The double mount/unmount leaves nothing behind (tested with spies, §10).
+ - React never calls `engine.destroy()`, because StrictMode remounts reuse the same engine; listener cleanup is enough.
+- The Next.js example drops `dynamic(..., { ssr: false })` and renders on the server.
+
+### 3.12 Accessibility (new)
+
+**Tabs (WAI-ARIA Tabs pattern).**
+- Tab bar: `role="tablist"`, with `aria-orientation="horizontal"` (`vertical` on a rotated strip).
+- Each tab: `role="tab"`, `id`, `aria-selected`, `aria-controls` pointing at the content id, and a roving `tabindex` (active `0`, others `-1`).
+- Content: `role="tabpanel"`, `aria-labelledby`, `tabindex="0"`.
+- Keys: ←/→ (↑/↓ on a vertical strip), Home and End move focus. With `tabActivation: 'automatic'`, focus also selects; with `'manual'`, Enter or Space selects. Delete closes a `closable` tab (calls `onTabClose`).
+
+**Splitters (Window Splitter pattern).**
+- `role="separator"`, `aria-orientation`: `vertical` for a bar between side-by-side panels, `horizontal` otherwise.
+- `aria-valuenow`/`aria-valuemin`/`aria-valuemax` as percentages of the pair, from `getSplitterBounds`.
+- `aria-controls` points at the leading panel. `aria-label` defaults to "Resize {before} and {after}", using tab titles.
+- `tabindex="0"`. Arrow keys move 10px (Shift: 50px); Home/End go to min/max. Enter folds or unfolds the leading tabset when it can fold, per APG "collapse".
+- A locked splitter gets `aria-disabled="true"`.
+
+**Toolbar.** Real ``s with `aria-label`, `aria-pressed` (maximize) and `aria-expanded` (fold), as in #83.
+
+**Keyboard tab move mode (proposed, feasible).**
+1. With a tab focused, **Ctrl/⌘ + Shift + M** starts move mode (`engine.startDrag`).
+2. ←/→/↑/↓ cycle through `engine.listDropTargets()` in reading order, and the drop indicator shows the target.
+3. An `aria-live="polite"` region announces it, for example "Move Terminal: right of Editor".
+4. **Enter** drops, and **Escape** cancels.
+
+Mouse-free moves are also possible through the imperative `moveTab`.
+
+**Shortcuts.** The maximize/fold shortcuts listen on the **root**, not `window`, and act on the focused tabset. This fixes B19, and two layouts on a page no longer both react.
+
+### 3.13 Styling
+
+- **`styles.css`** holds all defaults that v1 set as inline styles in `Default.tsx`, so `classNames` and CSS variables can override them.
+- **Class names:** `.dx-root`, `.dx-panel`, `.dx-tab-bar`, `.dx-tab`, `.dx-tab-close`, `.dx-tab-content`, `.dx-splitter`, `.dx-drop-indicator`, `.dx-root-drop-zone`, `.dx-toolbar`, `.dx-toolbar-button`.
+- **CSS variables:** `--dx-tab-bar-bg`, `--dx-tab-bg`, `--dx-tab-active-bg`, `--dx-tab-color`, `--dx-tab-radius`, `--dx-splitter-bg`, `--dx-splitter-hover-bg`, `--dx-drop-indicator-bg`, `--dx-drop-indicator-border`, `--dx-focus-ring`, `--dx-font`.
+- **State attributes:**
+ - `data-state="active|inactive"` (kept from v1);
+ - `data-dx-folded`, `data-dx-maximized`, `data-dx-rotated`, `data-dx-hidden`, `data-dx-dragging`, `data-dx-locked`, `data-dx-measuring`.
+- `classNames.{slot}` is appended to the default class, and `styles.{slot}` is merged under the positioning styles.
+
+### 3.14 React 18 and 19
+
+- **Peer dependencies:** `react` and `react-dom` `^18.0.0 || ^19.0.0` (P9).
+- **JSX runtime is external:** `external: [/^react($|\/)/, /^react-dom($|\/)/, /^@dynamix-layout\/core($|\/)/]` (P9b, B2).
+- `DynamixLayout` uses `forwardRef` (works on 18 and 19) and is typed so `ref` accepts `Ref`.
+- **Custom slot components must forward `ref`**: `forwardRef` on React 18, or a plain `ref` prop on 19. That's the same requirement as v1's `DivFC`; the docs show both.
+- CI gets a React 18 test job (§10).
+
+---
+
+## 4. Solid adapter port (minimal, D2)
+
+- The public props and components stay as they are.
+- Internally:
+ - one `createLayout` per component instance (no statics);
+ - subscription to `layout.subscribe`, applying rects with the shared core `dom/apply-rect`;
+ - `getTabOutput`'s `crypto.randomUUID` replaced by tab ids;
+ - root-relative coordinates (B25).
+- **Breaking for Solid:** `updateJSON` now emits `LayoutJSON` v2. `layoutTree` accepts v1 (auto-migrated) or v2. `LayoutTree` types are replaced by core's `LayoutJSON` / `LayoutTreeV1`.
+- **Kept as is:** HTML5 DnD, `window.resize`, the `||` defaults (B13), and the tuple `tabs`. These are listed as follow-ups in §12.
+
+---
+
+## 5. Naming
+
+### 5.1 Conventions (applied everywhere)
+
+- **Files:** `kebab-case.ts`; components `PascalCase.tsx`; hooks `use-x.ts` exporting `useX`.
+- **Full words.** Allowed short forms: `id`, `ref`, `props`, `x`, `y`.
+- **Verbs and prefixes:**
+ - functions start with a verb (`moveTab`, `computeLayoutRects`);
+ - factories are `createX`;
+ - callback props are `onX`;
+ - booleans use `is`/`has`/`can`/`should`.
+ - Boolean **props** follow React convention with `show*`/`allow*`, matching the brief's `showTabBar`.
+- **Types:** `PascalCase`, with no `I` prefix. Options types are `XOptions`, props `XProps`, return types `XResult`, and state shapes `XState`.
+- **One term per concept:**
+ - **splitter** (not bond or slider);
+ - **tabset**;
+ - **panel** (a tabset's box);
+ - **tab bar** (not tab head);
+ - **drop indicator** (not hover element);
+ - **fold** (not collapse);
+ - **weight** (not part or size).
+
+### 5.2 v1 → v2 rename table
+
+**Core.**
+
+| v1 | v2 |
+|---|---|
+| `new DynamixLayoutCore(options)` + statics `_root/_tree/_minW/_minH/_bond/_inst` | `createLayout(options)` instance; no statics |
+| `Node`, `Bond`, `Node.cache` | internal immutable `RowNode` / `TabsetNode` / `TabNode`; derived `SplitterState` |
+| `LayoutTree` | `LayoutJSON` (v2); old shape exported as `LayoutTreeV1` |
+| `NodeOptions` (tabset / bond / tab entries) | `TabsetState` / `SplitterState` / `TabState` + `snapshot.rects` |
+| `Dimension { w, h, x, y }` | `Rect { x, y, width, height }` |
+| `typNode` | `type` |
+| `uidNode` | `id` (tabs: the user's tab id; it was `nodName`) |
+| `nodName` | removed for rows and tabsets; for tabs it becomes `id` |
+| `nodPart` | `weight` |
+| `nodeDir` (boolean, inverted for tabsets) | `direction: 'horizontal' \| 'vertical'` on rows; `parentDirection` on tabsets |
+| `nodDims` | `snapshot.rects.tabsets/splitters.get(id)` |
+| `nodOpen` | `activeTabId` (tabset) / `isActive` (tab) |
+| `nodKids` | `children` (JSON, model) / `tabIds` (state) |
+| `nodFold` **[#83]** | `isFolded` |
+| `nodMaxd` **[#83]** | `maximizedTabsetId` (JSON, snapshot) / `isMaximized` (state) |
+| `nodHidden` / `nodLocked` / `nodFoldable` / `nodMaximizable` | `isHidden` / `isLocked` / `canFold` / `canMaximize` |
+| `minW` / `minH` | `minPanelSize: { width, height }` |
+| `bond` | `splitterSize` |
+| `collapsedSize` | `foldedSize` |
+| `uqid`, `tabsIds`, `timer` | removed |
+| `tabs: string[]` | `tabs: TabInit[]` (`{ id, target? }`) |
+| `tree` | `initialLayout` |
+| `updateDimension(dim, disableTimeout?, timeout?)` | `setContainerRect(rect)` (throttling moves to the adapter) |
+| `updateSlider(id, point, …)` / `updateSliderDimension` | `resizeSplitter(splitterId, point)`; `moveSplitterBy`; drags via `startDrag/updateDrag/endDrag` |
+| `updateTree(src, des, 'top'\|…\|'contain')` | `moveTab(tabId, target)` / `moveTabset(tabsetId, target)`; `'contain'` → `'center'` |
+| `collapse` / `expand` / `toggleCollapse` **[#83]** | `fold` / `unfold` / `toggleFold` |
+| `maximize` / `restore` / `toggleMaximize` / `maximizedId` | same names; `maximizedId` → `snapshot.maximizedTabsetId` |
+| `DynamixLayoutCore._root.toJSON()` | `layout.toJSON()` |
+| `clearAllCache()` | `destroy()` (per instance) |
+| iterators, `createNode*`, `calc*`, `insertNode*`, `remove*`, `moveRelative*`, `isOnlyContent`, `pruneStaleDirections`, `calculateRootAdjustment` | internal (pure functions in `tree/` and `geometry/`) |
+| `Queue`, `createReactiveState`, `areNodeOptions*Equal` | removed |
+| `getTabsetDropPreview` / `getNavbarDropPreview` / `getRootSplitPreview` / `isSameDropPreview` | `getDropTarget` + `getDropIndicatorRect` |
+| `setElementRect`, `placeTabbar`, `getTabbarPlacement`, `getTabBodyRect` | `applyRect` (`dom/`), `getTabBarRect`, `getTabContentRect` |
+| `createFrameScheduler` | unchanged (`dom/`) |
+
+**React.**
+
+| v1 | v2 |
+|---|---|
+| `tabs: [id, node][]` + `tabNames` | `tabs: { id, title?, content, closable?, target? }[]` |
+| `layoutTree` | `defaultLayout` (uncontrolled) / `layout` (controlled) |
+| `updateJSON` | `onLayoutChange(layout, { reason })` |
+| `bondWidth` | `splitterSize` |
+| `minTabWidth` / `minTabHeight` | `minPanelSize: { width, height }` |
+| `tabHeadHeight` / `enableTabbar` | `tabBarHeight` / `showTabBar` |
+| `pad: { t, b, l, r }` | `padding: number \| { top, right, bottom, left }` |
+| `enableMaximize` / `enableCollapse` / `enableDoubleClickMaximize` **[#83]** | `allowMaximize` / `allowFold` / `maximizeOnDoubleClick` |
+| `keyboardShortcuts` **[#83]** | unchanged (now scoped to focus inside the layout) |
+| `WrapTabPanel` / `WrapTabHead` / `WrapTabLabel` / `WrapTabBody` / `SliderElement` / `HoverElement` | `components.{ Panel, TabBar, Tab, TabContent, Splitter, DropIndicator }` |
+| `RootSplitterHoverEl` | `components.RootDropZone` |
+| `TabsetToolbar` **[#83]** | `components.TabsetToolbar` |
+| `TabsetToolbarProps { maximized, folded, rotated, rowIsHorizontal, showMaximize, showFold, … }` | `{ tabset, isRotated, canMaximize, canFold, onToggleMaximize, onToggleFold }` |
+| `*ElementStyles` (7) / `*ElementClass` (7) | `styles.{slot}` / `classNames.{slot}` |
+| `disableSliderTimeout` + `sliderUpdateTimeout` | removed (splitters always update once per frame) |
+| `disableResizeTimeout` + `windowResizeTimeout` | `resizeThrottleMs` |
+| `rootId` | standard `id` prop (optional; DOM ids come from `useId`) |
+| `ref` → `HTMLDivElement` | `ref` → `DynamixLayoutHandle` (`handle.element` is the div) |
+| `useDynamixLayout` (31 return values) | new headless `useDynamixLayout` + `useTabset` / `useTab` / `useSplitter` / `useLayoutState` / `useLayoutActions` / `useDragState` |
+| `getTabOutput`, `TabInput`, `TabOutput`, `TabEntry`, `DivFC`, `useDynamixLayoutOptions` | removed |
+| `DynamixLayoutProps` / `UseDynamixLayoutOptions` / `UseDynamixLayoutResult` (dead) | names reused for the real v2 types |
+| `Default*` components | `Panel`, `TabBar`, `Tab`, `TabContent`, `Splitter`, `DropIndicator`, `RootDropZone`, `TabsetToolbar` (exported for composition) |
+| `.DefaultWrapTabLabel` etc. classes | `.dx-*` classes (§3.13) |
+| `data-uid` / `data-type` | `data-dx-id` / `data-dx-slot` |
+| `@dynamix-layout/react/style.css` | `@dynamix-layout/react/styles.css` (`./style.css` kept as an alias in 2.x) |
+
+---
+
+## 6. Breaking changes (with reasons and replacements)
+
+Each item will appear in `docs/migration-v1-to-v2.md` with full before/after code. The core ones:
+
+**1. Engine construction.** Why: global state (P1).
+
+```ts
+// v1
+const engine = new DynamixLayoutCore({ tabs: ['a', 'b'], minW: 40, bond: 10 })
+engine.updateDimension({ x: 0, y: 0, w: 800, h: 600 }, true)
+const json = DynamixLayoutCore._root.toJSON()
+// v2
+const layout = createLayout({ tabs: [{ id: 'a' }, { id: 'b' }], minPanelSize: { width: 40, height: 40 }, splitterSize: 10 })
+layout.setContainerRect({ x: 0, y: 0, width: 800, height: 600 })
+const json = layout.toJSON()
+```
+
+**2. Reading state.** Why: static caches, mutable maps (B11).
+
+```ts
+// v1
+Node.cache.nodOpts.onChange((tabsets) => tabsets.forEach((t) => draw(t.uidNode, t.nodDims)))
+// v2
+layout.subscribe((s) => s.tabsets.forEach((t) => draw(t.id, s.rects.tabsets.get(t.id)!)))
+```
+
+**3. Moves.** Why: typed targets instead of loosely typed strings.
+
+```ts
+// v1
+engine.updateTree(srcUid, desUid, 'contain')
+// v2
+layout.moveTab('terminal', { type: 'tabset', tabsetId: 'ts-editor', position: 'center' })
+```
+
+**4. Debounce arguments removed from core.** Why: no timers in the engine (B12). Throttle in the adapter instead (`resizeThrottleMs`).
+
+**5. Serialized format.**
+- Why: readable names, an explicit version, and stable tab ids.
+- Old layouts load automatically. Saved data should be re-saved, or converted with `migrateLayoutFromV1`.
+- The `LayoutJSON` that apps receive is a new shape; anything reading `nodKids` and similar must change.
+
+**6. Removed internal exports.** `Node`, `Bond`, `Queue`, `createReactiveState`, the comparators, the drop-preview helpers and the DOM helpers (see the rename table). Why: they're internal (P4).
+
+**7. Import-time banner and `window.__DYNAMIX_LAYOUT__` removed.** Why: side effects (P11). Use `import { version } from '@dynamix-layout/core'` (new) for debugging.
+
+**8. Build outputs.** Why: correctness (B20, P9b).
+- `browser`, `iife` and `umd` builds are removed from react.
+- Core keeps one UMD under `unpkg`.
+- File names: `dist/index.js` and `dist/index.cjs`.
+
+**React** (each with before/after in the migration guide):
+
+9. `tabs` tuples → objects; `tabNames` → `title`.
+10. `layoutTree` → `defaultLayout` / `layout`; `updateJSON` → `onLayoutChange`, which **is no longer called on mount** and receives v2 JSON.
+11. Sizing props renamed or grouped (`minPanelSize`, `splitterSize`, `tabBarHeight`, `showTabBar`, `padding`).
+12. Debounce props: four become one (`resizeThrottleMs`). Splitter debounce is removed.
+13. Wrapper props → `components.*`. Their props change (state is passed explicitly), and `WrapTabPanel` becomes a visible background.
+14. Styling props → `classNames` / `styles`. Default class names change to `.dx-*`, and default inline styles move to `styles.css`. Custom CSS targeting `.DefaultWrap*` must be updated.
+15. `ref` now returns a `DynamixLayoutHandle`; use `handle.element` for the div.
+16. `useDynamixLayout` is a different hook (headless). The v1 internal hook and `getTabOutput` are removed.
+17. Tab drag uses pointer events. HTML5 drag events on custom wrappers no longer fire; use `useDragState`.
+18. Keyboard shortcuts only act when focus is inside the layout (v1: anywhere on the page).
+19. A tab that's in `tabs` but missing from the saved layout is now **added** (v1 dropped it silently). A layout tab that's missing from `tabs` is removed (v1 showed a label with no body).
+20. Duplicate tab ids throw in development.
+21. Peer dependencies: React 17 is dropped. React 18 now actually works.
+22. The stylesheet path is `@dynamix-layout/react/styles.css` (the old `./style.css` alias still works in 2.x).
+23. DOM data attributes change: `data-uid`/`data-type` → `data-dx-id`/`data-dx-slot`.
+
+**Solid** (D2): 24. `updateJSON` emits v2 JSON; types `LayoutTree` → `LayoutJSON` / `LayoutTreeV1`.
+
+**Behaviour kept on purpose:** default sizes, sizing math, the fold and maximize rules, clipping when the layout is smaller than its minimum, and content never remounting.
+
+---
+
+## 7. Final v1.x release — decision: **yes, a small one**
+
+Proposal: **react 1.x patch + solid 1.x patch** after #83, published from `main` before v2 lands. It contains:
+1. The jsx-runtime external fix and the peer dependency fix (`react ^18 || ^19`). React 18 users are broken today (B2) and shouldn't have to adopt a full rewrite to get a working package.
+1. The type declarations fix (B30): published types import a path into the repo's sources, so core types are `any` for every v1 user.
+2. The README CSS path fix (P10) and the repository link fixes (P12).
+3. `@deprecated` JSDoc on every prop and export that v2 removes, each linking to the migration guide. IDEs show the strikethrough, at zero runtime cost.
+
+**No runtime deprecation warnings.** Every v1 prop changes in v2, so a warning would fire on every use with no way to migrate step by step. That's noise, not guidance. Saved layouts need no v1-side work, because v2 migrates them automatically.
+
+---
+
+## 8. Packaging
+
+```jsonc
+// @dynamix-layout/react
+"type": "module",
+"exports": {
+ ".": {
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
+ },
+ "./styles.css": "./dist/styles.css",
+ "./style.css": "./dist/styles.css",
+ "./package.json": "./package.json"
+},
+"sideEffects": ["**/*.css"],
+"peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" },
+"dependencies": { "@dynamix-layout/core": "workspace:^" }
+```
+
+- **Core:** the same shape without CSS, plus `"sideEffects": false` (now true), and a `unpkg` UMD.
+- **Solid:** `solid-js` moves to `peerDependencies` (B21).
+- **Types:** `types` comes first in every condition. CJS types are a `.d.cts` copy produced after `vite-plugin-dts`, so `attw` passes for `node16` CJS and ESM.
+- **Build:** vite-plugin-dts and Vite library mode stay (the tooling is unchanged).
+- **Removed from the core build:** the `__LICENSE__` define and `vite-plugin-strip-comments`' licence duplication. A one-line `/*! @license MIT */` banner replaces them.
+- **Scripts:** the React package gets `test` and `--max-warnings 0` lint scripts (P13).
+
+---
+
+## 9. Bug fix coverage
+
+| Bug | Fixed by |
+|---|---|
+| B1 | §3.3 |
+| B2 | §3.14 / §7 |
+| B3 | §2 (instances) |
+| B4 | §2.6 ids + §3.11 |
+| B5 | §2.6 |
+| B6 | §3.5 |
+| B7 | invariant 3 |
+| B8 | D3 |
+| B9 | D5 |
+| B10 | §3.4 |
+| B11 | frozen snapshots |
+| B12 | no engine timers |
+| B13 | Solid follow-up (§12) |
+| B14 | `useId` |
+| B15 | controller owns its engine |
+| B16 | cleanups (§3.11) |
+| B17 | §3.4 |
+| B18 | boolean returns + `onWarning` |
+| B19 | §3.12 |
+| B20 | §8 |
+| B21 | §8 |
+| B22 | all option types exported |
+| B23 | D6 |
+| B24 | defaults move to `styles.css` |
+| B25 | D8 |
+
+---
+
+## 10. Test strategy
+
+- **Characterization first.** A script runs the **current v1 core** through scripted and seeded-random sequences:
+ - builds from 1 to 9 tabs;
+ - every `updateTree` area;
+ - splitter drags;
+ - fold and maximize;
+ - resize sweeps.
+
+ For each step it records the v1 JSON, the tabset and bond rects, and the tab order. The results are committed as fixtures under `packages/core/test/fixtures/v1/`. v2 tests replay the same operations through `migrateLayoutFromV1` and the new API, and assert:
+ - the same structure, modulo normalization;
+ - **pixel-identical rects** (D4 makes this possible);
+ - the same refusals.
+- **Core unit tests** for every pure function, including:
+ - edge cases: removing the last tab, moving a tab onto itself, min-size clamping, deep nesting, flattening;
+ - migration of every row in the §2.8 edge-case table;
+ - two independent instances in one process;
+ - frozen snapshots and structural-sharing identity.
+- **React (RTL plus Vitest, jsdom).**
+ - `renderToString` in a Node environment with no `window` (spied), producing deterministic markup.
+ - StrictMode mount/unmount with `addEventListener`/`removeEventListener`/`ResizeObserver.disconnect` spies balanced.
+ - Selector render counts (selecting a tab in one tabset doesn't re-render the others).
+ - **An inline `tabs` array with the same ids causes no engine calls and no content remounts.**
+ - Splitter pointer drag and keyboard resize.
+ - ARIA roles and attributes for tabs and splitters, plus arrow-key navigation.
+ - Controlled vs uncontrolled; the `onLayoutChange` payload has `version: 2`.
+ - Pointer tab drag: threshold, Escape cancel, drop.
+ - Zero React commits during a splitter drag (fast path).
+- **CI** adds a React 18 job (`pnpm --filter @dynamix-layout/react test` with react@18 installed via an override).
+- **Coverage target:** at least 90% for core.
+
+---
+
+## 11. Phase 3 plan (commits)
+
+**Order change.** The brief's order removes the static state (step 4) before the React rebuild (step 5), which would break React and Solid for several commits. To keep CI green on every commit, as the working rules require, the new core is built **alongside** the old one, and the old one is deleted only after both adapters are ported.
+
+The work happens on branch `v2` (no PR until you ask). Each line below is one commit:
+
+1. `chore: remove stale examples/svelte` (D6)
+2. `test(core): characterization fixtures and tests for v1 behaviour`
+3. `feat(core): immutable layout model and pure tree functions`
+4. `feat(core): pure geometry and splitter math`
+5. `feat(core): drop target and drop indicator calculation`
+6. `feat(core): v2 serialization and migrateLayoutFromV1`
+7. `feat(core): createLayout instance with store and snapshots` (the characterization fixtures pass against v2)
+8. `feat(react)!: rebuild on createLayout with context, headless hooks and slot components` (`BREAKING CHANGE:` footer)
+9. `feat(react): accessibility for tabs, splitters and keyboard move mode`
+10. `refactor(solid)!: port to createLayout`
+11. `refactor(core)!: remove DynamixLayoutCore, static caches, Queue, banner and window global`
+12. `docs(examples): update examples; add basic, custom-components, headless and controlled React examples; SSR in Next.js`
+13. `build: exports maps, peer deps, sideEffects, CJS types, use client banner`
+14. `chore: changesets — major for core, react and solid`
+
+Phases 4 (docs) and 5 (release checks) follow, as written in the brief.
+
+---
+
+## 12. Follow-ups (out of scope for 2.0)
+
+- **Solid alignment:**
+ - pointer DnD;
+ - ResizeObserver;
+ - object `tabs`, `classNames`/`styles`, `components`;
+ - `??` defaults (B13);
+ - reactive `tabs`/`layout`;
+ - a11y parity;
+ - headless primitives (`createTab`, `createSplitter`) mirroring the React hooks.
+- Auto-scrolling tab bars while dragging near their edges.
+- An optional scrollable mode when the container is smaller than the layout's minimum (F6).
+- Drag-out and popout windows.
+- A `@dynamix-layout/core/testing` helper with deterministic ids and a fake container, for user tests.
+- A changesets prerelease channel (`2.0.0-next.x`) for early adopters. It would need its own release branch, because pre mode on `main` blocks 1.x patches.
+
+---
+
+## 13. Risks
+
+| Risk | Mitigation |
+|---|---|
+| Pointer drags behave differently over iframes or on touch | Pointer capture + `data-dx-dragging` CSS (the same proven trick); manual agent-browser pass with an iframe tab; touch long-press tests |
+| The fast path fights React reconciliation | Render reads current rects, so writer and render agree; a "zero commits during drag" test |
+| Migration differs by a pixel | Fixtures from real v1 output; D4 keeps the same math, and flattening preserves extra-space shares |
+| Scope size | Each commit is reviewable on its own; the old core stays until step 11, so the branch stays shippable |
diff --git a/examples/nextjs/src/app/comp.tsx b/examples/nextjs/src/app/comp.tsx
index fe07816..62427ff 100644
--- a/examples/nextjs/src/app/comp.tsx
+++ b/examples/nextjs/src/app/comp.tsx
@@ -1,38 +1,11 @@
-export const tabs: [string, React.ReactNode][] = [
- [
- 'editor',
-
- Editor
-
,
- ],
- [
- 'preview',
-
- Preview
-
,
- ],
- [
- 'terminal',
-
- Terminal
-
,
- ],
+import type { TabItem } from '@dynamix-layout/react'
+
+const panel = (background: string, label: string) => (
+ {label}
+)
+
+export const tabs: TabItem[] = [
+ { id: 'editor', title: 'Editor', content: panel('#c0ca33', 'Editor') },
+ { id: 'preview', title: 'Preview', content: panel('#66bb6a', 'Preview') },
+ { id: 'terminal', title: 'Terminal', content: panel('#ffc400', 'Terminal') },
]
diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx
index d3a8128..1469ad6 100644
--- a/examples/nextjs/src/app/page.tsx
+++ b/examples/nextjs/src/app/page.tsx
@@ -1,30 +1,31 @@
'use client'
-import React from 'react'
-import dynamic from 'next/dynamic'
-import { LayoutProps } from '@dynamix-layout/react'
+import Link from 'next/link'
+import { DynamixLayout } from '@dynamix-layout/react'
+import '@dynamix-layout/react/styles.css'
import { tabs } from './comp'
-import '@dynamix-layout/react/style.css'
-
-const DynamixLayout = dynamic(
- async () => {
- const { DynamixLayout } = await import('@dynamix-layout/react')
- return {
- default: (props: LayoutProps) => (
-
- ),
- }
- },
- { ssr: false, loading: () => Loading...
}
-)
+// Rendered on the server too: v2's first render is deterministic, so it hydrates cleanly.
export default function Home() {
return (
- <>
+
+
+
+ Open the full feature showcase →
+
+
console.log(layoutTree)}
- style={{ height: '100vh', width: '100vw' }}
tabs={tabs}
+ onLayoutChange={(layout) => console.log(layout)}
+ style={{ flex: 1, minHeight: 0, width: '100vw' }}
/>
- >
+
)
}
diff --git a/examples/nextjs/src/app/showcase/Showcase.tsx b/examples/nextjs/src/app/showcase/Showcase.tsx
new file mode 100644
index 0000000..8d9396d
--- /dev/null
+++ b/examples/nextjs/src/app/showcase/Showcase.tsx
@@ -0,0 +1,260 @@
+'use client'
+
+import { useEffect, useRef, useState, type ReactNode } from 'react'
+import Link from 'next/link'
+import {
+ DynamixLayout,
+ type DynamixLayoutHandle,
+ type LayoutJSON,
+ type TabItem,
+} from '@dynamix-layout/react'
+import type { RowJSON } from '@dynamix-layout/core'
+import '@dynamix-layout/react/styles.css'
+
+const STORAGE_KEY = 'dynamix-layout:showcase'
+
+const loadSaved = (): LayoutJSON | undefined => {
+ try {
+ const json = localStorage.getItem(STORAGE_KEY)
+ return json ? (JSON.parse(json) as LayoutJSON) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const panel = (background: string, children: ReactNode) => (
+
+ {children}
+
+)
+
+const HELP = (
+
+
+ Drag a tab onto another tab bar, onto a panel's side to
+ split it, or onto a layout edge.
+
+
+ Drag a tab bar to move the whole panel.{' '}
+ Drag a splitter to resize.
+
+
+ Fold or maximize with the buttons at the end of a tab
+ bar (hover it), or double-click a tab bar to maximize.
+
+
+ Keyboard: arrows move between tabs, Alt += {' '}
+ maximizes, Alt +- folds, Ctrl /
+ ⌘ +Shift +M moves the focused tab.
+
+
+ Close tabs with × (or Delete ), add them
+ with the buttons above. The layout is saved to localStorage: reload
+ the page.
+
+
+)
+
+const initialTabs: TabItem[] = [
+ { id: 'guide', title: 'Guide', content: panel('#fffde7', HELP) },
+ {
+ id: 'editor',
+ title: 'Editor',
+ content: panel('#e3f2fd', 'Editor: focus or click to select'),
+ },
+ {
+ id: 'terminal',
+ title: 'Terminal',
+ content: panel(
+ '#212121',
+ $ _
+ ),
+ },
+ {
+ id: 'preview',
+ title: 'Preview',
+ content: panel('#e8f5e9', 'Preview'),
+ closable: true,
+ },
+ {
+ id: 'notes',
+ title: 'Notes',
+ content: panel('#fce4ec', 'Notes (closable)'),
+ closable: true,
+ },
+]
+
+const newTab = (n: number, target?: TabItem['target']): TabItem => ({
+ id: `new-${n}`,
+ title: `New ${n}`,
+ content: panel('#ede7f6', `Tab new-${n}`),
+ closable: true,
+ target,
+})
+const newTabNumber = (id: string) => Number(/^new-(\d+)$/.exec(id)?.[1] ?? 0)
+
+/** Tab ids in a saved layout, so added and closed tabs survive a reload too. */
+const tabIdsOf = (row: RowJSON): string[] =>
+ row.children.flatMap((child) =>
+ child.type === 'row'
+ ? tabIdsOf(child)
+ : child.children.map((tab) => tab.id)
+ )
+
+/** The tabs a saved layout had open, or the initial ones. */
+const restoreTabs = (saved: LayoutJSON | undefined): TabItem[] => {
+ if (!saved) return initialTabs
+ const known = new Map(initialTabs.map((tab) => [tab.id, tab]))
+ return tabIdsOf(saved.root).flatMap((id) => {
+ const tab =
+ known.get(id) ??
+ (newTabNumber(id) ? newTab(newTabNumber(id)) : null)
+ return tab ? [tab] : []
+ })
+}
+
+/** Every feature on one page: drag, split, resize, close, add, fold, maximize, restore, persist, reset. */
+export default function Showcase() {
+ const layout = useRef(null)
+ // localStorage only exists in the browser: read it after mount, then render the layout
+ // (client-only), so the server HTML and the first client render always match.
+ const [saved, setSaved] = useState()
+ const [mounted, setMounted] = useState(false)
+ const [tabs, setTabs] = useState(initialTabs)
+ const [log, setLog] = useState([])
+ const counter = useRef(0)
+ useEffect(() => {
+ const savedLayout = loadSaved()
+ const restored = restoreTabs(savedLayout)
+ setSaved(savedLayout)
+ setTabs(restored)
+ counter.current = Math.max(
+ 0,
+ ...restored.map((tab) => newTabNumber(tab.id))
+ )
+ setMounted(true)
+ }, [])
+
+ const tabsetOf = (tabId: string) =>
+ layout.current?.getSnapshot().tabs.get(tabId)?.tabsetId
+ const act = (name: string, run: () => boolean | void) => {
+ const result = run()
+ if (result === false)
+ setLog((current) =>
+ [`${name}: not possible right now`, ...current].slice(0, 8)
+ )
+ }
+
+ const addTab = (target?: TabItem['target']) => {
+ const tab = newTab(++counter.current, target)
+ setTabs((current) => [...current, tab])
+ }
+
+ // Tailwind's preflight strips button borders: restore the look of the React example.
+ const button = (label: string, onClick: () => void) => (
+
+ {label}
+
+ )
+
+ return (
+
+
+
+ ← Basic demo
+
+ {button('Add tab', () => addTab())}
+ {button('Add tab at the bottom', () =>
+ addTab({ type: 'root', position: 'bottom' })
+ )}
+ {button('Maximize / restore Editor', () =>
+ act('Maximize', () =>
+ layout.current?.toggleMaximize(tabsetOf('editor') ?? '')
+ )
+ )}
+ {button('Fold / unfold Terminal', () =>
+ act('Fold', () =>
+ layout.current?.toggleFold(tabsetOf('terminal') ?? '')
+ )
+ )}
+ {button('Select Preview', () =>
+ act('Select', () => layout.current?.selectTab('preview'))
+ )}
+ {button('Move Notes next to Editor', () =>
+ act('Move', () =>
+ layout.current?.moveTab('notes', {
+ type: 'tab',
+ tabId: 'editor',
+ position: 'after',
+ })
+ )
+ )}
+ {button('Reset layout', () => {
+ setTabs(initialTabs)
+ layout.current?.reset()
+ })}
+ {button('Forget saved layout', () =>
+ localStorage.removeItem(STORAGE_KEY)
+ )}
+
+
+ {log.length
+ ? log.join(' · ')
+ : 'Changes appear here (onLayoutChange reasons).'}
+
+ {mounted ? (
+
{
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(json))
+ setLog((current) => [reason, ...current].slice(0, 8))
+ }}
+ onTabClose={(id) =>
+ setTabs((current) =>
+ current.filter((tab) => tab.id !== id)
+ )
+ }
+ />
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/examples/nextjs/src/app/showcase/page.tsx b/examples/nextjs/src/app/showcase/page.tsx
new file mode 100644
index 0000000..62bd728
--- /dev/null
+++ b/examples/nextjs/src/app/showcase/page.tsx
@@ -0,0 +1,8 @@
+import type { Metadata } from 'next'
+import Showcase from './Showcase'
+
+export const metadata: Metadata = { title: 'Dynamix Layout showcase' }
+
+export default function ShowcasePage() {
+ return
+}
diff --git a/examples/nextjs/tsconfig.json b/examples/nextjs/tsconfig.json
index 7df89e7..1a04a15 100644
--- a/examples/nextjs/tsconfig.json
+++ b/examples/nextjs/tsconfig.json
@@ -19,7 +19,9 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "react": ["./node_modules/@types/react"],
+ "react/*": ["./node_modules/@types/react/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx
index 3ac4ddb..f55b74a 100644
--- a/examples/react/src/App.tsx
+++ b/examples/react/src/App.tsx
@@ -1,320 +1,77 @@
-import Editor from './components/Editor'
-import { Terminal } from './components/Terminal'
-import { DynamixLayout } from '@dynamix-layout/react'
-import { IframePreview } from './components/IframePreview'
-import '@dynamix-layout/react/style.css'
-import './App.css'
+import { lazy, Suspense, useEffect, useState } from 'react'
-import {
- DefaultSliderElement,
- DefaultHoverElement,
- DefaultWrapTabBody,
- DefaultWrapTabHead,
- DefaultWrapTabLabel,
- DefaultWrapTabPanel,
-} from './components/wrapper'
-import { Slider } from './components/ui/slider'
-import { cn } from './lib/utils'
-import { Card } from './components/ui/card'
-import { useState } from 'react'
+const EXAMPLES = {
+ showcase: {
+ label: 'All features',
+ Component: lazy(() => import('./examples/ShowcaseExample')),
+ },
+ custom: {
+ label: 'Custom components',
+ Component: lazy(() => import('./examples/CustomComponentsExample')),
+ },
+ basic: {
+ label: 'Basic',
+ Component: lazy(() => import('./examples/BasicExample')),
+ },
+ headless: {
+ label: 'Headless hooks',
+ Component: lazy(() => import('./examples/HeadlessExample')),
+ },
+ controlled: {
+ label: 'Controlled + localStorage',
+ Component: lazy(() => import('./examples/ControlledExample')),
+ },
+} as const
+type ExampleKey = keyof typeof EXAMPLES
-const dummyTsCode = `
-// TypeScript Dummy Code Example
-type User = {
- id: number;
- name: string;
- email: string;
- isActive: boolean;
-};
-
-const users: User[] = Array.from({ length: 100 }, (_, i) => ({
- id: i + 1,
- name: \`User\${i + 1}\`,
- email: \`user\${i + 1}@example.com\`,
- isActive: i % 2 === 0,
-}));
-
-function getActiveUsers(users: User[]): User[] {
- return users.filter(u => u.isActive);
-}
-
-function printUsers(users: User[]): void {
- users.forEach(u => {
- console.log(\`ID: \${u.id}, Name: \${u.name}, Email: \${u.email}, Active: \${u.isActive}\`);
- });
-}
-
-printUsers(getActiveUsers(users));
-`
-
-const dummyHtmlCode = `
-
-
-
-
-
- Dummy HTML
-
-
-
-
-
Dummy HTML Content
-
- ${Array.from({ length: 50 }, (_, i) => `Item ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. `).join('\n')}
-
-
-
-
-`
-
-const dummyCssCode = `
-/* Large Dummy CSS Example */
-body {
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- color: #333;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+const fromHash = (): ExampleKey => {
+ const key = window.location.hash.slice(1)
+ return key in EXAMPLES ? (key as ExampleKey) : 'showcase'
}
-.container {
- margin: 40px auto;
- max-width: 900px;
- background: #fff;
- border-radius: 12px;
- box-shadow: 0 4px 24px rgba(0,0,0,0.08);
- padding: 32px;
-}
-${Array.from({ length: 30 }, (_, i) => `.box${i + 1} { width: ${100 + i * 5}px; height: 40px; background: #${((Math.random() * 0xffffff) << 0).toString(16)}; margin-bottom: 8px; border-radius: 4px; }`).join('\n')}
-`
-function App() {
- const [minTabHeight, setMinTabHeight] = useState(40)
- const [minTabWidth, setMinTabWidth] = useState(40)
- const [bondWidth, setBondWidth] = useState(10)
+/** One page per API level, picked by the URL hash (#basic, #headless, …). */
+export default function App() {
+ const [current, setCurrent] = useState(fromHash)
+ useEffect(() => {
+ const onHashChange = () => setCurrent(fromHash())
+ window.addEventListener('hashchange', onHashChange)
+ return () => window.removeEventListener('hashchange', onHashChange)
+ }, [])
+ const { Component } = EXAMPLES[current]
return (
-
- {
-
console.log(layoutTree)}
- pad={{ t: 0, b: 0, l: 0, r: 0 }}
- SliderElement={DefaultSliderElement}
- HoverElement={DefaultHoverElement}
- WrapTabBody={DefaultWrapTabBody}
- WrapTabHead={DefaultWrapTabHead}
- WrapTabLabel={DefaultWrapTabLabel}
- WrapTabPanel={DefaultWrapTabPanel}
- minTabHeight={minTabHeight}
- minTabWidth={minTabWidth}
- tabHeadHeight={40}
- bondWidth={bondWidth}
- style={{ overflow: 'auto' }}
- tabBodyElementClass="rounded-b-2 border-b-2 shadow-lg"
- tabs={[
- [
- 'TS',
- ,
- ],
- [
- 'Html',
- ,
- ],
- ['preview', ],
- [
- 'css',
- ,
- ],
- ['terminal', ],
- ]}
- />
- }
-
-
-
-
- Min Tab Height
-
- setMinTabHeight(val)}
- min={1}
- max={800}
- step={1}
- className={cn('w-[60%]')}
- />
-
-
-
- Bond Width
-
- setBondWidth(val)}
- min={1}
- max={100}
- step={1}
- className={cn('w-[60%]')}
- />
-
-
-
- Min Tab Width
-
- setMinTabWidth(val)}
- min={1}
- max={1400}
- step={1}
- className={cn('w-[60%]')}
- />
-
-
-
+
)
}
-
-export default App
diff --git a/examples/react/src/components/layout-config.ts b/examples/react/src/components/layout-config.ts
new file mode 100644
index 0000000..b4c6280
--- /dev/null
+++ b/examples/react/src/components/layout-config.ts
@@ -0,0 +1,9 @@
+import type { LayoutComponents, LayoutSlot } from '@dynamix-layout/react'
+import { Splitter, Tab } from './wrapper'
+
+export const components: Partial = { Tab, Splitter }
+
+export const classNames: Partial> = {
+ tabBar: 'bg-gray-200 text-muted-foreground rounded-t-sm',
+ tabContent: 'rounded-b-2 border border-b-2 shadow-lg bg-background',
+}
diff --git a/examples/react/src/components/wrapper.tsx b/examples/react/src/components/wrapper.tsx
index 5f8c4cd..f380140 100644
--- a/examples/react/src/components/wrapper.tsx
+++ b/examples/react/src/components/wrapper.tsx
@@ -1,285 +1,82 @@
import { cn } from '@/lib/utils'
-import { forwardRef, type ReactNode, type HTMLAttributes } from 'react'
+import { forwardRef, type HTMLAttributes, type Ref } from 'react'
+import type { SplitterProps, TabProps } from '@dynamix-layout/react'
-export const DefaultSliderElement = forwardRef<
- HTMLDivElement,
+/** A shadcn-style tab: the library passes ARIA, data-state and handlers in `props`. */
+export const Tab = forwardRef(function Tab(
{
- children?: ReactNode
- direction?: boolean
- } & HTMLAttributes
->(({ children, className, direction = false, ...props }, ref) => (
-
+ tab,
+ isActive,
+ isDragging,
+ onClose,
+ closeButtonProps,
+ className,
+ ...props
+ },
+ ref
+) {
+ return (
}
+ {...(props as HTMLAttributes)}
className={cn(
- 'flex items-center gap-1',
- direction ? 'flex-col' : 'flex-row'
+ className,
+ 'inline-flex h-[calc(100%-6px)] items-center gap-1.5 rounded-md border border-transparent px-2 text-sm font-medium whitespace-nowrap text-foreground',
+ 'data-[state=active]:bg-background data-[state=active]:shadow-sm dark:data-[state=active]:bg-input/30',
+ 'focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50',
+ isActive && 'font-semibold',
+ isDragging && 'opacity-60'
)}
>
- {!direction ? (
-
-
-
-
-
-
-
-
- ) : (
+ ×
+
+ )}
+
+ )
+})
+
+/** A transparent splitter with a lucide grip, rotated for vertical rows. */
+export const Splitter = forwardRef
(
+ function Splitter({ splitter, isDragging, className, ...props }, ref) {
+ return (
+
-
-
-
-
-
-
+
+
+
+
+
+
- )}
-
- {children}
-
-))
-
-DefaultSliderElement.displayName = 'DefaultSliderElement'
-
-export const DefaultWrapTabLabel = forwardRef<
- HTMLDivElement,
- { children?: ReactNode; active?: boolean } & HTMLAttributes
->(({ children, active = false, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabLabel.displayName = 'DefaultWrapTabLabel'
-
-export const DefaultWrapTabBody = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabBody.displayName = 'DefaultWrapTabBody'
-
-export const DefaultWrapTabHead = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabHead.displayName = 'DefaultWrapTabHead'
-
-export const DefaultWrapTabPanel = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabPanel.displayName = 'DefaultWrapTabPanel'
-
-export const DefaultHoverElement = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultHoverElement.displayName = 'DefaultHoverElement'
-
-export const RootSplitterHoverEl = forwardRef<
- HTMLDivElement,
- {
- children?: ReactNode
- area: 'left' | 'right' | 'top' | 'bottom'
- size?: { w: string; h: string }
- gap?: string
- } & HTMLAttributes
->(
- (
- {
- children,
- area,
- size = { h: '20%', w: '6px' },
- gap = '0px',
- ...props
- },
- ref
- ) => {
- props.style = { ...props.style }
-
- if (area === 'left') {
- props.style = {
- borderTopRightRadius: '16px',
- borderBottomRightRadius: '16px',
- height: size.h,
- left: gap,
- width: size.w,
- ...props.style,
- }
- props.style.top = `calc(50% - ${props.style.height} / 2)`
- } else if (area === 'top') {
- props.style = {
- borderBottomLeftRadius: '16px',
- borderBottomRightRadius: '16px',
- width: size.h,
- top: gap,
- height: size.w,
- ...props.style,
- }
- props.style.left = `calc(50% - ${props.style.width} / 2)`
- } else if (area === 'right') {
- props.style = {
- borderTopLeftRadius: '16px',
- borderBottomLeftRadius: '16px',
- height: size.h,
- right: gap,
- width: size.w,
- ...props.style,
- }
- props.style.top = `calc(50% - ${props.style.height} / 2)`
- } else if (area === 'bottom') {
- props.style = {
- borderTopLeftRadius: '16px',
- borderTopRightRadius: '16px',
- width: size.h,
- bottom: gap,
- height: size.w,
- ...props.style,
- }
- props.style.left = `calc(50% - ${props.style.width} / 2)`
- }
-
- return (
-
- {children}
)
}
)
-RootSplitterHoverEl.displayName = 'RootSplitterHoverEl'
diff --git a/examples/react/src/examples/BasicExample.tsx b/examples/react/src/examples/BasicExample.tsx
new file mode 100644
index 0000000..67b6e2e
--- /dev/null
+++ b/examples/react/src/examples/BasicExample.tsx
@@ -0,0 +1,23 @@
+import { DynamixLayout, type TabItem } from '@dynamix-layout/react'
+import '@dynamix-layout/react/styles.css'
+
+const box = (background: string, text: string) => (
+ {text}
+)
+
+const tabs: TabItem[] = [
+ { id: 'editor', title: 'Editor', content: box('#fffde7', 'Editor') },
+ { id: 'terminal', title: 'Terminal', content: box('#e8f5e9', 'Terminal') },
+ { id: 'preview', title: 'Preview', content: box('#e3f2fd', 'Preview') },
+ {
+ id: 'notes',
+ title: 'Notes',
+ content: box('#fce4ec', 'Drag me'),
+ closable: false,
+ },
+]
+
+/** Level 1: the component with default styles. Drag tabs, splitters, double-click a tab bar. */
+export default function BasicExample() {
+ return
+}
diff --git a/examples/react/src/examples/ControlledExample.tsx b/examples/react/src/examples/ControlledExample.tsx
new file mode 100644
index 0000000..a9564bd
--- /dev/null
+++ b/examples/react/src/examples/ControlledExample.tsx
@@ -0,0 +1,81 @@
+import { useState } from 'react'
+import {
+ DynamixLayout,
+ type LayoutJSON,
+ type TabItem,
+} from '@dynamix-layout/react'
+import '@dynamix-layout/react/styles.css'
+
+const STORAGE_KEY = 'dynamix-layout:controlled-example'
+
+const load = (): LayoutJSON | undefined => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY)
+ return saved ? (JSON.parse(saved) as LayoutJSON) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+/** Controlled mode: the layout lives in state and is persisted to localStorage. */
+export default function ControlledExample() {
+ const [layout, setLayout] = useState(load)
+ const [tabs, setTabs] = useState(() =>
+ ['a', 'b', 'c'].map((id) => ({
+ id,
+ title: `Tab ${id.toUpperCase()}`,
+ content: Tab {id}
,
+ closable: true,
+ }))
+ )
+
+ const onLayoutChange = (next: LayoutJSON) => {
+ setLayout(next)
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
+ }
+ const addTab = () => {
+ const id = `t${Date.now().toString(36)}`
+ setTabs((current) => [
+ ...current,
+ {
+ id,
+ title: `New ${current.length + 1}`,
+ content: {id}
,
+ closable: true,
+ },
+ ])
+ }
+
+ return (
+
+
+ Add tab
+ {
+ localStorage.removeItem(STORAGE_KEY)
+ setLayout(undefined)
+ }}
+ >
+ Forget saved layout
+
+
+ Reload the page: the layout is restored.
+
+
+
+ setTabs((current) => current.filter((tab) => tab.id !== id))
+ }
+ />
+
+ )
+}
diff --git a/examples/react/src/examples/CustomComponentsExample.tsx b/examples/react/src/examples/CustomComponentsExample.tsx
new file mode 100644
index 0000000..08797ad
--- /dev/null
+++ b/examples/react/src/examples/CustomComponentsExample.tsx
@@ -0,0 +1,291 @@
+import Editor from '../components/Editor'
+import { Terminal } from '../components/Terminal'
+import {
+ DynamixLayout,
+ type LayoutTreeV1,
+ type TabItem,
+} from '@dynamix-layout/react'
+import { IframePreview } from '../components/IframePreview'
+import '@dynamix-layout/react/styles.css'
+import '../App.css'
+
+import { classNames, components } from '../components/layout-config'
+import { Slider } from '../components/ui/slider'
+import { cn } from '../lib/utils'
+import { Card } from '../components/ui/card'
+import { useState } from 'react'
+
+const dummyTsCode = `
+// TypeScript Dummy Code Example
+type User = {
+ id: number;
+ name: string;
+ email: string;
+ isActive: boolean;
+};
+
+const users: User[] = Array.from({ length: 100 }, (_, i) => ({
+ id: i + 1,
+ name: \`User\${i + 1}\`,
+ email: \`user\${i + 1}@example.com\`,
+ isActive: i % 2 === 0,
+}));
+
+function getActiveUsers(users: User[]): User[] {
+ return users.filter(u => u.isActive);
+}
+
+function printUsers(users: User[]): void {
+ users.forEach(u => {
+ console.log(\`ID: \${u.id}, Name: \${u.name}, Email: \${u.email}, Active: \${u.isActive}\`);
+ });
+}
+
+printUsers(getActiveUsers(users));
+`
+
+const dummyHtmlCode = `
+
+
+
+
+
+ Dummy HTML
+
+
+
+
+
Dummy HTML Content
+
+ ${Array.from({ length: 50 }, (_, i) => `Item ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. `).join('\n')}
+
+
+
+
+`
+
+const dummyCssCode = `
+/* Large Dummy CSS Example */
+body {
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ color: #333;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+.container {
+ margin: 40px auto;
+ max-width: 900px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
+ padding: 32px;
+}
+${Array.from({ length: 30 }, (_, i) => `.box${i + 1} { width: ${100 + i * 5}px; height: 40px; background: #${((Math.random() * 0xffffff) << 0).toString(16)}; margin-bottom: 8px; border-radius: 4px; }`).join('\n')}
+`
+
+/** A layout saved by v1; v2 migrates it on load. */
+const savedV1Layout: LayoutTreeV1 = {
+ typNode: 'row',
+ nodPart: 100,
+ nodName: 'dynamix-layout-root',
+ uidNode: 'dynamix-layout-root',
+ nodKids: [
+ {
+ typNode: 'row',
+ nodPart: 95.13923576423574,
+ nodName: '',
+ uidNode: 'ad18bbee-2260-4172-bcbe-bead8ab919cf',
+ nodOpen: 'preview',
+ nodKids: [
+ {
+ typNode: 'row',
+ nodPart: 253.63885510746962,
+ nodName: '',
+ uidNode: 'e4917519-3c9c-4ab9-adea-3a608fa8d45e',
+ nodOpen: 'TS',
+ nodKids: [
+ {
+ typNode: 'tabset',
+ nodPart: 95.68121135993162,
+ nodName: '',
+ uidNode: '4c880a2e-df03-4183-9043-9d0cbc25d1a9',
+ nodOpen: 'TS',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodPart: 100,
+ nodName: 'TS',
+ uidNode:
+ '9c5b720f-63b4-480b-ba80-717000b4f6df',
+ },
+ ],
+ },
+ {
+ typNode: 'tabset',
+ nodPart: 102.46562527840646,
+ nodName: '',
+ uidNode: '94a19aba-384a-4a4f-977a-2b3e5cf45453',
+ nodOpen: 'preview',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodPart: 100,
+ nodName: 'preview',
+ uidNode:
+ '84932e7f-9efe-4437-b27e-242db949b47d',
+ },
+ ],
+ },
+ {
+ typNode: 'row',
+ nodPart: 101.85316336166194,
+ nodName: '',
+ uidNode: 'd06eb31a-ca54-4c17-ab58-ccc55c5a25e4',
+ nodOpen: 'css',
+ nodKids: [
+ {
+ typNode: 'tabset',
+ nodPart: 100,
+ nodName: '',
+ uidNode:
+ 'f4fb69bf-d5ee-426c-b420-867ecc111f25',
+ nodOpen: 'css',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodPart: 100,
+ nodName: 'css',
+ uidNode:
+ '595562f3-908e-4002-b107-1f86fd2f621a',
+ },
+ ],
+ },
+ {
+ typNode: 'tabset',
+ nodPart: 100,
+ nodName: '',
+ uidNode:
+ 'f6453908-1037-4d62-9a8d-cb8684ef311c',
+ nodOpen: 'Html',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodPart: 100,
+ nodName: 'Html',
+ uidNode:
+ 'd19f9840-85b4-4a98-ba7d-dd2445c26500',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ typNode: 'tabset',
+ nodPart: 97.75714553534206,
+ nodName: '',
+ uidNode: 'f2b421d9-b10d-41db-a410-df41f6b01e8a',
+ nodOpen: 'terminal',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodPart: 100,
+ nodName: 'terminal',
+ uidNode: '0c9bce4c-2bc5-4f68-95e5-1db51d143b9c',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+}
+
+const tabs: TabItem[] = [
+ { id: 'TS', content: },
+ { id: 'Html', content: },
+ { id: 'preview', title: 'Preview', content: },
+ {
+ id: 'css',
+ title: 'CSS',
+ content: ,
+ },
+ { id: 'terminal', title: 'Terminal', content: },
+]
+
+/** Level 1 with custom slot components (shadcn-style), a saved v1 layout and live size controls. */
+export default function CustomComponentsExample() {
+ const [minTabHeight, setMinTabHeight] = useState(40)
+ const [minTabWidth, setMinTabWidth] = useState(40)
+ const [bondWidth, setBondWidth] = useState(10)
+
+ return (
+
+ {
+
+ console.log(reason, layout)
+ }
+ components={components}
+ classNames={classNames}
+ minPanelSize={{ width: minTabWidth, height: minTabHeight }}
+ tabBarHeight={40}
+ splitterSize={bondWidth}
+ tabs={tabs}
+ />
+ }
+
+
+
+
+ Min Panel Height
+
+ setMinTabHeight(val)}
+ min={1}
+ max={800}
+ step={1}
+ className={cn('w-[60%]')}
+ />
+
+
+
+ Splitter Size
+
+ setBondWidth(val)}
+ min={1}
+ max={100}
+ step={1}
+ className={cn('w-[60%]')}
+ />
+
+
+
+ Min Panel Width
+
+ setMinTabWidth(val)}
+ min={1}
+ max={1400}
+ step={1}
+ className={cn('w-[60%]')}
+ />
+
+
+
+
+ )
+}
diff --git a/examples/react/src/examples/HeadlessExample.tsx b/examples/react/src/examples/HeadlessExample.tsx
new file mode 100644
index 0000000..e686c81
--- /dev/null
+++ b/examples/react/src/examples/HeadlessExample.tsx
@@ -0,0 +1,131 @@
+import {
+ DynamixLayoutProvider,
+ useDynamixLayout,
+ useSplitter,
+ useTab,
+ useTabset,
+ type HeadlessTabItem,
+} from '@dynamix-layout/react'
+
+const tabs: HeadlessTabItem[] = [
+ { id: 'one', title: 'One' },
+ { id: 'two', title: 'Two' },
+ { id: 'three', title: 'Three' },
+]
+
+function Tab({ tabId }: { tabId: string }) {
+ const { getTabProps, isActive, item } = useTab(tabId)
+ return (
+
+ {item?.title}
+
+ )
+}
+
+function Tabset({ tabsetId }: { tabsetId: string }) {
+ const { tabset, getPanelProps, getTabBarProps } = useTabset(tabsetId)
+ if (!tabset) return null
+ return (
+ <>
+
+
+ {tabset.tabIds.map((id) => (
+
+ ))}
+
+ >
+ )
+}
+
+function Content({ tabId }: { tabId: string }) {
+ const { getTabContentProps, item } = useTab(tabId)
+ return (
+
+ Content of {item?.title}
+
+ )
+}
+
+function Splitter({ splitterId }: { splitterId: string }) {
+ const { getSplitterProps, isDragging } = useSplitter(splitterId)
+ return (
+
+ )
+}
+
+/** Level 2: no default components or styles, only hooks and prop getters. */
+export default function HeadlessExample() {
+ const {
+ controller,
+ getRootProps,
+ tabsets,
+ splitters,
+ tabIds,
+ dropIndicator,
+ } = useDynamixLayout({
+ tabs,
+ tabBarHeight: 36,
+ splitterSize: 6,
+ padding: 8,
+ })
+ return (
+
+
+ {tabsets.map((tabset) => (
+
+ ))}
+ {tabIds.map((id) => (
+
+ ))}
+ {splitters.map((splitter) => (
+
+ ))}
+ {dropIndicator && (
+
+ )}
+
+
+ )
+}
diff --git a/examples/react/src/examples/ShowcaseExample.tsx b/examples/react/src/examples/ShowcaseExample.tsx
new file mode 100644
index 0000000..7238081
--- /dev/null
+++ b/examples/react/src/examples/ShowcaseExample.tsx
@@ -0,0 +1,225 @@
+import { useRef, useState, type ReactNode } from 'react'
+import {
+ DynamixLayout,
+ type DynamixLayoutHandle,
+ type LayoutJSON,
+ type TabItem,
+} from '@dynamix-layout/react'
+import type { RowJSON } from '@dynamix-layout/core'
+import '@dynamix-layout/react/styles.css'
+
+const STORAGE_KEY = 'dynamix-layout:showcase'
+
+const loadSaved = (): LayoutJSON | undefined => {
+ try {
+ const json = localStorage.getItem(STORAGE_KEY)
+ return json ? (JSON.parse(json) as LayoutJSON) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const panel = (background: string, children: ReactNode) => (
+
+ {children}
+
+)
+
+const HELP = (
+
+
+ Drag a tab onto another tab bar, onto a panel's side to split
+ it, or onto a layout edge.
+
+
+ Drag a tab bar to move the whole panel.{' '}
+ Drag a splitter to resize.
+
+
+ Fold or maximize with the buttons at the end of a tab
+ bar (hover it), or double-click a tab bar to maximize.
+
+
+ Keyboard: arrows move between tabs, Alt += {' '}
+ maximizes, Alt +- folds, Ctrl /
+ ⌘ +Shift +M moves the focused tab.
+
+
+ Close tabs with × (or Delete ), add them
+ with the buttons above. The layout is saved to localStorage: reload
+ the page.
+
+
+)
+
+const initialTabs: TabItem[] = [
+ { id: 'guide', title: 'Guide', content: panel('#fffde7', HELP) },
+ {
+ id: 'editor',
+ title: 'Editor',
+ content: panel('#e3f2fd', 'Editor: focus or click to select'),
+ },
+ {
+ id: 'terminal',
+ title: 'Terminal',
+ content: panel(
+ '#212121',
+ $ _
+ ),
+ },
+ {
+ id: 'preview',
+ title: 'Preview',
+ content: panel('#e8f5e9', 'Preview'),
+ closable: true,
+ },
+ {
+ id: 'notes',
+ title: 'Notes',
+ content: panel('#fce4ec', 'Notes (closable)'),
+ closable: true,
+ },
+]
+
+const newTab = (n: number, target?: TabItem['target']): TabItem => ({
+ id: `new-${n}`,
+ title: `New ${n}`,
+ content: panel('#ede7f6', `Tab new-${n}`),
+ closable: true,
+ target,
+})
+const newTabNumber = (id: string) => Number(/^new-(\d+)$/.exec(id)?.[1] ?? 0)
+
+/** Tab ids in a saved layout, so added and closed tabs survive a reload too. */
+const tabIdsOf = (row: RowJSON): string[] =>
+ row.children.flatMap((child) =>
+ child.type === 'row'
+ ? tabIdsOf(child)
+ : child.children.map((tab) => tab.id)
+ )
+
+/** The tabs a saved layout had open, or the initial ones. */
+const restoreTabs = (saved: LayoutJSON | undefined): TabItem[] => {
+ if (!saved) return initialTabs
+ const known = new Map(initialTabs.map((tab) => [tab.id, tab]))
+ return tabIdsOf(saved.root).flatMap((id) => {
+ const tab =
+ known.get(id) ??
+ (newTabNumber(id) ? newTab(newTabNumber(id)) : null)
+ return tab ? [tab] : []
+ })
+}
+
+/** Every feature on one page: drag, split, resize, close, add, fold, maximize, restore, persist, reset. */
+export default function ShowcaseExample() {
+ const layout = useRef(null)
+ const [saved] = useState(loadSaved)
+ const [tabs, setTabs] = useState(() => restoreTabs(saved))
+ const [log, setLog] = useState([])
+ const counter = useRef(
+ Math.max(0, ...tabs.map((tab) => newTabNumber(tab.id)))
+ )
+
+ const tabsetOf = (tabId: string) =>
+ layout.current?.getSnapshot().tabs.get(tabId)?.tabsetId
+ const act = (name: string, run: () => boolean | void) => {
+ const result = run()
+ if (result === false)
+ setLog((current) =>
+ [`${name}: not possible right now`, ...current].slice(0, 8)
+ )
+ }
+
+ const addTab = (target?: TabItem['target']) => {
+ const tab = newTab(++counter.current, target)
+ setTabs((current) => [...current, tab])
+ }
+
+ const button = (label: string, onClick: () => void) => (
+
+ {label}
+
+ )
+
+ return (
+
+
+ {button('Add tab', () => addTab())}
+ {button('Add tab at the bottom', () =>
+ addTab({ type: 'root', position: 'bottom' })
+ )}
+ {button('Maximize / restore Editor', () =>
+ act('Maximize', () =>
+ layout.current?.toggleMaximize(tabsetOf('editor') ?? '')
+ )
+ )}
+ {button('Fold / unfold Terminal', () =>
+ act('Fold', () =>
+ layout.current?.toggleFold(tabsetOf('terminal') ?? '')
+ )
+ )}
+ {button('Select Preview', () =>
+ act('Select', () => layout.current?.selectTab('preview'))
+ )}
+ {button('Move Notes next to Editor', () =>
+ act('Move', () =>
+ layout.current?.moveTab('notes', {
+ type: 'tab',
+ tabId: 'editor',
+ position: 'after',
+ })
+ )
+ )}
+ {button('Reset layout', () => {
+ setTabs(initialTabs)
+ layout.current?.reset()
+ })}
+ {button('Forget saved layout', () =>
+ localStorage.removeItem(STORAGE_KEY)
+ )}
+
+
+ {log.length
+ ? log.join(' · ')
+ : 'Changes appear here (onLayoutChange reasons).'}
+
+
{
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(json))
+ setLog((current) => [reason, ...current].slice(0, 8))
+ }}
+ onTabClose={(id) =>
+ setTabs((current) => current.filter((tab) => tab.id !== id))
+ }
+ />
+
+ )
+}
diff --git a/examples/solid/src/App.tsx b/examples/solid/src/App.tsx
index 7bbd242..6081c06 100644
--- a/examples/solid/src/App.tsx
+++ b/examples/solid/src/App.tsx
@@ -1,22 +1,214 @@
-import { tabs } from './comp'
-import { DynamixLayout } from '@dynamix-layout/solid'
+import { createSignal, For, type JSX } from 'solid-js'
+import {
+ DynamixLayout,
+ type LayoutJSON,
+ type TabItem,
+} from '@dynamix-layout/solid'
+import type { DropTarget, Layout, RowJSON } from '@dynamix-layout/core'
import '@dynamix-layout/solid/style.css'
import './App.css'
-function App() {
+const STORAGE_KEY = 'dynamix-layout:solid-showcase'
+
+const loadSaved = (): LayoutJSON | undefined => {
+ try {
+ const json = localStorage.getItem(STORAGE_KEY)
+ return json ? (JSON.parse(json) as LayoutJSON) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const panel = (background: string, children: JSX.Element) => (
+
+ {children}
+
+)
+
+const guide = (
+
+
+ Drag a tab onto another tab bar, onto a panel's side to split
+ it, or onto a layout edge.
+
+
+ Drag a tab bar to move the whole panel.{' '}
+ Drag a splitter to resize.
+
+
+ Fold or maximize with the buttons at the end of a tab
+ bar, or double-click a tab bar. Alt+= maximizes and Alt+- folds the
+ panel you last clicked.
+
+
+ Close tabs with ×, add them with the buttons above.
+ The layout is saved to localStorage: reload the page.
+
+
+)
+
+const initialTabs = (): TabItem[] => [
+ ['guide', panel('#fffde7', guide), { title: 'Guide' }],
+ ['editor', panel('#e3f2fd', 'Editor'), { title: 'Editor' }],
+ [
+ 'terminal',
+ panel('#212121', $ _ ),
+ { title: 'Terminal' },
+ ],
+ [
+ 'preview',
+ panel('#e8f5e9', 'Preview'),
+ { title: 'Preview', closable: true },
+ ],
+ [
+ 'notes',
+ panel('#fce4ec', 'Notes (closable)'),
+ { title: 'Notes', closable: true },
+ ],
+]
+
+const newTab = (n: number): TabItem => [
+ `new-${n}`,
+ panel('#ede7f6', `Tab new-${n}`),
+ { title: `New ${n}`, closable: true },
+]
+const newTabNumber = (id: string) => Number(/^new-(\d+)$/.exec(id)?.[1] ?? 0)
+
+/** Tab ids in a saved layout, so added and closed tabs survive a reload too. */
+const tabIdsOf = (row: RowJSON): string[] =>
+ row.children.flatMap((child) =>
+ child.type === 'row'
+ ? tabIdsOf(child)
+ : child.children.map((tab) => tab.id)
+ )
+
+/** The tabs a saved layout had open, or the initial ones. */
+const restoreTabs = (saved: LayoutJSON | undefined): TabItem[] => {
+ if (!saved) return initialTabs()
+ const known = new Map(initialTabs().map((tab) => [tab[0], tab]))
+ return tabIdsOf(saved.root).flatMap((id) => {
+ const tab =
+ known.get(id) ??
+ (newTabNumber(id) ? newTab(newTabNumber(id)) : null)
+ return tab ? [tab] : []
+ })
+}
+
+/** Every feature on one page: drag, split, resize, close, add, fold, maximize, restore, persist, reset. */
+export default function App() {
+ let layout: Layout | undefined
+ const saved = loadSaved()
+ const [tabs, setTabs] = createSignal(restoreTabs(saved))
+ const [log, setLog] = createSignal([])
+ let counter = Math.max(0, ...tabs().map(([id]) => newTabNumber(id)))
+
+ const tabsetOf = (tabId: string) =>
+ layout?.getSnapshot().tabs.get(tabId)?.tabsetId ?? ''
+ const note = (entry: string) =>
+ setLog((current) => [entry, ...current].slice(0, 8))
+ const act = (name: string, done: boolean | undefined) => {
+ if (done === false) note(`${name}: not possible right now`)
+ }
+ const addTab = (target?: DropTarget) => {
+ const entry = newTab(++counter)
+ const id = entry[0]
+ // `tabs` places new tabs in the last active tabset; to drop one at a
+ // target, add it to the engine first and `tabs` keeps it where it is.
+ if (target) layout?.addTab({ id }, target)
+ setTabs([...tabs(), entry])
+ }
+
+ const actions: [string, () => void][] = [
+ ['Add tab', () => addTab()],
+ [
+ 'Add tab at the bottom',
+ () => addTab({ type: 'root', position: 'bottom' }),
+ ],
+ [
+ 'Maximize / restore Editor',
+ () => act('Maximize', layout?.toggleMaximize(tabsetOf('editor'))),
+ ],
+ [
+ 'Fold / unfold Terminal',
+ () => act('Fold', layout?.toggleFold(tabsetOf('terminal'))),
+ ],
+ ['Select Preview', () => act('Select', layout?.selectTab('preview'))],
+ [
+ 'Move Notes next to Editor',
+ () =>
+ act(
+ 'Move',
+ layout?.moveTab('notes', {
+ type: 'tab',
+ tabId: 'editor',
+ position: 'after',
+ })
+ ),
+ ],
+ [
+ 'Reset layout',
+ () => {
+ setTabs(initialTabs())
+ layout?.reset()
+ },
+ ],
+ ['Forget saved layout', () => localStorage.removeItem(STORAGE_KEY)],
+ ]
+
return (
- <>
+
+
+
+ {([label, run]) => {label} }
+
+
+
+ {log().length ? log().join(' · ') : 'Changes appear here.'}
+
console.log(layoutTree)}
- tabs={tabs}
- pad={{ t: 0, b: 0, l: 0, r: 0 }}
- tabHeadHeight={40}
- bondWidth={10}
- minTabHeight={40}
- minTabWidth={100}
+ tabs={tabs()}
+ layoutTree={saved}
+ onReady={(engine) => (layout = engine)}
+ onTabClose={(id) =>
+ setTabs(tabs().filter(([tabId]) => tabId !== id))
+ }
+ updateJSON={(json, reason) => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(json))
+ if (reason !== 'mount') note(reason)
+ }}
+ tabHeadHeight={36}
+ bondWidth={6}
+ minTabWidth={120}
+ minTabHeight={80}
/>
- >
+
)
}
-
-export default App
diff --git a/examples/solidstart/src/components/Showcase.tsx b/examples/solidstart/src/components/Showcase.tsx
new file mode 100644
index 0000000..75373ba
--- /dev/null
+++ b/examples/solidstart/src/components/Showcase.tsx
@@ -0,0 +1,231 @@
+import { batch, createSignal, For, type JSX } from 'solid-js'
+import {
+ DynamixLayout,
+ type LayoutJSON,
+ type TabItem,
+} from '@dynamix-layout/solid'
+import type { DropTarget, Layout, RowJSON } from '@dynamix-layout/core'
+import '@dynamix-layout/solid/style.css'
+
+// This module touches `localStorage` and the DOM, so the route loads it with
+// `clientOnly`: it never runs during server rendering, and hydration has
+// nothing to mismatch.
+
+const STORAGE_KEY = 'dynamix-layout:solidstart-showcase'
+
+const loadSaved = (): LayoutJSON | undefined => {
+ try {
+ const json = localStorage.getItem(STORAGE_KEY)
+ return json ? (JSON.parse(json) as LayoutJSON) : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const panel = (background: string, children: JSX.Element) => (
+
+ {children}
+
+)
+
+const guide = () => (
+
+
+ Drag a tab onto another tab bar, onto a panel's side to split
+ it, or onto a layout edge.
+
+
+ Drag a tab bar to move the whole panel.{' '}
+ Drag a splitter to resize.
+
+
+ Fold or maximize with the buttons at the end of a tab
+ bar, or double-click a tab bar. Alt+= maximizes and Alt+- folds the
+ panel you last clicked.
+
+
+ Close tabs with ×, add them with the buttons above.
+ The layout is saved to localStorage: reload the page.
+
+
+)
+
+const initialTabs = (): TabItem[] => [
+ ['guide', panel('#fffde7', guide()), { title: 'Guide' }],
+ ['editor', panel('#e3f2fd', 'Editor'), { title: 'Editor' }],
+ [
+ 'terminal',
+ panel('#212121', $ _ ),
+ { title: 'Terminal' },
+ ],
+ [
+ 'preview',
+ panel('#e8f5e9', 'Preview'),
+ { title: 'Preview', closable: true },
+ ],
+ [
+ 'notes',
+ panel('#fce4ec', 'Notes (closable)'),
+ { title: 'Notes', closable: true },
+ ],
+]
+
+const newTab = (n: number): TabItem => [
+ `new-${n}`,
+ panel('#ede7f6', `Tab new-${n}`),
+ { title: `New ${n}`, closable: true },
+]
+const newTabNumber = (id: string) => Number(/^new-(\d+)$/.exec(id)?.[1] ?? 0)
+
+/** Tab ids in a saved layout, so added and closed tabs survive a reload too. */
+const tabIdsOf = (row: RowJSON): string[] =>
+ row.children.flatMap((child) =>
+ child.type === 'row'
+ ? tabIdsOf(child)
+ : child.children.map((tab) => tab.id)
+ )
+
+/** The tabs a saved layout had open, or the initial ones. */
+const restoreTabs = (saved: LayoutJSON | undefined): TabItem[] => {
+ if (!saved) return initialTabs()
+ const known = new Map(initialTabs().map((tab) => [tab[0], tab]))
+ return tabIdsOf(saved.root).flatMap((id) => {
+ const tab =
+ known.get(id) ??
+ (newTabNumber(id) ? newTab(newTabNumber(id)) : null)
+ return tab ? [tab] : []
+ })
+}
+
+/** Every feature on one page: drag, split, resize, close, add, fold, maximize, restore, persist, reset. */
+export default function Showcase() {
+ let layout: Layout | undefined
+ const saved = loadSaved()
+ const [tabs, setTabs] = createSignal(restoreTabs(saved))
+ const [log, setLog] = createSignal([])
+ let counter = Math.max(0, ...tabs().map(([id]) => newTabNumber(id)))
+
+ const tabsetOf = (tabId: string) =>
+ layout?.getSnapshot().tabs.get(tabId)?.tabsetId ?? ''
+ const note = (entry: string) =>
+ setLog((current) => [entry, ...current].slice(0, 8))
+ const act = (name: string, done: boolean | undefined) => {
+ if (done === false) note(`${name}: not possible right now`)
+ }
+ const addTab = (target?: DropTarget) => {
+ const entry = newTab(++counter)
+ const id = entry[0]
+ batch(() => {
+ // The Solid adapter places tabs added through `tabs` by default;
+ // to drop one at a target, add it to the engine first. The
+ // reactive `tabs` sync then keeps it where it is.
+ if (target) layout?.addTab({ id }, target)
+ setTabs([...tabs(), entry])
+ })
+ }
+
+ const actions: [string, () => void][] = [
+ ['Add tab', () => addTab()],
+ [
+ 'Add tab at the bottom',
+ () => addTab({ type: 'root', position: 'bottom' }),
+ ],
+ [
+ 'Maximize / restore Editor',
+ () => act('Maximize', layout?.toggleMaximize(tabsetOf('editor'))),
+ ],
+ [
+ 'Fold / unfold Terminal',
+ () => act('Fold', layout?.toggleFold(tabsetOf('terminal'))),
+ ],
+ ['Select Preview', () => act('Select', layout?.selectTab('preview'))],
+ [
+ 'Move Notes next to Editor',
+ () =>
+ act(
+ 'Move',
+ layout?.moveTab('notes', {
+ type: 'tab',
+ tabId: 'editor',
+ position: 'after',
+ })
+ ),
+ ],
+ [
+ 'Reset layout',
+ () => {
+ setTabs(initialTabs())
+ layout?.reset()
+ },
+ ],
+ [
+ 'Forget saved layout',
+ () => {
+ localStorage.removeItem(STORAGE_KEY)
+ note('saved layout forgotten')
+ },
+ ],
+ ]
+
+ return (
+
+
+
+ {([label, run]) => (
+
+ {label}
+
+ )}
+
+
+
+ {log().length ? log().join(' · ') : 'Changes appear here.'}
+
+
(layout = engine)}
+ onTabClose={(id) =>
+ setTabs(tabs().filter(([tabId]) => tabId !== id))
+ }
+ updateJSON={(json, reason) => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(json))
+ if (reason !== 'mount') note(reason)
+ }}
+ tabHeadHeight={36}
+ bondWidth={6}
+ minTabWidth={120}
+ minTabHeight={80}
+ />
+
+ )
+}
diff --git a/examples/solidstart/src/routes/index.tsx b/examples/solidstart/src/routes/index.tsx
index 846312a..0d87a28 100644
--- a/examples/solidstart/src/routes/index.tsx
+++ b/examples/solidstart/src/routes/index.tsx
@@ -35,7 +35,16 @@ export default function Home() {
//----------------------------------
return (
-
+
+
+ Showcase: every feature →
+
{/* 4. Use a component.
It will only render its children when the `when` condition is true.
Since `isClient()` is false on the server, nothing inside the
diff --git a/examples/solidstart/src/routes/showcase.tsx b/examples/solidstart/src/routes/showcase.tsx
new file mode 100644
index 0000000..fe73790
--- /dev/null
+++ b/examples/solidstart/src/routes/showcase.tsx
@@ -0,0 +1,26 @@
+import { Title } from '@solidjs/meta'
+import { clientOnly } from '@solidjs/start'
+
+// The showcase reads its saved layout from localStorage, so it renders on the
+// client only. The server sends the page shell and the fallback.
+const Showcase = clientOnly(() => import('../components/Showcase'))
+
+export default function ShowcasePage() {
+ return (
+
+ SolidStart - Showcase
+
+ ← Basic SSR example
+
+ Loading layout… }
+ />
+
+ )
+}
diff --git a/examples/vanilla/README.md b/examples/vanilla/README.md
new file mode 100644
index 0000000..ab36ce6
--- /dev/null
+++ b/examples/vanilla/README.md
@@ -0,0 +1,18 @@
+# Plain JavaScript example
+
+Dynamix Layout with no framework: [`src/dom-layout.ts`](./src/dom-layout.ts) renders `@dynamix-layout/core` snapshots with plain DOM, and [`src/main.ts`](./src/main.ts) is the same all-features showcase as the React and Solid examples.
+
+```bash
+pnpm --filter @repo/vanilla-example dev # http://localhost:3012
+```
+
+What it shows:
+
+- **Drag** a tab onto another tab bar, onto a panel's side to split it, or onto a layout edge; drag a tab bar to move the whole panel. Escape cancels.
+- **Resize** with splitters (pointer, or arrow keys when focused).
+- **Close** closable tabs with × or Delete; **add** tabs, optionally at a target (`{ type: 'root', position: 'bottom' }`).
+- **Fold** (collapse) and **maximize/restore** from the tab bar toolbar, by double-clicking a tab bar, with Alt +- / Alt += , or from code.
+- **Select** and **move** tabs from code, **persist** the layout to localStorage, **reset** it.
+- Tab content is created once and never re-parented, so form state (try the Editor's textarea) survives every move.
+
+`createDomLayout(root, options)` returns `{ engine, setTabs, destroy }`; `engine` is the core [`Layout`](../../docs/api/create-layout.md), so every action is one call.
diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html
new file mode 100644
index 0000000..deae5fb
--- /dev/null
+++ b/examples/vanilla/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Dynamix Layout: plain JavaScript
+
+
+
+
+
+
diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json
new file mode 100644
index 0000000..d40f223
--- /dev/null
+++ b/examples/vanilla/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@repo/vanilla-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@dynamix-layout/core": "workspace:*",
+ "typescript": "~5.8.3",
+ "vite": "^7.3.6"
+ },
+ "packageManager": "pnpm@10.13.1",
+ "engines": {
+ "node": "^22.0.0",
+ "pnpm": "^10.0.0"
+ }
+}
diff --git a/examples/vanilla/src/dom-layout.ts b/examples/vanilla/src/dom-layout.ts
new file mode 100644
index 0000000..2f43662
--- /dev/null
+++ b/examples/vanilla/src/dom-layout.ts
@@ -0,0 +1,506 @@
+import {
+ applyRect,
+ createFrameScheduler,
+ createLayout,
+ getTabBarPlacement,
+ getTabContentRect,
+ type DragSource,
+ type DropMeasurements,
+ type DropTarget,
+ type Layout,
+ type LayoutChangeReason,
+ type LayoutJSON,
+ type LayoutSnapshot,
+ type Point,
+ type Rect,
+ type TabBarMeasurement,
+ type TabsetState,
+} from '@dynamix-layout/core'
+
+/** A tab as the app describes it. */
+export interface TabDef {
+ readonly id: string
+ readonly title: string
+ /** Created once and never moved between parents, so its state survives every move. */
+ readonly content: HTMLElement
+ readonly closable?: boolean
+ /** Where the tab goes the first time it appears. */
+ readonly target?: DropTarget
+}
+
+export interface DomLayoutOptions {
+ readonly tabs: readonly TabDef[]
+ readonly initialLayout?: LayoutJSON | null
+ readonly tabBarHeight?: number
+ readonly splitterSize?: number
+ readonly padding?: number
+ readonly minPanelSize?: { width: number; height: number }
+ readonly onLayoutChange?: (
+ layout: LayoutJSON,
+ reason: LayoutChangeReason
+ ) => void
+ /** The × button or Delete key asks to close a closable tab. */
+ readonly onTabClose?: (tabId: string) => void
+}
+
+export interface DomLayout {
+ readonly engine: Layout
+ /** Replaces the open tabs; content elements of kept tabs stay where they are. */
+ setTabs(tabs: readonly TabDef[]): void
+ destroy(): void
+}
+
+/** Pixels a pointer must travel before a press on a tab becomes a drag. */
+const DRAG_THRESHOLD = 4
+
+const el = (
+ tag: K,
+ className: string,
+ parent?: HTMLElement
+) => {
+ const element = document.createElement(tag)
+ element.className = className
+ parent?.append(element)
+ return element
+}
+
+const relativeRect = (rect: DOMRect, origin: Point): Rect => ({
+ x: rect.left - origin.x,
+ y: rect.top - origin.y,
+ width: rect.width,
+ height: rect.height,
+})
+
+/**
+ * Renders a layout into `root` with plain DOM: tab bars, close buttons,
+ * fold/maximize toolbars, splitters and a drop indicator, all driven by the
+ * engine's snapshots. Every interaction is one engine call.
+ */
+export function createDomLayout(
+ root: HTMLElement,
+ options: DomLayoutOptions
+): DomLayout {
+ const tabBarHeight = options.tabBarHeight ?? 36
+ const padding = options.padding ?? 0
+ let defs = new Map(options.tabs.map((tab) => [tab.id, tab]))
+
+ const engine = createLayout({
+ tabs: options.tabs,
+ initialLayout: options.initialLayout,
+ splitterSize: options.splitterSize ?? 6,
+ minPanelSize: options.minPanelSize ?? {
+ width: 80,
+ height: tabBarHeight,
+ },
+ foldedSize: tabBarHeight,
+ onLayoutChange: options.onLayoutChange,
+ })
+
+ root.classList.add('dx-root')
+ const panels = new Map()
+ const bars = new Map()
+ const barStates = new Map()
+ const contents = new Map()
+ const splitters = new Map()
+ const indicator = el('div', 'dx-drop-indicator', root)
+ indicator.hidden = true
+
+ // --- Coordinates and measurements ---
+
+ const toRootPoint = (event: PointerEvent): Point => {
+ const box = root.getBoundingClientRect()
+ return {
+ x: event.clientX - box.left - root.clientLeft,
+ y: event.clientY - box.top - root.clientTop,
+ }
+ }
+
+ const measure = (): DropMeasurements => {
+ const box = root.getBoundingClientRect()
+ const origin = {
+ x: box.left + root.clientLeft,
+ y: box.top + root.clientTop,
+ }
+ const tabBars = new Map()
+ bars.forEach((bar, tabsetId) => {
+ if (bar.hidden) return
+ tabBars.set(tabsetId, {
+ rect: relativeRect(bar.getBoundingClientRect(), origin),
+ isRotated: bar.hasAttribute('data-dx-rotated'),
+ tabs: Array.from(
+ bar.querySelectorAll('.dx-tab'),
+ (tab) => ({
+ id: tab.dataset.id!,
+ rect: relativeRect(tab.getBoundingClientRect(), origin),
+ })
+ ),
+ })
+ })
+ return { tabBars }
+ }
+
+ const resize = () =>
+ engine.setContainerRect({
+ x: padding,
+ y: padding,
+ width: Math.max(0, root.clientWidth - padding * 2),
+ height: Math.max(0, root.clientHeight - padding * 2),
+ })
+ const resizeFrame = createFrameScheduler(resize)
+ const observer = new ResizeObserver(() => resizeFrame.schedule(null))
+ observer.observe(root)
+
+ // --- Pointer drags (tabs, tab bars, splitters) ---
+
+ let stopDrag: (() => void) | null = null
+
+ /**
+ * Follows a press: `onClick` if it never moved past the threshold,
+ * otherwise a drag of `source` that ends in `endDrag` (or `cancelDrag` on Escape).
+ */
+ const trackPress = (
+ event: PointerEvent,
+ source: DragSource,
+ onClick?: () => void
+ ) => {
+ if (event.button !== 0 || stopDrag) return
+ const start = { x: event.clientX, y: event.clientY }
+ const isSplitter = source.type === 'splitter'
+ let dragging = false
+ let measurements: DropMeasurements | undefined
+ const frame = createFrameScheduler((point: Point) =>
+ engine.updateDrag(point, measurements)
+ )
+
+ const begin = () => {
+ if (!isSplitter) measurements = measure()
+ dragging = engine.startDrag(source)
+ if (dragging) root.setAttribute('data-dx-dragging', '')
+ return dragging
+ }
+ const move = (e: PointerEvent) => {
+ if (
+ !dragging &&
+ Math.hypot(e.clientX - start.x, e.clientY - start.y) <
+ DRAG_THRESHOLD
+ )
+ return
+ if (!dragging && !begin()) return finish()
+ frame.schedule(toRootPoint(e))
+ }
+ const up = () => {
+ if (dragging) {
+ frame.flush()
+ engine.endDrag()
+ } else onClick?.()
+ finish()
+ }
+ const key = (e: KeyboardEvent) => {
+ if (e.key !== 'Escape') return
+ frame.cancel()
+ if (dragging) engine.cancelDrag()
+ finish()
+ }
+ const finish = () => {
+ frame.cancel()
+ root.removeAttribute('data-dx-dragging')
+ window.removeEventListener('pointermove', move)
+ window.removeEventListener('pointerup', up)
+ window.removeEventListener('pointercancel', up)
+ window.removeEventListener('keydown', key)
+ stopDrag = null
+ }
+
+ stopDrag = () => {
+ if (dragging) engine.cancelDrag()
+ finish()
+ }
+ window.addEventListener('pointermove', move)
+ window.addEventListener('pointerup', up)
+ window.addEventListener('pointercancel', up)
+ window.addEventListener('keydown', key)
+ // A splitter drags from the first pixel; tabs wait for the threshold.
+ if (isSplitter && !begin()) finish()
+ }
+
+ // --- Tab bars ---
+
+ const closeTab = (tabId: string) => {
+ if (defs.get(tabId)?.closable) options.onTabClose?.(tabId)
+ }
+
+ const toolbarButton = (
+ parent: HTMLElement,
+ label: string,
+ text: string,
+ run: () => void
+ ) => {
+ const button = el('button', 'dx-toolbar-button', parent)
+ button.type = 'button'
+ button.title = label
+ button.setAttribute('aria-label', label)
+ button.textContent = text
+ button.addEventListener('pointerdown', (e) => e.stopPropagation())
+ button.addEventListener('click', run)
+ }
+
+ const fillTabBar = (bar: HTMLElement, tabset: TabsetState) => {
+ bar.replaceChildren()
+ bar.setAttribute('role', 'tablist')
+ for (const tabId of tabset.tabIds) {
+ const def = defs.get(tabId)
+ const isActive = tabId === tabset.activeTabId
+ const tab = el('div', 'dx-tab', bar)
+ tab.dataset.id = tabId
+ tab.dataset.state = isActive ? 'active' : 'inactive'
+ tab.setAttribute('role', 'tab')
+ tab.setAttribute('aria-selected', String(isActive))
+ tab.tabIndex = isActive ? 0 : -1
+ el('span', 'dx-tab-title', tab).textContent = def?.title ?? tabId
+ if (def?.closable) {
+ const close = el('button', 'dx-tab-close', tab)
+ close.type = 'button'
+ close.textContent = '×'
+ close.tabIndex = -1
+ close.setAttribute('aria-label', `Close ${def.title}`)
+ close.addEventListener('pointerdown', (e) =>
+ e.stopPropagation()
+ )
+ close.addEventListener('click', () => closeTab(tabId))
+ }
+ tab.addEventListener('pointerdown', (e) => {
+ e.stopPropagation()
+ trackPress(e, { type: 'tab', tabId }, () => {
+ engine.selectTab(tabId)
+ focusTab(tabId)
+ })
+ })
+ tab.addEventListener('keydown', (e) => onTabKey(e, tabset, tabId))
+ }
+
+ const toolbar = el('div', 'dx-toolbar', bar)
+ if (tabset.canFold)
+ toolbarButton(
+ toolbar,
+ tabset.isFolded ? 'Unfold' : 'Fold',
+ tabset.isFolded ? '▸' : '▾',
+ () => engine.toggleFold(tabset.id)
+ )
+ if (tabset.canMaximize)
+ toolbarButton(
+ toolbar,
+ tabset.isMaximized ? 'Restore' : 'Maximize',
+ tabset.isMaximized ? '❐' : '□',
+ () => engine.toggleMaximize(tabset.id)
+ )
+ bar.toggleAttribute(
+ 'data-dx-has-toolbar',
+ toolbar.childElementCount > 0
+ )
+ }
+
+ const focusTab = (tabId: string) =>
+ root
+ .querySelector(
+ `.dx-tab[data-id="${CSS.escape(tabId)}"]`
+ )
+ ?.focus()
+
+ const onTabKey = (e: KeyboardEvent, tabset: TabsetState, tabId: string) => {
+ const index = tabset.tabIds.indexOf(tabId)
+ const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0
+ if (step) {
+ const next =
+ tabset.tabIds[
+ (index + step + tabset.tabIds.length) % tabset.tabIds.length
+ ]
+ engine.selectTab(next)
+ focusTab(next)
+ } else if (e.key === 'Delete') closeTab(tabId)
+ else if (e.altKey && e.key === '=') engine.toggleMaximize(tabset.id)
+ else if (e.altKey && e.key === '-') engine.toggleFold(tabset.id)
+ else return
+ e.preventDefault()
+ }
+
+ const createTabBar = (tabsetId: string) => {
+ const bar = el('div', 'dx-tab-bar', root)
+ // Pressing the empty part of a tab bar drags the whole tabset.
+ bar.addEventListener('pointerdown', (e) =>
+ trackPress(e, { type: 'tabset', tabsetId })
+ )
+ bar.addEventListener('dblclick', (e) => {
+ if ((e.target as HTMLElement).closest('.dx-toolbar')) return
+ engine.toggleMaximize(tabsetId)
+ })
+ return bar
+ }
+
+ // --- Splitters ---
+
+ const createSplitter = (splitterId: string) => {
+ const splitter = el('div', 'dx-splitter', root)
+ splitter.tabIndex = 0
+ splitter.setAttribute('role', 'separator')
+ splitter.addEventListener('pointerdown', (e) =>
+ trackPress(e, { type: 'splitter', splitterId })
+ )
+ splitter.addEventListener('keydown', (e) => {
+ const delta = {
+ ArrowLeft: -10,
+ ArrowUp: -10,
+ ArrowRight: 10,
+ ArrowDown: 10,
+ }[e.key]
+ if (!delta) return
+ e.preventDefault()
+ engine.moveSplitterBy(splitterId, delta)
+ })
+ return splitter
+ }
+
+ // --- Rendering ---
+
+ /** Creates, updates and removes one element per id in `ids`. */
+ const sync = (
+ elements: Map,
+ items: ReadonlyMap,
+ create: (id: string) => HTMLElement,
+ update: (element: HTMLElement, item: T, id: string) => void
+ ) => {
+ elements.forEach((element, id) => {
+ if (items.has(id)) return
+ element.remove()
+ elements.delete(id)
+ })
+ items.forEach((item, id) => {
+ let element = elements.get(id)
+ if (!element) elements.set(id, (element = create(id)))
+ update(element, item, id)
+ })
+ }
+
+ const render = (snapshot: LayoutSnapshot) => {
+ const { rects } = snapshot
+
+ sync(
+ panels,
+ snapshot.tabsets,
+ () => el('div', 'dx-panel', root),
+ (panel, tabset, id) => {
+ panel.hidden = tabset.isHidden
+ applyRect(panel, rects.tabsets.get(id)!)
+ }
+ )
+
+ sync(bars, snapshot.tabsets, createTabBar, (bar, tabset, id) => {
+ // Snapshots share unchanged tabset states, so `!==` means "redraw the tabs".
+ if (barStates.get(id) !== tabset || !bar.firstChild) {
+ fillTabBar(bar, tabset)
+ barStates.set(id, tabset)
+ }
+ for (const tabId of tabset.tabIds) {
+ const title = defs.get(tabId)?.title ?? tabId
+ const label = bar.querySelector(
+ `.dx-tab[data-id="${CSS.escape(tabId)}"] .dx-tab-title`
+ )
+ if (label && label.textContent !== title)
+ label.textContent = title
+ }
+ const placement = getTabBarPlacement(
+ rects.tabsets.get(id)!,
+ tabset,
+ tabBarHeight
+ )
+ bar.hidden = tabset.isHidden
+ bar.toggleAttribute('data-dx-rotated', placement.isRotated)
+ bar.toggleAttribute('data-dx-folded', tabset.isFolded)
+ bar.style.transform = placement.isRotated ? 'rotate(90deg)' : ''
+ applyRect(bar, placement.rect)
+ })
+ barStates.forEach((_, id) => {
+ if (!snapshot.tabsets.has(id)) barStates.delete(id)
+ })
+
+ sync(
+ contents,
+ snapshot.tabs,
+ (id) => {
+ const content = el('div', 'dx-tab-content', root)
+ content.setAttribute('role', 'tabpanel')
+ const def = defs.get(id)
+ if (def) content.append(def.content)
+ return content
+ },
+ (content, tab) => {
+ content.hidden = !tab.isVisible
+ if (tab.isVisible)
+ applyRect(
+ content,
+ getTabContentRect(
+ rects.tabsets.get(tab.tabsetId)!,
+ tabBarHeight
+ )
+ )
+ }
+ )
+
+ sync(
+ splitters,
+ snapshot.splitters,
+ createSplitter,
+ (splitter, state, id) => {
+ splitter.hidden = state.isHidden
+ splitter.dataset.dxDirection = state.direction
+ splitter.toggleAttribute('data-dx-locked', state.isLocked)
+ splitter.setAttribute(
+ 'aria-orientation',
+ state.direction === 'horizontal' ? 'vertical' : 'horizontal'
+ )
+ const bounds = engine.getSplitterBounds(id)
+ if (bounds) {
+ splitter.setAttribute(
+ 'aria-valuenow',
+ String(Math.round(bounds.value))
+ )
+ splitter.setAttribute(
+ 'aria-valuemin',
+ String(Math.round(bounds.min))
+ )
+ splitter.setAttribute(
+ 'aria-valuemax',
+ String(Math.round(bounds.max))
+ )
+ }
+ applyRect(splitter, rects.splitters.get(id)!)
+ }
+ )
+
+ const box = snapshot.drag?.indicator
+ indicator.hidden = !box
+ if (box) applyRect(indicator, box)
+ }
+
+ const unsubscribe = engine.subscribe(render)
+ resize()
+ render(engine.getSnapshot())
+
+ return {
+ engine,
+ setTabs(tabs) {
+ defs = new Map(tabs.map((tab) => [tab.id, tab]))
+ barStates.clear() // titles or closable may have changed
+ engine.setTabs(tabs)
+ render(engine.getSnapshot())
+ },
+ destroy() {
+ stopDrag?.()
+ resizeFrame.cancel()
+ observer.disconnect()
+ unsubscribe()
+ engine.destroy()
+ root.replaceChildren()
+ root.classList.remove('dx-root')
+ },
+ }
+}
diff --git a/examples/vanilla/src/main.ts b/examples/vanilla/src/main.ts
new file mode 100644
index 0000000..f8bd60b
--- /dev/null
+++ b/examples/vanilla/src/main.ts
@@ -0,0 +1,178 @@
+import type { DropTarget, LayoutJSON, RowJSON } from '@dynamix-layout/core'
+import { createDomLayout, type TabDef } from './dom-layout'
+import './style.css'
+
+const STORAGE_KEY = 'dynamix-layout:vanilla-showcase'
+
+const loadSaved = (): LayoutJSON | null => {
+ try {
+ const json = localStorage.getItem(STORAGE_KEY)
+ return json ? (JSON.parse(json) as LayoutJSON) : null
+ } catch {
+ return null
+ }
+}
+
+const panel = (background: string, html: string) => {
+ const element = document.createElement('div')
+ element.className = 'panel'
+ element.style.background = background
+ element.innerHTML = html
+ return element
+}
+
+const HELP = `
+ Drag a tab onto another tab bar, onto a panel's side to split it, or onto a layout edge.
+ Drag a tab bar to move the whole panel. Drag a splitter (or focus it and use the arrows) to resize.
+ Fold or maximize with the buttons at the end of a tab bar (hover it), or double-click a tab bar to maximize.
+ Keyboard: arrows move between tabs, Alt += maximizes, Alt +- folds, Escape cancels a drag.
+ Close tabs with × (or Delete ), add them with the buttons above. The layout is saved to localStorage: reload the page.
+ `
+
+const initialTabs: TabDef[] = [
+ { id: 'guide', title: 'Guide', content: panel('#fffde7', HELP) },
+ {
+ id: 'editor',
+ title: 'Editor',
+ content: panel(
+ '#e3f2fd',
+ 'Editor: type here, then move the tab. The text survives because content is never re-created. '
+ ),
+ },
+ {
+ id: 'terminal',
+ title: 'Terminal',
+ content: panel('#212121', '$ _ '),
+ },
+ {
+ id: 'preview',
+ title: 'Preview',
+ content: panel('#e8f5e9', 'Preview'),
+ closable: true,
+ },
+ {
+ id: 'notes',
+ title: 'Notes',
+ content: panel('#fce4ec', 'Notes (closable)'),
+ closable: true,
+ },
+]
+
+const newTab = (n: number, target?: DropTarget): TabDef => ({
+ id: `new-${n}`,
+ title: `New ${n}`,
+ content: panel('#ede7f6', `Tab new-${n}`),
+ closable: true,
+ target,
+})
+const newTabNumber = (id: string) => Number(/^new-(\d+)$/.exec(id)?.[1] ?? 0)
+
+/** Tab ids in a saved layout, so added and closed tabs survive a reload too. */
+const tabIdsOf = (row: RowJSON): string[] =>
+ row.children.flatMap((child) =>
+ child.type === 'row'
+ ? tabIdsOf(child)
+ : child.children.map((tab) => tab.id)
+ )
+
+/** The tabs a saved layout had open, or the initial ones. */
+const restoreTabs = (saved: LayoutJSON | null): TabDef[] => {
+ if (!saved) return initialTabs
+ const known = new Map(initialTabs.map((tab) => [tab.id, tab]))
+ return tabIdsOf(saved.root).flatMap((id) => {
+ const tab =
+ known.get(id) ??
+ (newTabNumber(id) ? newTab(newTabNumber(id)) : null)
+ return tab ? [tab] : []
+ })
+}
+
+const app = document.querySelector('#app')!
+const controls = document.createElement('div')
+controls.className = 'controls'
+const log = document.createElement('div')
+log.className = 'log'
+log.textContent = 'Changes appear here (onLayoutChange reasons).'
+const root = document.createElement('div')
+app.append(controls, log, root)
+
+const saved = loadSaved()
+let tabs = restoreTabs(saved)
+let counter = Math.max(0, ...tabs.map((tab) => newTabNumber(tab.id)))
+const entries: string[] = []
+const write = (entry: string) => {
+ entries.unshift(entry)
+ log.textContent = entries.slice(0, 8).join(' · ')
+}
+
+const layout = createDomLayout(root, {
+ tabs,
+ initialLayout: saved,
+ padding: 6,
+ splitterSize: 6,
+ tabBarHeight: 36,
+ minPanelSize: { width: 120, height: 80 },
+ onLayoutChange: (json, reason) => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(json))
+ write(reason)
+ },
+ onTabClose: (id) => setTabs(tabs.filter((tab) => tab.id !== id)),
+})
+const { engine } = layout
+
+const setTabs = (next: TabDef[]) => {
+ tabs = next
+ layout.setTabs(tabs)
+}
+
+const tabsetOf = (tabId: string) =>
+ engine.getSnapshot().tabs.get(tabId)?.tabsetId ?? ''
+
+/** Runs an engine action and logs it when the engine refuses it. */
+const act = (name: string, run: () => boolean) => {
+ if (!run()) write(`${name}: not possible right now`)
+}
+
+const addTab = (target?: DropTarget) => {
+ setTabs([...tabs, newTab(++counter, target)])
+}
+
+const button = (label: string, onClick: () => void) => {
+ const element = document.createElement('button')
+ element.type = 'button'
+ element.textContent = label
+ element.addEventListener('click', onClick)
+ controls.append(element)
+}
+
+button('Add tab', () => addTab())
+button('Add tab at the bottom', () =>
+ addTab({ type: 'root', position: 'bottom' })
+)
+button('Maximize / restore Editor', () =>
+ act('Maximize', () => engine.toggleMaximize(tabsetOf('editor')))
+)
+button('Fold / unfold Terminal', () =>
+ act('Fold', () => engine.toggleFold(tabsetOf('terminal')))
+)
+button('Select Preview', () => act('Select', () => engine.selectTab('preview')))
+button('Move Notes next to Editor', () =>
+ act('Move', () =>
+ engine.moveTab('notes', {
+ type: 'tab',
+ tabId: 'editor',
+ position: 'after',
+ })
+ )
+)
+button('Reset layout', () => {
+ setTabs(initialTabs)
+ engine.reset()
+})
+button('Forget saved layout', () => localStorage.removeItem(STORAGE_KEY))
+
+// Vite HMR: tear the old instance down before the module re-runs.
+import.meta.hot?.dispose(() => {
+ layout.destroy()
+ app.replaceChildren()
+})
diff --git a/examples/vanilla/src/style.css b/examples/vanilla/src/style.css
new file mode 100644
index 0000000..985bac3
--- /dev/null
+++ b/examples/vanilla/src/style.css
@@ -0,0 +1,283 @@
+/* The same look as @dynamix-layout/react/styles.css, for plain DOM. */
+.dx-root {
+ --dx-font: inherit;
+ --dx-tab-bar-bg: #dfdfdf;
+ --dx-tab-bg: #dfdfdf;
+ --dx-tab-active-bg: #ffffff;
+ --dx-tab-color: inherit;
+ --dx-tab-radius: 4px;
+ --dx-tab-gap: 6px;
+ --dx-splitter-bg: #ffffff;
+ --dx-splitter-hover-bg: #e8f2ff;
+ --dx-grip-color: #9a9a9a;
+ --dx-drop-indicator-bg: rgba(0, 175, 249, 0.35);
+ --dx-drop-indicator-border: 2px dashed rgb(0, 196, 42);
+ --dx-root-drop-zone-bg: #0081f9;
+ --dx-focus-ring: 2px solid #0081f9;
+
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ box-sizing: border-box;
+ font-family: var(--dx-font);
+}
+
+/* Server render and first client render: nothing is measured yet. */
+.dx-root[data-dx-measuring] > * {
+ visibility: hidden;
+}
+
+.dx-root *,
+.dx-root *::before,
+.dx-root *::after {
+ box-sizing: border-box;
+}
+
+.dx-panel,
+.dx-tab-bar,
+.dx-tab-content,
+.dx-splitter,
+.dx-drop-indicator,
+.dx-root-drop-zone {
+ position: absolute;
+}
+
+.dx-panel {
+ z-index: 1;
+}
+
+/* --- Tab bar and tabs --- */
+.dx-tab-bar {
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ gap: var(--dx-tab-gap);
+ padding-left: 8px;
+ background: var(--dx-tab-bar-bg);
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ transform-origin: 0 0;
+ touch-action: pan-x;
+ cursor: default;
+}
+.dx-tab-bar::-webkit-scrollbar {
+ display: none;
+}
+.dx-tab-bar[data-dx-has-toolbar] {
+ padding-right: 64px;
+}
+
+.dx-tab {
+ display: inline-flex;
+ flex: none;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 6px;
+ border-radius: var(--dx-tab-radius);
+ background: var(--dx-tab-bg);
+ color: var(--dx-tab-color);
+ font-size: 14px;
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+}
+.dx-tab[data-state='active'] {
+ background: var(--dx-tab-active-bg);
+}
+.dx-tab:focus-visible,
+.dx-splitter:focus-visible,
+.dx-toolbar-button:focus-visible {
+ outline: var(--dx-focus-ring);
+ outline-offset: -2px;
+}
+
+.dx-tab-close {
+ display: grid;
+ place-items: center;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: 0;
+ border-radius: 3px;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+.dx-tab-close:hover {
+ background: rgba(0, 0, 0, 0.08);
+}
+
+/* --- Tab content --- */
+.dx-tab-content {
+ z-index: 2;
+ overflow: auto;
+ overscroll-behavior: contain;
+ scrollbar-width: none;
+}
+.dx-tab-content::-webkit-scrollbar {
+ display: none;
+}
+/* While dragging, iframes and editors inside tabs must not swallow pointer events. */
+.dx-root[data-dx-dragging] .dx-tab-content > * {
+ pointer-events: none;
+}
+.dx-root[data-dx-dragging],
+.dx-root[data-dx-dragging] * {
+ cursor: grabbing;
+}
+
+/* --- Splitters --- */
+.dx-splitter {
+ z-index: 4;
+ display: grid;
+ place-items: center;
+ background: var(--dx-splitter-bg);
+ touch-action: none;
+}
+.dx-splitter[data-dx-direction='horizontal'] {
+ cursor: ew-resize;
+}
+.dx-splitter[data-dx-direction='vertical'] {
+ cursor: ns-resize;
+}
+.dx-splitter:hover {
+ background: var(--dx-splitter-hover-bg);
+}
+.dx-splitter[data-dx-locked] {
+ pointer-events: none;
+}
+/* A grip of three dots across the bar. */
+.dx-splitter::after {
+ content: '';
+ width: 3px;
+ height: 3px;
+ border-radius: 50%;
+ background: var(--dx-grip-color);
+ box-shadow:
+ 0 -6px 0 var(--dx-grip-color),
+ 0 6px 0 var(--dx-grip-color);
+}
+.dx-splitter[data-dx-direction='vertical']::after {
+ box-shadow:
+ -6px 0 0 var(--dx-grip-color),
+ 6px 0 0 var(--dx-grip-color);
+}
+
+/* --- Drag feedback --- */
+.dx-drop-indicator {
+ z-index: 6;
+ background: var(--dx-drop-indicator-bg);
+ border: var(--dx-drop-indicator-border);
+ border-radius: 10px;
+ pointer-events: none;
+ transition: all 0.15s ease;
+}
+
+.dx-root-drop-zone {
+ z-index: 5;
+ background: var(--dx-root-drop-zone-bg);
+ opacity: 0.6;
+ pointer-events: none;
+}
+.dx-root-drop-zone[data-dx-active] {
+ opacity: 1;
+}
+.dx-root-drop-zone[data-dx-side='left'] {
+ border-radius: 0 16px 16px 0;
+}
+.dx-root-drop-zone[data-dx-side='right'] {
+ border-radius: 16px 0 0 16px;
+}
+.dx-root-drop-zone[data-dx-side='top'] {
+ border-radius: 0 0 16px 16px;
+}
+.dx-root-drop-zone[data-dx-side='bottom'] {
+ border-radius: 16px 16px 0 0;
+}
+
+/* --- Tabset toolbar (maximize / fold) --- */
+/* Shown while the tab bar is hovered or focused; folded strips always show it. */
+.dx-toolbar {
+ position: absolute;
+ top: 0;
+ right: 6px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding-left: 6px;
+ /* Opaque, so overflowing tabs slide underneath instead of showing through. */
+ background: inherit;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+.dx-tab-bar:hover .dx-toolbar,
+.dx-tab-bar[data-dx-folded] .dx-toolbar,
+.dx-toolbar:focus-within {
+ opacity: 1;
+}
+@media (hover: none) {
+ .dx-toolbar {
+ opacity: 1;
+ }
+}
+
+.dx-toolbar-button {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+.dx-toolbar-button:hover {
+ background-color: rgba(0, 0, 0, 0.08);
+}
+
+.dx-root [hidden] {
+ display: none !important;
+}
+
+/* --- Page --- */
+html,
+body,
+#app {
+ height: 100%;
+ margin: 0;
+ font-family: system-ui, sans-serif;
+}
+#app {
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+}
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ padding: 8px;
+ border-bottom: 1px solid #eee;
+}
+.controls button {
+ padding: 4px 10px;
+}
+.log {
+ min-height: 20px;
+ padding: 4px 8px;
+ font-size: 12px;
+ color: #666;
+}
+.panel {
+ height: 100%;
+ padding: 16px;
+ line-height: 1.6;
+}
+.panel ul {
+ margin: 0;
+ padding-left: 18px;
+}
diff --git a/examples/vanilla/src/vite-env.d.ts b/examples/vanilla/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/examples/vanilla/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/vanilla/tsconfig.json b/examples/vanilla/tsconfig.json
new file mode 100644
index 0000000..b3d4465
--- /dev/null
+++ b/examples/vanilla/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts
new file mode 100644
index 0000000..cb4096c
--- /dev/null
+++ b/examples/vanilla/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ server: {
+ port: 3012,
+ },
+})
diff --git a/package.json b/package.json
index 97c330a..7502c4c 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@testing-library/react": "^16.3.3",
"@types/node": "^24.19.1",
"@vitejs/plugin-react": "^4.7.0",
+ "@vitest/coverage-v8": "4.1.11",
"@vitest/ui": "^4.1.11",
"eslint": "^9.39.5",
"eslint-plugin-react": "^7.37.5",
diff --git a/packages/core/README.md b/packages/core/README.md
index 8d89746..1e396b4 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -1,207 +1,38 @@
-
+# @dynamix-layout/core
-[](https://www.patreon.com/akashaman)
-[](https://www.buymeacoffee.com/akashaman)
-[](mailto:sir.akashaman@gmail.com)
+The framework-agnostic engine behind Dynamix Layout. It handles the layout tree (rows, tabsets and tabs), geometry and minimum sizes, splitters, drag-and-drop targets, maximize and fold, and versioned serialization. It has no dependencies, no global state and no DOM access. Every `createLayout()` is an independent instance.
-
+Use it directly to build an adapter for another framework, or through [`@dynamix-layout/react`](https://www.npmjs.com/package/@dynamix-layout/react) or [`@dynamix-layout/solid`](https://www.npmjs.com/package/@dynamix-layout/solid).
-# 🚀 @dynamix-layout/core
+## Install
-## Overview
-
-**@dynamix-layout/core** is the **engine** that powers the **creation**, **management**, and **dynamic modification** of your layout structures. It handles the **internal logic** of the layout tree, including **node creation**, **dimension calculation**, and **state updates**. This package works with a **hierarchical tree structure** composed of `Node` objects and exposes a **reactive state** for seamless integration with modern UI frameworks.
-
----
-
-### Made with ❤️ by [Akash Aman](https://linktr.ee/akash_aman)
-
----
-
-
-
----
-
-## Core Classes
-
-The package is built around three primary classes: `DynamixLayoutCore`, `Node`, and `Bond`.
-
-1. **`DynamixLayoutCore`**: The main controller class that you instantiate to create and manage the entire layout. It serves as the primary public API.
-2. **`Node`**: The fundamental building block of the layout tree. Each `Node` can be a `row`, a `tabset`, or a `tab`.
-3. **`Bond`**: A special object that represents the draggable separator between two nodes, allowing for resizing.
-
----
-
-## `DynamixLayoutCore` Class
-
-The `DynamixLayoutCore` class is the central point of control for your entire layout configuration.
-
-### Constructor
-
-When initializing the `DynamixLayoutCore` class, you provide a single **options object**.
-
-`new DynamixLayoutCore(options)`
-
-- **`options`** (`object`): An object containing configuration properties.
- - **`tabs`** (`string[]`): An array of tab names to generate an initial binary tree layout. Defaults to `[]`.
- - **`tree`** (`LayoutTree | null`): A serialized JSON representation of a previously saved layout (`LayoutTree` object). If provided, it will recreate that specific layout. Defaults to `null`.
- - **`tabsIds`** (`Map`): A map where keys are tab names and values are their unique IDs. This is useful for persisting tab identities across sessions.
- - **`minW`** (`number`): The minimum width for a `tabset`. Defaults to `40`.
- - **`minH`** (`number`): The minimum height for a `tabset`. Defaults to `40`.
- - **`bond`** (`number`): The width or height (in pixels) of the draggable separator (`Bond`) between nodes. Defaults to `10`.
-
-### Reactive Properties
-
-The layout's state is exposed through static reactive properties on the `Node` class. You can subscribe to these to drive your UI.
-
-- **`Node.cache.nodOpts`**: A reactive map of all `tabset` nodes and their options.
-- **`Node.cache.bndOpts`**: A reactive map of all `bond` objects.
-- **`Node.cache.tabOpts`**: A reactive map of all `tab` nodes within their respective tabsets.
-
-### Methods
-
-#### 🌳 `updateTree(src, des, layout)`
-
-This is the core method for rearranging the layout. It moves a source node (`src`) to a new position relative to a destination node (`des`).
-
-- **`src`** (`string`): The unique ID of the node to move (can be a `tab` or a `tabset`).
-- **`des`** (`string`): The unique ID of the destination node.
-- **`layout`** (`'top' | 'bottom' | 'left' | 'right' | 'contain'`): Specifies how the source should be placed relative to the destination.
- - `top`, `bottom`, `left`, `right`: Places the source node by splitting the destination area.
- - `contain`: Places a source `tab` into the destination `tabset`.
-
-**Returns**: `boolean` - `true` if the update was successful, `false` otherwise.
-
-```javascript
-// Moves the node with 'sidebar-id' to be to the left of 'main-content-id'
-layout.updateTree('sidebar-id', 'main-content-id', 'left')
+```bash
+npm install @dynamix-layout/core
```
----
+## Quick start
-#### 📏 `updateDimension(dim, disableTimeout, timeout)`
+```ts
+import { createLayout } from '@dynamix-layout/core'
-Recalculates all node dimensions within the layout, typically after the main container has been resized.
-
-- **`dim`** (`{ w, h, x, y }`): An object representing the new dimensions of the root container.
-- **`disableTimeout`** (`boolean`): If `true`, the update runs instantly. Otherwise, it's debounced. Defaults to `false`.
-- **`timeout`** (`number`): The debounce delay in milliseconds. Defaults to `2`.
-
-
-
-```javascript
-// On window resize, update the layout dimensions
-layout.updateDimension({
- w: window.innerWidth,
- h: window.innerHeight,
- x: 0,
- y: 0,
+const layout = createLayout({
+ tabs: [{ id: 'editor' }, { id: 'terminal' }, { id: 'preview' }],
+ onLayoutChange: (json) => localStorage.setItem('layout', JSON.stringify(json)),
})
-```
-
----
-
-#### ↔️ `updateSlider(id, dim, disableTimeout, timeout)`
-
-Updates the layout in response to a user dragging a `Bond` (slider).
-
-- **`id`** (`string`): The unique ID of the `Bond` being moved.
-- **`dim`** (`{ x, y }`): An object with the new coordinates of the slider.
-- **`disableTimeout`** (`boolean`): If `true`, the update runs instantly. Defaults to `false`.
-- **`timeout`** (`number`): The debounce delay in milliseconds. Defaults to `2`.
-
-
-```javascript
-// When a user drags a separator
-layout.updateSlider('bond-123', { x: 450, y: 300 })
-```
-
----
-
-#### 🗑️ `clearAllCache()`
-
-Clears all internal caches for nodes, bonds, dimensions, and other layout data. Use this when completely destroying or resetting the layout.
-
-```javascript
-layout.clearAllCache()
-```
-
----
-
-## `Node` Class
-
-The `Node` represents a single element in the layout tree. You will typically interact with Node data through the reactive properties rather than manipulating `Node` objects directly.
-
-### Constructor
-
-`new Node(options)`
-
-- **`options`** (`object`): An object containing node properties like `type`, `host`, `name`, `part`, `dims`, `unId`, and `open`.
-
-### Static Cache (`Node.cache`)
-
-The `Node` class contains a static `cache` object. This centralized cache holds the state for the entire layout, including maps of all nodes, dimensions, and the **reactive options** that your UI will consume. **Direct manipulation of the cache is not recommended.**
-
-### Methods
-
-#### 💾 `toJSON()`
-
-Serializes the node and its entire subtree into a JSON object (`LayoutTree`). This is used to save the current state of the layout.
-
-```javascript
-const rootNode = DynamixLayoutCore._root // Access the static root node
-const layoutJSON = rootNode.toJSON()
-// localStorage.setItem('savedLayout', JSON.stringify(layoutJSON));
-```
-
-## Example Usage
-
-```javascript
-import { DynamixLayoutCore, Node } from '@dynamix-layout/core'
-
-// 1. Initialize the Layout class
-// Provide an options object to the constructor
-const layout = new DynamixLayoutCore({
- tabs: ['editor', 'terminal', 'preview'],
-})
-
-// ...or restore a layout from a saved JSON object
-// const savedLayout = JSON.parse(localStorage.getItem('savedLayout'));
-// const layout = new DynamixLayoutCore({ tree: savedLayout });
-
-// 2. Set the initial dimensions of your container
-layout.updateDimension({ w: 1200, h: 800, x: 0, y: 0 }, true)
-
-// 3. Subscribe to reactive state to render your UI.
-const unsubscribe = Node.cache.nodOpts.onChange((nodeOptionsMap) => {
- console.log('Layout updated!', nodeOptionsMap)
- // Your UI rendering logic here...
+layout.subscribe((snapshot) => {
+ for (const [id, rect] of snapshot.rects.tabsets) draw(id, rect)
})
-
-// 4. Move a tab to a new location
-// (In a real app, you'd get these IDs from the reactive state)
-setTimeout(() => {
- const terminalTabId = 'some-unique-id-for-terminal-tab'
- const editorTabsetId = 'some-unique-id-for-editor-tabset'
-
- // Move the terminal tab into the editor's tabset
- layout.updateTree(terminalTabId, editorTabsetId, 'contain')
-}, 2000)
-
-// 5. Clean up when the component unmounts
-// unsubscribe();
-// layout.clearAllCache();
+layout.setContainerRect({ x: 0, y: 0, width: 1200, height: 800 })
+layout.moveTab('terminal', { type: 'root', position: 'bottom' })
```
----
-
-
-
-[](https://www.patreon.com/akashaman)
-[](https://www.buymeacoffee.com/akashaman)
-[](mailto:sir.akashaman@gmail.com)
+## Documentation
-### Made with ❤️ by [Akash Aman](https://linktr.ee/akash_aman)
+- [Using the core without React](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/guides/core-without-react.md)
+- [`createLayout` API](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/api/create-layout.md)
+- [`LayoutJSON`](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/api/layout-json.md)
+- [`migrateLayoutFromV1`](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/api/migrate-layout-from-v1.md)
+- [Migrating from v1](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md)
-
+MIT © [Akash Aman](https://linktr.ee/akash_aman)
diff --git a/packages/core/package.json b/packages/core/package.json
index 129297a..ab1326a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -28,21 +28,23 @@
"url": "https://github.com/xcode-studio/dynamix-layout/issues"
},
"homepage": "https://github.com/xcode-studio/dynamix-layout#readme",
- "main": "./dist/core.cjs",
- "module": "./dist/core.es.js",
- "types": "./dist/types/index.d.ts",
- "typings": "./dist/types/index.d.ts",
+ "type": "module",
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "unpkg": "./dist/index.umd.js",
"exports": {
".": {
"import": {
- "types": "./dist/types/index.d.ts",
- "default": "./dist/core.es.js"
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
},
"require": {
- "types": "./dist/types/index.d.cts",
- "default": "./dist/core.cjs"
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
}
- }
+ },
+ "./package.json": "./package.json"
},
"files": [
"dist",
@@ -55,7 +57,6 @@
"publishConfig": {
"access": "public"
},
- "type": "module",
"engines": {
"node": ">=18"
},
diff --git a/packages/core/src/app/comparator.ts b/packages/core/src/app/comparator.ts
deleted file mode 100644
index 1496584..0000000
--- a/packages/core/src/app/comparator.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { NodeOptions } from '../type'
-
-/** @deprecated Internal; removed in 2.0. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export function areNodeOptionsMapEqual(
- mapA: Map,
- mapB: Map
-): boolean {
- if (mapA.size !== mapB.size) return false
-
- for (const [key, valA] of mapA.entries()) {
- const valB = mapB.get(key)
- if (!valB) return false
-
- if (!areNodeOptionsEqual(valA, valB)) {
- return false
- }
- }
-
- return true
-}
-
-/** @deprecated Internal; removed in 2.0. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export function areNodeOptionsEqual(a: NodeOptions, b: NodeOptions): boolean {
- if (a.typNode !== b.typNode) return false
- if (a.nodName !== b.nodName) return false
- if (a.uidNode !== b.uidNode) return false
- if (a.nodPart !== b.nodPart) return false
- if (a.nodFold !== b.nodFold) return false
- if (a.nodMaxd !== b.nodMaxd) return false
- if (a.nodHidden !== b.nodHidden) return false
- if (a.nodLocked !== b.nodLocked) return false
-
- const dimA = a.nodDims
- const dimB = b.nodDims
-
- return (
- dimA.x === dimB.x &&
- dimA.y === dimB.y &&
- dimA.w === dimB.w &&
- dimA.h === dimB.h
- )
-}
diff --git a/packages/core/src/app/dom.ts b/packages/core/src/app/dom.ts
deleted file mode 100644
index 49057b7..0000000
--- a/packages/core/src/app/dom.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import type { Dimension, NodeOptions } from '../type'
-
-/** @deprecated Replaced in 2.0 by `applyRect(element, rect)`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export function setElementRect(el: HTMLElement, { x, y, w, h }: Dimension) {
- el.style.left = `${x}px`
- el.style.top = `${y}px`
- el.style.width = `${w}px`
- el.style.height = `${h}px`
-}
-
-/**
- * Area of a tabset below its tab bar; never negative when squeezed.
- * @deprecated Replaced in 2.0 by `getTabContentRect(tabsetRect, tabBarHeight)`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md
- */
-export function getTabBodyRect(
- tabset: Dimension,
- tabbarHeight: number
-): Dimension {
- return {
- x: tabset.x,
- y: tabset.y + tabbarHeight,
- w: tabset.w,
- h: Math.max(0, tabset.h - tabbarHeight),
- }
-}
-
-export interface TabbarPlacement {
- /** Box to give the tab bar element before `rotated` is applied. */
- rect: Dimension
- /** Folded tabset in a side-by-side row: the bar is drawn as a vertical strip. */
- rotated: boolean
-}
-
-/**
- * Where a tabset's tab bar goes. A folded tabset in a side-by-side row shows
- * its tab bar rotated 90 degrees: an element `h` wide and `w` tall, rotated
- * around its top-left corner and placed at `x + w`, covers exactly the strip.
- */
-export function getTabbarPlacement(
- tabset: Pick,
- tabbarHeight: number
-): TabbarPlacement {
- const { x, y, w, h } = tabset.nodDims
- // A tabset's own direction is the opposite of its row's.
- const rowIsHorizontal = tabset.nodeDir === false
- if (tabset.nodFold && rowIsHorizontal) {
- return { rect: { x: x + w, y, w: h, h: w }, rotated: true }
- }
- return { rect: { x, y, w, h: tabbarHeight }, rotated: false }
-}
-
-export function placeTabbar(el: HTMLElement, placement: TabbarPlacement) {
- setElementRect(el, placement.rect)
- el.style.transformOrigin = '0 0'
- el.style.transform = placement.rotated ? 'rotate(90deg)' : ''
-}
-
-export interface FrameScheduler {
- /** Remember `value`; `onFlush` runs with the latest value on the next frame. */
- schedule: (value: T) => void
- /** Run a pending flush now (e.g. on pointer up). */
- flush: () => void
- /** Drop a pending flush (e.g. on unmount). */
- cancel: () => void
-}
-
-/**
- * Coalesces bursts of updates (pointer events can fire several times per
- * frame) into at most one `onFlush` call per animation frame.
- */
-export function createFrameScheduler(
- onFlush: (value: T) => void
-): FrameScheduler {
- let pending: { value: T } | null = null
- let frame: number | null = null
-
- const run = () => {
- frame = null
- const next = pending
- pending = null
- if (next) onFlush(next.value)
- }
-
- return {
- schedule(value) {
- pending = { value }
- if (frame === null) frame = requestAnimationFrame(run)
- },
- flush() {
- if (frame === null) return
- cancelAnimationFrame(frame)
- run()
- },
- cancel() {
- if (frame !== null) cancelAnimationFrame(frame)
- frame = null
- pending = null
- },
- }
-}
diff --git a/packages/core/src/app/drop-preview.ts b/packages/core/src/app/drop-preview.ts
deleted file mode 100644
index cb206cc..0000000
--- a/packages/core/src/app/drop-preview.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import type { Dimension } from '../type'
-
-export type DropArea = 'top' | 'bottom' | 'left' | 'right' | 'contain'
-export type RootSide = Exclude
-
-/** Where a dragged tab or tabset would land, in viewport coordinates. */
-export interface DropPreview {
- area: DropArea
- left: number
- top: number
- width: number
- height: number
-}
-
-export type PreviewRect = Pick<
- DOMRect,
- 'left' | 'top' | 'right' | 'bottom' | 'width' | 'height'
->
-
-const DEFAULT_TAB_GAP = 6
-
-/**
- * Splits a tabset into thirds: the outer thirds drop beside it, the middle
- * drops into it as a tab.
- * @deprecated Replaced in 2.0 by `layout.getDropTarget()` and `layout.getDropIndicatorRect()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md
- */
-export function getTabsetDropPreview(
- rect: PreviewRect,
- clientX: number,
- clientY: number
-): DropPreview {
- const { left, top, width: w, height: h } = rect
- const x = clientX - left
- const y = clientY - top
-
- if (x < w / 3) return { area: 'left', left, top, width: w / 2, height: h }
- if (x > (2 * w) / 3)
- return {
- area: 'right',
- left: left + w / 2,
- top,
- width: w / 2,
- height: h,
- }
- if (y < h / 3) return { area: 'top', left, top, width: w, height: h / 2 }
- if (y > (2 * h) / 3)
- return {
- area: 'bottom',
- left,
- top: top + h / 2,
- width: w,
- height: h / 2,
- }
-
- return {
- area: 'contain',
- left: left + w * 0.1,
- top: top + h * 0.1,
- width: w * 0.8,
- height: h * 0.8,
- }
-}
-
-/**
- * The half of the whole layout a root-edge drop would take.
- * @deprecated Replaced in 2.0 by `layout.getDropTarget()` and `layout.getDropIndicatorRect()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md
- */
-export function getRootSplitPreview(
- container: Dimension,
- side: RootSide
-): DropPreview {
- const { x, y, w, h } = container
- switch (side) {
- case 'left':
- return { area: side, left: x, top: y, width: w / 2, height: h }
- case 'right':
- return {
- area: side,
- left: x + w / 2,
- top: y,
- width: w / 2,
- height: h,
- }
- case 'top':
- return { area: side, left: x, top: y, width: w, height: h / 2 }
- case 'bottom':
- return {
- area: side,
- left: x,
- top: y + h / 2,
- width: w,
- height: h / 2,
- }
- }
-}
-
-/**
- * Insertion marker between the tabs of a tab bar. `index` is the tab the
- * marker sits next to. Returns null when the pointer is above or below the bar.
- * @deprecated Replaced in 2.0 by `layout.getDropTarget()` and `layout.getDropIndicatorRect()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md
- */
-export function getNavbarDropPreview(
- navbarRect: PreviewRect,
- tabRects: PreviewRect[],
- clientX: number,
- clientY: number
-): (DropPreview & { index: number }) | null {
- if (tabRects.length === 0) return null
- if (clientY < navbarRect.top || clientY > navbarRect.bottom) return null
-
- const first = tabRects[0]
- const last = tabRects[tabRects.length - 1]
- let gap = DEFAULT_TAB_GAP
-
- if (clientX > last.right) {
- return {
- index: tabRects.length - 1,
- area: 'right',
- left: last.right + 1,
- top: last.top,
- width: gap - 2,
- height: last.height,
- }
- }
-
- if (clientX < first.left) {
- return {
- index: 0,
- area: 'left',
- left: first.left - gap + 1,
- top: first.top,
- width: gap - 2,
- height: first.height,
- }
- }
-
- let index = 0
- let minDistance = Infinity
- let isLeftSide = false
- tabRects.forEach((rect, i) => {
- const center = rect.left + rect.width / 2
- const distance = Math.abs(clientX - center)
- if (distance < minDistance) {
- minDistance = distance
- index = i
- isLeftSide = clientX < center
- }
- })
-
- const hasNeighbour = isLeftSide ? index > 0 : index < tabRects.length - 1
- if (tabRects.length > 1 && hasNeighbour)
- gap = tabRects[1].left - first.right
-
- const rect = tabRects[index]
- return {
- index,
- area: isLeftSide ? 'left' : 'right',
- left: isLeftSide ? rect.left - gap + 1 : rect.right + 1,
- top: rect.top,
- width: gap - 2,
- height: rect.height,
- }
-}
-
-/** @deprecated Replaced in 2.0 by `layout.getDropTarget()` and `layout.getDropIndicatorRect()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export function isSameDropPreview(
- a: Partial,
- b: DropPreview
-): boolean {
- return (
- a.area === b.area &&
- a.left === b.left &&
- a.top === b.top &&
- a.width === b.width &&
- a.height === b.height
- )
-}
diff --git a/packages/core/src/app/dynamix.ts b/packages/core/src/app/dynamix.ts
deleted file mode 100644
index 7cfe551..0000000
--- a/packages/core/src/app/dynamix.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { Queue } from './queue'
-import { createReactiveState } from './reactive-state'
-import { areNodeOptionsMapEqual } from './comparator'
-import { Node, Bond } from './node'
-import { layoutState, defaultCreateId, defaultTimer } from './state'
-import type { Timer } from './state'
-import type {
- Dimension,
- LayoutTree,
- NodeOptions,
- RootAdjustment,
- ReactiveValue,
- TabsIds,
-} from '../type'
-import * as treeBuilder from './tree-builder'
-import * as geometry from './geometry'
-import * as slider from './slider'
-import * as treeMutations from './tree-mutations'
-import * as viewState from './view-state'
-
-/** @deprecated Replaced in 2.0 by `createLayout(options)`, which returns an independent instance (no static state). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-class Layout {
- static get _root(): Node {
- return layoutState.root
- }
-
- static set _root(value: Node) {
- layoutState.root = value
- }
-
- static get _tree(): LayoutTree | null {
- return layoutState.tree
- }
-
- static set _tree(value: LayoutTree | null) {
- layoutState.tree = value
- }
-
- static get _minW(): number {
- return layoutState.minW
- }
-
- static set _minW(value: number) {
- layoutState.minW = value
- }
-
- static get _minH(): number {
- return layoutState.minH
- }
-
- static set _minH(value: number) {
- layoutState.minH = value
- }
-
- static get _bond(): number {
- return layoutState.bond
- }
-
- static set _bond(value: number) {
- layoutState.bond = value
- }
-
- static get _inst(): Layout | null {
- return layoutState.inst
- }
-
- static set _inst(value: Layout | null) {
- layoutState.inst = value
- }
-
- nodeOps: ReactiveValue>
- cntdown: ReturnType | null = null
- tabsIds: TabsIds = new Map()
- constructor(
- options: {
- tabs?: string[]
- tree?: LayoutTree | null
- minW?: number
- minH?: number
- bond?: number
- uqid?: string
- tabsIds?: TabsIds
- /** Generates ids for new nodes and bonds (default `crypto.randomUUID`). */
- createId?: () => string
- /** Schedules deferred updates (default `setTimeout`/`clearTimeout`). */
- timer?: Timer
- /** Size of a folded tabset along its row; pass the tab bar height. */
- collapsedSize?: number
- } = {}
- ) {
- const config = {
- tabs: [],
- tree: null,
- minW: 40,
- minH: 40,
- bond: 10,
- uqid: 'dynamix-layout-root',
- tabsIds: new Map(),
- ...options,
- }
-
- this.tabsIds = config.tabsIds
- layoutState.createId = config.createId ?? defaultCreateId
- layoutState.timer = config.timer ?? defaultTimer
- layoutState.collapsedSize = config.collapsedSize ?? config.minH
-
- // Reset on every construction so a tabs-only layout never reuses the
- // tree of a previously created layout.
- Layout._tree = config.tree ?? null
- layoutState.maximized = config.tree?.nodMaxd ?? null
-
- Layout._root = new Node({
- unId: config.uqid,
- name: config.uqid,
- type: 'row',
- })
-
- this.nodeOps = createReactiveState(
- new Map(),
- areNodeOptionsMapEqual
- )
-
- Layout._minW = config.minW
- Layout._minH = config.minH
- Layout._bond = config.bond
-
- const tabsQueue = new Queue(config.tabs.length, config.tabs)
- if (Layout._tree) this.createNodeLayout()
- else this.createBinaryNodeTreeFromQueue(tabsQueue)
-
- Layout._inst = this
- }
-
- *JSONLayoutIterator(root: LayoutTree): IterableIterator {
- yield* treeBuilder.JSONLayoutIterator(root)
- }
-
- *NodeLayoutIterator(root: Node): IterableIterator {
- yield* treeBuilder.NodeLayoutIterator(root)
- }
-
- *NodeLayoutRecursiveIterator(root: Node): IterableIterator {
- yield* treeBuilder.NodeLayoutRecursiveIterator(this, root)
- }
-
- updateChildDirections(parent: Node) {
- return treeBuilder.updateChildDirections(this, parent)
- }
-
- createNodeLayout(root: LayoutTree | null = Layout._tree) {
- return treeBuilder.createNodeLayout(this, root)
- }
-
- createNodeTreeFromJSONTree(json: LayoutTree, host: Node) {
- return treeBuilder.createNodeTreeFromJSONTree(this, json, host)
- }
-
- createBinaryNodeTreeFromQueue(
- queue: Queue,
- host: Node = Layout._root
- ) {
- return treeBuilder.createBinaryNodeTreeFromQueue(this, queue, host)
- }
-
- createNodeFromJSON(json: LayoutTree): Node {
- return treeBuilder.createNodeFromJSON(this, json)
- }
-
- calcTabsetCountAndMinDim(root: Node = Layout._root, clear: boolean = true) {
- return geometry.calcTabsetCountAndMinDim(this, root, clear)
- }
-
- pruneStaleDirections() {
- return geometry.pruneStaleDirections()
- }
-
- calculateRootAdjustment(): RootAdjustment {
- return geometry.calculateRootAdjustment()
- }
-
- calcDimensions(root: Node = Layout._root, supress: boolean = false) {
- return geometry.calcDimensions(this, root, supress)
- }
-
- updateDimension(
- dim: Dimension,
- disableTimeout: boolean = false,
- timeout: number = 2
- ) {
- return geometry.updateDimension(this, dim, disableTimeout, timeout)
- }
-
- updateSlider(
- id: string,
- dim: { x: number; y: number },
- disableTimeout: boolean = false,
- timeout: number = 2
- ) {
- return slider.updateSlider(this, id, dim, disableTimeout, timeout)
- }
-
- updateSliderDimension(id: string, dim: { x: number; y: number }) {
- return slider.updateSliderDimension(this, id, dim)
- }
-
- updateTree(
- src: string,
- des: string,
- layout: 'top' | 'bottom' | 'left' | 'right' | 'contain'
- ): boolean {
- return treeMutations.updateTree(this, src, des, layout)
- }
-
- isOnlyContent(node: Node): boolean {
- return treeMutations.isOnlyContent(this, node)
- }
-
- moveNodeAsTab(src: Node, des: Node, layout: 'left' | 'right' | 'contain') {
- return treeMutations.moveNodeAsTab(this, src, des, layout)
- }
-
- moveRelativeToRoot(
- src: Node,
- des: Node,
- layout: 'top' | 'bottom' | 'left' | 'right'
- ) {
- return treeMutations.moveRelativeToRoot(this, src, des, layout)
- }
-
- moveRelativeToTabset(
- src: Node,
- des: Node,
- layout: 'top' | 'bottom' | 'left' | 'right'
- ) {
- return treeMutations.moveRelativeToTabset(this, src, des, layout)
- }
-
- insertNode(des: Node, src: Node, flag: boolean) {
- return treeMutations.insertNode(this, des, src, flag)
- }
-
- insertNodeWithNewParent(src: Node, des: Node, flag: boolean) {
- return treeMutations.insertNodeWithNewParent(this, src, des, flag)
- }
-
- moveAdjacentNodeToGrandParent(src: Node) {
- return treeMutations.moveAdjacentNodeToGrandParent(this, src)
- }
-
- removeRelation(src: Node, fix: boolean = true) {
- return treeMutations.removeRelation(src, fix)
- }
-
- removeNode(src: Node) {
- return treeMutations.removeNode(this, src)
- }
-
- removeKid(kid: Node) {
- return treeMutations.removeKid(kid)
- }
-
- clearAllCache() {
- return treeMutations.clearAllCache()
- }
-
- /** Uid of the maximized tabset, or null. */
- get maximizedId(): string | null {
- return viewState.getMaximizedTabset()?.unId ?? null
- }
-
- /** Show one tabset over the whole layout; the others stay mounted. */
- maximize(id: string): boolean {
- return viewState.maximize(this, id)
- }
-
- /** Leave maximized mode; the previous split is restored exactly. */
- restore(): boolean {
- return viewState.restore(this)
- }
-
- toggleMaximize(id: string): boolean {
- return viewState.toggleMaximize(this, id)
- }
-
- /** Fold a tabset to a strip along its row (its tab bar). */
- collapse(id: string): boolean {
- return viewState.collapse(this, id)
- }
-
- /** Unfold a tabset; it returns to its previous size. */
- expand(id: string): boolean {
- return viewState.expand(this, id)
- }
-
- toggleCollapse(id: string): boolean {
- return viewState.toggleCollapse(this, id)
- }
-}
-
-export { Node, Bond }
-export { Layout as DynamixLayoutCore }
diff --git a/packages/core/src/app/geometry.ts b/packages/core/src/app/geometry.ts
deleted file mode 100644
index b10b07e..0000000
--- a/packages/core/src/app/geometry.ts
+++ /dev/null
@@ -1,285 +0,0 @@
-import { Node } from './node'
-import { layoutState } from './state'
-import {
- canCollapse,
- canMaximize,
- getMaximizedTabset,
- normalizeViewState,
-} from './view-state'
-import type {
- Dimension,
- NodeOptions,
- RootAdjustment,
- ReactiveValue,
-} from '../type'
-import type { DynamixLayoutCore as Layout } from './dynamix'
-
-/** Minimum sizes and the position/size of every tabset, tab and bond. */
-
-export function calcTabsetCountAndMinDim(
- engine: Layout,
- root: Node = layoutState.root,
- clear: boolean = true
-) {
- if (root === layoutState.root && clear) {
- normalizeViewState(root)
- Node.cache.tabCnts.clear()
- Node.cache.dimMins.clear()
- Node.cache.mapElem.clear()
- Node.cache.nodOpts.get().clear()
- Node.cache.bndOpts.get().clear()
- }
-
- for (const node of engine.NodeLayoutRecursiveIterator(root)) {
- const tabCnts = Node.cache.tabCnts
- const dimMins = Node.cache.dimMins
-
- Node.cache.mapElem.set(node.unId, node)
-
- const hostCnt = tabCnts
- .set(node.unId, {
- horizontal: 0,
- vertical: 0,
- })
- .get(node.unId)
-
- const hostDim = dimMins
- .set(node.unId, {
- minWidth: 0,
- minHeight: 0,
- })
- .get(node.unId)
-
- const nodeDir = Node.cache.mapDirs.get(node.unId)
-
- if (node.type === 'tab') {
- Node.cache.mapElem.set(node.unId, node)
- dimMins.set(node.unId, {
- minWidth: 0,
- minHeight: 0,
- })
- }
-
- if (node.next) Node.cache.mapElem.set(node.next.unId, node.next)
-
- if (node.type === 'tabset') {
- Node.cache.mapElem.set(node.unId, node)
- dimMins.set(node.unId, {
- minWidth: layoutState.minW,
- minHeight: layoutState.minH,
- })
-
- if (nodeDir) {
- tabCnts.set(node.unId, {
- horizontal: 0,
- vertical: 1,
- })
- } else {
- tabCnts.set(node.unId, {
- horizontal: 1,
- vertical: 0,
- })
- }
-
- // A folded tabset is only its tab bar along the parent row.
- if (node.collapsed && node.host) {
- const rowIsHorizontal = Node.cache.mapDirs.get(node.host.unId)
- if (rowIsHorizontal) {
- dimMins.set(node.unId, {
- minWidth: layoutState.collapsedSize,
- minHeight: layoutState.minH,
- })
- tabCnts.get(node.unId)!.horizontal = 0
- } else {
- dimMins.set(node.unId, {
- minWidth: layoutState.minW,
- minHeight: layoutState.collapsedSize,
- })
- tabCnts.get(node.unId)!.vertical = 0
- }
- }
- }
-
- if (node.type === 'row') {
- for (const child of node.kids) {
- const kidCnt = tabCnts.get(child.unId)
- const kidDim = dimMins.get(child.unId)
-
- if (!kidCnt) continue
- hostCnt!.horizontal += kidCnt!.horizontal
- hostCnt!.vertical += kidCnt!.vertical
-
- if (nodeDir) {
- hostDim!.minHeight = Math.max(
- hostDim!.minHeight,
- kidDim!.minHeight!
- )
- hostDim!.minWidth += kidDim!.minWidth!
- } else {
- hostDim!.minHeight += kidDim!.minHeight!
- hostDim!.minWidth = Math.max(
- hostDim!.minWidth,
- kidDim!.minWidth!
- )
- }
- }
-
- if (nodeDir) {
- hostDim!.minWidth += (node.kids.size() - 1) * layoutState.bond
- } else {
- hostDim!.minHeight += (node.kids.size() - 1) * layoutState.bond
- }
- }
- }
-
- if (root === layoutState.root && clear) engine.pruneStaleDirections()
-}
-
-export function pruneStaleDirections() {
- for (const id of Node.cache.mapDirs.keys()) {
- if (!Node.cache.mapElem.has(id)) Node.cache.mapDirs.delete(id)
- }
-}
-
-export function calculateRootAdjustment(): RootAdjustment {
- const reqDims = layoutState.root.getReqDimension()
-
- let wasAdjusted = false
- let adjustedW = layoutState.root.dims.w
- let adjustedH = layoutState.root.dims.h
-
- if (layoutState.root.dims.w < reqDims.width) {
- adjustedW = reqDims.width
- wasAdjusted = true
- }
- if (layoutState.root.dims.h < reqDims.height) {
- adjustedH = reqDims.height
- wasAdjusted = true
- }
-
- if (wasAdjusted) {
- layoutState.root.dims.w = adjustedW
- layoutState.root.dims.h = adjustedH
- }
-
- return {
- adjustedW,
- adjustedH,
- wasAdjusted,
- }
-}
-
-export function calcDimensions(
- engine: Layout,
- root: Node = layoutState.root,
- supress: boolean = false
-) {
- const isRoot = root === layoutState.root
-
- // A subtree pass (e.g. slider drag) only recomputes part of the tree, so
- // it starts from the current maps to keep entries outside the subtree.
- const seed = (current: ReactiveValue>) =>
- isRoot ? new Map() : new Map(current.get())
-
- const nodeOpts = seed(Node.cache.nodOpts)
- const bondOpts = seed(Node.cache.bndOpts)
- const tabsIds = seed(Node.cache.tabOpts)
-
- if (isRoot) engine.calculateRootAdjustment()
-
- // While a tabset is maximized it takes the whole layout and everything
- // else is hidden. Node dims are still computed normally, so restoring is
- // exact.
- const maximized = getMaximizedTabset()
- const maximizable = canMaximize()
-
- for (const node of engine.NodeLayoutIterator(root)) {
- node.calcDimensions()
-
- const nodeDir = Node.cache.mapDirs.get(node.unId)!
-
- if (node.type === 'tabset') {
- const isMaximized = node === maximized
- const hidden = !!maximized && !isMaximized
- const dims = isMaximized
- ? { ...layoutState.root.dims }
- : { ...node.dims }
-
- const nodeOption: NodeOptions = {
- typNode: node.type,
- nodName: node.name,
- uidNode: node.unId,
- nodPart: node.part,
- nodOpen: node.open,
- nodeDir: nodeDir,
- nodFold: node.collapsed && !isMaximized,
- nodMaxd: isMaximized,
- nodHidden: hidden,
- nodFoldable: canCollapse(node),
- nodMaximizable: maximizable,
-
- nodKids: Array.from(node.kids).map((kid) => {
- const KidNode: NodeOptions = {
- typNode: kid.type,
- nodName: kid.name,
- uidNode: kid.unId,
- nodPart: kid.part,
- nodOpen: node.open === kid.name ? true : false,
- nodeDir: !nodeDir,
- nodHidden: hidden,
- nodFold: node.collapsed && !isMaximized,
- nodDims: { ...dims },
- }
- tabsIds.set(kid.unId, KidNode)
- return KidNode
- }),
- nodDims: dims,
- }
- nodeOpts.set(node.unId, nodeOption)
- }
-
- if (node.next && node.type !== 'tab') {
- const bondOption: NodeOptions = {
- typNode: 'bond',
- nodName: node.name,
- uidNode: node.next.unId,
- nodeDir: nodeDir,
- nodPart: 0,
- nodHidden: !!maximized,
- nodLocked: node.collapsed || !!node.next.next?.collapsed,
- nodDims: {
- ...node.next.dims,
- },
- }
-
- bondOpts.set(node.next.unId, bondOption)
- }
- }
- Node.cache.nodOpts.set(nodeOpts, supress)
- Node.cache.bndOpts.set(bondOpts, supress)
- Node.cache.tabOpts.set(tabsIds, supress)
-}
-
-export function updateDimension(
- engine: Layout,
- dim: Dimension,
- disableTimeout: boolean = false,
- timeout: number = 2
-) {
- layoutState.root.dims = dim
-
- if (disableTimeout) {
- engine.calcDimensions(layoutState.root)
- return
- }
-
- if (engine.cntdown) {
- layoutState.timer.clear(engine.cntdown)
- engine.cntdown = null
- }
-
- engine.cntdown = layoutState.timer.set(() => {
- engine.cntdown = null
- engine.calcDimensions()
- }, timeout)
-}
diff --git a/packages/core/src/app/index.ts b/packages/core/src/app/index.ts
deleted file mode 100644
index 3f203e1..0000000
--- a/packages/core/src/app/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from './dynamix'
-export * from './comparator'
-export * from './queue'
-export * from './reactive-state'
-export * from './drop-preview'
-export * from './dom'
diff --git a/packages/core/src/app/node.ts b/packages/core/src/app/node.ts
deleted file mode 100644
index 159bb9f..0000000
--- a/packages/core/src/app/node.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-import { Queue } from './queue'
-import { createReactiveState } from './reactive-state'
-import { areNodeOptionsMapEqual } from './comparator'
-import { layoutState } from './state'
-import type { Dimension, LayoutTree, NodeCache, NodeOptions } from '../type'
-
-/** @deprecated Internal; removed in 2.0. Read layout state from `layout.getSnapshot()` / `subscribe()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export class Node {
- static cache: NodeCache = {
- dimMins: new Map(),
- mapDirs: new Map(),
- tabCnts: new Map(),
- mapElem: new Map(),
- tabOpts: createReactiveState(
- new Map(),
- areNodeOptionsMapEqual
- ),
- nodOpts: createReactiveState(
- new Map(),
- areNodeOptionsMapEqual
- ),
- bndOpts: createReactiveState(
- new Map(),
- areNodeOptionsMapEqual
- ),
- }
-
- kids: Queue = new Queue()
- dims: Dimension = { w: 0, h: 0, x: 0, y: 0 }
- unId: string
- next: Bond | null = null
- prev: Bond | null = null
- type: 'row' | 'tabset' | 'tab' | 'bond' = 'row'
- name: string = ''
- part: number = 0
- host: Node | null = null
- open: string = ''
- /** Tabset only: folded to a strip along its parent row. */
- collapsed: boolean = false
- /** Fold order, used to pick which sibling to unfold. */
- foldedAt: number = 0
-
- constructor(
- options: {
- type?: 'row' | 'tabset' | 'tab' | 'bond'
- host?: Node | null
- name?: string
- part?: number
- dims?: Dimension
- unId?: string
- open?: string
- } = {}
- ) {
- const defaults = {
- type: 'row' as 'row' | 'tabset' | 'tab' | 'bond',
- host: null,
- name: '',
- part: 100,
- dims: { w: 0, h: 0, x: 0, y: 0 },
- unId: layoutState.createId(),
- open: '',
- }
-
- this.type = options.type ?? defaults.type
- this.host = options.host ?? defaults.host
- this.name = options.name ?? defaults.name
- this.part = options.part ?? defaults.part
- this.dims = options.dims ?? defaults.dims
- this.open = options.open ?? defaults.open
-
- this.unId = options.unId || defaults.unId
- }
-
- addBond(parent: Node | null, prev: Node | null = null) {
- if (!prev) return
-
- const bond = new Bond(parent, this, prev)
- Node.cache.mapElem.set(bond.unId, bond)
- }
-
- getReqDimension(): { width: number; height: number } {
- const minDims = Node.cache.dimMins.get(this.unId)
- const tabCnts = Node.cache.tabCnts.get(this.unId)
-
- const reqW = Math.max(
- minDims!.minWidth,
- tabCnts!.horizontal * layoutState.minW +
- Math.max(0, tabCnts!.horizontal - 1) * layoutState.bond
- )
-
- const reqH = Math.max(
- minDims!.minHeight,
- tabCnts!.vertical * layoutState.minH +
- Math.max(0, tabCnts!.vertical - 1) * layoutState.bond
- )
-
- return {
- width: reqW,
- height: reqH,
- }
- }
-
- calcDimensions() {
- const nodeDir = Node.cache.mapDirs.get(this.unId)
-
- if (!this.kids) return
-
- let totalWeight = 0
- const totlMinSize = Node.cache.dimMins.get(this.unId) || {
- minWidth: 0,
- minHeight: 0,
- }
- let curntOffset = 0
-
- // Folded tabsets keep their strip size; only open kids share the
- // extra space. Their parts are left untouched so unfolding restores
- // the previous split.
- let lastFlexKid: Node | undefined
- for (const kid of this.kids) {
- if (kid.collapsed) continue
- totalWeight += kid.part
- lastFlexKid = kid
- }
-
- if (totalWeight === 0) return
-
- let extraSpace
- if (nodeDir) {
- extraSpace = this.dims.w - totlMinSize.minWidth
- } else {
- extraSpace = this.dims.h - totlMinSize.minHeight
- }
-
- const isBelowMinSize = extraSpace < 0
-
- // Round cumulative boundaries instead of individual sizes. Each edge
- // stays within half a pixel of its exact position, moves at most one
- // pixel per pixel of resize, and edges outside a dragged pair never move
- // (their cumulative part is unchanged).
- let cumWeight = 0
- let prevBoundary = 0
-
- for (const kid of this.kids) {
- const kidDims = Node.cache.dimMins.get(kid.unId) || {
- minWidth: 0,
- minHeight: 0,
- }
-
- let extraChildSpace = 0
- if (!isBelowMinSize && !kid.collapsed) {
- cumWeight += kid.part
- const boundary =
- kid === lastFlexKid
- ? extraSpace
- : Math.round((extraSpace * cumWeight) / totalWeight)
- extraChildSpace = boundary - prevBoundary
- prevBoundary = boundary
- }
-
- if (nodeDir) {
- kid.dims.w = kidDims.minWidth + extraChildSpace
- kid.dims.h = this.dims.h
- kid.dims.x = this.dims.x + curntOffset
- kid.dims.y = this.dims.y
- curntOffset += kid.dims.w + layoutState.bond
- } else {
- kid.dims.h = kidDims.minHeight + extraChildSpace
- kid.dims.w = this.dims.w
- kid.dims.y = this.dims.y + curntOffset
- kid.dims.x = this.dims.x
- curntOffset += kid.dims.h + layoutState.bond
- }
-
- if (kid.next) {
- if (nodeDir) {
- kid.next!.dims = {
- w: layoutState.bond,
- h: kid.dims.h,
- x: kid.dims.x + kid.dims.w,
- y: kid.dims.y,
- }
- } else {
- kid.next!.dims = {
- w: kid.dims.w,
- h: layoutState.bond,
- x: kid.dims.x,
- y: kid.dims.y + kid.dims.h,
- }
- }
- }
- }
- }
-
- toJSON(): LayoutTree {
- const children: LayoutTree[] = []
-
- for (const child of this.kids) {
- children.push(child.toJSON())
- }
-
- const result: LayoutTree = {
- typNode: this.type,
- nodPart: this.part,
- nodName: this.name,
- uidNode: this.unId,
- }
-
- if (this.open) {
- result.nodOpen = this.open
- }
-
- if (this.collapsed) {
- result.nodFold = true
- }
-
- if (this === layoutState.root && layoutState.maximized) {
- result.nodMaxd = layoutState.maximized
- }
-
- if (children.length > 0) {
- result.nodKids = children
- }
-
- return result
- }
-}
-
-/** @deprecated Internal; removed in 2.0. Splitters are `snapshot.splitters`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export class Bond {
- dims: Dimension = { w: 0, h: 0, x: 0, y: 0 }
- unId: string = layoutState.createId()
- next: Node | null = null
- prev: Node | null = null
- host: Node | null = null
-
- constructor(
- parent: Node | null = null,
- next: Node | null = null,
- prev: Node | null = null
- ) {
- this.host = parent
- this.next = next
- this.prev = prev
- if (this.next) this.next.prev = this
- if (this.prev) this.prev.next = this
- }
-
- toJSON(): LayoutTree {
- return {
- nodName: this.host?.name || '',
- typNode: 'bond',
- uidNode: this.unId,
- nodPart: 0,
- nodOpen: '',
- nodKids: [],
- }
- }
-}
diff --git a/packages/core/src/app/queue.ts b/packages/core/src/app/queue.ts
deleted file mode 100644
index c0e7246..0000000
--- a/packages/core/src/app/queue.ts
+++ /dev/null
@@ -1,703 +0,0 @@
-/** @deprecated Internal; removed in 2.0. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-class Queue {
- private buffer: (T | undefined)[]
- private head: number = 0
- private tail: number = 0
- private count: number = 0
- private capacity: number
-
- constructor(initialCapacity: number = 16, array: T[] = []) {
- this.capacity = Math.max(initialCapacity, array.length)
- this.buffer = new Array(this.capacity)
-
- array.forEach((element) => {
- this.enqueue(element)
- })
- }
-
- // ==================== INSERTION OPERATIONS ====================
-
- /**
- * Add element to the back of the queue - O(1) amortized
- * Used for FIFO and FILO patterns
- */
- enqueue(item: T): void {
- if (this.count === this.capacity) {
- this.resize()
- }
-
- this.buffer[this.tail] = item
- this.tail = (this.tail + 1) % this.capacity
- this.count++
- }
-
- /**
- * Add element to the front of the queue - O(1) amortized
- * Used for LIFO and LILO patterns
- */
- enqueueFront(item: T): void {
- if (this.count === this.capacity) {
- this.resize()
- }
-
- this.head = (this.head - 1 + this.capacity) % this.capacity
- this.buffer[this.head] = item
- this.count++
- }
-
- /**
- * Insert element at specific logical index - O(n)
- * All elements at and after the index are shifted right
- * @param index - Logical index (0 = front of queue)
- * @param item - Element to insert
- * @returns true if insertion successful, false if index out of bounds
- */
- insert(index: number, item: T): boolean {
- // Validate index bounds
- if (index < 0 || index > this.count) {
- return false
- }
-
- // Handle edge cases with existing O(1) operations
- if (index === 0) {
- this.enqueueFront(item)
- return true
- }
- if (index === this.count) {
- this.enqueue(item)
- return true
- }
-
- // Ensure we have capacity
- if (this.count === this.capacity) {
- this.resize()
- }
-
- // Determine which direction to shift (minimize operations)
- const distanceFromHead = index
- const distanceFromTail = this.count - index
-
- if (distanceFromHead <= distanceFromTail) {
- // Shift elements towards head (left shift)
- this.shiftLeft(index, item)
- } else {
- // Shift elements towards tail (right shift)
- this.shiftRight(index, item)
- }
-
- this.count++
- return true
- }
-
- /**
- * Insert array of elements at specific logical index - O(n + m) where n is elements to shift, m is array length
- * All elements at and after the index are shifted right to make room for the entire array
- * @param index - Logical index (0 = front of queue)
- * @param items - Array of elements to insert
- * @returns true if insertion successful, false if index out of bounds
- */
- insertArray(index: number, items: T[]): boolean {
- // Validate index bounds
- if (index < 0 || index > this.count) {
- return false
- }
-
- // Handle empty array case
- if (items.length === 0) {
- return true
- }
-
- // Handle edge cases - append/prepend entire array
- if (index === 0) {
- // Insert at beginning - add all items to front in reverse order
- for (let i = items.length - 1; i >= 0; i--) {
- this.enqueueFront(items[i])
- }
- return true
- }
- if (index === this.count) {
- // Insert at end - add all items to back
- items.forEach((item) => this.enqueue(item))
- return true
- }
-
- // Ensure we have enough capacity
- while (this.count + items.length > this.capacity) {
- this.resize()
- }
-
- // Determine which direction to shift (minimize operations)
- const distanceFromHead = index
- const distanceFromTail = this.count - index
- const insertLength = items.length
-
- if (distanceFromHead <= distanceFromTail) {
- // Shift elements towards head (left shift)
- this.shiftLeftArray(index, items)
- } else {
- // Shift elements towards tail (right shift)
- this.shiftRightArray(index, items)
- }
-
- this.count += insertLength
- return true
- }
-
- /**
- * Insert another queue at specific logical index - O(n + m) where n is elements to shift, m is queue size
- * All elements at and after the index are shifted right to make room for the entire queue
- * @param index - Logical index (0 = front of queue)
- * @param queue - Queue to insert
- * @returns true if insertion successful, false if index out of bounds
- */
- insertQueue(index: number, queue: Queue): boolean {
- // Validate index bounds
- if (index < 0 || index > this.count) {
- return false
- }
-
- // Handle empty queue case
- if (queue.isEmpty()) {
- return true
- }
-
- const queueSize = queue.size()
-
- // Handle edge cases - append/prepend entire queue
- if (index === 0) {
- // Insert at beginning - add all items to front in reverse order
- // Use reverse iterator to maintain order
- const items = queue.toArray()
- for (let i = items.length - 1; i >= 0; i--) {
- this.enqueueFront(items[i])
- }
- return true
- }
- if (index === this.count) {
- // Insert at end - add all items to back
- for (const item of queue) {
- this.enqueue(item)
- }
- return true
- }
-
- // Ensure we have enough capacity
- while (this.count + queueSize > this.capacity) {
- this.resize()
- }
-
- // Convert queue to array for efficient insertion
- const queueItems = queue.toArray()
-
- // Determine which direction to shift (minimize operations)
- const distanceFromHead = index
- const distanceFromTail = this.count - index
-
- if (distanceFromHead <= distanceFromTail) {
- // Shift elements towards head (left shift)
- this.shiftLeftArray(index, queueItems)
- } else {
- // Shift elements towards tail (right shift)
- this.shiftRightArray(index, queueItems)
- }
-
- this.count += queueSize
- return true
- }
-
- /**
- * Alias for enqueue (add to back)
- */
- push(item: T): void {
- this.enqueue(item)
- }
-
- /**
- * Alias for enqueueFront (add to front)
- */
- unshift(item: T): void {
- this.enqueueFront(item)
- }
-
- // ==================== REMOVAL OPERATIONS ====================
-
- /**
- * Remove and return element from front of queue - O(1)
- * Used for FIFO and LIFO patterns
- */
- dequeue(): T | undefined {
- if (this.count === 0) {
- return undefined
- }
-
- const item = this.buffer[this.head]
- // Don't store undefined - just move the head pointer
- this.head = (this.head + 1) % this.capacity
- this.count--
-
- return item
- }
-
- /**
- * Remove and return element from back of queue - O(1)
- * Used for FILO and LILO patterns
- */
- dequeueBack(): T | undefined {
- if (this.count === 0) {
- return undefined
- }
-
- this.tail = (this.tail - 1 + this.capacity) % this.capacity
- const item = this.buffer[this.tail]
- // Don't store undefined - the tail pointer now excludes this position
- this.count--
-
- return item
- }
-
- indexOf(item: T): number {
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- if (this.buffer[actualIndex] === item) {
- return i // Return logical index
- }
- }
- return -1 // Not found
- }
-
- remove(item: T): boolean {
- const index = this.indexOf(item)
- if (index === -1) return false
- this.removeAt(index)
- return true
- }
-
- /**
- * Remove element at specific logical index - O(n)
- * All elements after the index are shifted left
- * @param index - Logical index (0 = front of queue)
- * @returns The removed element, or undefined if index out of bounds
- */
- removeAt(index: number): T | undefined {
- // Validate index bounds
- if (index < 0 || index >= this.count) {
- return undefined
- }
-
- // Handle edge cases with existing O(1) operations
- if (index === 0) {
- return this.dequeue()
- }
- if (index === this.count - 1) {
- return this.dequeueBack()
- }
-
- // Get the element to remove
- const actualIndex = (this.head + index) % this.capacity
- const removedItem = this.buffer[actualIndex]
-
- // Determine which direction to shift (minimize operations)
- const distanceFromHead = index
- const distanceFromTail = this.count - index - 1
-
- if (distanceFromHead <= distanceFromTail) {
- // Shift elements from head towards the gap (right shift)
- this.shiftRightForRemoval(index)
- } else {
- // Shift elements from tail towards the gap (left shift)
- this.shiftLeftForRemoval(index)
- }
-
- this.count--
- return removedItem
- }
-
- /**
- * Alias for dequeue (remove from front)
- */
- shift(): T | undefined {
- return this.dequeue()
- }
-
- /**
- * Alias for dequeueBack (remove from back)
- */
- pop(): T | undefined {
- return this.dequeueBack()
- }
-
- // ==================== PATTERN-SPECIFIC METHODS ====================
-
- /**
- * FIFO: First In, First Out
- * Add to back, remove from front
- */
- fifoAdd(item: T): void {
- this.enqueue(item)
- }
-
- fifoRemove(): T | undefined {
- return this.dequeue()
- }
-
- /**
- * FILO: First In, Last Out
- * Add to back, remove from back
- */
- filoAdd(item: T): void {
- this.enqueue(item)
- }
-
- filoRemove(): T | undefined {
- return this.dequeueBack()
- }
-
- /**
- * LIFO: Last In, First Out
- * Add to front, remove from front
- */
- lifoAdd(item: T): void {
- this.enqueueFront(item)
- }
-
- lifoRemove(): T | undefined {
- return this.dequeue()
- }
-
- /**
- * LILO: Last In, Last Out
- * Add to front, remove from back
- */
- liloAdd(item: T): void {
- this.enqueueFront(item)
- }
-
- liloRemove(): T | undefined {
- return this.dequeueBack()
- }
-
- // ==================== ACCESS OPERATIONS ====================
-
- /**
- * Get element at logical index (0 = front of queue) - O(1)
- */
- get(index: number): T | undefined {
- if (index < 0 || index >= this.count) {
- return undefined
- }
-
- const actualIndex = (this.head + index) % this.capacity
- return this.buffer[actualIndex]
- }
-
- /**
- * Set element at logical index - O(1)
- */
- set(index: number, item: T): boolean {
- if (index < 0 || index >= this.count) {
- return false
- }
-
- const actualIndex = (this.head + index) % this.capacity
- this.buffer[actualIndex] = item
- return true
- }
-
- /**
- * Get the front element without removing it - O(1)
- */
- peek(): T | undefined {
- return this.count > 0 ? this.buffer[this.head] : undefined
- }
-
- /**
- * Get the back element without removing it - O(1)
- */
- peekBack(): T | undefined {
- if (this.count === 0) return undefined
- const backIndex = (this.tail - 1 + this.capacity) % this.capacity
- return this.buffer[backIndex]
- }
-
- /**
- * Alias for peek (front element)
- */
- front(): T | undefined {
- return this.peek()
- }
-
- /**
- * Alias for peekBack (back element)
- */
- back(): T | undefined {
- return this.peekBack()
- }
-
- // ==================== UTILITY METHODS ====================
-
- /**
- * Get current size of queue - O(1)
- */
- size(): number {
- return this.count
- }
-
- /**
- * Get current capacity - O(1)
- */
- getCapacity(): number {
- return this.capacity
- }
-
- /**
- * Check if queue is empty - O(1)
- */
- isEmpty(): boolean {
- return this.count === 0
- }
-
- /**
- * Check if queue is full - O(1)
- */
- isFull(): boolean {
- return this.count === this.capacity
- }
-
- /**
- * Clear all elements from queue - O(1)
- */
- clear(): void {
- // Don't create new array, just reset pointers
- this.head = 0
- this.tail = 0
- this.count = 0
- }
-
- /**
- * Convert queue to array (maintains order from front to back) - O(n)
- */
- toArray(): T[] {
- const result: T[] = []
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- result.push(this.buffer[actualIndex] as T)
- }
- return result
- }
-
- /**
- * Forward iterator support for for...of loops - O(n) total
- * Iterates from front to back
- */
- *[Symbol.iterator](): IterableIterator {
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- yield this.buffer[actualIndex] as T
- }
- }
-
- /**
- * Forward iterator (same as Symbol.iterator) - O(n) total
- * Iterates from front to back
- */
- *forward(): IterableIterator {
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- yield this.buffer[actualIndex] as T
- }
- }
-
- /**
- * Reverse iterator - O(n) total
- * Iterates from back to front
- */
- *reverse(): IterableIterator {
- for (let i = this.count - 1; i >= 0; i--) {
- const actualIndex = (this.head + i) % this.capacity
- yield this.buffer[actualIndex] as T
- }
- }
-
- /**
- * Iterator with index information - O(n) total
- * Returns [index, value] pairs from front to back
- */
- *entries(): IterableIterator<[number, T]> {
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- yield [i, this.buffer[actualIndex] as T]
- }
- }
-
- /**
- * Iterator for keys (indices) - O(n) total
- */
- *keys(): IterableIterator {
- for (let i = 0; i < this.count; i++) {
- yield i
- }
- }
-
- /**
- * Iterator for values only - O(n) total
- * Same as Symbol.iterator but more explicit
- */
- *values(): IterableIterator {
- for (let i = 0; i < this.count; i++) {
- const actualIndex = (this.head + i) % this.capacity
- yield this.buffer[actualIndex] as T
- }
- }
-
- /**
- * Get string representation - O(n)
- */
- toString(): string {
- return `Queue(${this.toArray().join(', ')})`
- }
-
- // ==================== PRIVATE HELPER METHODS ====================
-
- /**
- * Private method to resize buffer when capacity is exceeded
- */
- private resize(): void {
- const newCapacity = this.capacity * 2
- const newBuffer = new Array(newCapacity)
-
- // Copy elements in correct order to new buffer
- for (let i = 0; i < this.count; i++) {
- const oldIndex = (this.head + i) % this.capacity
- newBuffer[i] = this.buffer[oldIndex]
- }
-
- this.buffer = newBuffer
- this.capacity = newCapacity
- this.head = 0
- this.tail = this.count
- }
-
- /**
- * Shift elements left (towards head) to make room for insertion
- * Used when inserting closer to head
- */
- private shiftLeft(insertIndex: number, item: T): void {
- // Move head one position left
- this.head = (this.head - 1 + this.capacity) % this.capacity
-
- // Shift elements from old head position to insert position
- for (let i = 0; i < insertIndex; i++) {
- const fromIndex = (this.head + 1 + i) % this.capacity
- const toIndex = (this.head + i) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Insert the new item
- const actualInsertIndex = (this.head + insertIndex) % this.capacity
- this.buffer[actualInsertIndex] = item
- }
-
- /**
- * Shift elements right (towards tail) to make room for insertion
- * Used when inserting closer to tail
- */
- private shiftRight(insertIndex: number, item: T): void {
- // Shift elements from insert position to tail
- for (let i = this.count - 1; i >= insertIndex; i--) {
- const fromIndex = (this.head + i) % this.capacity
- const toIndex = (this.head + i + 1) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Insert the new item
- const actualInsertIndex = (this.head + insertIndex) % this.capacity
- this.buffer[actualInsertIndex] = item
-
- // Move tail one position right
- this.tail = (this.tail + 1) % this.capacity
- }
-
- /**
- * Shift elements right (towards tail) to fill gap after removal
- * Used when removing closer to head
- */
- private shiftRightForRemoval(removeIndex: number): void {
- // Shift elements from head to remove position
- for (let i = removeIndex; i > 0; i--) {
- const fromIndex = (this.head + i - 1) % this.capacity
- const toIndex = (this.head + i) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Move head pointer (don't store undefined)
- this.head = (this.head + 1) % this.capacity
- }
-
- /**
- * Shift elements left (towards head) to fill gap after removal
- * Used when removing closer to tail
- */
- private shiftLeftForRemoval(removeIndex: number): void {
- // Shift elements from remove position to tail
- for (let i = removeIndex; i < this.count - 1; i++) {
- const fromIndex = (this.head + i + 1) % this.capacity
- const toIndex = (this.head + i) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Move tail pointer (don't store undefined)
- this.tail = (this.tail - 1 + this.capacity) % this.capacity
- }
-
- /**
- * Shift elements left (towards head) to make room for array insertion
- * Used when inserting array closer to head
- */
- private shiftLeftArray(insertIndex: number, items: T[]): void {
- const insertLength = items.length
-
- // Move head left by insertLength positions
- this.head = (this.head - insertLength + this.capacity) % this.capacity
-
- // Shift existing elements from old head position to insert position
- for (let i = 0; i < insertIndex; i++) {
- const fromIndex = (this.head + insertLength + i) % this.capacity
- const toIndex = (this.head + i) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Insert the new items
- for (let i = 0; i < insertLength; i++) {
- const actualInsertIndex =
- (this.head + insertIndex + i) % this.capacity
- this.buffer[actualInsertIndex] = items[i]
- }
- }
-
- /**
- * Shift elements right (towards tail) to make room for array insertion
- * Used when inserting array closer to tail
- */
- private shiftRightArray(insertIndex: number, items: T[]): void {
- const insertLength = items.length
-
- // Shift existing elements from insert position to tail, moving right
- for (let i = this.count - 1; i >= insertIndex; i--) {
- const fromIndex = (this.head + i) % this.capacity
- const toIndex = (this.head + i + insertLength) % this.capacity
- this.buffer[toIndex] = this.buffer[fromIndex]
- }
-
- // Insert the new items
- for (let i = 0; i < insertLength; i++) {
- const actualInsertIndex =
- (this.head + insertIndex + i) % this.capacity
- this.buffer[actualInsertIndex] = items[i]
- }
-
- // Move tail right by insertLength positions
- this.tail = (this.tail + insertLength) % this.capacity
- }
-}
-
-export { Queue }
diff --git a/packages/core/src/app/reactive-state.ts b/packages/core/src/app/reactive-state.ts
deleted file mode 100644
index f071f55..0000000
--- a/packages/core/src/app/reactive-state.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import type { ReactiveValue, ChangeListener } from '../type'
-
-/** @deprecated Internal; removed in 2.0. Use `layout.subscribe()`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-function createReactiveState(
- initialValue: T,
- comparator: (a: T, b: T) => boolean = (a, b) => a === b
-): ReactiveValue {
- let value = initialValue
- const listeners = new Set>()
- let pendingResolvers: Array<(val: T) => void> = []
-
- const get = (): T => value
-
- const set = (newValue: T, flag: boolean = false): void => {
- if (flag) {
- value = newValue
- return
- }
-
- if (!comparator(value, newValue)) {
- value = newValue
-
- listeners.forEach((cb) => cb(value))
- pendingResolvers.forEach((resolve) => resolve(value))
- pendingResolvers = []
- }
- }
-
- const triggerChange = (): void => {
- listeners.forEach((cb) => cb(value))
- pendingResolvers.forEach((resolve) => resolve(value))
- pendingResolvers = []
- }
-
- const onChange = (listener: ChangeListener): (() => void) => {
- listeners.add(listener)
- return () => listeners.delete(listener)
- }
-
- const nextChange = (): Promise => {
- return new Promise((resolve) => {
- pendingResolvers.push(resolve)
- })
- }
-
- const onChangePromise = (listener: ChangeListener): (() => void) => {
- let active = true
-
- const loop = async () => {
- while (active) {
- const val = await nextChange()
- if (!active) break
- listener(val)
- }
- }
-
- loop()
-
- return () => {
- active = false
- }
- }
-
- return {
- get,
- set,
-
- onChange,
- nextChange,
- triggerChange,
- onChangePromise,
- }
-}
-
-export { createReactiveState }
diff --git a/packages/core/src/app/slider.ts b/packages/core/src/app/slider.ts
deleted file mode 100644
index dfc254c..0000000
--- a/packages/core/src/app/slider.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { Node, Bond } from './node'
-import { layoutState } from './state'
-import type { DynamixLayoutCore as Layout } from './dynamix'
-
-/** Moving a bond (slider) between two siblings. */
-
-export function updateSlider(
- engine: Layout,
- id: string,
- dim: { x: number; y: number },
- disableTimeout: boolean = false,
- timeout: number = 2
-) {
- if (disableTimeout) {
- engine.updateSliderDimension(id, dim)
- return
- }
-
- if (engine.cntdown) {
- layoutState.timer.clear(engine.cntdown)
- engine.cntdown = null
- }
-
- engine.cntdown = layoutState.timer.set(() => {
- engine.cntdown = null
- engine.updateSliderDimension(id, dim)
- }, timeout)
-}
-
-export function updateSliderDimension(
- engine: Layout,
- id: string,
- dim: { x: number; y: number }
-) {
- const bnd = Node.cache.mapElem.get(id)
-
- if (!bnd || !(bnd instanceof Bond)) {
- console.warn(`Bond with id ${id} not found or is not a Bond instance`)
- return
- }
-
- const dir = Node.cache.mapDirs.get(bnd.host!.unId)
-
- const prev = bnd.prev
- const next = bnd.next
- const host = bnd.host
-
- if (!prev || !next || !host) {
- console.warn('Missing prev, next, or host node', prev, next, host)
- return
- }
-
- // Bonds next to a folded tabset are locked, and nothing moves while a
- // tabset is maximized.
- if (prev.collapsed || next.collapsed || layoutState.maximized) {
- return
- }
-
- const prevMinDim = Node.cache.dimMins.get(prev.unId) || {
- minWidth: 0,
- minHeight: 0,
- }
- const nextMinDim = Node.cache.dimMins.get(next.unId) || {
- minWidth: 0,
- minHeight: 0,
- }
- const half = layoutState.bond / 2
-
- if (dir) {
- const endX = next.dims.x + next.dims.w
-
- const minX = prev.dims.x + prevMinDim.minWidth + half
- const maxX = endX - nextMinDim.minWidth - half
-
- const clampedX = Math.max(minX, Math.min(maxX, dim.x))
-
- bnd.dims.x = clampedX - half
-
- prev.dims.w = bnd.dims.x - prev.dims.x
-
- next.dims.x = clampedX + half
- next.dims.w = endX - next.dims.x
-
- const prevExtraSpace = Math.max(0, prev.dims.w - prevMinDim.minWidth)
- const nextExtraSpace = Math.max(0, next.dims.w - nextMinDim.minWidth)
- const totalExtraSpace = prevExtraSpace + nextExtraSpace
- const totalPart = prev.part + next.part
-
- if (totalExtraSpace > 0) {
- prev.part = (prevExtraSpace / totalExtraSpace) * totalPart
- next.part = (nextExtraSpace / totalExtraSpace) * totalPart
- } else {
- prev.part = totalPart / 2
- next.part = totalPart / 2
- }
- } else {
- const endY = next.dims.y + next.dims.h
-
- const minY = prev.dims.y + prevMinDim.minHeight + half
- const maxY = endY - nextMinDim.minHeight - half
-
- const clampedY = Math.max(minY, Math.min(maxY, dim.y))
-
- bnd.dims.y = clampedY - half
-
- prev.dims.h = bnd.dims.y - prev.dims.y
-
- next.dims.y = clampedY + half
- next.dims.h = endY - next.dims.y
-
- const prevExtraSpace = Math.max(0, prev.dims.h - prevMinDim.minHeight)
- const nextExtraSpace = Math.max(0, next.dims.h - nextMinDim.minHeight)
- const totalExtraSpace = prevExtraSpace + nextExtraSpace
- const totalPart = prev.part + next.part
-
- if (totalExtraSpace > 0) {
- prev.part = (prevExtraSpace / totalExtraSpace) * totalPart
- next.part = (nextExtraSpace / totalExtraSpace) * totalPart
- } else {
- prev.part = totalPart / 2
- next.part = totalPart / 2
- }
- }
-
- engine.calcDimensions(host)
-}
diff --git a/packages/core/src/app/state.ts b/packages/core/src/app/state.ts
deleted file mode 100644
index dc4063f..0000000
--- a/packages/core/src/app/state.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { Node } from './node'
-import type { DynamixLayoutCore } from './dynamix'
-import type { LayoutTree } from '../type'
-
-export type Timer = {
- set: (callback: () => void, ms: number) => ReturnType
- clear: (handle: ReturnType) => void
-}
-
-export const defaultCreateId = (): string => crypto.randomUUID()
-
-export const defaultTimer: Timer = {
- set: (callback, ms) => setTimeout(callback, ms),
- clear: (handle) => clearTimeout(handle),
-}
-
-/** Shared layout settings and the current root, used by every layout module. */
-export const layoutState = {
- createId: defaultCreateId,
- timer: defaultTimer,
- root: undefined as unknown as Node,
- tree: null as LayoutTree | null,
- minW: 40,
- minH: 40,
- bond: 10,
- inst: null as DynamixLayoutCore | null,
- /** Uid of the maximized tabset, if any. */
- maximized: null as string | null,
- /** Size of a folded tabset along its parent row (its tab bar height). */
- collapsedSize: 40,
- /** Increments on every fold so the most recently folded tabset is known. */
- foldSeq: 0,
-}
diff --git a/packages/core/src/app/tree-builder.ts b/packages/core/src/app/tree-builder.ts
deleted file mode 100644
index 0748a6c..0000000
--- a/packages/core/src/app/tree-builder.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-import { Queue } from './queue'
-import { Node } from './node'
-import { layoutState } from './state'
-import type { LayoutTree } from '../type'
-import type { DynamixLayoutCore as Layout } from './dynamix'
-
-/** Building the node tree from a saved layout or a list of tabs, and walking it. */
-
-export function* JSONLayoutIterator(
- root: LayoutTree
-): IterableIterator {
- const queue = new Queue()
- queue.enqueue(root)
-
- while (!queue.isEmpty()) {
- const current = queue.dequeue()
- if (!current) continue
-
- yield current
-
- if (current.nodKids && current.nodKids.length > 0) {
- for (const child of current.nodKids) {
- queue.enqueue(child)
- }
- }
- }
-}
-
-export function* NodeLayoutIterator(root: Node): IterableIterator {
- const queue = new Queue()
- queue.enqueue(root)
-
- while (!queue.isEmpty()) {
- const current = queue.dequeue()
- if (!current) continue
-
- Node.cache.mapElem.set(current.unId, current)
-
- yield current
-
- if (current.kids.size() > 0) {
- for (const child of current.kids) {
- queue.enqueue(child)
- }
- }
- }
-}
-
-export function* NodeLayoutRecursiveIterator(
- engine: Layout,
- root: Node
-): IterableIterator {
- for (const child of root.kids) {
- yield* engine.NodeLayoutRecursiveIterator(child)
- }
-
- yield root
-}
-
-export function updateChildDirections(engine: Layout, parent: Node) {
- const parentDir = Node.cache.mapDirs.get(parent.unId)
- if (parentDir === undefined) {
- console.warn(
- `Cannot update child directions for node ${parent.unId}; its own direction is not set.`
- )
- return
- }
- for (const child of parent.kids) {
- Node.cache.mapDirs.set(child.unId, !parentDir)
- if (child.kids.size() > 0) {
- child.host = parent
- engine.updateChildDirections(child)
-
- if (child.next) {
- Node.cache.mapDirs.set(child.next.unId, !parentDir)
- }
- }
- }
-}
-
-export function createNodeLayout(
- engine: Layout,
- root: LayoutTree | null = layoutState.tree
-) {
- if (!root) return
-
- const JSONIterator = engine.JSONLayoutIterator(root)
- const nodeIterator = engine.NodeLayoutIterator(layoutState.root)
-
- Node.cache.mapDirs.set(layoutState.root.unId, true)
-
- while (true) {
- const jsonNode = JSONIterator.next()
- const nodeNode = nodeIterator.next()
-
- if (jsonNode.done || nodeNode.done) {
- break
- }
-
- const jsonValue = jsonNode.value
- const nodeValue = nodeNode.value
-
- if (!jsonValue || !nodeValue) {
- continue
- }
-
- engine.createNodeTreeFromJSONTree(jsonValue, nodeValue)
- }
-
- engine.calcTabsetCountAndMinDim()
-}
-
-export function createNodeTreeFromJSONTree(
- engine: Layout,
- json: LayoutTree,
- host: Node
-) {
- const direction = Node.cache.mapDirs.get(host.unId)
-
- for (const child of json.nodKids || []) {
- const node = engine.createNodeFromJSON(child)
- const prev = host.kids.peekBack()
- node.host = host
- node.addBond(node.host, prev)
- host.kids.enqueue(node)
-
- Node.cache.mapDirs.set(node.unId, !direction!)
- }
-
- if (host.type === 'tabset') Node.cache.mapElem.set(host.unId, host)
- if (host.type === 'tab') Node.cache.mapElem.set(host.unId, host)
-}
-
-export function createBinaryNodeTreeFromQueue(
- engine: Layout,
- queue: Queue,
- host: Node = layoutState.root
-) {
- const nodeQueue = new Queue()
- nodeQueue.enqueue(host)
-
- Node.cache.mapDirs.set(layoutState.root.unId, true)
-
- while (!queue.isEmpty()) {
- const host = nodeQueue.dequeue()
- if (!host) {
- break
- }
- const dir = Node.cache.mapDirs.get(host.unId)
-
- // A single tab gets one tabset; splitting would leave an empty row
- // sibling taking half of the space.
- if (queue.size() === 1) {
- const name = queue.dequeue()
- const only = new Node({ type: 'tabset', host })
- const tab = new Node({
- type: 'tab',
- host: only,
- name,
- unId: engine.tabsIds.get(name ?? '') ?? layoutState.createId(),
- })
- only.open = tab.name
- only.kids.enqueue(tab)
- host.kids.enqueue(only)
-
- Node.cache.mapDirs.set(only.unId, !dir!)
- Node.cache.mapDirs.set(tab.unId, dir!)
- Node.cache.mapElem.set(only.unId, only)
- Node.cache.mapElem.set(tab.unId, tab)
- break
- }
-
- if (queue.size() == 2) {
- const leftName = queue.dequeue()
- const rghtName = queue.dequeue()
- const left = new Node({ type: 'tabset', host })
- const rght = new Node({ type: 'tabset', host })
-
- const leftKid = new Node({
- type: 'tab',
- host: left,
- name: leftName,
- unId:
- engine.tabsIds.get(leftName ?? '') ??
- layoutState.createId(),
- })
- const rghtKid = new Node({
- type: 'tab',
- host: rght,
- name: rghtName,
- unId:
- engine.tabsIds.get(rghtName ?? '') ??
- layoutState.createId(),
- })
- left.open = leftKid.name
- rght.open = rghtKid.name
- left.kids.enqueue(leftKid)
- rght.kids.enqueue(rghtKid)
- host.kids.enqueue(left)
- host.kids.enqueue(rght)
-
- rght.addBond(host, left)
-
- Node.cache.mapDirs.set(left.unId, !dir!)
- Node.cache.mapDirs.set(rght.unId, !dir!)
- Node.cache.mapElem.set(left.unId, left)
- Node.cache.mapElem.set(rght.unId, rght)
- Node.cache.mapDirs.set(leftKid.unId, dir!)
- Node.cache.mapDirs.set(rghtKid.unId, dir!)
- Node.cache.mapElem.set(leftKid.unId, leftKid)
- Node.cache.mapElem.set(rghtKid.unId, rghtKid)
-
- break
- }
- const leftName = queue.dequeue()
- const left = new Node({ type: 'tabset', host })
- const rght = new Node({ type: 'row', host })
- const leftKid = new Node({
- type: 'tab',
- host: left,
- name: leftName,
- unId: engine.tabsIds.get(leftName ?? '') ?? layoutState.createId(),
- })
- left.open = leftKid.name
-
- left.kids.enqueue(leftKid)
- host.kids.enqueue(left)
- host.kids.enqueue(rght)
-
- rght.addBond(host, left)
-
- Node.cache.mapDirs.set(left.unId, !dir!)
- Node.cache.mapDirs.set(rght.unId, !dir!)
- Node.cache.mapElem.set(left.unId, left)
- Node.cache.mapDirs.set(leftKid.unId, dir!)
- Node.cache.mapElem.set(leftKid.unId, leftKid)
-
- nodeQueue.enqueue(rght)
- }
-
- engine.calcTabsetCountAndMinDim()
-}
-
-export function createNodeFromJSON(engine: Layout, json: LayoutTree): Node {
- const newNode = new Node({
- type: json.typNode as 'row' | 'tabset' | 'tab',
- name: json.nodName,
- unId: json.uidNode,
- part: json.nodPart,
- dims: { w: 0, h: 0, x: 0, y: 0 },
- open: typeof json.nodOpen === 'string' ? json.nodOpen : '',
- })
- newNode.collapsed = json.typNode === 'tabset' && json.nodFold === true
-
- if (json.typNode === 'tab') {
- newNode.unId = engine.tabsIds.get(json.nodName) || json.uidNode
- }
-
- return newNode
-}
diff --git a/packages/core/src/app/tree-mutations.ts b/packages/core/src/app/tree-mutations.ts
deleted file mode 100644
index 0d3cca0..0000000
--- a/packages/core/src/app/tree-mutations.ts
+++ /dev/null
@@ -1,640 +0,0 @@
-import { Queue } from './queue'
-import { Node } from './node'
-import { layoutState } from './state'
-import type { DynamixLayoutCore as Layout } from './dynamix'
-
-/** Drag and drop: moving tabs/tabsets and keeping bonds and caches consistent. */
-
-export function updateTree(
- engine: Layout,
- src: string,
- des: string,
- layout: 'top' | 'bottom' | 'left' | 'right' | 'contain'
-): boolean {
- const srcNode = Node.cache.mapElem.get(src)
- let desNode = Node.cache.mapElem.get(des)
-
- if (
- !srcNode ||
- !(srcNode instanceof Node) ||
- !desNode ||
- !(desNode instanceof Node)
- ) {
- console.warn(`Source node ${src} not found or is not a Node instance`)
- return false
- }
-
- if (
- srcNode?.type == 'tab' &&
- srcNode.host === desNode &&
- desNode.kids.size() < 2
- ) {
- console.warn(`Cannot move to self as it has only one tab`)
- return false
- }
-
- if (srcNode?.type == 'tabset' && desNode?.host === srcNode) {
- console.warn(`Cannot move tabset to self as it is already a child`)
- return false
- }
-
- if (!srcNode || !desNode) {
- console.warn(`Source or destination node not found: ${src}, ${des}`)
- return false
- }
-
- if (srcNode === desNode) {
- console.warn(
- 'Source and destination nodes are the same, no action taken.'
- )
- return false
- }
-
- if (!srcNode.host || (!desNode.host && desNode.type !== 'row')) {
- console.warn(
- `Source node or destination node's host is null: ${srcNode.unId}, ${desNode.unId}`
- )
- return false
- }
-
- // Only the root row is a valid row target; moveRelativeToRoot assumes it.
- if (desNode.type === 'row' && desNode !== layoutState.root) {
- console.warn(`Cannot move relative to a nested row: ${desNode.unId}`)
- return false
- }
-
- if (desNode.type === 'row' && layout === 'contain') {
- console.warn(
- `Cannot move to a row with 'contain' layout: ${desNode.unId}`
- )
- return false
- }
-
- if (srcNode.type === 'row') {
- return false
- }
-
- if (desNode.type === 'tabset' && layout === 'contain') {
- desNode = desNode.kids.peekBack() || desNode
- }
-
- // Dropping the last tab of a tabset onto that same tabset is a no-op.
- if (srcNode === desNode) return false
-
- if (engine.isOnlyContent(srcNode)) {
- console.warn('Cannot move the only tabset or tab in the layout')
- return false
- }
-
- // Any move ends maximize, and a dragged tabset arrives unfolded.
- layoutState.maximized = null
- srcNode.collapsed = false
-
- if (
- desNode.type === 'tab' &&
- (layout === 'contain' || layout === 'left' || layout === 'right')
- ) {
- engine.moveNodeAsTab(srcNode, desNode, layout)
- } else if (desNode.type === 'row' && layout !== 'contain') {
- engine.moveRelativeToRoot(srcNode, desNode, layout)
- } else if (desNode.type === 'tabset' && layout !== 'contain') {
- engine.moveRelativeToTabset(srcNode, desNode, layout)
- }
-
- engine.calcTabsetCountAndMinDim()
- engine.calcDimensions(layoutState.root, true)
- return true
-}
-
-export function isOnlyContent(engine: Layout, node: Node): boolean {
- let tabsets = 0
- for (const n of engine.NodeLayoutRecursiveIterator(layoutState.root)) {
- if (n.type === 'tabset') tabsets++
- }
- if (tabsets !== 1) return false
-
- return (
- node.type === 'tabset' ||
- (node.type === 'tab' && node.host?.kids.size() === 1)
- )
-}
-
-export function moveNodeAsTab(
- engine: Layout,
- src: Node,
- des: Node,
- layout: 'left' | 'right' | 'contain'
-) {
- if (!des || !des.host) {
- console.warn(`Destination node ${des.unId} has no host, cannot move.`)
- return
- }
-
- engine.removeNode(src)
-
- const desHst = des.host
- const desIdx = desHst.kids.indexOf(des)
-
- if (desIdx === undefined || desIdx === -1) {
- return
- }
-
- const insertFlg = layout === 'contain' || layout === 'left'
- const insertIdx = insertFlg ? desIdx : desIdx + 1
-
- const desNxt = desHst.kids.get(insertIdx + 1)
- const desPre = desHst.kids.get(insertIdx - 1)
- const nxtBnd = des.next
- const prvBnd = des.prev
-
- if (!insertFlg && nxtBnd) {
- Node.cache.mapElem.delete(nxtBnd.unId)
- } else if (insertFlg && prvBnd) {
- Node.cache.mapElem.delete(prvBnd.unId)
- }
-
- if (src.type == 'tab') {
- if (insertFlg) {
- if (desPre) src.addBond(desHst, desPre)
- des.addBond(desHst, src)
- } else {
- src.addBond(desHst, des)
- if (desNxt) desNxt.addBond(desHst, src)
- }
- desHst.kids.insert(insertIdx, src)
- src.host = desHst
- desHst.open = src.name
- desHst.collapsed = false
- } else if (src.type == 'tabset') {
- const kidFst = src.kids.peek()
- const kidLst = src.kids.peekBack()
-
- if (insertFlg) {
- if (kidFst && desPre) kidFst.addBond(desHst, desPre)
- if (kidLst && des) des.addBond(desHst, kidLst)
- } else {
- if (kidFst && des) kidFst.addBond(desHst, des)
- if (kidLst && desNxt) desNxt.addBond(desHst, kidLst)
- }
-
- desHst.kids.insertQueue(insertIdx, src.kids)
-
- for (const kid of src.kids) {
- kid.host = desHst
- if (kid.next) kid.next.host = desHst
- }
-
- desHst.open = kidLst ? kidLst.name : ''
- desHst.collapsed = false
- }
-
- engine.updateChildDirections(desHst)
-}
-
-type Side = 'top' | 'bottom' | 'left' | 'right'
-
-/**
- * Whether dropping on a side of the target needs a new parent (the drop
- * crosses the target's layout axis) or can be inserted as a sibling.
- * `vertical` means the target lays its children out top-to-bottom.
- */
-const needsNewParent = {
- root: (vertical: boolean, sideways: boolean) => vertical === sideways,
- tabset: (vertical: boolean, sideways: boolean) => vertical !== sideways,
-}
-
-function moveRelative(
- engine: Layout,
- src: Node,
- des: Node,
- side: Side,
- target: keyof typeof needsNewParent
-) {
- engine.removeNode(src)
- if (src.type !== 'tab' && src.type !== 'tabset') return
-
- const vertical = Node.cache.mapDirs.get(des.unId) === false
- const sideways = side === 'left' || side === 'right'
- const before = side === 'top' || side === 'left'
-
- let moved = src
- if (src.type === 'tab') {
- moved = new Node({ type: 'tabset', host: des.host })
- moved.kids.enqueue(src)
- moved.open = src.name
- src.host = moved
- }
-
- if (needsNewParent[target](vertical, sideways)) {
- engine.insertNodeWithNewParent(moved, des, before)
- } else {
- engine.insertNode(des, moved, before)
- }
-}
-
-export function moveRelativeToRoot(
- engine: Layout,
- src: Node,
- des: Node,
- layout: Side
-) {
- moveRelative(engine, src, des, layout, 'root')
-}
-
-export function moveRelativeToTabset(
- engine: Layout,
- src: Node,
- des: Node,
- layout: Side
-) {
- moveRelative(engine, src, des, layout, 'tabset')
-}
-
-export function insertNode(
- engine: Layout,
- des: Node,
- src: Node,
- flag: boolean
-) {
- const desHost = des.host
- if (des == layoutState.root) {
- const kidhead = layoutState.root.kids.peek()
- const kidTail = layoutState.root.kids.peekBack()
-
- if (flag) {
- layoutState.root.kids.enqueueFront(src)
- if (kidhead) kidhead.addBond(layoutState.root, src)
- } else {
- layoutState.root.kids.enqueue(src)
- if (kidTail) src.addBond(layoutState.root, kidTail)
- }
-
- src!.host = layoutState.root
- engine.updateChildDirections(layoutState.root)
- return
- }
-
- if (!desHost) {
- console.warn(`Destination node ${des.unId} has no host, cannot insert.`)
- return
- }
-
- const desIdx = desHost.kids.indexOf(des)
- if (desIdx === -1) {
- console.warn(
- `Destination node ${des.unId} not found in its host's kids.`
- )
- return
- }
-
- const hostDir = Node.cache.mapDirs.get(desHost.unId)
- if (hostDir) Node.cache.mapDirs.set(src.unId, !hostDir)
-
- const desNxt = desHost.kids.get(desIdx + 1)
- const desPre = desHost.kids.get(desIdx - 1)
- const bndNxt = des.next
- const bndPre = des.prev
-
- src.host = desHost
-
- if (flag) {
- desHost.kids.insert(desIdx, src)
-
- des.addBond(desHost, src)
- if (desPre) src.addBond(desHost, desPre)
-
- if (bndPre) {
- Node.cache.mapElem.delete(bndPre.unId)
- }
- } else {
- desHost.kids.insert(desIdx + 1, src)
- src.addBond(desHost, des)
- if (desNxt) desNxt.addBond(desHost, src)
-
- if (bndNxt) {
- Node.cache.mapElem.delete(bndNxt.unId)
- }
- }
-
- engine.updateChildDirections(desHost)
-}
-
-export function insertNodeWithNewParent(
- engine: Layout,
- src: Node,
- des: Node,
- flag: boolean
-) {
- const kids = new Queue()
- const node = new Node({ type: 'tabset', host: des.host })
-
- if (!des || !src) {
- console.warn(`Destination node ${des.unId} has no host, cannot insert.`)
- return
- }
-
- if (des === layoutState.root) {
- const row = new Node({ type: 'row', host: des })
- if (des.kids.size() === 0) {
- des.kids.enqueue(src)
- src.host = des
- } else if (des.kids.size() == 1 && des.kids.peek()?.type === 'tabset') {
- const kid = des.kids.dequeue()
-
- if (!kid) {
- console.warn(
- `No kid found in destination node ${des.unId}, cannot insert.`
- )
- return
- }
-
- if (flag) {
- row.kids.enqueue(src)
- row.kids.enqueue(kid)
- des.kids.enqueue(row)
- kid.addBond(row, src)
- } else {
- row.kids.enqueue(kid)
- row.kids.enqueue(src)
- src.addBond(row, kid)
- des.kids.enqueue(row)
- }
- src.host = row
- kid.host = row
- } else if (des.kids.size() == 1 && des.kids.peek()?.type === 'row') {
- const kid = des.kids.peek()
-
- const kidhead = kid?.kids.peek()
- const kidTail = kid?.kids.peekBack()
-
- if (flag && kid) {
- kid.kids.enqueueFront(src)
- if (kidhead) kidhead.addBond(kid, src)
- } else if (kid) {
- kid.kids.enqueue(src)
- if (kidTail) src.addBond(kid, kidTail)
- }
-
- src!.host = kid!
- } else if (des.kids.size() > 1) {
- const rowA = new Node({ type: 'row', host: des })
- const rowB = new Node({ type: 'row', host: rowA })
-
- for (const kid of des.kids) {
- rowB.kids.enqueue(kid)
- kid.host = rowB
- if (kid.next) kid.next.host = rowB
- }
-
- src.host = rowA
- des.kids.clear()
-
- if (flag) {
- rowA.kids.enqueueFront(src)
- rowA.kids.enqueue(rowB)
- rowB.addBond(rowA, src)
- } else {
- rowA.kids.enqueue(rowB)
- rowA.kids.enqueue(src)
- src.addBond(rowA, rowB)
- }
-
- des.kids.enqueue(rowA)
- }
-
- engine.updateChildDirections(des)
- return
- }
-
- src.host = des
-
- node.host = des
- node.kids = des.kids
- node.open = des.open
-
- const desDir = Node.cache.mapDirs.get(des.unId)
-
- for (const kid of node.kids) {
- kid.host = node
- if (kid.next) kid.next.host = node
- Node.cache.mapDirs.set(kid.unId, desDir!)
- }
-
- for (const kid of src.kids) {
- kid.host = src
- if (kid.next) kid.next.host = src
- Node.cache.mapDirs.set(kid.unId, desDir!)
- }
-
- Node.cache.mapDirs.set(node.unId, !desDir!)
- Node.cache.mapDirs.set(src.unId, !desDir!)
-
- if (flag) {
- kids.enqueue(src)
- kids.enqueue(node)
- des.kids = kids
- des.type = 'row'
- node.addBond(des, src)
- } else {
- kids.enqueue(node)
- kids.enqueue(src)
- des.kids = kids
- des.type = 'row'
- src.addBond(des, node)
- }
-}
-
-export function moveAdjacentNodeToGrandParent(engine: Layout, src: Node) {
- if (!src.host || !src.host.host) {
- console.warn(`Source node ${src.unId} has no host, cannot move.`)
- return
- }
-
- const srcHost = src.host
- const gndHost = src.host.host
-
- const srcHostIndex = gndHost.kids.indexOf(srcHost)
- const hostPrev = gndHost.kids.get(srcHostIndex - 1)
- const hostNext = gndHost.kids.get(srcHostIndex + 1)
- const fstKid = src.kids.peek()
- const lstKid = src.kids.peekBack()
-
- engine.removeKid(srcHost)
-
- let insrtIndex = srcHostIndex
-
- if (src.type == 'row' && srcHost) {
- if (hostPrev && fstKid) fstKid.addBond(gndHost, hostPrev)
- if (hostNext && lstKid) hostNext.addBond(gndHost, lstKid)
-
- for (const kid of src.kids) {
- kid.host = gndHost
- if (kid.next) kid.next.host = gndHost
- gndHost.kids.insert(insrtIndex, kid)
-
- Node.cache.mapDirs.set(
- kid.unId,
- !Node.cache.mapDirs.get(gndHost.unId)!
- )
- Node.cache.dimMins.delete(kid.unId)
- insrtIndex++
- }
- } else if (src.type == 'tabset') {
- if (hostPrev) src.addBond(gndHost, hostPrev)
- if (hostNext) hostNext.addBond(gndHost, src)
-
- gndHost.kids.insert(insrtIndex, src)
-
- src.host = gndHost
-
- Node.cache.mapDirs.set(src.unId, !Node.cache.mapDirs.get(gndHost.unId)!)
-
- for (const kid of src.kids) {
- Node.cache.mapDirs.set(
- kid.unId,
- Node.cache.mapDirs.get(gndHost.unId)!
- )
- Node.cache.dimMins.delete(kid.unId)
- }
- }
-}
-
-export function removeRelation(src: Node, fix: boolean = true) {
- const host = src.host
- const prev = src.prev
- const next = src.next
-
- src.host = null
- src.next = null
- src.prev = null
-
- if (prev) prev.next = null
- if (next) next.prev = null
-
- if (!fix) {
- return src
- }
-
- if (prev && next && prev.prev && next.next) {
- prev.prev.addBond(host, next.next)
- } else if (prev && prev.prev) {
- prev.prev.next = null
- } else if (next && next.next) {
- next.next.prev = null
- }
-}
-
-export function removeNode(engine: Layout, src: Node) {
- if (!src.host) {
- console.warn(`Kid node has no host, cannot remove.`)
- return
- }
-
- const papa = src?.host
- const dada = papa?.host
-
- engine.removeKid(src)
-
- if (src.type === 'tabset' && papa.kids.size() === 1) {
- /**
- *
- */
- const kid = papa.kids.peek()
- if (kid) engine.moveAdjacentNodeToGrandParent(kid)
- } else if (
- /**
- *
- */
- src.type === 'tab' &&
- papa!.kids.size() === 0 &&
- dada!.kids.size() === 2
- ) {
- engine.removeKid(papa)
- const kid = dada?.kids.peek()
- if (kid) engine.moveAdjacentNodeToGrandParent(kid)
- } else if (
- /**
- *
- */
- src.type === 'tab' &&
- papa!.kids.size() === 0 &&
- dada!.kids.size() > 2
- ) {
- engine.removeKid(papa)
- } else if (
- /**
- *
- */
- src.type === 'tab' &&
- papa!.open === src.name &&
- papa!.kids.size() > 0
- ) {
- const kid = papa.kids.peek()
- if (kid) papa.open = kid.name
- }
-}
-
-export function removeKid(kid: Node) {
- if (!kid.host) {
- console.warn(`Kid node has no host, cannot remove.`)
- return
- }
-
- const host = kid.host
- const idx = host.kids.indexOf(kid)
-
- const nextNode = host.kids.get(idx + 1)
- const prevNode = host.kids.get(idx - 1)
- const nextBond = kid.next
- const prevBond = kid.prev
-
- if (idx === -1) {
- console.warn(`Kid node not found in its host's kids.`)
- return
- }
-
- if (nextBond) {
- Node.cache.mapElem.delete(nextBond.unId)
- }
-
- if (prevBond) {
- Node.cache.mapElem.delete(prevBond.unId)
- }
-
- if (nextNode && prevNode) {
- nextNode.addBond(host, prevNode)
- } else if (nextNode) {
- nextNode.prev = null
- } else if (prevNode) {
- prevNode.next = null
- }
-
- host.kids.removeAt(idx)
-
- Node.cache.mapElem.delete(kid.unId)
- Node.cache.dimMins.delete(kid.unId)
- Node.cache.mapDirs.delete(kid.unId)
- Node.cache.tabCnts.delete(kid.unId)
-
- if (kid.type === 'tabset') {
- Node.cache.mapElem.delete(kid.unId)
- }
-
- if (kid.type === 'tab') {
- Node.cache.mapElem.delete(kid.unId)
- }
-
- kid.host = null
- kid.next = null
- kid.prev = null
-}
-
-export function clearAllCache() {
- Node.cache.dimMins.clear()
- Node.cache.mapDirs.clear()
- Node.cache.tabCnts.clear()
- Node.cache.mapElem.clear()
-
- Node.cache.nodOpts.get().clear()
- Node.cache.bndOpts.get().clear()
-}
diff --git a/packages/core/src/app/view-state.ts b/packages/core/src/app/view-state.ts
deleted file mode 100644
index 7aee166..0000000
--- a/packages/core/src/app/view-state.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { Node } from './node'
-import { layoutState } from './state'
-import type { DynamixLayoutCore as Layout } from './dynamix'
-
-/** Maximize and fold: view state on top of the tree that never changes parts. */
-
-function findTabset(id: string): Node | null {
- const node = Node.cache.mapElem.get(id)
- return node instanceof Node && node.type === 'tabset' ? node : null
-}
-
-function countTabsets(root: Node = layoutState.root): number {
- let count = 0
- const walk = (node: Node) => {
- if (node.type === 'tabset') count++
- for (const kid of node.kids) walk(kid)
- }
- walk(root)
- return count
-}
-
-/** A tabset can fold when its row has another child to take the space. */
-export function canCollapse(node: Node): boolean {
- return node.type === 'tabset' && !!node.host && node.host.kids.size() > 1
-}
-
-export function canMaximize(): boolean {
- return countTabsets() > 1
-}
-
-/** The maximized tabset, or null when nothing is maximized. */
-export function getMaximizedTabset(): Node | null {
- const id = layoutState.maximized
- return id ? findTabset(id) : null
-}
-
-/** Unfold the most recently folded child of a row other than `except`. */
-function unfoldMostRecent(row: Node, except?: Node) {
- let pick: Node | null = null
- for (const kid of row.kids) {
- if (kid === except || !kid.collapsed) continue
- if (!pick || kid.foldedAt > pick.foldedAt) pick = kid
- }
- if (pick) pick.collapsed = false
-}
-
-/**
- * Keeps view state valid after any tree change: a tabset alone in its row
- * cannot stay folded, every row keeps at least one open child, and a
- * maximized tabset that left the tree is forgotten.
- */
-export function normalizeViewState(root: Node = layoutState.root) {
- let maximizedFound = false
-
- const walk = (node: Node) => {
- if (node.type === 'tabset') {
- if (node.unId === layoutState.maximized) maximizedFound = true
- if (node.collapsed && !canCollapse(node)) node.collapsed = false
- }
-
- if (node.type !== 'tab' && node.kids.size() > 0) {
- const allFolded = [...node.kids].every((kid) => kid.collapsed)
- if (allFolded) unfoldMostRecent(node)
- }
-
- for (const kid of node.kids) walk(kid)
- }
- walk(root)
-
- if (!maximizedFound) layoutState.maximized = null
-}
-
-export function maximize(engine: Layout, id: string): boolean {
- if (!findTabset(id) || !canMaximize()) return false
- if (layoutState.maximized === id) return false
-
- layoutState.maximized = id
- engine.calcDimensions()
- return true
-}
-
-export function restore(engine: Layout): boolean {
- if (!layoutState.maximized) return false
-
- layoutState.maximized = null
- engine.calcDimensions()
- return true
-}
-
-export function toggleMaximize(engine: Layout, id: string): boolean {
- return layoutState.maximized === id ? restore(engine) : maximize(engine, id)
-}
-
-/** Folding the last open child of a row unfolds its most recently folded sibling. */
-export function collapse(engine: Layout, id: string): boolean {
- const node = findTabset(id)
- if (!node || node.collapsed || !canCollapse(node)) return false
-
- layoutState.maximized = null
- node.collapsed = true
- node.foldedAt = ++layoutState.foldSeq
-
- const row = node.host!
- if ([...row.kids].every((kid) => kid.collapsed)) unfoldMostRecent(row, node)
-
- engine.calcTabsetCountAndMinDim()
- engine.calcDimensions()
- return true
-}
-
-export function expand(engine: Layout, id: string): boolean {
- const node = findTabset(id)
- if (!node || !node.collapsed) return false
-
- node.collapsed = false
- engine.calcTabsetCountAndMinDim()
- engine.calcDimensions()
- return true
-}
-
-export function toggleCollapse(engine: Layout, id: string): boolean {
- const node = findTabset(id)
- if (!node) return false
- return node.collapsed ? expand(engine, id) : collapse(engine, id)
-}
diff --git a/packages/core/src/create-layout.ts b/packages/core/src/create-layout.ts
new file mode 100644
index 0000000..572283a
--- /dev/null
+++ b/packages/core/src/create-layout.ts
@@ -0,0 +1,260 @@
+import { defaultWarningHandler, isDev } from './dev'
+import { createDragActions } from './drag-actions'
+import { getDropIndicatorRect } from './drop/drop-indicator'
+import { getDropTarget } from './drop/drop-target'
+import { listDropTargets } from './drop/drop-targets'
+import { DynamixLayoutError, type WarningHandler } from './errors'
+import { DEFAULT_GEOMETRY, type GeometryConfig } from './geometry/config'
+import { getSplitterBounds, resizeSplitterWeights } from './geometry/splitter'
+import { createIdGenerator } from './ids'
+import type { Layout, LayoutOptions, LayoutSizeOptions } from './layout-types'
+import type { LayoutModel } from './model/types'
+import { parseLayout } from './serialize/parse'
+import type { LayoutJSON, LayoutTreeV1 } from './serialize/schema'
+import { createLayoutStore } from './store/layout-store'
+import { buildDefaultTree } from './tree/build'
+import { collectNodeIds, findTabsetOfTab } from './tree/find'
+import { moveNode } from './tree/move'
+import {
+ addTabToModel,
+ reconcileTabs,
+ type TabInit,
+} from './tree/reconcile-tabs'
+import { removeTab } from './tree/remove'
+import { replaceNode } from './tree/update'
+import * as viewState from './tree/view-state'
+
+const ROOT_ID = 'root'
+
+/**
+ * Creates a layout instance.
+ *
+ * @param options - Open tabs, an optional saved layout (v1 or v2), sizes and callbacks.
+ * @returns An instance that shares nothing with other instances. It never
+ * touches the DOM; call `setContainerRect` to give it a size.
+ * @throws {DynamixLayoutError} `INVALID_LAYOUT` or `UNSUPPORTED_VERSION` for a
+ * bad `initialLayout`; `DUPLICATE_TAB_ID` for duplicate tab ids in development.
+ * @example
+ * const layout = createLayout({
+ * tabs: [{ id: 'editor' }, { id: 'terminal' }],
+ * onLayoutChange: (json) => localStorage.setItem('layout', JSON.stringify(json)),
+ * })
+ * layout.subscribe((snapshot) => render(snapshot))
+ * layout.setContainerRect({ x: 0, y: 0, width: 1200, height: 800 })
+ */
+export function createLayout(options: LayoutOptions): Layout {
+ const warn = options.onWarning ?? defaultWarningHandler
+ const ids = createIdGenerator(options.createId)
+ const { createId, reserve } = ids
+ let tabs = uniqueTabs(options.tabs, warn)
+
+ const fromLayout = (input: LayoutJSON | LayoutTreeV1): LayoutModel => {
+ const parsed = parseLayout(input, warn)
+ reserve(collectNodeIds(parsed.root))
+ const loaded = { ...parsed, lastActiveTabsetId: null, foldOrder: [] }
+ return reconcileTabs(loaded, tabs, createId, false)
+ }
+ const initialModel = (): LayoutModel => {
+ if (options.initialLayout) return fromLayout(options.initialLayout)
+ reserve([ROOT_ID])
+ const tabIds = tabs.map((tab) => tab.id)
+ return {
+ root: buildDefaultTree(tabIds, createId, ROOT_ID),
+ maximizedTabsetId: null,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+ }
+ }
+
+ const store = createLayoutStore(
+ initialModel(),
+ configFrom(options, DEFAULT_GEOMETRY),
+ options.onLayoutChange,
+ warn
+ )
+ const unknown = (kind: string, id: string) => {
+ warn({ code: 'UNKNOWN_ID', message: `Unknown ${kind} "${id}"` })
+ return false
+ }
+
+ const layout: Layout = {
+ getSnapshot: () => store.snapshot,
+ subscribe: (listener) => store.subscribe(listener),
+ setContainerRect: (rect) => store.setContainer(rect),
+ setOptions: (next) => store.setConfig(configFrom(next, store.config)),
+ setTabs(next) {
+ tabs = uniqueTabs(next, warn)
+ store.commit(
+ reconcileTabs(store.model, tabs, createId, true),
+ 'tabs'
+ )
+ },
+
+ moveTab: (tabId, target) =>
+ store.commit(
+ moveNode(store.model, { type: 'tab', tabId }, target, createId),
+ 'move'
+ ),
+ moveTabset: (tabsetId, target) =>
+ store.commit(
+ moveNode(
+ store.model,
+ { type: 'tabset', tabsetId },
+ target,
+ createId
+ ),
+ 'move'
+ ),
+ selectTab(tabId) {
+ const { model } = store
+ const tabset = findTabsetOfTab(model.root, tabId)
+ if (!tabset) return unknown('tab', tabId)
+ if (
+ tabset.activeTabId === tabId &&
+ model.lastActiveTabsetId === tabset.id
+ )
+ return false
+ const root = replaceNode(model.root, tabset.id, () => ({
+ ...tabset,
+ activeTabId: tabId,
+ }))
+ return store.commit(
+ { ...model, root, lastActiveTabsetId: tabset.id },
+ 'select'
+ )
+ },
+ addTab(tab, target) {
+ if (tabs.some((t) => t.id === tab.id)) return false
+ const init = target ? { ...tab, target } : tab
+ tabs = [...tabs, init]
+ return store.commit(
+ addTabToModel(store.model, init, createId, true),
+ 'tabs'
+ )
+ },
+ removeTab(tabId) {
+ if (!tabs.some((t) => t.id === tabId)) return unknown('tab', tabId)
+ tabs = tabs.filter((t) => t.id !== tabId)
+ const { model } = store
+ const root = removeTab(model.root, tabId)
+ return store.commit(
+ viewState.normalizeViewState({ ...model, root }),
+ 'tabs'
+ )
+ },
+
+ resizeSplitter(id, point) {
+ const { model, rects, config } = store
+ if (model.maximizedTabsetId) return false
+ const root = resizeSplitterWeights(
+ model.root,
+ rects,
+ config,
+ id,
+ point
+ )
+ return store.commit(root && { ...model, root }, 'resize')
+ },
+ moveSplitterBy(id, delta) {
+ const rect = store.rects.splitters.get(id)
+ const splitter = store.snapshot.splitters.get(id)
+ if (!rect || !splitter) return unknown('splitter', id)
+ const x = rect.x + rect.width / 2
+ const y = rect.y + rect.height / 2
+ const point =
+ splitter.direction === 'horizontal'
+ ? { x: x + delta, y }
+ : { x, y: y + delta }
+ return layout.resizeSplitter(id, point)
+ },
+
+ maximize: (id) =>
+ store.commit(viewState.maximize(store.model, id), 'maximize'),
+ restore: () => store.commit(viewState.restore(store.model), 'maximize'),
+ toggleMaximize: (id) =>
+ store.model.maximizedTabsetId === id
+ ? layout.restore()
+ : layout.maximize(id),
+ fold: (id) => store.commit(viewState.fold(store.model, id), 'fold'),
+ unfold: (id) => store.commit(viewState.unfold(store.model, id), 'fold'),
+ toggleFold(id) {
+ const tabset = store.snapshot.tabsets.get(id)
+ if (!tabset) return unknown('tabset', id)
+ return tabset.isFolded ? layout.unfold(id) : layout.fold(id)
+ },
+
+ ...createDragActions(store, createId),
+
+ getDropTarget: (point, source, measurements) =>
+ source.type === 'splitter'
+ ? null
+ : getDropTarget(
+ store.model,
+ store.rects,
+ point,
+ source,
+ measurements
+ ),
+ getDropIndicatorRect: (target, measurements) =>
+ getDropIndicatorRect(
+ store.model,
+ store.rects,
+ target,
+ measurements
+ ),
+ listDropTargets: (source) =>
+ source.type === 'splitter'
+ ? []
+ : listDropTargets(store.model, source),
+ getSplitterBounds: (id) =>
+ getSplitterBounds(store.model.root, store.rects, store.config, id),
+
+ load: (input) => void store.commit(fromLayout(input)),
+ reset() {
+ ids.reset()
+ store.commit(initialModel(), 'reset')
+ },
+ toJSON: () => store.toJSON(),
+ destroy: () => store.destroy(),
+ }
+ return layout
+}
+
+/**
+ * `foldedSize` follows `minPanelSize.height` unless set; adapters pass it
+ * explicitly (the tab bar height).
+ */
+function configFrom(
+ options: LayoutSizeOptions,
+ fallback: GeometryConfig
+): GeometryConfig {
+ return {
+ minPanelSize: options.minPanelSize ?? fallback.minPanelSize,
+ splitterSize: options.splitterSize ?? fallback.splitterSize,
+ foldedSize:
+ options.foldedSize ??
+ options.minPanelSize?.height ??
+ fallback.foldedSize,
+ }
+}
+
+/** Duplicate ids throw in development; in production the first one wins. */
+function uniqueTabs(tabs: readonly TabInit[], warn: WarningHandler): TabInit[] {
+ const seen = new Set()
+ return tabs.filter((tab) => {
+ if (!seen.has(tab.id)) {
+ seen.add(tab.id)
+ return true
+ }
+ if (isDev())
+ throw new DynamixLayoutError(
+ 'DUPLICATE_TAB_ID',
+ `Duplicate tab id "${tab.id}"`
+ )
+ warn({
+ code: 'DUPLICATE_TAB_ID',
+ message: `Duplicate tab id "${tab.id}" ignored`,
+ })
+ return false
+ })
+}
diff --git a/packages/core/src/dev.ts b/packages/core/src/dev.ts
new file mode 100644
index 0000000..1bcfa25
--- /dev/null
+++ b/packages/core/src/dev.ts
@@ -0,0 +1,21 @@
+import type { WarningHandler } from './errors'
+
+declare const process: { env: { NODE_ENV?: string } } | undefined
+
+/**
+ * Development unless the bundler set `process.env.NODE_ENV` to "production".
+ * Written so bundlers can replace the expression; never evaluated at import.
+ */
+export function isDev(): boolean {
+ return (
+ typeof process === 'undefined' ||
+ // Read at runtime by consumers' bundlers, not a build input of this repo.
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
+ process.env.NODE_ENV !== 'production'
+ )
+}
+
+/** Default warning handler: `console.warn` in development, silent in production. */
+export const defaultWarningHandler: WarningHandler = (warning) => {
+ if (isDev()) console.warn(`[dynamix-layout] ${warning.message}`)
+}
diff --git a/packages/core/src/dom/apply-rect.ts b/packages/core/src/dom/apply-rect.ts
new file mode 100644
index 0000000..8213b8c
--- /dev/null
+++ b/packages/core/src/dom/apply-rect.ts
@@ -0,0 +1,17 @@
+import type { Rect } from '../model/types'
+
+/**
+ * Positions an absolutely positioned element. Adapters use this on their fast
+ * path (drags), so pointer moves never need a framework re-render.
+ * @param element - An absolutely positioned element inside the layout root.
+ * @param rect - Where to put it, in layout-root coordinates.
+ * @example
+ * layout.subscribe((s) => s.rects.tabsets.forEach((rect, id) => applyRect(panels.get(id)!, rect)))
+ */
+export function applyRect(element: HTMLElement, rect: Rect): void {
+ const { style } = element
+ style.left = `${rect.x}px`
+ style.top = `${rect.y}px`
+ style.width = `${rect.width}px`
+ style.height = `${rect.height}px`
+}
diff --git a/packages/core/src/dom/frame-scheduler.ts b/packages/core/src/dom/frame-scheduler.ts
new file mode 100644
index 0000000..ac76b68
--- /dev/null
+++ b/packages/core/src/dom/frame-scheduler.ts
@@ -0,0 +1,49 @@
+export interface FrameScheduler {
+ /** Remember `value`; `onFlush` runs with the latest value on the next frame. */
+ schedule: (value: T) => void
+ /** Run a pending flush now (e.g. on pointer up). */
+ flush: () => void
+ /** Drop a pending flush (e.g. on unmount). */
+ cancel: () => void
+}
+
+/**
+ * Coalesces bursts of updates (pointer events can fire several times per
+ * frame) into at most one `onFlush` call per animation frame.
+ * @param onFlush - Called with the latest scheduled value, at most once per frame.
+ * @returns `schedule`, `flush` and `cancel`.
+ * @example
+ * const frame = createFrameScheduler((point: Point) => layout.updateDrag(point))
+ * element.addEventListener('pointermove', (e) => frame.schedule(toRootPoint(e)))
+ * element.addEventListener('pointerup', () => { frame.flush(); layout.endDrag() })
+ */
+export function createFrameScheduler(
+ onFlush: (value: T) => void
+): FrameScheduler {
+ let pending: { value: T } | null = null
+ let frame: number | null = null
+
+ const run = () => {
+ frame = null
+ const next = pending
+ pending = null
+ if (next) onFlush(next.value)
+ }
+
+ return {
+ schedule(value) {
+ pending = { value }
+ if (frame === null) frame = requestAnimationFrame(run)
+ },
+ flush() {
+ if (frame === null) return
+ cancelAnimationFrame(frame)
+ run()
+ },
+ cancel() {
+ if (frame !== null) cancelAnimationFrame(frame)
+ frame = null
+ pending = null
+ },
+ }
+}
diff --git a/packages/core/src/drag-actions.ts b/packages/core/src/drag-actions.ts
new file mode 100644
index 0000000..a1559ba
--- /dev/null
+++ b/packages/core/src/drag-actions.ts
@@ -0,0 +1,115 @@
+import { getDropIndicatorRect } from './drop/drop-indicator'
+import { getDropTarget } from './drop/drop-target'
+import { resizeSplitterWeights } from './geometry/splitter'
+import type { Layout } from './layout-types'
+import type { CreateId } from './model/types'
+import { isSameTarget, type LayoutStore } from './store/layout-store'
+import { moveNode } from './tree/move'
+import { restore } from './tree/view-state'
+
+type DragActions = Pick<
+ Layout,
+ 'startDrag' | 'updateDrag' | 'setDragTarget' | 'endDrag' | 'cancelDrag'
+>
+
+/**
+ * One drag model for tabs, tabsets and splitters. Frames are transient
+ * (published but not reported); `endDrag` commits with a single
+ * `onLayoutChange`, and `cancelDrag` puts a dragged splitter back.
+ */
+export function createDragActions(
+ store: LayoutStore,
+ createId: CreateId
+): DragActions {
+ const actions: DragActions = {
+ startDrag(source) {
+ if (store.isDestroyed) return false
+ const { snapshot, model } = store
+ if (source.type === 'splitter') {
+ const splitter = snapshot.splitters.get(source.splitterId)
+ if (!splitter || splitter.isLocked || model.maximizedTabsetId)
+ return false
+ } else {
+ const exists =
+ source.type === 'tab'
+ ? snapshot.tabs.has(source.tabId)
+ : snapshot.tabsets.has(source.tabsetId)
+ if (!exists) return false
+ // Every target must be visible while dragging.
+ store.commit(restore(model), 'maximize')
+ }
+ store.update({
+ drag: {
+ source,
+ target: null,
+ indicator: null,
+ startRoot: store.model.root,
+ },
+ })
+ return true
+ },
+
+ updateDrag(point, measurements) {
+ const { drag, model, rects, config } = store
+ if (!drag) return
+ if (drag.source.type === 'splitter') {
+ const root = resizeSplitterWeights(
+ model.root,
+ rects,
+ config,
+ drag.source.splitterId,
+ point
+ )
+ if (root) store.update({ model: { ...model, root } })
+ return
+ }
+ actions.setDragTarget(
+ getDropTarget(model, rects, point, drag.source, measurements),
+ measurements
+ )
+ },
+
+ setDragTarget(target, measurements) {
+ const { drag, model, rects } = store
+ if (
+ !drag ||
+ drag.source.type === 'splitter' ||
+ isSameTarget(drag.target, target)
+ )
+ return
+ const indicator =
+ target &&
+ getDropIndicatorRect(model, rects, target, measurements)
+ store.update({ drag: { ...drag, target, indicator } })
+ },
+
+ endDrag() {
+ const { drag, model } = store
+ if (!drag) return false
+ store.update({ drag: null })
+ if (drag.source.type === 'splitter') {
+ if (model.root === drag.startRoot) return false
+ store.report('resize')
+ return true
+ }
+ const moved =
+ drag.target &&
+ moveNode(model, drag.source, drag.target, createId)
+ return store.commit(moved, 'move')
+ },
+
+ cancelDrag() {
+ const { drag, model } = store
+ if (!drag) return
+ const putBack =
+ drag.source.type === 'splitter' && model.root !== drag.startRoot
+ store.update({
+ drag: null,
+ ...(putBack
+ ? { model: { ...model, root: drag.startRoot } }
+ : {}),
+ })
+ },
+ }
+ return actions
+}
diff --git a/packages/core/src/drop/drop-indicator.ts b/packages/core/src/drop/drop-indicator.ts
new file mode 100644
index 0000000..abfe1e0
--- /dev/null
+++ b/packages/core/src/drop/drop-indicator.ts
@@ -0,0 +1,72 @@
+import type { LayoutRects } from '../geometry/compute-rects'
+import type { DropTarget, LayoutModel, Rect } from '../model/types'
+import { findTabset, findTabsetOfTab } from '../tree/find'
+import type { DropMeasurements } from './drop-target'
+
+const DEFAULT_TAB_GAP = 6
+
+/**
+ * Rect of the drop indicator for a target (ported from v1 drop previews):
+ * half of a tabset or of the layout for side drops, the middle 80% of a
+ * tabset for center drops (the whole strip when folded), and a thin marker
+ * between tabs for tab bar drops.
+ *
+ * @returns The rect, or `null` when the target's geometry is unknown.
+ */
+export function getDropIndicatorRect(
+ model: LayoutModel,
+ rects: LayoutRects,
+ target: DropTarget,
+ measurements?: DropMeasurements
+): Rect | null {
+ if (target.type === 'root') return half(rects.container, target.position)
+
+ if (target.type === 'tabset') {
+ const rect = rects.tabsets.get(target.tabsetId)
+ if (!rect) return null
+ if (target.position !== 'center') return half(rect, target.position)
+ if (findTabset(model.root, target.tabsetId)?.isFolded) return rect
+ return {
+ x: rect.x + rect.width * 0.1,
+ y: rect.y + rect.height * 0.1,
+ width: rect.width * 0.8,
+ height: rect.height * 0.8,
+ }
+ }
+
+ const tabset = findTabsetOfTab(model.root, target.tabId)
+ const tabs = tabset && measurements?.tabBars.get(tabset.id)?.tabs
+ const index = tabs?.findIndex((tab) => tab.id === target.tabId) ?? -1
+ if (!tabs || index < 0) return null
+ const gap =
+ tabs.length > 1
+ ? Math.max(
+ 2,
+ tabs[1].rect.x - (tabs[0].rect.x + tabs[0].rect.width)
+ )
+ : DEFAULT_TAB_GAP
+ const { rect } = tabs[index]
+ return {
+ x:
+ target.position === 'before'
+ ? rect.x - gap + 1
+ : rect.x + rect.width + 1,
+ y: rect.y,
+ width: Math.max(2, gap - 2),
+ height: rect.height,
+ }
+}
+
+function half(rect: Rect, side: 'top' | 'bottom' | 'left' | 'right'): Rect {
+ const { x, y, width, height } = rect
+ switch (side) {
+ case 'left':
+ return { x, y, width: width / 2, height }
+ case 'right':
+ return { x: x + width / 2, y, width: width / 2, height }
+ case 'top':
+ return { x, y, width, height: height / 2 }
+ case 'bottom':
+ return { x, y: y + height / 2, width, height: height / 2 }
+ }
+}
diff --git a/packages/core/src/drop/drop-target.ts b/packages/core/src/drop/drop-target.ts
new file mode 100644
index 0000000..6a8dca9
--- /dev/null
+++ b/packages/core/src/drop/drop-target.ts
@@ -0,0 +1,174 @@
+import type { LayoutRects } from '../geometry/compute-rects'
+import type { DropTarget, LayoutModel, Point, Rect, Side } from '../model/types'
+import { collectTabsets } from '../tree/find'
+import { canMove, type MoveSource } from '../tree/move'
+
+/** A tab bar and its tabs as rendered, in layout-root coordinates. */
+export interface TabBarMeasurement {
+ readonly rect: Rect
+ /** Drawn as a rotated strip (a folded tabset in a side-by-side row). */
+ readonly isRotated?: boolean
+ readonly tabs: readonly { readonly id: string; readonly rect: Rect }[]
+}
+
+/** Tab bar geometry the engine can't know (label widths), keyed by tabset id. */
+export interface DropMeasurements {
+ readonly tabBars: ReadonlyMap
+}
+
+/** Size of the root-edge drop zones (v1 `RootSplitterHoverEl`). */
+export interface RootDropZoneOptions {
+ /** Share of the edge the zone covers. @default 0.25 */
+ readonly length?: number
+ /** Thickness in px. @default 8 */
+ readonly thickness?: number
+}
+
+const SIDES: readonly Side[] = ['left', 'right', 'top', 'bottom']
+
+const contains = (rect: Rect, point: Point) =>
+ point.x >= rect.x &&
+ point.x <= rect.x + rect.width &&
+ point.y >= rect.y &&
+ point.y <= rect.y + rect.height
+
+/**
+ * Rect of the drop zone on one edge of the layout, centred on that edge.
+ * @param container - The layout's container rect (`snapshot.rects.container`).
+ * @param side - The edge.
+ * @param options - Zone length (share of the edge) and thickness.
+ * @returns The zone rect, in layout-root coordinates.
+ * @example
+ * const zone = getRootDropZoneRect(snapshot.rects.container, 'left')
+ */
+export function getRootDropZoneRect(
+ container: Rect,
+ side: Side,
+ options: RootDropZoneOptions = {}
+): Rect {
+ const { length = 0.25, thickness = 8 } = options
+ const { x, y, width, height } = container
+ switch (side) {
+ case 'left':
+ return {
+ x,
+ y: y + (height * (1 - length)) / 2,
+ width: thickness,
+ height: height * length,
+ }
+ case 'right':
+ return {
+ x: x + width - thickness,
+ y: y + (height * (1 - length)) / 2,
+ width: thickness,
+ height: height * length,
+ }
+ case 'top':
+ return {
+ x: x + (width * (1 - length)) / 2,
+ y,
+ width: width * length,
+ height: thickness,
+ }
+ case 'bottom':
+ return {
+ x: x + (width * (1 - length)) / 2,
+ y: y + height - thickness,
+ width: width * length,
+ height: thickness,
+ }
+ }
+}
+
+/** Thirds of a tabset: the outer thirds drop beside it, the middle into it. */
+export function getTabsetDropPosition(
+ rect: Rect,
+ point: Point
+): Side | 'center' {
+ const x = point.x - rect.x
+ const y = point.y - rect.y
+ if (x < rect.width / 3) return 'left'
+ if (x > (2 * rect.width) / 3) return 'right'
+ if (y < rect.height / 3) return 'top'
+ if (y > (2 * rect.height) / 3) return 'bottom'
+ return 'center'
+}
+
+/** The tab a pointer over a tab bar would insert next to. */
+function getTabBarTarget(
+ tabs: readonly { readonly id: string; readonly rect: Rect }[],
+ point: Point
+): DropTarget | null {
+ if (tabs.length === 0) return null
+ const last = tabs[tabs.length - 1]
+ if (point.x > last.rect.x + last.rect.width)
+ return { type: 'tab', tabId: last.id, position: 'after' }
+ if (point.x < tabs[0].rect.x)
+ return { type: 'tab', tabId: tabs[0].id, position: 'before' }
+
+ let closest = tabs[0]
+ let distance = Infinity
+ for (const tab of tabs) {
+ const center = tab.rect.x + tab.rect.width / 2
+ if (Math.abs(point.x - center) < distance) {
+ distance = Math.abs(point.x - center)
+ closest = tab
+ }
+ }
+ const center = closest.rect.x + closest.rect.width / 2
+ return {
+ type: 'tab',
+ tabId: closest.id,
+ position: point.x < center ? 'before' : 'after',
+ }
+}
+
+/**
+ * Where a drag source would land at `point`, or `null` when nothing valid is
+ * under the pointer. Checked in the order the adapters stack their elements:
+ * root-edge zones, then tab bars (a folded strip always means "into it"),
+ * then tabset areas split into thirds.
+ */
+export function getDropTarget(
+ model: LayoutModel,
+ rects: LayoutRects,
+ point: Point,
+ source: MoveSource,
+ measurements?: DropMeasurements,
+ options?: RootDropZoneOptions
+): DropTarget | null {
+ const valid = (target: DropTarget | null) =>
+ target && canMove(model.root, source, target) ? target : null
+
+ for (const side of SIDES) {
+ if (
+ contains(getRootDropZoneRect(rects.container, side, options), point)
+ )
+ return valid({ type: 'root', position: side })
+ }
+
+ const tabsets = collectTabsets(model.root)
+ for (const tabset of tabsets) {
+ const bar = measurements?.tabBars.get(tabset.id)
+ if (!bar || !contains(bar.rect, point)) continue
+ if (bar.isRotated || tabset.isFolded)
+ return valid({
+ type: 'tabset',
+ tabsetId: tabset.id,
+ position: 'center',
+ })
+ return valid(getTabBarTarget(bar.tabs, point))
+ }
+
+ for (const tabset of tabsets) {
+ if (model.maximizedTabsetId && model.maximizedTabsetId !== tabset.id)
+ continue
+ const rect = rects.tabsets.get(tabset.id)
+ if (!rect || !contains(rect, point)) continue
+ const position = tabset.isFolded
+ ? 'center'
+ : getTabsetDropPosition(rect, point)
+ return valid({ type: 'tabset', tabsetId: tabset.id, position })
+ }
+ return null
+}
diff --git a/packages/core/src/drop/drop-targets.ts b/packages/core/src/drop/drop-targets.ts
new file mode 100644
index 0000000..85e7114
--- /dev/null
+++ b/packages/core/src/drop/drop-targets.ts
@@ -0,0 +1,29 @@
+import type { DropTarget, LayoutModel, Side } from '../model/types'
+import { collectTabsets } from '../tree/find'
+import { canMove, type MoveSource } from '../tree/move'
+
+const POSITIONS: readonly (Side | 'center')[] = [
+ 'center',
+ 'left',
+ 'right',
+ 'top',
+ 'bottom',
+]
+const ROOT_SIDES: readonly Side[] = ['left', 'right', 'top', 'bottom']
+
+/**
+ * Every valid target for a source, in reading order: each tabset's center and
+ * sides, then the layout edges. Used by keyboard move mode to cycle targets.
+ */
+export function listDropTargets(
+ model: LayoutModel,
+ source: MoveSource
+): DropTarget[] {
+ const targets: DropTarget[] = []
+ for (const tabset of collectTabsets(model.root)) {
+ for (const position of POSITIONS)
+ targets.push({ type: 'tabset', tabsetId: tabset.id, position })
+ }
+ for (const position of ROOT_SIDES) targets.push({ type: 'root', position })
+ return targets.filter((target) => canMove(model.root, source, target))
+}
diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts
new file mode 100644
index 0000000..3273e36
--- /dev/null
+++ b/packages/core/src/errors.ts
@@ -0,0 +1,46 @@
+/** Codes of errors thrown by the layout. */
+export type DynamixLayoutErrorCode =
+ 'INVALID_LAYOUT' | 'UNSUPPORTED_VERSION' | 'DUPLICATE_TAB_ID'
+
+/**
+ * Thrown for invalid input: malformed saved layouts, unsupported versions,
+ * and duplicate tab ids in development. Actions never throw.
+ * @example
+ * try {
+ * layout.load(JSON.parse(stored))
+ * } catch (error) {
+ * if (error instanceof DynamixLayoutError) console.warn(error.code, error.path)
+ * else throw error
+ * }
+ */
+export class DynamixLayoutError extends Error {
+ readonly code: DynamixLayoutErrorCode
+ /** Where in the input the problem is, e.g. `root.children[1].weight`. */
+ readonly path?: string
+
+ constructor(code: DynamixLayoutErrorCode, message: string, path?: string) {
+ super(path ? `${message} (at ${path})` : message)
+ this.name = 'DynamixLayoutError'
+ this.code = code
+ this.path = path
+ }
+}
+
+/** Codes of non-fatal problems reported through `onWarning`. */
+export type LayoutWarningCode =
+ | 'MIGRATED_FROM_V1'
+ | 'DUPLICATE_TAB_ID'
+ | 'INVALID_ACTIVE_TAB'
+ | 'INVALID_WEIGHT'
+ | 'UNKNOWN_ID'
+ | 'DESTROYED'
+
+/** A non-fatal problem, such as a repaired saved layout. */
+export interface LayoutWarning {
+ readonly code: LayoutWarningCode
+ readonly message: string
+ readonly path?: string
+}
+
+/** Receives warnings. */
+export type WarningHandler = (warning: LayoutWarning) => void
diff --git a/packages/core/src/geometry/compute-rects.ts b/packages/core/src/geometry/compute-rects.ts
new file mode 100644
index 0000000..91ad6e5
--- /dev/null
+++ b/packages/core/src/geometry/compute-rects.ts
@@ -0,0 +1,184 @@
+import { isRow } from '../model/guards'
+import type { Rect, RowNode } from '../model/types'
+import type { GeometryConfig } from './config'
+import { computeMinSizes, requiredRootSize, type MinSize } from './min-size'
+
+/** Every rect of a layout, in layout-root coordinates. */
+export interface LayoutRects {
+ /** Area the layout fills; larger than the container when it can't fit. */
+ readonly container: Rect
+ readonly rows: ReadonlyMap
+ /** As displayed: a maximized tabset gets the whole container. */
+ readonly tabsets: ReadonlyMap
+ readonly splitters: ReadonlyMap
+}
+
+/** Id of the splitter between two adjacent children of a row. */
+export const splitterId = (beforeId: string, afterId: string) =>
+ `${beforeId}~${afterId}`
+
+/** Same keys in the same order, each mapped to the same object. */
+const sameEntries = (
+ a: ReadonlyMap,
+ b: ReadonlyMap
+) => {
+ if (a.size !== b.size) return false
+ const other = b.entries()
+ for (const [id, rect] of a) {
+ const [otherId, otherRect] = other.next().value!
+ if (id !== otherId || rect !== otherRect) return false
+ }
+ return true
+}
+
+const sameRect = (a: Rect | undefined, b: Rect) =>
+ !!a &&
+ a.x === b.x &&
+ a.y === b.y &&
+ a.width === b.width &&
+ a.height === b.height
+
+/**
+ * Positions every row, tabset and splitter. Each row gives its children their
+ * minimum size plus a share of the remaining space proportional to their
+ * weights (folded tabsets get no share). Boundaries are rounded cumulatively,
+ * so every edge is within half a pixel of its exact position and edges outside
+ * a resized pair never move. Ported from v1 `Node.calcDimensions`.
+ *
+ * When the container is smaller than the layout's minimum, the layout grows
+ * past it (and is clipped by the adapter), as in v1.
+ *
+ * @param previous - Rects to reuse by reference when unchanged.
+ */
+export function computeLayoutRects(
+ root: RowNode,
+ container: Rect,
+ config: GeometryConfig,
+ maximizedTabsetId: string | null,
+ previous?: LayoutRects
+): LayoutRects {
+ const minSizes = computeMinSizes(root, config)
+ const required = requiredRootSize(minSizes.get(root.id)!, config)
+ const rootRect: Rect = {
+ x: container.x,
+ y: container.y,
+ width: Math.max(container.width, required.width),
+ height: Math.max(container.height, required.height),
+ }
+
+ const rows = new Map()
+ const tabsets = new Map()
+ const splitters = new Map()
+ const keep = (
+ map: Map,
+ old: ReadonlyMap | undefined,
+ id: string,
+ rect: Rect
+ ) => map.set(id, sameRect(old?.get(id), rect) ? old!.get(id)! : rect)
+
+ const layoutRow = (row: RowNode, rect: Rect) => {
+ keep(rows, previous?.rows, row.id, rect)
+ const horizontal = row.direction === 'horizontal'
+ const rowMin = minSizes.get(row.id)!
+ const extraSpace = horizontal
+ ? rect.width - rowMin.width
+ : rect.height - rowMin.height
+ const isBelowMin = extraSpace < 0
+
+ let totalWeight = 0
+ let lastFlexId: string | undefined
+ let flexCount = 0
+ for (const child of row.children) {
+ if (!isRow(child) && child.isFolded) continue
+ totalWeight += child.weight
+ lastFlexId = child.id
+ flexCount++
+ }
+ // Every open child at weight 0 (each dragged to its minimum): share
+ // equally rather than leaving the space unassigned.
+ const allZero = totalWeight === 0
+ const weightOf = (weight: number) => (allZero ? 1 : weight)
+ if (allZero) totalWeight = flexCount
+
+ let offset = 0
+ let cumulativeWeight = 0
+ let previousBoundary = 0
+ row.children.forEach((child, index) => {
+ const childMin: MinSize = minSizes.get(child.id)!
+ const isFlexible = isRow(child) || !child.isFolded
+ let extra = 0
+ if (!isBelowMin && isFlexible && totalWeight > 0) {
+ cumulativeWeight += weightOf(child.weight)
+ const boundary =
+ child.id === lastFlexId
+ ? extraSpace
+ : Math.round(
+ (extraSpace * cumulativeWeight) / totalWeight
+ )
+ extra = boundary - previousBoundary
+ previousBoundary = boundary
+ }
+
+ const childRect: Rect = horizontal
+ ? {
+ x: rect.x + offset,
+ y: rect.y,
+ width: childMin.width + extra,
+ height: rect.height,
+ }
+ : {
+ x: rect.x,
+ y: rect.y + offset,
+ width: rect.width,
+ height: childMin.height + extra,
+ }
+ offset +=
+ (horizontal ? childRect.width : childRect.height) +
+ config.splitterSize
+
+ if (isRow(child)) layoutRow(child, childRect)
+ else keep(tabsets, previous?.tabsets, child.id, childRect)
+
+ const next = row.children[index + 1]
+ if (next) {
+ const splitter: Rect = horizontal
+ ? {
+ x: childRect.x + childRect.width,
+ y: childRect.y,
+ width: config.splitterSize,
+ height: childRect.height,
+ }
+ : {
+ x: childRect.x,
+ y: childRect.y + childRect.height,
+ width: childRect.width,
+ height: config.splitterSize,
+ }
+ keep(
+ splitters,
+ previous?.splitters,
+ splitterId(child.id, next.id),
+ splitter
+ )
+ }
+ })
+ }
+
+ layoutRow(root, rootRect)
+ if (maximizedTabsetId && tabsets.has(maximizedTabsetId)) {
+ keep(tabsets, previous?.tabsets, maximizedTabsetId, rootRect)
+ }
+
+ const containerRect = sameRect(previous?.container, rootRect)
+ ? previous!.container
+ : rootRect
+ if (
+ previous &&
+ containerRect === previous.container &&
+ sameEntries(rows, previous.rows) &&
+ sameEntries(tabsets, previous.tabsets) &&
+ sameEntries(splitters, previous.splitters)
+ )
+ return previous
+ return { container: containerRect, rows, tabsets, splitters }
+}
diff --git a/packages/core/src/geometry/config.ts b/packages/core/src/geometry/config.ts
new file mode 100644
index 0000000..beb8f97
--- /dev/null
+++ b/packages/core/src/geometry/config.ts
@@ -0,0 +1,16 @@
+/** Sizes the geometry depends on. */
+export interface GeometryConfig {
+ /** Smallest width and height of a tabset. */
+ readonly minPanelSize: { readonly width: number; readonly height: number }
+ /** Thickness of the splitters between siblings. */
+ readonly splitterSize: number
+ /** Size of a folded tabset along its row (usually the tab bar height). */
+ readonly foldedSize: number
+}
+
+/** Default sizes, the same as v1. */
+export const DEFAULT_GEOMETRY: GeometryConfig = {
+ minPanelSize: { width: 40, height: 40 },
+ splitterSize: 10,
+ foldedSize: 40,
+}
diff --git a/packages/core/src/geometry/min-size.ts b/packages/core/src/geometry/min-size.ts
new file mode 100644
index 0000000..7e627f2
--- /dev/null
+++ b/packages/core/src/geometry/min-size.ts
@@ -0,0 +1,101 @@
+import { isRow } from '../model/guards'
+import type { Direction, RowChild, RowNode } from '../model/types'
+import type { GeometryConfig } from './config'
+
+/** Minimum size of a node, plus how many tabsets it stacks along each axis. */
+export interface MinSize {
+ readonly width: number
+ readonly height: number
+ /** Tabsets inside rows of each direction (v1 `tabCnts`). */
+ readonly horizontalCount: number
+ readonly verticalCount: number
+}
+
+/**
+ * Minimum sizes of every row and tabset, keyed by id. A tabset needs
+ * `minPanelSize` (a folded one only `foldedSize` along its row); a row needs
+ * the sum of its children along its direction plus splitters, and the largest
+ * child across it. Ported from v1 `calcTabsetCountAndMinDim`.
+ */
+export function computeMinSizes(
+ root: RowNode,
+ config: GeometryConfig
+): Map {
+ const sizes = new Map()
+ const { width: minWidth, height: minHeight } = config.minPanelSize
+
+ const visit = (node: RowChild, parentDirection: Direction): MinSize => {
+ let size: MinSize
+ if (!isRow(node)) {
+ const alongHorizontal = parentDirection === 'horizontal'
+ size = node.isFolded
+ ? alongHorizontal
+ ? {
+ width: config.foldedSize,
+ height: minHeight,
+ horizontalCount: 0,
+ verticalCount: 0,
+ }
+ : {
+ width: minWidth,
+ height: config.foldedSize,
+ horizontalCount: 0,
+ verticalCount: 0,
+ }
+ : {
+ width: minWidth,
+ height: minHeight,
+ horizontalCount: alongHorizontal ? 1 : 0,
+ verticalCount: alongHorizontal ? 0 : 1,
+ }
+ } else {
+ let width = 0
+ let height = 0
+ let horizontalCount = 0
+ let verticalCount = 0
+ for (const child of node.children) {
+ const childSize = visit(child, node.direction)
+ horizontalCount += childSize.horizontalCount
+ verticalCount += childSize.verticalCount
+ if (node.direction === 'horizontal') {
+ width += childSize.width
+ height = Math.max(height, childSize.height)
+ } else {
+ height += childSize.height
+ width = Math.max(width, childSize.width)
+ }
+ }
+ const splitters = (node.children.length - 1) * config.splitterSize
+ if (node.direction === 'horizontal') width += splitters
+ else height += splitters
+ size = { width, height, horizontalCount, verticalCount }
+ }
+ sizes.set(node.id, size)
+ return size
+ }
+
+ visit(root, root.direction)
+ return sizes
+}
+
+/**
+ * Size the root needs. Like v1 `getReqDimension`, this also requires room for
+ * every tabset counted along each axis.
+ */
+export function requiredRootSize(
+ rootMin: MinSize,
+ config: GeometryConfig
+): { width: number; height: number } {
+ const along = (count: number, size: number) =>
+ count * size + Math.max(0, count - 1) * config.splitterSize
+ return {
+ width: Math.max(
+ rootMin.width,
+ along(rootMin.horizontalCount, config.minPanelSize.width)
+ ),
+ height: Math.max(
+ rootMin.height,
+ along(rootMin.verticalCount, config.minPanelSize.height)
+ ),
+ }
+}
diff --git a/packages/core/src/geometry/splitter.ts b/packages/core/src/geometry/splitter.ts
new file mode 100644
index 0000000..6d4dc26
--- /dev/null
+++ b/packages/core/src/geometry/splitter.ts
@@ -0,0 +1,141 @@
+import { isRow } from '../model/guards'
+import type { Point, RowChild, RowNode } from '../model/types'
+import { replaceNode, withChildren } from '../tree/update'
+import type { GeometryConfig } from './config'
+import { splitterId, type LayoutRects } from './compute-rects'
+import { computeMinSizes } from './min-size'
+
+/** The row and the two children a splitter sits between. */
+export interface SplitterPair {
+ readonly row: RowNode
+ readonly before: RowChild
+ readonly after: RowChild
+}
+
+/** Finds the row and neighbours of a splitter id (`before~after`). */
+export function findSplitterPair(
+ root: RowNode,
+ id: string
+): SplitterPair | null {
+ const visit = (row: RowNode): SplitterPair | null => {
+ for (let i = 0; i < row.children.length; i++) {
+ const child = row.children[i]
+ const next = row.children[i + 1]
+ if (next && splitterId(child.id, next.id) === id)
+ return { row, before: child, after: next }
+ if (isRow(child)) {
+ const found = visit(child)
+ if (found) return found
+ }
+ }
+ return null
+ }
+ return visit(root)
+}
+
+const isFolded = (node: RowChild) => !isRow(node) && node.isFolded
+
+/** Whether a splitter can't move because a neighbour is folded. */
+export function isSplitterLocked(pair: SplitterPair): boolean {
+ return isFolded(pair.before) || isFolded(pair.after)
+}
+
+const rectOf = (rects: LayoutRects, node: RowChild) =>
+ (isRow(node) ? rects.rows : rects.tabsets).get(node.id)
+
+/**
+ * Splitter position and limits along its row, in px: `value` is the size of
+ * the panel before it, `min`/`max` the limits set by both neighbours' minimum
+ * sizes.
+ */
+export function getSplitterBounds(
+ root: RowNode,
+ rects: LayoutRects,
+ config: GeometryConfig,
+ id: string
+): { value: number; min: number; max: number } | null {
+ const pair = findSplitterPair(root, id)
+ if (!pair) return null
+ const before = rectOf(rects, pair.before)
+ const after = rectOf(rects, pair.after)
+ if (!before || !after) return null
+ const minSizes = computeMinSizes(root, config)
+ const horizontal = pair.row.direction === 'horizontal'
+ const size = (rect: { width: number; height: number }) =>
+ horizontal ? rect.width : rect.height
+ const minOf = (node: RowChild) => {
+ const min = minSizes.get(node.id)!
+ return horizontal ? min.width : min.height
+ }
+ return {
+ value: size(before),
+ min: minOf(pair.before),
+ max: size(before) + size(after) - minOf(pair.after),
+ }
+}
+
+/**
+ * Moves a splitter so its centre is at `point`, clamped so both neighbours
+ * keep their minimum size. Only the two neighbours' weights change, split in
+ * proportion to their space above the minimum, so nothing outside the pair
+ * moves. Ported from v1 `updateSliderDimension`.
+ *
+ * @returns The new root, or `null` when the splitter doesn't exist or is locked.
+ */
+export function resizeSplitterWeights(
+ root: RowNode,
+ rects: LayoutRects,
+ config: GeometryConfig,
+ id: string,
+ point: Point
+): RowNode | null {
+ const pair = findSplitterPair(root, id)
+ if (!pair || isSplitterLocked(pair)) return null
+ const beforeRect = rectOf(rects, pair.before)
+ const afterRect = rectOf(rects, pair.after)
+ if (!beforeRect || !afterRect) return null
+
+ const minSizes = computeMinSizes(root, config)
+ const horizontal = pair.row.direction === 'horizontal'
+ const start = horizontal ? beforeRect.x : beforeRect.y
+ const end = horizontal
+ ? afterRect.x + afterRect.width
+ : afterRect.y + afterRect.height
+ const beforeMinSize = minSizes.get(pair.before.id)!
+ const afterMinSize = minSizes.get(pair.after.id)!
+ const beforeMin = horizontal ? beforeMinSize.width : beforeMinSize.height
+ const afterMin = horizontal ? afterMinSize.width : afterMinSize.height
+ const half = config.splitterSize / 2
+
+ const position = horizontal ? point.x : point.y
+ const clamped = Math.max(
+ start + beforeMin + half,
+ Math.min(end - afterMin - half, position)
+ )
+ const beforeSize = clamped - half - start
+ const afterSize = end - (clamped + half)
+
+ const beforeExtra = Math.max(0, beforeSize - beforeMin)
+ const afterExtra = Math.max(0, afterSize - afterMin)
+ const totalExtra = beforeExtra + afterExtra
+ const totalWeight = pair.before.weight + pair.after.weight
+ const beforeWeight =
+ totalExtra > 0
+ ? (beforeExtra / totalExtra) * totalWeight
+ : totalWeight / 2
+ const afterWeight =
+ totalExtra > 0
+ ? (afterExtra / totalExtra) * totalWeight
+ : totalWeight / 2
+
+ const children = pair.row.children.map((child) =>
+ child === pair.before
+ ? { ...child, weight: beforeWeight }
+ : child === pair.after
+ ? { ...child, weight: afterWeight }
+ : child
+ )
+ return replaceNode(root, pair.row.id, () =>
+ withChildren(pair.row, children)
+ )
+}
diff --git a/packages/core/src/geometry/tab-bar.ts b/packages/core/src/geometry/tab-bar.ts
new file mode 100644
index 0000000..525f757
--- /dev/null
+++ b/packages/core/src/geometry/tab-bar.ts
@@ -0,0 +1,57 @@
+import type { Direction, Rect } from '../model/types'
+
+/** Where a tabset's tab bar goes. */
+export interface TabBarPlacement {
+ /** Box to give the tab bar element before `isRotated` is applied. */
+ readonly rect: Rect
+ /**
+ * A folded tabset in a side-by-side row shows its tab bar as a vertical
+ * strip: an element `height` wide and `width` tall, rotated 90° around its
+ * top-left corner and placed at the strip's right edge.
+ */
+ readonly isRotated: boolean
+}
+
+/**
+ * @param tabset - The tabset's rect and state.
+ * @param tabBarHeight - Height of a horizontal tab bar.
+ * @returns The rect to give the tab bar element, and whether to rotate it.
+ * @example
+ * const { rect, isRotated } = getTabBarPlacement(snapshot.rects.tabsets.get(id)!, snapshot.tabsets.get(id)!, 40)
+ * applyRect(tabBar, rect)
+ * tabBar.style.transform = isRotated ? 'rotate(90deg)' : ''
+ */
+export function getTabBarPlacement(
+ tabsetRect: Rect,
+ tabset: { readonly isFolded: boolean; readonly parentDirection: Direction },
+ tabBarHeight: number
+): TabBarPlacement {
+ const { x, y, width, height } = tabsetRect
+ if (tabset.isFolded && tabset.parentDirection === 'horizontal') {
+ return {
+ rect: { x: x + width, y, width: height, height: width },
+ isRotated: true,
+ }
+ }
+ return { rect: { x, y, width, height: tabBarHeight }, isRotated: false }
+}
+
+/**
+ * Area of a tabset below its tab bar; never negative when squeezed.
+ * @param tabsetRect - The tabset's rect.
+ * @param tabBarHeight - Height of its tab bar (0 without one).
+ * @returns The content area.
+ * @example
+ * applyRect(content, getTabContentRect(snapshot.rects.tabsets.get(tab.tabsetId)!, 40))
+ */
+export function getTabContentRect(
+ tabsetRect: Rect,
+ tabBarHeight: number
+): Rect {
+ return {
+ x: tabsetRect.x,
+ y: tabsetRect.y + tabBarHeight,
+ width: tabsetRect.width,
+ height: Math.max(0, tabsetRect.height - tabBarHeight),
+ }
+}
diff --git a/packages/core/src/ids.ts b/packages/core/src/ids.ts
new file mode 100644
index 0000000..b1ccee9
--- /dev/null
+++ b/packages/core/src/ids.ts
@@ -0,0 +1,45 @@
+import type { CreateId } from './model/types'
+
+/** An id factory that also knows which ids are taken. */
+export interface IdGenerator {
+ readonly createId: CreateId
+ /** Marks ids from a loaded layout as taken. */
+ reserve(ids: Iterable): void
+ /** Forgets every id, so rebuilding a layout produces the same ids again. */
+ reset(): void
+}
+
+const PREFIX = { row: 'row', tabset: 'ts' } as const
+
+/**
+ * Deterministic ids: the same calls produce the same ids on the server and
+ * the client. A tabset is named after a tab when a hint is given (`ts-editor`);
+ * otherwise a per-instance counter is used. Taken ids are always skipped.
+ */
+export function createIdGenerator(custom?: CreateId): IdGenerator {
+ const taken = new Set()
+ let counter = 0
+
+ const createId: CreateId = (kind, hint) => {
+ if (custom) {
+ const id = custom(kind, hint)
+ taken.add(id)
+ return id
+ }
+ let id = hint === undefined ? '' : `${PREFIX[kind]}-${hint}`
+ while (id === '' || taken.has(id)) id = `${PREFIX[kind]}-${++counter}`
+ taken.add(id)
+ return id
+ }
+
+ return {
+ createId,
+ reserve(ids) {
+ for (const id of ids) taken.add(id)
+ },
+ reset() {
+ taken.clear()
+ counter = 0
+ },
+ }
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 33546ca..d2333d1 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,54 +1,21 @@
-import { DynamixLayoutCore } from './app/dynamix'
-
/**
- * Self-executing function to log a message to the console when the module is imported.
- * This provides information and a link back to the project for developers.
+ * @dynamix-layout/core: a framework-agnostic docking layout engine.
+ *
+ * Importing this module has no side effects. Everything here is public API;
+ * the tree, geometry and store modules are internal.
*/
-declare const __LICENSE__: string
-;(() => {
- if (typeof window === 'undefined') return
-
- const titleStyle = `color: #03A9F4; font-weight: bold; line-height: 1.2;`
- const textStyle = 'color: #E0E0E0; font-size: 12px;'
- const linkStyle = 'color: #42a5f5; font-size: 12px;'
- const licenseStyle = 'color: #9E9E9E; font-size: 10px;'
-
- const asciiArt = `
-██████╗ ██╗ ██╗ ███╗ ██╗ █████╗ ███╗ ███╗ ██████╗ ██╗ ██╗
-██╔══██╗╚██╗ ██╔╝ ████╗ ██║ ██╔══██╗ ████╗ ████║ ╚═██╔═╝ ╚██╗██╔╝
-██║ ██║ ╚████╔╝ ██╔██╗ ██║ ███████║ ██╔████╔██║ ██║ ╚███╔╝
-██║ ██║ ╚██╔╝ ██║╚██╗██║ ██╔══██║ ██║╚██╔╝██║ ██║ ██╔██╗
-██████╔╝ ██║ ██║ ╚████║ ██║ ██║ ██║ ╚═╝ ██║ ██████╗ ██╔╝ ██╗
-╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
-██╗ █████╗ ██╗ ██╗ ██████╗ ██╗ ██╗████████╗
-██║ ██╔══██╗ ╚██╗ ██╔╝ ██╔═══██╗ ██║ ██║╚══██╔══╝
-██║ ███████║ ╚████╔╝ ██║ ██║ ██║ ██║ ██║
-██║ ██╔══██║ ╚██╔╝ ██║ ██║ ██║ ██║ ██║
-███████╗ ██║ ██║ ██║ ╚██████╔╝ ╚█████╔╝ ██║
-╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚════╝ ╚═╝
- `
+export * from './public-api'
+export {
+ createFrameScheduler,
+ type FrameScheduler,
+} from './dom/frame-scheduler'
- console.groupCollapsed(
- `%cDYNAMIX LAYOUT (click to expand)`,
- 'color: #03A9F4; font-weight: bold; font-size: 14px;'
- )
- console.log(`%c${asciiArt}`, titleStyle)
- console.log('%c A powerful, dynamic layout system 🚀', textStyle)
- console.log(
- '%c ⚡ For docs and more info, \n ⚡ visit: https://github.com/xcode-studio/dynamix-layout',
- linkStyle
- )
- console.log(`%c${__LICENSE__}`, licenseStyle)
- console.groupEnd()
+declare const __VERSION__: string
- if (typeof window !== 'undefined') {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ;(window as any).__DYNAMIX_LAYOUT__ = {
- __LICENSE__: __LICENSE__,
- __LAYOUT__: DynamixLayoutCore,
- }
- }
-})()
-
-export * from './app/index'
-export type * from './type'
+/**
+ * The version of this package, for debugging (replaces v1's console banner).
+ * @example
+ * console.info('dynamix-layout', version)
+ */
+export const version: string =
+ typeof __VERSION__ === 'undefined' ? 'dev' : __VERSION__
diff --git a/packages/core/src/layout-types.ts b/packages/core/src/layout-types.ts
new file mode 100644
index 0000000..96d231b
--- /dev/null
+++ b/packages/core/src/layout-types.ts
@@ -0,0 +1,121 @@
+import type { DropMeasurements } from './drop/drop-target'
+import type { WarningHandler } from './errors'
+import type { CreateId, DropTarget, Point, Rect } from './model/types'
+import type { LayoutJSON, LayoutTreeV1 } from './serialize/schema'
+import type { DragSource, LayoutSnapshot } from './store/snapshot'
+import type { TabInit } from './tree/reconcile-tabs'
+
+/** Why `onLayoutChange` was called. */
+export type LayoutChangeReason =
+ 'move' | 'resize' | 'select' | 'fold' | 'maximize' | 'tabs' | 'reset'
+
+/** Sizes that can change after creation. */
+export interface LayoutSizeOptions {
+ /** Smallest size of a tabset. @default { width: 40, height: 40 } */
+ minPanelSize?: { width: number; height: number }
+ /** Thickness of splitters. @default 10 */
+ splitterSize?: number
+ /** Size of a folded tabset along its row. @default minPanelSize.height */
+ foldedSize?: number
+}
+
+/** Options for `createLayout`. */
+export interface LayoutOptions extends LayoutSizeOptions {
+ /** Open tabs. Their order only matters for the default layout. */
+ tabs: readonly TabInit[]
+ /**
+ * A saved layout (v2, or a v1 tree that is migrated automatically).
+ * @default the default layout built from `tabs`
+ */
+ initialLayout?: LayoutJSON | LayoutTreeV1 | null
+ /**
+ * Id factory for rows and tabsets.
+ * @default deterministic per instance (`ts-`, `row-`)
+ */
+ createId?: CreateId
+ /** Called after every committed change; never for drag frames or `load`. */
+ onLayoutChange?: (layout: LayoutJSON, reason: LayoutChangeReason) => void
+ /** @default `console.warn` in development, silent in production */
+ onWarning?: WarningHandler
+}
+
+/** Splitter position and limits in px along its row. */
+export interface SplitterBounds {
+ /** Size of the panel before the splitter. */
+ readonly value: number
+ readonly min: number
+ readonly max: number
+}
+
+/**
+ * A layout instance. Independent of every other instance; holds no timers and
+ * never touches the DOM. Actions return `true` when they changed the layout and
+ * `false` when they were refused or did nothing; they never throw.
+ */
+export interface Layout {
+ /** The current immutable snapshot. */
+ getSnapshot(): LayoutSnapshot
+ /** Calls `listener` after every change. @returns A function that unsubscribes. */
+ subscribe(listener: (snapshot: LayoutSnapshot) => void): () => void
+ /** Area to fill, in the coordinates every rect uses (`x`/`y` is usually the padding). */
+ setContainerRect(rect: Rect): void
+ /** Replaces the open tabs: unlisted tabs are removed, new ones added and activated. */
+ setTabs(tabs: readonly TabInit[]): void
+ /** Changes sizes without rebuilding anything. */
+ setOptions(options: LayoutSizeOptions): void
+
+ moveTab(tabId: string, target: DropTarget): boolean
+ moveTabset(tabsetId: string, target: DropTarget): boolean
+ selectTab(tabId: string): boolean
+ addTab(tab: TabInit, target?: DropTarget): boolean
+ removeTab(tabId: string): boolean
+
+ /** Moves a splitter so its centre is at `point`, clamped to the neighbours' minimum sizes. */
+ resizeSplitter(splitterId: string, point: Point): boolean
+ /** Moves a splitter by `delta` px along its row (keyboard resizing). */
+ moveSplitterBy(splitterId: string, delta: number): boolean
+
+ maximize(tabsetId: string): boolean
+ restore(): boolean
+ toggleMaximize(tabsetId: string): boolean
+ fold(tabsetId: string): boolean
+ unfold(tabsetId: string): boolean
+ toggleFold(tabsetId: string): boolean
+
+ /** Starts a pointer or keyboard drag. Tab and tabset drags leave maximized mode first. */
+ startDrag(source: DragSource): boolean
+ /** Updates the drop target (tabs) or splitter position (splitters); never calls `onLayoutChange`. */
+ updateDrag(point: Point, measurements?: DropMeasurements): void
+ /** Sets the drop target directly (keyboard move mode). */
+ setDragTarget(
+ target: DropTarget | null,
+ measurements?: DropMeasurements
+ ): void
+ /** Commits the drag with one `onLayoutChange`. @returns Whether the layout changed. */
+ endDrag(): boolean
+ /** Ends the drag and undoes any splitter movement. */
+ cancelDrag(): void
+
+ getDropTarget(
+ point: Point,
+ source: DragSource,
+ measurements?: DropMeasurements
+ ): DropTarget | null
+ getDropIndicatorRect(
+ target: DropTarget,
+ measurements?: DropMeasurements
+ ): Rect | null
+ /** Every valid target for `source`, in reading order. */
+ listDropTargets(source: DragSource): readonly DropTarget[]
+ /** For `aria-valuenow`, `aria-valuemin` and `aria-valuemax`. */
+ getSplitterBounds(splitterId: string): SplitterBounds | null
+
+ /** Replaces the layout (v1 or v2) without calling `onLayoutChange`. */
+ load(layout: LayoutJSON | LayoutTreeV1): void
+ /** Back to `initialLayout`, or the default layout of the current tabs. */
+ reset(): void
+ /** The current layout. The same object is returned until the layout changes. */
+ toJSON(): LayoutJSON
+ /** Removes every listener; later actions are ignored with a warning. */
+ destroy(): void
+}
diff --git a/packages/core/src/model/guards.ts b/packages/core/src/model/guards.ts
new file mode 100644
index 0000000..8f7118c
--- /dev/null
+++ b/packages/core/src/model/guards.ts
@@ -0,0 +1,11 @@
+import type { LayoutNode, RowNode, TabNode, TabsetNode } from './types'
+
+/** @returns `true` when `node` is a row. */
+export const isRow = (node: LayoutNode): node is RowNode => node.type === 'row'
+
+/** @returns `true` when `node` is a tabset. */
+export const isTabset = (node: LayoutNode): node is TabsetNode =>
+ node.type === 'tabset'
+
+/** @returns `true` when `node` is a tab. */
+export const isTab = (node: LayoutNode): node is TabNode => node.type === 'tab'
diff --git a/packages/core/src/model/types.ts b/packages/core/src/model/types.ts
new file mode 100644
index 0000000..793d07f
--- /dev/null
+++ b/packages/core/src/model/types.ts
@@ -0,0 +1,89 @@
+/**
+ * Direction a row lays out its children in.
+ * `'horizontal'` places them side by side; `'vertical'` stacks them.
+ */
+export type Direction = 'horizontal' | 'vertical'
+
+/** A point in layout-root coordinates (the root's top-left corner is 0,0). */
+export interface Point {
+ readonly x: number
+ readonly y: number
+}
+
+/** A rectangle in layout-root coordinates. */
+export interface Rect {
+ readonly x: number
+ readonly y: number
+ readonly width: number
+ readonly height: number
+}
+
+/** A side of a tabset or of the whole layout. */
+export type Side = 'top' | 'bottom' | 'left' | 'right'
+
+/** A tab. Its `id` is the id the application gave the tab. */
+export interface TabNode {
+ readonly type: 'tab'
+ readonly id: string
+}
+
+/** A group of tabs that shows one of them at a time. */
+export interface TabsetNode {
+ readonly type: 'tabset'
+ readonly id: string
+ /**
+ * Share of the parent row's space above the minimum sizes, like CSS
+ * `flex-grow` with `flex-basis` set to the minimum size.
+ */
+ readonly weight: number
+ /** Always the id of one of `children`. */
+ readonly activeTabId: string
+ /** Folded to a strip along its row. */
+ readonly isFolded: boolean
+ /** Never empty. */
+ readonly children: readonly TabNode[]
+}
+
+/** A row of tabsets and rows separated by splitters. */
+export interface RowNode {
+ readonly type: 'row'
+ readonly id: string
+ /** See {@link TabsetNode.weight}. Ignored on the root. */
+ readonly weight: number
+ readonly direction: Direction
+ readonly children: readonly (RowNode | TabsetNode)[]
+}
+
+/** Any node of the layout tree. */
+export type LayoutNode = RowNode | TabsetNode | TabNode
+
+/** A child of a row. */
+export type RowChild = RowNode | TabsetNode
+
+/** The whole layout: the tree plus view state that is not part of any node. */
+export interface LayoutModel {
+ readonly root: RowNode
+ /** Id of the maximized tabset, or `null`. */
+ readonly maximizedTabsetId: string | null
+ /** Most recently focused or used tabset; new tabs go here. */
+ readonly lastActiveTabsetId: string | null
+ /** Fold order, so the most recently folded tabset can be unfolded first. */
+ readonly foldOrder: readonly string[]
+}
+
+/** Generates ids for new rows and tabsets. */
+export type CreateId = (kind: 'row' | 'tabset', hint?: string) => string
+
+/** Where a dragged tab or tabset is dropped. */
+export type DropTarget =
+ | {
+ readonly type: 'tabset'
+ readonly tabsetId: string
+ readonly position: Side | 'center'
+ }
+ | {
+ readonly type: 'tab'
+ readonly tabId: string
+ readonly position: 'before' | 'after'
+ }
+ | { readonly type: 'root'; readonly position: Side }
diff --git a/packages/core/src/public-api.ts b/packages/core/src/public-api.ts
new file mode 100644
index 0000000..dac7f48
--- /dev/null
+++ b/packages/core/src/public-api.ts
@@ -0,0 +1,60 @@
+/** The public API. Internal modules (tree, geometry, store) are not exported. */
+
+export { createLayout } from './create-layout'
+export type {
+ Layout,
+ LayoutChangeReason,
+ LayoutOptions,
+ LayoutSizeOptions,
+ SplitterBounds,
+} from './layout-types'
+export type {
+ CreateId,
+ Direction,
+ DropTarget,
+ LayoutNode,
+ Point,
+ Rect,
+ RowNode,
+ Side,
+ TabNode,
+ TabsetNode,
+} from './model/types'
+export type {
+ DragSource,
+ DragState,
+ LayoutSnapshot,
+ SplitterState,
+ TabState,
+ TabsetState,
+} from './store/snapshot'
+export type { LayoutRects } from './geometry/compute-rects'
+export type { TabInit } from './tree/reconcile-tabs'
+export type {
+ DropMeasurements,
+ RootDropZoneOptions,
+ TabBarMeasurement,
+} from './drop/drop-target'
+export { getRootDropZoneRect } from './drop/drop-target'
+export {
+ getTabBarPlacement,
+ getTabContentRect,
+ type TabBarPlacement,
+} from './geometry/tab-bar'
+export { applyRect } from './dom/apply-rect'
+export {
+ DynamixLayoutError,
+ type DynamixLayoutErrorCode,
+ type LayoutWarning,
+ type LayoutWarningCode,
+ type WarningHandler,
+} from './errors'
+export {
+ LAYOUT_VERSION,
+ type LayoutJSON,
+ type LayoutTreeV1,
+ type RowJSON,
+ type TabJSON,
+ type TabsetJSON,
+} from './serialize/schema'
+export { isLayoutV1, migrateLayoutFromV1 } from './serialize/migrate-v1'
diff --git a/packages/core/src/serialize/from-json.ts b/packages/core/src/serialize/from-json.ts
new file mode 100644
index 0000000..72f863c
--- /dev/null
+++ b/packages/core/src/serialize/from-json.ts
@@ -0,0 +1,151 @@
+import { DynamixLayoutError, type WarningHandler } from '../errors'
+import type {
+ Direction,
+ LayoutModel,
+ RowChild,
+ RowNode,
+ TabsetNode,
+} from '../model/types'
+import { normalizeTree } from '../tree/normalize'
+import { normalizeViewState } from '../tree/view-state'
+import type { LayoutJSON } from './schema'
+
+type Json = Record
+
+const isObject = (value: unknown): value is Json =>
+ typeof value === 'object' && value !== null && !Array.isArray(value)
+
+const fail = (message: string, path: string): never => {
+ throw new DynamixLayoutError('INVALID_LAYOUT', message, path)
+}
+
+const DIRECTIONS: readonly Direction[] = ['horizontal', 'vertical']
+
+/**
+ * Validates a v2 layout and turns it into a normalized model. Problems that
+ * can be repaired (invalid weights or active tabs, duplicate tabs) are
+ * repaired and reported; anything else throws.
+ *
+ * @throws {DynamixLayoutError} `INVALID_LAYOUT` with the path of the problem.
+ */
+export function layoutFromJSON(
+ input: LayoutJSON,
+ onWarning: WarningHandler
+): Pick {
+ const seenNodes = new Set()
+ const seenTabs = new Set()
+
+ const readId = (node: Json, path: string) => {
+ if (typeof node.id !== 'string' || node.id === '')
+ fail('Expected a non-empty string id', `${path}.id`)
+ const id = node.id as string
+ if (seenNodes.has(id)) fail(`Duplicate node id "${id}"`, `${path}.id`)
+ seenNodes.add(id)
+ return id
+ }
+
+ const readWeight = (node: Json, path: string) => {
+ const weight = node.weight
+ if (
+ typeof weight === 'number' &&
+ Number.isFinite(weight) &&
+ weight >= 0
+ )
+ return weight
+ onWarning({
+ code: 'INVALID_WEIGHT',
+ message: `Invalid weight replaced by 100`,
+ path: `${path}.weight`,
+ })
+ return 100
+ }
+
+ const readTabset = (node: Json, path: string): TabsetNode => {
+ const id = readId(node, path)
+ if (!Array.isArray(node.children))
+ fail('Expected a children array', `${path}.children`)
+ const children = (node.children as unknown[]).flatMap((tab, i) => {
+ const tabPath = `${path}.children[${i}]`
+ if (
+ !isObject(tab) ||
+ tab.type !== 'tab' ||
+ typeof tab.id !== 'string' ||
+ tab.id === ''
+ )
+ fail('Expected { type: "tab", id: string }', tabPath)
+ const tabId = (tab as Json).id as string
+ if (seenTabs.has(tabId)) {
+ onWarning({
+ code: 'DUPLICATE_TAB_ID',
+ message: `Duplicate tab "${tabId}" dropped`,
+ path: tabPath,
+ })
+ return []
+ }
+ seenTabs.add(tabId)
+ return [{ type: 'tab' as const, id: tabId }]
+ })
+ const active = node.activeTabId
+ if (active !== undefined && !children.some((tab) => tab.id === active))
+ onWarning({
+ code: 'INVALID_ACTIVE_TAB',
+ message: `Active tab "${String(active)}" is not in the tabset`,
+ path: `${path}.activeTabId`,
+ })
+ return {
+ type: 'tabset',
+ id,
+ weight: readWeight(node, path),
+ activeTabId:
+ typeof active === 'string' ? active : (children[0]?.id ?? ''),
+ isFolded: node.isFolded === true,
+ children,
+ }
+ }
+
+ const readRow = (node: Json, path: string): RowNode => {
+ const id = readId(node, path)
+ if (!DIRECTIONS.includes(node.direction as Direction))
+ fail(
+ 'Expected direction "horizontal" or "vertical"',
+ `${path}.direction`
+ )
+ if (!Array.isArray(node.children))
+ fail('Expected a children array', `${path}.children`)
+ const children = (node.children as unknown[]).map(
+ (child, i): RowChild => {
+ const childPath = `${path}.children[${i}]`
+ if (!isObject(child))
+ return fail('Expected a row or tabset', childPath)
+ if (child.type === 'row') return readRow(child, childPath)
+ if (child.type === 'tabset') return readTabset(child, childPath)
+ return fail(
+ 'Expected type "row" or "tabset"',
+ `${childPath}.type`
+ )
+ }
+ )
+ return {
+ type: 'row',
+ id,
+ weight: readWeight(node, path),
+ direction: node.direction as Direction,
+ children,
+ }
+ }
+
+ if (!isObject(input.root) || input.root.type !== 'row')
+ fail('Expected a root row', 'root')
+ const root = normalizeTree(readRow(input.root as unknown as Json, 'root'))
+ const maximized =
+ typeof input.maximizedTabsetId === 'string'
+ ? input.maximizedTabsetId
+ : null
+ const model = normalizeViewState({
+ root,
+ maximizedTabsetId: maximized,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+ })
+ return { root: model.root, maximizedTabsetId: model.maximizedTabsetId }
+}
diff --git a/packages/core/src/serialize/migrate-v1.ts b/packages/core/src/serialize/migrate-v1.ts
new file mode 100644
index 0000000..266fada
--- /dev/null
+++ b/packages/core/src/serialize/migrate-v1.ts
@@ -0,0 +1,185 @@
+import { DynamixLayoutError, type WarningHandler } from '../errors'
+import type { Direction } from '../model/types'
+import { layoutFromJSON } from './from-json'
+import type { LayoutJSON, LayoutTreeV1, RowJSON, TabsetJSON } from './schema'
+import { toLayoutJSON } from './to-json'
+
+/**
+ * @returns `true` when `input` looks like a layout saved by v1.
+ * @param input - Anything, e.g. parsed JSON from storage.
+ * @example
+ * const saved = JSON.parse(localStorage.getItem('layout') ?? 'null')
+ * const layout = isLayoutV1(saved) ? migrateLayoutFromV1(saved) : saved
+ */
+export function isLayoutV1(input: unknown): input is LayoutTreeV1 {
+ return (
+ typeof input === 'object' &&
+ input !== null &&
+ !('version' in input) &&
+ typeof (input as { typNode?: unknown }).typNode === 'string'
+ )
+}
+
+const isValidWeight = (part: unknown): part is number =>
+ typeof part === 'number' && Number.isFinite(part) && part >= 0
+
+/**
+ * Converts a layout saved by v1 to the v2 format, losslessly:
+ * - a tab's identity was its label (`nodName`), which becomes its `id`; v1
+ * tab `uidNode`s were per-session and are dropped;
+ * - `nodPart` becomes `weight` (same meaning), so the layout renders
+ * pixel-identically;
+ * - directions, implicit in v1 (the root is horizontal and every level flips),
+ * become explicit;
+ * - `nodOpen`, `nodFold` and `nodMaxd` become `activeTabId`, `isFolded` and
+ * `maximizedTabsetId`.
+ *
+ * Damaged input is repaired where possible (see the migration guide):
+ * duplicate tabs keep their first occurrence, unknown active tabs fall back to
+ * the first tab, empty tabsets and rows are removed, stray tabs are wrapped in
+ * a tabset, and the result is normalized.
+ *
+ * @param tree - The v1 layout (`DynamixLayoutCore._root.toJSON()`).
+ * @returns The equivalent v2 layout.
+ * @throws {DynamixLayoutError} `INVALID_LAYOUT` when `tree` is not a v1 layout.
+ * @example
+ * const saved = JSON.parse(localStorage.getItem('layout')!)
+ * const layout = isLayoutV1(saved) ? migrateLayoutFromV1(saved) : saved
+ */
+export function migrateLayoutFromV1(
+ tree: LayoutTreeV1,
+ options: { onWarning?: WarningHandler } = {}
+): LayoutJSON {
+ const warn: WarningHandler = options.onWarning ?? (() => {})
+ const weightOf = (node: LayoutTreeV1, path: string) => {
+ if (isValidWeight(node.nodPart)) return node.nodPart
+ warn({
+ code: 'INVALID_WEIGHT',
+ message: 'Invalid weight replaced by 100',
+ path: `${path}.nodPart`,
+ })
+ return 100
+ }
+ if (!isLayoutV1(tree))
+ throw new DynamixLayoutError('INVALID_LAYOUT', 'Not a v1 layout', '')
+
+ const usedIds = new Set()
+ const collect = (node: LayoutTreeV1) => {
+ if (typeof node.uidNode === 'string') usedIds.add(node.uidNode)
+ node.nodKids?.forEach(collect)
+ }
+ collect(tree)
+ const freshId = (base: string) => {
+ let id = base
+ for (let n = 1; usedIds.has(id); n++) id = `${base}-${n}`
+ usedIds.add(id)
+ return id
+ }
+ const nodeId = (node: LayoutTreeV1, fallback: string) =>
+ typeof node.uidNode === 'string' && node.uidNode !== ''
+ ? node.uidNode
+ : freshId(fallback)
+
+ const seenTabs = new Set()
+ const readTabs = (kids: readonly LayoutTreeV1[], path: string) =>
+ kids.flatMap((kid, i) => {
+ if (kid?.typNode !== 'tab') return []
+ const id = kid.nodName
+ if (typeof id !== 'string' || id === '' || seenTabs.has(id)) {
+ warn({
+ code: 'DUPLICATE_TAB_ID',
+ message: `Duplicate or unnamed tab "${String(id)}" dropped`,
+ path: `${path}.nodKids[${i}]`,
+ })
+ return []
+ }
+ seenTabs.add(id)
+ return [{ type: 'tab' as const, id }]
+ })
+
+ const toTabset = (node: LayoutTreeV1, path: string): TabsetJSON => {
+ const children = readTabs(node.nodKids ?? [], path)
+ const open = typeof node.nodOpen === 'string' ? node.nodOpen : ''
+ const hasOpen = children.some((tab) => tab.id === open)
+ if (open && !hasOpen && children.length > 0)
+ warn({
+ code: 'INVALID_ACTIVE_TAB',
+ message: `Active tab "${open}" is not in the tabset`,
+ path: `${path}.nodOpen`,
+ })
+ return {
+ type: 'tabset',
+ id: nodeId(node, 'ts'),
+ weight: weightOf(node, path),
+ ...(children.length > 0
+ ? { activeTabId: hasOpen ? open : children[0].id }
+ : {}),
+ ...(node.nodFold === true ? { isFolded: true } : {}),
+ children,
+ }
+ }
+
+ const toRow = (
+ node: LayoutTreeV1,
+ depth: number,
+ path: string
+ ): RowJSON => {
+ const direction: Direction = depth % 2 === 0 ? 'horizontal' : 'vertical'
+ const children: RowJSON['children'] = []
+ ;(node.nodKids ?? []).forEach((kid, i) => {
+ const kidPath = `${path}.nodKids[${i}]`
+ if (kid?.typNode === 'row')
+ children.push(toRow(kid, depth + 1, kidPath))
+ else if (kid?.typNode === 'tabset')
+ children.push(toTabset(kid, kidPath))
+ else if (kid?.typNode === 'tab')
+ children.push({
+ ...toTabset(
+ {
+ ...kid,
+ typNode: 'tabset',
+ uidNode: freshId(`ts-${kid.nodName}`),
+ nodKids: [kid],
+ nodOpen: kid.nodName,
+ },
+ kidPath
+ ),
+ })
+ })
+ return {
+ type: 'row',
+ id: nodeId(node, 'row'),
+ weight: weightOf(node, path),
+ direction,
+ children,
+ }
+ }
+
+ const root: RowJSON =
+ tree.typNode === 'row'
+ ? toRow(tree, 0, 'root')
+ : tree.typNode === 'tabset'
+ ? {
+ type: 'row',
+ id: freshId('root'),
+ weight: 100,
+ direction: 'horizontal',
+ children: [toTabset(tree, 'root')],
+ }
+ : (() => {
+ throw new DynamixLayoutError(
+ 'INVALID_LAYOUT',
+ `A v1 layout root must be a row, not "${tree.typNode}"`,
+ 'root'
+ )
+ })()
+
+ const json: LayoutJSON = {
+ version: 2,
+ root,
+ ...(typeof tree.nodMaxd === 'string'
+ ? { maximizedTabsetId: tree.nodMaxd }
+ : {}),
+ }
+ return toLayoutJSON(layoutFromJSON(json, warn))
+}
diff --git a/packages/core/src/serialize/parse.ts b/packages/core/src/serialize/parse.ts
new file mode 100644
index 0000000..693493d
--- /dev/null
+++ b/packages/core/src/serialize/parse.ts
@@ -0,0 +1,44 @@
+import { DynamixLayoutError, type WarningHandler } from '../errors'
+import type { LayoutModel } from '../model/types'
+import { layoutFromJSON } from './from-json'
+import { isLayoutV1, migrateLayoutFromV1 } from './migrate-v1'
+import { LAYOUT_VERSION, type LayoutJSON } from './schema'
+
+const isObject = (value: unknown): value is Record =>
+ typeof value === 'object' && value !== null && !Array.isArray(value)
+
+/**
+ * Accepts a v2 layout or a v1 tree (migrated, with a `MIGRATED_FROM_V1`
+ * warning) and returns a normalized model.
+ *
+ * @throws {DynamixLayoutError} `UNSUPPORTED_VERSION` for other versions,
+ * `INVALID_LAYOUT` for anything malformed.
+ */
+export function parseLayout(
+ input: unknown,
+ onWarning: WarningHandler
+): Pick {
+ if (isLayoutV1(input)) {
+ onWarning({
+ code: 'MIGRATED_FROM_V1',
+ message:
+ 'Loaded a v1 layout; save it again to store the v2 format.',
+ })
+ return layoutFromJSON(
+ migrateLayoutFromV1(input, { onWarning }),
+ onWarning
+ )
+ }
+ if (!isObject(input))
+ throw new DynamixLayoutError(
+ 'INVALID_LAYOUT',
+ 'Expected a layout object'
+ )
+ if (input.version !== LAYOUT_VERSION)
+ throw new DynamixLayoutError(
+ 'UNSUPPORTED_VERSION',
+ `Unsupported layout version ${String(input.version)}; expected ${LAYOUT_VERSION}`,
+ 'version'
+ )
+ return layoutFromJSON(input as unknown as LayoutJSON, onWarning)
+}
diff --git a/packages/core/src/serialize/schema.ts b/packages/core/src/serialize/schema.ts
new file mode 100644
index 0000000..a73947f
--- /dev/null
+++ b/packages/core/src/serialize/schema.ts
@@ -0,0 +1,54 @@
+import type { Direction } from '../model/types'
+
+/** Version written by {@link toLayoutJSON}. */
+export const LAYOUT_VERSION = 2
+
+/** A saved layout. Splitters and pixel sizes are not stored. */
+export interface LayoutJSON {
+ version: 2
+ root: RowJSON
+ /** Id of the maximized tabset, if any. */
+ maximizedTabsetId?: string
+}
+
+/** A saved row. */
+export interface RowJSON {
+ type: 'row'
+ id: string
+ /** Share of the parent's space above the minimum sizes (like CSS flex-grow). */
+ weight: number
+ direction: Direction
+ children: (RowJSON | TabsetJSON)[]
+}
+
+/** A saved tabset. */
+export interface TabsetJSON {
+ type: 'tabset'
+ id: string
+ weight: number
+ activeTabId?: string
+ /** Written only when true. */
+ isFolded?: boolean
+ children: TabJSON[]
+}
+
+/** A saved tab: only the id the application gave it. */
+export interface TabJSON {
+ type: 'tab'
+ id: string
+}
+
+/**
+ * A layout saved by v1 (`DynamixLayoutCore._root.toJSON()`). Accepted
+ * everywhere a {@link LayoutJSON} is, and migrated automatically.
+ */
+export interface LayoutTreeV1 {
+ typNode: 'row' | 'tabset' | 'tab' | 'bond'
+ nodName: string
+ uidNode: string
+ nodPart: number
+ nodOpen?: string | boolean
+ nodKids?: readonly LayoutTreeV1[]
+ nodFold?: boolean
+ nodMaxd?: string
+}
diff --git a/packages/core/src/serialize/to-json.ts b/packages/core/src/serialize/to-json.ts
new file mode 100644
index 0000000..f1278db
--- /dev/null
+++ b/packages/core/src/serialize/to-json.ts
@@ -0,0 +1,38 @@
+import { isRow } from '../model/guards'
+import type { LayoutModel, RowChild, RowNode, TabsetNode } from '../model/types'
+import type { LayoutJSON, RowJSON, TabsetJSON } from './schema'
+
+const tabsetToJSON = (tabset: TabsetNode): TabsetJSON => ({
+ type: 'tabset',
+ id: tabset.id,
+ weight: tabset.weight,
+ activeTabId: tabset.activeTabId,
+ ...(tabset.isFolded ? { isFolded: true } : {}),
+ children: tabset.children.map((tab) => ({ type: 'tab', id: tab.id })),
+})
+
+const rowToJSON = (row: RowNode): RowJSON => ({
+ type: 'row',
+ id: row.id,
+ weight: row.weight,
+ direction: row.direction,
+ children: row.children.map((child: RowChild) =>
+ isRow(child) ? rowToJSON(child) : tabsetToJSON(child)
+ ),
+})
+
+/**
+ * Serializes a model. Weights are kept as they are, so a container resize
+ * never changes the result.
+ */
+export function toLayoutJSON(
+ model: Pick
+): LayoutJSON {
+ return {
+ version: 2,
+ root: rowToJSON(model.root),
+ ...(model.maximizedTabsetId
+ ? { maximizedTabsetId: model.maximizedTabsetId }
+ : {}),
+ }
+}
diff --git a/packages/core/src/store/layout-store.ts b/packages/core/src/store/layout-store.ts
new file mode 100644
index 0000000..6d4b471
--- /dev/null
+++ b/packages/core/src/store/layout-store.ts
@@ -0,0 +1,177 @@
+import type { WarningHandler } from '../errors'
+import { computeLayoutRects, type LayoutRects } from '../geometry/compute-rects'
+import type { GeometryConfig } from '../geometry/config'
+import type { LayoutChangeReason } from '../layout-types'
+import type { DropTarget, LayoutModel, Rect, RowNode } from '../model/types'
+import type { LayoutJSON } from '../serialize/schema'
+import { toLayoutJSON } from '../serialize/to-json'
+import { deriveSnapshot, type DragState, type LayoutSnapshot } from './snapshot'
+
+/** A drag in progress, plus what's needed to cancel it. */
+export interface Drag extends DragState {
+ /** Root when the drag started, restored on cancel. */
+ readonly startRoot: RowNode
+}
+
+/** Everything a layout instance holds, with one place that publishes changes. */
+export interface LayoutStore {
+ readonly model: LayoutModel
+ readonly rects: LayoutRects
+ readonly config: GeometryConfig
+ readonly snapshot: LayoutSnapshot
+ readonly drag: Drag | null
+ readonly isDestroyed: boolean
+ subscribe(listener: (snapshot: LayoutSnapshot) => void): () => void
+ setContainer(rect: Rect): void
+ setConfig(config: GeometryConfig): void
+ /** A transient change (drag frames): published, but not reported as a layout change. */
+ update(change: { model?: LayoutModel; drag?: Drag | null }): void
+ /**
+ * Applies a committed change and reports it through `onLayoutChange`.
+ * @returns Whether anything changed.
+ */
+ commit(next: LayoutModel | null, reason?: LayoutChangeReason): boolean
+ /** Reports the current layout (after a splitter drag already applied its frames). */
+ report(reason: LayoutChangeReason): void
+ toJSON(): LayoutJSON
+ destroy(): void
+}
+
+/** Creates the state holder of one layout instance. */
+export function createLayoutStore(
+ initial: LayoutModel,
+ initialConfig: GeometryConfig,
+ onLayoutChange:
+ ((layout: LayoutJSON, reason: LayoutChangeReason) => void) | undefined,
+ warn: WarningHandler
+): LayoutStore {
+ const listeners = new Set<(snapshot: LayoutSnapshot) => void>()
+ let model = initial
+ let config = initialConfig
+ let container: Rect = { x: 0, y: 0, width: 0, height: 0 }
+ let rects = computeLayoutRects(
+ model.root,
+ container,
+ config,
+ model.maximizedTabsetId
+ )
+ let drag: Drag | null = null
+ let snapshot = deriveSnapshot(model, rects, null)
+ let destroyed = false
+ let json: { model: LayoutModel; value: LayoutJSON } | null = null
+
+ const publish = () => {
+ rects = computeLayoutRects(
+ model.root,
+ container,
+ config,
+ model.maximizedTabsetId,
+ rects
+ )
+ const dragState = sameDrag(snapshot.drag, drag)
+ ? snapshot.drag
+ : drag && toDragState(drag)
+ const next = deriveSnapshot(model, rects, dragState, snapshot)
+ if (next === snapshot) return
+ snapshot = next
+ listeners.forEach((listener) => listener(snapshot))
+ }
+
+ const toJSON = () => {
+ const unchanged =
+ json &&
+ json.model.root === model.root &&
+ json.model.maximizedTabsetId === model.maximizedTabsetId
+ if (!unchanged) json = { model, value: toLayoutJSON(model) }
+ return json!.value
+ }
+
+ return {
+ get model() {
+ return model
+ },
+ get rects() {
+ return rects
+ },
+ get config() {
+ return config
+ },
+ get snapshot() {
+ return snapshot
+ },
+ get drag() {
+ return drag
+ },
+ get isDestroyed() {
+ return destroyed
+ },
+ subscribe(listener) {
+ listeners.add(listener)
+ return () => listeners.delete(listener)
+ },
+ setContainer(rect) {
+ container = rect
+ if (!destroyed) publish()
+ },
+ setConfig(next) {
+ config = next
+ if (!destroyed) publish()
+ },
+ update(change) {
+ if (destroyed) return
+ if (change.model) model = change.model
+ if (change.drag !== undefined) drag = change.drag
+ publish()
+ },
+ commit(next, reason) {
+ if (destroyed) {
+ warn({
+ code: 'DESTROYED',
+ message: 'This layout was destroyed; the call was ignored.',
+ })
+ return false
+ }
+ if (!next || next === model) return false
+ model = next
+ publish()
+ if (reason) onLayoutChange?.(toJSON(), reason)
+ return true
+ },
+ report(reason) {
+ if (!destroyed) onLayoutChange?.(toJSON(), reason)
+ },
+ toJSON,
+ destroy() {
+ destroyed = true
+ drag = null
+ listeners.clear()
+ },
+ }
+}
+
+const toDragState = ({ source, target, indicator }: Drag): DragState => ({
+ source,
+ target,
+ indicator,
+})
+
+function sameDrag(state: DragState | null, drag: Drag | null): boolean {
+ if (!state || !drag) return state === drag
+ return (
+ state.source === drag.source &&
+ state.target === drag.target &&
+ state.indicator === drag.indicator
+ )
+}
+
+/** @returns Whether two drop targets point at the same place. */
+export function isSameTarget(
+ a: DropTarget | null,
+ b: DropTarget | null
+): boolean {
+ if (a === b) return true
+ if (!a || !b || a.type !== b.type || a.position !== b.position) return false
+ if (a.type === 'tab') return a.tabId === (b as typeof a).tabId
+ if (a.type === 'tabset') return a.tabsetId === (b as typeof a).tabsetId
+ return true
+}
diff --git a/packages/core/src/store/snapshot.ts b/packages/core/src/store/snapshot.ts
new file mode 100644
index 0000000..a46dfa5
--- /dev/null
+++ b/packages/core/src/store/snapshot.ts
@@ -0,0 +1,201 @@
+import type { LayoutRects } from '../geometry/compute-rects'
+import { splitterId } from '../geometry/compute-rects'
+import { isRow } from '../model/guards'
+import type {
+ Direction,
+ DropTarget,
+ LayoutModel,
+ Rect,
+ RowNode,
+} from '../model/types'
+import { isDev } from '../dev'
+
+/** Derived state of one tabset. */
+export interface TabsetState {
+ readonly id: string
+ readonly tabIds: readonly string[]
+ readonly activeTabId: string
+ /** Direction of the row this tabset sits in. */
+ readonly parentDirection: Direction
+ readonly isFolded: boolean
+ readonly isMaximized: boolean
+ /** Hidden because another tabset is maximized (its content stays mounted). */
+ readonly isHidden: boolean
+ readonly canFold: boolean
+ readonly canMaximize: boolean
+}
+
+/** Derived state of one splitter. */
+export interface SplitterState {
+ readonly id: string
+ readonly rowId: string
+ /** The row's direction: a `'horizontal'` row has a vertical splitter bar. */
+ readonly direction: Direction
+ readonly beforeId: string
+ readonly afterId: string
+ /** Next to a folded tabset, so it can't be dragged. */
+ readonly isLocked: boolean
+ readonly isHidden: boolean
+}
+
+/** Derived state of one tab. */
+export interface TabState {
+ readonly id: string
+ readonly tabsetId: string
+ readonly isActive: boolean
+ /** Active and its tabset neither folded nor hidden. */
+ readonly isVisible: boolean
+}
+
+/** What is being dragged. */
+export type DragSource =
+ | { readonly type: 'tab'; readonly tabId: string }
+ | { readonly type: 'tabset'; readonly tabsetId: string }
+ | { readonly type: 'splitter'; readonly splitterId: string }
+
+/** The current drag, if any. */
+export interface DragState {
+ readonly source: DragSource
+ /** Where a tab or tabset would land; `null` over nothing valid. */
+ readonly target: DropTarget | null
+ /** Rect of the drop indicator for `target`. */
+ readonly indicator: Rect | null
+}
+
+/**
+ * An immutable view of the layout. Every field keeps its previous reference
+ * unless it changed, and so does every entry of every map, so consumers can
+ * compare by reference at any depth.
+ */
+export interface LayoutSnapshot {
+ readonly root: RowNode
+ readonly tabsets: ReadonlyMap
+ readonly splitters: ReadonlyMap
+ readonly tabs: ReadonlyMap
+ readonly rects: LayoutRects
+ readonly maximizedTabsetId: string | null
+ readonly drag: DragState | null
+}
+
+const shallowEqual = (a: T, b: T) => {
+ for (const key of Object.keys(a) as (keyof T)[]) {
+ const x = a[key]
+ const y = b[key]
+ if (Array.isArray(x) && Array.isArray(y)) {
+ if (x.length !== y.length || x.some((item, i) => item !== y[i]))
+ return false
+ } else if (x !== y) return false
+ }
+ return true
+}
+
+const freeze = (value: T): T => (isDev() ? Object.freeze(value) : value)
+
+/** Reuses `previous` entries (and the map itself) when nothing changed. */
+function share(
+ next: Map,
+ previous: ReadonlyMap | undefined
+): ReadonlyMap {
+ if (!previous) {
+ next.forEach((value) => freeze(value))
+ return next
+ }
+ let changed = next.size !== previous.size
+ const previousKeys = [...previous.keys()]
+ let index = 0
+ for (const [id, value] of next) {
+ const old = previous.get(id)
+ if (old && shallowEqual(old, value)) next.set(id, old)
+ else {
+ freeze(value)
+ changed = true
+ }
+ if (previousKeys[index++] !== id) changed = true
+ }
+ return changed ? next : previous
+}
+
+/** Builds the next snapshot from the model, sharing everything unchanged with `previous`. */
+export function deriveSnapshot(
+ model: LayoutModel,
+ rects: LayoutRects,
+ drag: DragState | null,
+ previous?: LayoutSnapshot
+): LayoutSnapshot {
+ if (
+ previous &&
+ previous.root === model.root &&
+ previous.maximizedTabsetId === model.maximizedTabsetId &&
+ previous.rects === rects &&
+ previous.drag === drag
+ )
+ return previous
+
+ const tabsets = new Map()
+ const splitters = new Map()
+ const tabs = new Map()
+ const maximized = model.maximizedTabsetId
+ let tabsetCount = 0
+ const countTabsets = (row: RowNode) =>
+ row.children.forEach((child) =>
+ isRow(child) ? countTabsets(child) : tabsetCount++
+ )
+ countTabsets(model.root)
+
+ const visit = (row: RowNode) => {
+ row.children.forEach((child, index) => {
+ if (isRow(child)) visit(child)
+ else {
+ const isMaximized = child.id === maximized
+ const isHidden = !!maximized && !isMaximized
+ const isFolded = child.isFolded && !isMaximized
+ tabsets.set(child.id, {
+ id: child.id,
+ tabIds: child.children.map((tab) => tab.id),
+ activeTabId: child.activeTabId,
+ parentDirection: row.direction,
+ isFolded,
+ isMaximized,
+ isHidden,
+ canFold: row.children.length > 1,
+ canMaximize: tabsetCount > 1,
+ })
+ for (const tab of child.children) {
+ const isActive = tab.id === child.activeTabId
+ tabs.set(tab.id, {
+ id: tab.id,
+ tabsetId: child.id,
+ isActive,
+ isVisible: isActive && !isFolded && !isHidden,
+ })
+ }
+ }
+ const next = row.children[index + 1]
+ if (next) {
+ const id = splitterId(child.id, next.id)
+ const folded = (node: typeof child) =>
+ !isRow(node) && node.isFolded
+ splitters.set(id, {
+ id,
+ rowId: row.id,
+ direction: row.direction,
+ beforeId: child.id,
+ afterId: next.id,
+ isLocked: folded(child) || folded(next),
+ isHidden: !!maximized,
+ })
+ }
+ })
+ }
+ visit(model.root)
+
+ return freeze({
+ root: model.root,
+ tabsets: share(tabsets, previous?.tabsets),
+ splitters: share(splitters, previous?.splitters),
+ tabs: share(tabs, previous?.tabs),
+ rects,
+ maximizedTabsetId: maximized,
+ drag,
+ })
+}
diff --git a/packages/core/src/tree/build.ts b/packages/core/src/tree/build.ts
new file mode 100644
index 0000000..a2a5267
--- /dev/null
+++ b/packages/core/src/tree/build.ts
@@ -0,0 +1,48 @@
+import type { CreateId, Direction, RowNode, TabsetNode } from '../model/types'
+import { DEFAULT_WEIGHT } from './normalize'
+
+const flip = (direction: Direction): Direction =>
+ direction === 'horizontal' ? 'vertical' : 'horizontal'
+
+/** A tabset holding a single tab. */
+export function createTabset(tabId: string, createId: CreateId): TabsetNode {
+ return {
+ type: 'tabset',
+ id: createId('tabset', tabId),
+ weight: DEFAULT_WEIGHT,
+ activeTabId: tabId,
+ isFolded: false,
+ children: [{ type: 'tab', id: tabId }],
+ }
+}
+
+/**
+ * The layout used when there is no saved one: each tab gets its own tabset,
+ * nested so that directions alternate (`t1 | (t2 / (t3 | …))`). This matches
+ * v1's default layout exactly.
+ *
+ * @param tabIds - Tabs in display order.
+ * @param createId - Id factory for the rows and tabsets.
+ * @param rootId - Id of the root row.
+ */
+export function buildDefaultTree(
+ tabIds: readonly string[],
+ createId: CreateId,
+ rootId = 'root'
+): RowNode {
+ const build = (
+ ids: readonly string[],
+ direction: Direction,
+ id: string
+ ): RowNode => {
+ const children: RowNode['children'] =
+ ids.length <= 2
+ ? ids.map((tabId) => createTabset(tabId, createId))
+ : [
+ createTabset(ids[0], createId),
+ build(ids.slice(1), flip(direction), createId('row')),
+ ]
+ return { type: 'row', id, weight: DEFAULT_WEIGHT, direction, children }
+ }
+ return build(tabIds, 'horizontal', rootId)
+}
diff --git a/packages/core/src/tree/find.ts b/packages/core/src/tree/find.ts
new file mode 100644
index 0000000..8921503
--- /dev/null
+++ b/packages/core/src/tree/find.ts
@@ -0,0 +1,72 @@
+import { isRow } from '../model/guards'
+import type { RowChild, RowNode, TabsetNode } from '../model/types'
+
+/** Every tabset in reading order (depth first, left to right). */
+export function collectTabsets(root: RowNode): TabsetNode[] {
+ const tabsets: TabsetNode[] = []
+ const visit = (node: RowChild) => {
+ if (isRow(node)) node.children.forEach(visit)
+ else tabsets.push(node)
+ }
+ root.children.forEach(visit)
+ return tabsets
+}
+
+/** Every tab id in reading order. */
+export function collectTabIds(root: RowNode): string[] {
+ return collectTabsets(root).flatMap((tabset) =>
+ tabset.children.map((tab) => tab.id)
+ )
+}
+
+/** Every row and tabset id, including the root's. */
+export function collectNodeIds(root: RowNode): string[] {
+ const ids: string[] = []
+ const visit = (node: RowChild) => {
+ ids.push(node.id)
+ if (isRow(node)) node.children.forEach(visit)
+ }
+ visit(root)
+ return ids
+}
+
+export function findTabset(
+ root: RowNode,
+ tabsetId: string
+): TabsetNode | undefined {
+ return collectTabsets(root).find((tabset) => tabset.id === tabsetId)
+}
+
+export function findTabsetOfTab(
+ root: RowNode,
+ tabId: string
+): TabsetNode | undefined {
+ return collectTabsets(root).find((tabset) =>
+ tabset.children.some((tab) => tab.id === tabId)
+ )
+}
+
+/** The row that directly contains the row or tabset `childId`. */
+export function findParentRow(
+ root: RowNode,
+ childId: string
+): RowNode | undefined {
+ for (const child of root.children) {
+ if (child.id === childId) return root
+ if (isRow(child)) {
+ const found = findParentRow(child, childId)
+ if (found) return found
+ }
+ }
+ return undefined
+}
+
+/** Id of the first tab inside a row or tabset. */
+export function firstTabId(node: RowChild): string | undefined {
+ if (!isRow(node)) return node.children[0]?.id
+ for (const child of node.children) {
+ const id = firstTabId(child)
+ if (id !== undefined) return id
+ }
+ return undefined
+}
diff --git a/packages/core/src/tree/insert.ts b/packages/core/src/tree/insert.ts
new file mode 100644
index 0000000..a4750f8
--- /dev/null
+++ b/packages/core/src/tree/insert.ts
@@ -0,0 +1,124 @@
+import type {
+ CreateId,
+ Direction,
+ RowNode,
+ Side,
+ TabNode,
+ TabsetNode,
+} from '../model/types'
+import { findParentRow, findTabset } from './find'
+import { DEFAULT_WEIGHT } from './normalize'
+import { replaceNode, withChildren } from './update'
+
+const flip = (direction: Direction): Direction =>
+ direction === 'horizontal' ? 'vertical' : 'horizontal'
+
+const isBefore = (side: Side) => side === 'top' || side === 'left'
+
+/** Whether `side` lies along a row of `direction` (left/right in a horizontal row). */
+const isAlong = (side: Side, direction: Direction) =>
+ (side === 'left' || side === 'right') === (direction === 'horizontal')
+
+/**
+ * Inserts tabs into a tabset at `index` and makes `activeTabId` active. The
+ * tabset unfolds, since it just received content.
+ */
+export function insertTabs(
+ root: RowNode,
+ tabsetId: string,
+ tabs: readonly TabNode[],
+ index: number,
+ activeTabId: string
+): RowNode {
+ return replaceNode(root, tabsetId, (node) => {
+ const tabset = node as TabsetNode
+ const children = [...tabset.children]
+ children.splice(index, 0, ...tabs)
+ return { ...tabset, children, activeTabId, isFolded: false }
+ })
+}
+
+/**
+ * Places `moved` on one side of a tabset. Along the tabset's row it becomes a
+ * sibling; across it, the target is wrapped in a new row with `moved`. The new
+ * row takes the target's weight, and the target restarts at the default
+ * weight, as in v1. A folded target unfolds (v1 left the new row stuck at its
+ * minimum size here; audit B27).
+ */
+export function insertBeside(
+ root: RowNode,
+ targetId: string,
+ moved: TabsetNode,
+ side: Side,
+ createId: CreateId
+): RowNode {
+ const parent = findParentRow(root, targetId)
+ const target = findTabset(root, targetId)
+ if (!parent || !target) return root
+
+ if (isAlong(side, parent.direction)) {
+ const index = parent.children.indexOf(target) + (isBefore(side) ? 0 : 1)
+ const children = [...parent.children]
+ children.splice(index, 0, moved)
+ return replaceNode(root, parent.id, () =>
+ withChildren(parent, children)
+ )
+ }
+
+ const resized: TabsetNode = {
+ ...target,
+ weight: DEFAULT_WEIGHT,
+ isFolded: false,
+ }
+ const wrapper: RowNode = {
+ type: 'row',
+ id: createId('row'),
+ weight: target.weight,
+ direction: flip(parent.direction),
+ children: isBefore(side) ? [moved, resized] : [resized, moved],
+ }
+ return replaceNode(root, targetId, () => wrapper)
+}
+
+/**
+ * Docks `moved` at an edge of the whole layout. Along the root's direction it
+ * becomes the first or last child; across it, the current content is wrapped
+ * in a row and the root flips direction.
+ *
+ * The root's `weight` stands in for v1's single child row (see
+ * `normalizeTree`), so the wrapped row reuses it to stay pixel-identical.
+ */
+export function dockAtRoot(
+ root: RowNode,
+ moved: TabsetNode,
+ side: Side,
+ createId: CreateId
+): RowNode {
+ const before = isBefore(side)
+ if (root.children.length === 0) return withChildren(root, [moved])
+
+ if (isAlong(side, root.direction)) {
+ return withChildren(
+ root,
+ before ? [moved, ...root.children] : [...root.children, moved]
+ )
+ }
+
+ const content =
+ root.children.length === 1
+ ? root.children[0]
+ : ({
+ type: 'row',
+ id: createId('row'),
+ weight: root.weight,
+ direction: root.direction,
+ children: root.children,
+ } satisfies RowNode)
+
+ return {
+ ...root,
+ weight: DEFAULT_WEIGHT,
+ direction: flip(root.direction),
+ children: before ? [moved, content] : [content, moved],
+ }
+}
diff --git a/packages/core/src/tree/move.ts b/packages/core/src/tree/move.ts
new file mode 100644
index 0000000..c6300af
--- /dev/null
+++ b/packages/core/src/tree/move.ts
@@ -0,0 +1,155 @@
+import type {
+ CreateId,
+ DropTarget,
+ LayoutModel,
+ RowNode,
+ TabNode,
+ TabsetNode,
+} from '../model/types'
+import { createTabset } from './build'
+import { collectTabsets, findTabset, findTabsetOfTab } from './find'
+import { dockAtRoot, insertBeside, insertTabs } from './insert'
+import { normalizeTree } from './normalize'
+import { removeTab, removeTabset } from './remove'
+import { normalizeViewState } from './view-state'
+
+/** What is being moved. */
+export type MoveSource =
+ | { readonly type: 'tab'; readonly tabId: string }
+ | { readonly type: 'tabset'; readonly tabsetId: string }
+
+/** The tabset a source currently lives in (the tabset itself for a tabset). */
+function sourceTabset(
+ root: RowNode,
+ source: MoveSource
+): TabsetNode | undefined {
+ return source.type === 'tab'
+ ? findTabsetOfTab(root, source.tabId)
+ : findTabset(root, source.tabsetId)
+}
+
+/**
+ * Whether a move is allowed. Mirrors v1's guards (`tree-mutations.ts:17-87`):
+ * nothing moves onto itself, a lone tab can't be dropped on its own tabset,
+ * and the only tabset in the layout can't move.
+ */
+export function canMove(
+ root: RowNode,
+ source: MoveSource,
+ target: DropTarget
+): boolean {
+ const from = sourceTabset(root, source)
+ if (!from) return false
+
+ const tabsets = collectTabsets(root)
+ const isOnlyContent =
+ tabsets.length === 1 &&
+ (source.type === 'tabset' || from.children.length === 1)
+ if (isOnlyContent) return false
+
+ switch (target.type) {
+ case 'root':
+ return true
+ case 'tab': {
+ const targetTabset = findTabsetOfTab(root, target.tabId)
+ if (!targetTabset) return false
+ if (source.type === 'tab') return source.tabId !== target.tabId
+ return targetTabset !== from
+ }
+ case 'tabset': {
+ const targetTabset = findTabset(root, target.tabsetId)
+ if (!targetTabset) return false
+ if (source.type === 'tabset') return targetTabset !== from
+ if (targetTabset !== from) return true
+ if (from.children.length < 2) return false
+ // Center on its own tabset moves the tab to the end; already there is a no-op.
+ return (
+ target.position !== 'center' ||
+ from.children[from.children.length - 1].id !== source.tabId
+ )
+ }
+ }
+}
+
+/**
+ * Moves a tab or a whole tabset. The source is removed first (dissolving rows
+ * left with one child), then inserted at the target:
+ * - `tab` target: into that tab's tabset, before or after it;
+ * - `tabset` + `center`: appended to that tabset (v1 inserted before its last
+ * tab; audit B26);
+ * - `tabset` + side: beside that tabset (see `insertBeside`);
+ * - `root` + side: docked at a layout edge (see `dockAtRoot`).
+ *
+ * Moving ends maximized mode; the moved tabs arrive unfolded and the receiving
+ * tabset becomes the last active one.
+ *
+ * @returns The new model, or `null` when the move isn't allowed.
+ */
+export function moveNode(
+ model: LayoutModel,
+ source: MoveSource,
+ target: DropTarget,
+ createId: CreateId
+): LayoutModel | null {
+ if (!canMove(model.root, source, target)) return null
+ const from = sourceTabset(model.root, source)!
+
+ const tabs: readonly TabNode[] =
+ source.type === 'tab'
+ ? [{ type: 'tab', id: source.tabId }]
+ : from.children
+ const activeTabId =
+ source.type === 'tab' ? source.tabId : tabs[tabs.length - 1].id
+ const asTabset = (): TabsetNode =>
+ source.type === 'tab'
+ ? createTabset(source.tabId, createId)
+ : { ...from, isFolded: false }
+
+ let root =
+ source.type === 'tab'
+ ? removeTab(model.root, source.tabId)
+ : removeTabset(model.root, from.id)
+
+ switch (target.type) {
+ case 'tab': {
+ const tabset = findTabsetOfTab(root, target.tabId)!
+ const index =
+ tabset.children.findIndex((tab) => tab.id === target.tabId) +
+ (target.position === 'after' ? 1 : 0)
+ root = insertTabs(root, tabset.id, tabs, index, activeTabId)
+ break
+ }
+ case 'tabset': {
+ if (target.position === 'center') {
+ const tabset = findTabset(root, target.tabsetId)!
+ root = insertTabs(
+ root,
+ tabset.id,
+ tabs,
+ tabset.children.length,
+ activeTabId
+ )
+ } else {
+ root = insertBeside(
+ root,
+ target.tabsetId,
+ asTabset(),
+ target.position,
+ createId
+ )
+ }
+ break
+ }
+ case 'root':
+ root = dockAtRoot(root, asTabset(), target.position, createId)
+ break
+ }
+
+ root = normalizeTree(root)
+ return normalizeViewState({
+ ...model,
+ root,
+ maximizedTabsetId: null,
+ lastActiveTabsetId: findTabsetOfTab(root, activeTabId)?.id ?? null,
+ })
+}
diff --git a/packages/core/src/tree/normalize.ts b/packages/core/src/tree/normalize.ts
new file mode 100644
index 0000000..33b33e2
--- /dev/null
+++ b/packages/core/src/tree/normalize.ts
@@ -0,0 +1,85 @@
+import { isRow } from '../model/guards'
+import type { RowChild, RowNode, TabsetNode } from '../model/types'
+import { withChildren } from './update'
+
+/** Weight given to new nodes and to nodes with an invalid weight. */
+export const DEFAULT_WEIGHT = 100
+
+const validWeight = (weight: number) =>
+ Number.isFinite(weight) && weight >= 0 ? weight : DEFAULT_WEIGHT
+
+function normalizeTabset(tabset: TabsetNode): TabsetNode | null {
+ if (tabset.children.length === 0) return null
+ const weight = validWeight(tabset.weight)
+ const hasActive = tabset.children.some(
+ (tab) => tab.id === tabset.activeTabId
+ )
+ if (weight === tabset.weight && hasActive) return tabset
+ return {
+ ...tabset,
+ weight,
+ activeTabId: hasActive ? tabset.activeTabId : tabset.children[0].id,
+ }
+}
+
+/**
+ * Children of a dissolved row move up **keeping their own weights**. This is
+ * what v1 does (`moveAdjacentNodeToGrandParent`), and keeping it means saved
+ * v1 layouts and v1 interactions give pixel-identical results.
+ */
+function normalizeRow(row: RowNode): RowNode {
+ const children: RowChild[] = []
+ for (const child of row.children) {
+ if (!isRow(child)) {
+ const tabset = normalizeTabset(child)
+ if (tabset) children.push(tabset)
+ continue
+ }
+ let next: RowChild = normalizeRow(child)
+ if (isRow(next) && next.children.length === 1) next = next.children[0]
+ if (isRow(next) && next.children.length === 0) continue
+ if (isRow(next) && next.direction === row.direction)
+ children.push(...next.children)
+ else children.push(next)
+ }
+ const weight = validWeight(row.weight)
+ const normalized = withChildren(row, children)
+ return weight === row.weight ? normalized : { ...normalized, weight }
+}
+
+/**
+ * Restores the tree invariants after any change:
+ * - tabsets are never empty and their `activeTabId` is one of their tabs;
+ * - non-root rows have at least two children and never share their parent's
+ * direction;
+ * - weights are finite and not negative (0 is valid: a panel dragged to its
+ * minimum has no share of the extra space).
+ *
+ * A root with a single row child takes over that row, which renders
+ * identically. The root's own weight stands for that absorbed row: v1 kept a
+ * horizontal root with one vertical row child there, and the row's weight
+ * matters again if content is later docked beside it. A root that is
+ * horizontal, or has at most one child, is a plain root (weight 100, and
+ * horizontal, since its direction then doesn't matter). Unchanged nodes keep
+ * their identity.
+ */
+export function normalizeTree(root: RowNode): RowNode {
+ let result = normalizeRow(root)
+ while (result.children.length === 1 && isRow(result.children[0])) {
+ const only: RowNode = result.children[0]
+ result = {
+ ...result,
+ weight: only.weight,
+ direction: only.direction,
+ children: only.children,
+ }
+ }
+ const isPlain =
+ result.children.length <= 1 || result.direction === 'horizontal'
+ if (
+ isPlain &&
+ (result.direction !== 'horizontal' || result.weight !== DEFAULT_WEIGHT)
+ )
+ result = { ...result, direction: 'horizontal', weight: DEFAULT_WEIGHT }
+ return result
+}
diff --git a/packages/core/src/tree/reconcile-tabs.ts b/packages/core/src/tree/reconcile-tabs.ts
new file mode 100644
index 0000000..c8ad228
--- /dev/null
+++ b/packages/core/src/tree/reconcile-tabs.ts
@@ -0,0 +1,129 @@
+import type { CreateId, DropTarget, LayoutModel, RowNode } from '../model/types'
+import { createTabset } from './build'
+import {
+ collectTabIds,
+ collectTabsets,
+ findTabset,
+ findTabsetOfTab,
+} from './find'
+import { dockAtRoot, insertBeside, insertTabs } from './insert'
+import { normalizeTree } from './normalize'
+import { removeTab } from './remove'
+import { normalizeViewState } from './view-state'
+
+/** A tab the layout should contain. */
+export interface TabInit {
+ /** Unique, stable id. It's also what saved layouts store. */
+ readonly id: string
+ /** Where the tab goes the first time it appears. @default the last active tabset */
+ readonly target?: DropTarget
+}
+
+/** The tabset new tabs go to: the last active one, else the first. */
+function defaultTabsetId(
+ model: LayoutModel,
+ root: RowNode
+): string | undefined {
+ if (model.lastActiveTabsetId && findTabset(root, model.lastActiveTabsetId))
+ return model.lastActiveTabsetId
+ return collectTabsets(root)[0]?.id
+}
+
+/**
+ * Adds one tab at `target` (or the default tabset). A target that no longer
+ * exists falls back to the default tabset.
+ *
+ * @param activate - Make the new tab the active one in its tabset.
+ */
+export function addTabToModel(
+ model: LayoutModel,
+ tab: TabInit,
+ createId: CreateId,
+ activate = true
+): LayoutModel {
+ if (findTabsetOfTab(model.root, tab.id)) return model
+ const node = { type: 'tab', id: tab.id } as const
+ const target = tab.target
+ let root = model.root
+
+ const appendTo = (tabsetId: string) => {
+ const tabset = findTabset(root, tabsetId)!
+ const activeTabId = activate ? tab.id : tabset.activeTabId
+ return insertTabs(
+ root,
+ tabsetId,
+ [node],
+ tabset.children.length,
+ activeTabId
+ )
+ }
+
+ if (target?.type === 'tab' && findTabsetOfTab(root, target.tabId)) {
+ const tabset = findTabsetOfTab(root, target.tabId)!
+ const index =
+ tabset.children.findIndex((t) => t.id === target.tabId) +
+ (target.position === 'after' ? 1 : 0)
+ root = insertTabs(
+ root,
+ tabset.id,
+ [node],
+ index,
+ activate ? tab.id : tabset.activeTabId
+ )
+ } else if (target?.type === 'tabset' && findTabset(root, target.tabsetId)) {
+ root =
+ target.position === 'center'
+ ? appendTo(target.tabsetId)
+ : insertBeside(
+ root,
+ target.tabsetId,
+ createTabset(tab.id, createId),
+ target.position,
+ createId
+ )
+ } else if (target?.type === 'root' || collectTabsets(root).length === 0) {
+ root = dockAtRoot(
+ root,
+ createTabset(tab.id, createId),
+ target?.type === 'root' ? target.position : 'right',
+ createId
+ )
+ } else {
+ root = appendTo(defaultTabsetId(model, root)!)
+ }
+
+ root = normalizeTree(root)
+ return normalizeViewState({
+ ...model,
+ root,
+ lastActiveTabsetId: activate
+ ? (findTabsetOfTab(root, tab.id)?.id ?? null)
+ : model.lastActiveTabsetId,
+ })
+}
+
+/**
+ * Makes the layout contain exactly `tabs`: tabs that are no longer listed are
+ * removed, and new ones are added at their `target` or the default tabset.
+ * Order of `tabs` doesn't reorder tabs already placed.
+ *
+ * @param activate - Make newly added tabs active (true for runtime additions,
+ * false when filling gaps in a just-loaded layout).
+ */
+export function reconcileTabs(
+ model: LayoutModel,
+ tabs: readonly TabInit[],
+ createId: CreateId,
+ activate = true
+): LayoutModel {
+ const wanted = new Set(tabs.map((tab) => tab.id))
+ let root = model.root
+ for (const id of collectTabIds(root)) {
+ if (!wanted.has(id)) root = removeTab(root, id)
+ }
+ let next =
+ root === model.root ? model : normalizeViewState({ ...model, root })
+
+ for (const tab of tabs) next = addTabToModel(next, tab, createId, activate)
+ return next
+}
diff --git a/packages/core/src/tree/remove.ts b/packages/core/src/tree/remove.ts
new file mode 100644
index 0000000..801b059
--- /dev/null
+++ b/packages/core/src/tree/remove.ts
@@ -0,0 +1,35 @@
+import type { RowNode } from '../model/types'
+import { findTabsetOfTab } from './find'
+import { normalizeTree } from './normalize'
+import { replaceNode } from './update'
+
+/**
+ * Removes a tab. An emptied tabset is removed too, and rows left with one
+ * child are dissolved. If the removed tab was active, the first remaining tab
+ * becomes active.
+ *
+ * @returns The new root, or `root` itself when the tab does not exist.
+ */
+export function removeTab(root: RowNode, tabId: string): RowNode {
+ const tabset = findTabsetOfTab(root, tabId)
+ if (!tabset) return root
+ const children = tabset.children.filter((tab) => tab.id !== tabId)
+ const next = replaceNode(root, tabset.id, () =>
+ children.length === 0
+ ? []
+ : {
+ ...tabset,
+ children,
+ activeTabId:
+ tabset.activeTabId === tabId
+ ? children[0].id
+ : tabset.activeTabId,
+ }
+ )
+ return normalizeTree(next)
+}
+
+/** Removes a whole tabset and dissolves rows left with one child. */
+export function removeTabset(root: RowNode, tabsetId: string): RowNode {
+ return normalizeTree(replaceNode(root, tabsetId, () => []))
+}
diff --git a/packages/core/src/tree/update.ts b/packages/core/src/tree/update.ts
new file mode 100644
index 0000000..874f9c8
--- /dev/null
+++ b/packages/core/src/tree/update.ts
@@ -0,0 +1,45 @@
+import { isRow } from '../model/guards'
+import type { RowChild, RowNode } from '../model/types'
+
+const sameItems = (a: readonly T[], b: readonly T[]) =>
+ a.length === b.length && a.every((item, i) => item === b[i])
+
+/** A copy of `row` with new children, or `row` itself when nothing changed. */
+export function withChildren(
+ row: RowNode,
+ children: readonly RowChild[]
+): RowNode {
+ return sameItems(row.children, children) ? row : { ...row, children }
+}
+
+/**
+ * Replaces the row or tabset `id` with the result of `replace` (an array to
+ * splice several nodes in its place, or an empty array to remove it). Only the
+ * path to the node is copied; every other node keeps its identity.
+ */
+export function replaceNode(
+ root: RowNode,
+ id: string,
+ replace: (node: RowChild) => RowChild | readonly RowChild[]
+): RowNode {
+ if (root.id === id) {
+ const result = replace(root)
+ if (Array.isArray(result) || !isRow(result as RowChild))
+ throw new Error('The root can only be replaced by a row.')
+ return result as RowNode
+ }
+ const visit = (row: RowNode): RowNode => {
+ const children: RowChild[] = []
+ for (const child of row.children) {
+ if (child.id === id) {
+ const result = replace(child)
+ if (Array.isArray(result)) children.push(...result)
+ else children.push(result as RowChild)
+ } else {
+ children.push(isRow(child) ? visit(child) : child)
+ }
+ }
+ return withChildren(row, children)
+ }
+ return visit(root)
+}
diff --git a/packages/core/src/tree/view-state.ts b/packages/core/src/tree/view-state.ts
new file mode 100644
index 0000000..49216b3
--- /dev/null
+++ b/packages/core/src/tree/view-state.ts
@@ -0,0 +1,147 @@
+import { isRow } from '../model/guards'
+import type { LayoutModel, RowChild, RowNode, TabsetNode } from '../model/types'
+import {
+ collectNodeIds,
+ collectTabsets,
+ findParentRow,
+ findTabset,
+} from './find'
+import { replaceNode, withChildren } from './update'
+
+/** Maximize and fold: view state that never changes weights, so both restore exactly. */
+
+const rankOf = (model: LayoutModel, id: string) =>
+ model.foldOrder.indexOf(id) + 1
+
+/** Unfolds the most recently folded child of `children` (other than `exceptId`). */
+function unfoldMostRecent(
+ model: LayoutModel,
+ children: readonly RowChild[],
+ exceptId?: string
+): RowChild[] {
+ let pick: TabsetNode | null = null
+ for (const child of children) {
+ if (child.id === exceptId || isRow(child) || !child.isFolded) continue
+ if (!pick || rankOf(model, child.id) > rankOf(model, pick.id))
+ pick = child
+ }
+ return children.map((child) =>
+ child === pick ? { ...pick, isFolded: false } : child
+ )
+}
+
+const isFoldedChild = (child: RowChild) => !isRow(child) && child.isFolded
+
+/** A tabset can fold when its row has another child to take the space. */
+export function canFold(root: RowNode, tabsetId: string): boolean {
+ const parent = findParentRow(root, tabsetId)
+ return !!parent && parent.children.length > 1
+}
+
+/** Maximizing needs at least two tabsets. */
+export function canMaximize(root: RowNode): boolean {
+ return collectTabsets(root).length > 1
+}
+
+/**
+ * Keeps view state valid after any change: a tabset alone in its row can't
+ * stay folded, every row keeps at least one unfolded child, a maximized
+ * tabset that left the tree is forgotten, and the fold order only lists
+ * existing tabsets.
+ */
+export function normalizeViewState(model: LayoutModel): LayoutModel {
+ const visit = (row: RowNode): RowNode => {
+ let children: readonly RowChild[] = row.children
+ if (children.length > 0 && children.every(isFoldedChild)) {
+ children = unfoldMostRecent(model, children)
+ }
+ children = children.map((child) => {
+ if (isRow(child)) return visit(child)
+ return child.isFolded && children.length < 2
+ ? { ...child, isFolded: false }
+ : child
+ })
+ return withChildren(row, children)
+ }
+
+ const root = visit(model.root)
+ const ids = new Set(collectNodeIds(root))
+ const maximizedTabsetId =
+ model.maximizedTabsetId && findTabset(root, model.maximizedTabsetId)
+ ? model.maximizedTabsetId
+ : null
+ const foldOrder = model.foldOrder.filter((id) => ids.has(id))
+ const lastActiveTabsetId =
+ model.lastActiveTabsetId && findTabset(root, model.lastActiveTabsetId)
+ ? model.lastActiveTabsetId
+ : null
+
+ if (
+ root === model.root &&
+ maximizedTabsetId === model.maximizedTabsetId &&
+ foldOrder.length === model.foldOrder.length &&
+ lastActiveTabsetId === model.lastActiveTabsetId
+ )
+ return model
+ return { root, maximizedTabsetId, foldOrder, lastActiveTabsetId }
+}
+
+/** @returns The new model, or `null` when the tabset can't be maximized. */
+export function maximize(
+ model: LayoutModel,
+ tabsetId: string
+): LayoutModel | null {
+ if (model.maximizedTabsetId === tabsetId) return null
+ if (!findTabset(model.root, tabsetId) || !canMaximize(model.root))
+ return null
+ return { ...model, maximizedTabsetId: tabsetId }
+}
+
+/** @returns The new model, or `null` when nothing is maximized. */
+export function restore(model: LayoutModel): LayoutModel | null {
+ return model.maximizedTabsetId
+ ? { ...model, maximizedTabsetId: null }
+ : null
+}
+
+/**
+ * Folds a tabset to a strip. Folding the last unfolded child of a row
+ * unfolds its most recently folded sibling instead (LeetCode-style swap).
+ * Folding leaves maximized mode.
+ */
+export function fold(model: LayoutModel, tabsetId: string): LayoutModel | null {
+ const tabset = findTabset(model.root, tabsetId)
+ const parent = findParentRow(model.root, tabsetId)
+ if (!tabset || !parent || tabset.isFolded || parent.children.length < 2)
+ return null
+
+ const foldOrder = [
+ ...model.foldOrder.filter((id) => id !== tabsetId),
+ tabsetId,
+ ]
+ const next: LayoutModel = { ...model, foldOrder, maximizedTabsetId: null }
+ let children = parent.children.map((child) =>
+ child === tabset ? { ...tabset, isFolded: true } : child
+ )
+ if (children.every(isFoldedChild))
+ children = unfoldMostRecent(next, children, tabsetId)
+
+ const root = replaceNode(model.root, parent.id, () =>
+ withChildren(parent, children)
+ )
+ return normalizeViewState({ ...next, root })
+}
+
+/** @returns The new model, or `null` when the tabset isn't folded. */
+export function unfold(
+ model: LayoutModel,
+ tabsetId: string
+): LayoutModel | null {
+ const tabset = findTabset(model.root, tabsetId)
+ if (!tabset || !tabset.isFolded) return null
+ const root = replaceNode(model.root, tabsetId, () => ({
+ ...tabset,
+ isFolded: false,
+ }))
+ return normalizeViewState({ ...model, root })
+}
diff --git a/packages/core/src/type/index.ts b/packages/core/src/type/index.ts
deleted file mode 100644
index e53663e..0000000
--- a/packages/core/src/type/index.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Node, Bond } from '../app/dynamix'
-
-/** @deprecated Replaced in 2.0 by `Rect { x, y, width, height }`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export type Dimension = {
- w: number
- h: number
- x: number
- y: number
-}
-
-export interface NodeCache {
- dimMins: Map
- mapDirs: Map
- tabCnts: Map
- mapElem: Map
- nodOpts: ReactiveValue>
- bndOpts: ReactiveValue>
- tabOpts: ReactiveValue>
-}
-
-export type NodeType = 'row' | 'tabset' | 'tab'
-export type NodeTypeWithBond = NodeType | 'bond'
-
-export type TabsIds = Map
-
-export interface BaseNode {
- typNode: NodeTypeWithBond
- nodName: string
- uidNode: string
- nodPart: number
- nodeDir: boolean
- nodDims: Dimension
- nodOpen?: string | boolean
-}
-
-/** @deprecated Replaced in 2.0 by `LayoutJSON` (v1 trees load automatically; `LayoutTreeV1` describes this shape). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export type LayoutTree = Omit & {
- nodKids?: LayoutTree[]
- /** Tabset only: folded to a strip along its parent row. */
- nodFold?: boolean
- /** Root only: uid of the maximized tabset. */
- nodMaxd?: string
-}
-
-/** @deprecated Replaced in 2.0 by `TabsetState`, `SplitterState`, `TabState` and `snapshot.rects`. See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export interface NodeOptions extends BaseNode {
- nodKids?: BaseNode[]
- /** Tabset: folded to a strip (ignored while it is maximized). */
- nodFold?: boolean
- /** Tabset: currently maximized over the whole layout. */
- nodMaxd?: boolean
- /** Hidden because another tabset is maximized (content stays mounted). */
- nodHidden?: boolean
- /** Bond: next to a folded tabset, so it cannot be dragged. */
- nodLocked?: boolean
- /** Tabset: can be folded (it has siblings in its row). */
- nodFoldable?: boolean
- /** Tabset: can be maximized (the layout has more than one tabset). */
- nodMaximizable?: boolean
-}
-
-export interface RootAdjustment {
- adjustedW: number
- adjustedH: number
- wasAdjusted: boolean
-}
-
-export type ChangeListener = (newValue: T) => void
-
-export interface ReactiveValue {
- get: () => T
- set: (newValue: T, flag?: boolean) => void
- onChange: (listener: ChangeListener) => () => void
- nextChange: () => Promise
- onChangePromise: (listener: ChangeListener) => () => void
- triggerChange: () => void
-}
diff --git a/packages/core/test/characterization/fixtures.ts b/packages/core/test/characterization/fixtures.ts
new file mode 100644
index 0000000..fa0d125
--- /dev/null
+++ b/packages/core/test/characterization/fixtures.ts
@@ -0,0 +1,103 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import type { Observation, Scenario } from './scenario'
+
+const FIXTURE_DIR = resolve(__dirname, '../fixtures/v1')
+
+/** All stored v1 scenarios. */
+export function loadScenarios(): Scenario[] {
+ return Array.from({ length: 16 }, (_, i) => {
+ const file = `scenario-${String(i + 1).padStart(2, '0')}.json`
+ return JSON.parse(
+ readFileSync(resolve(FIXTURE_DIR, file), 'utf8')
+ ) as Scenario
+ })
+}
+
+/** Each saved v1 layout with the container size and v1's observation at that moment. */
+export function savedStates(scenario: Scenario) {
+ const states: {
+ saved: unknown
+ container: { width: number; height: number }
+ observation: Observation
+ label: string
+ }[] = [
+ {
+ saved: scenario.initialSavedV1,
+ container: scenario.container,
+ observation: scenario.initial,
+ label: 'initial',
+ },
+ ]
+ let container = scenario.container
+ scenario.steps.forEach((step, i) => {
+ if (step.operation.op === 'resize')
+ container = {
+ width: step.operation.width,
+ height: step.operation.height,
+ }
+ if (step.savedV1)
+ states.push({
+ saved: step.savedV1,
+ container,
+ observation: step.observation,
+ label: `step ${i + 1}`,
+ })
+ })
+ return states
+}
+
+/**
+ * v1 never shrinks the root back after it outgrew a too-small container until
+ * the next window resize (audit B29), so its rects in that state depend on
+ * history that isn't saved. Those states are compared by structure only.
+ */
+export function overflows(
+ observation: Observation,
+ container: { width: number; height: number }
+) {
+ return Object.values(observation.tabsets).some(
+ ([x, y, width, height]) =>
+ x + width > container.width || y + height > container.height
+ )
+}
+
+/**
+ * v1 kept the fold flag when a folded tabset turned into a row (a tab dropped
+ * beside it), leaving that row stuck at its minimum size (audit B27). v2
+ * doesn't reproduce the bug, so these states are compared by structure only.
+ */
+export function hasFoldedRow(saved: unknown): boolean {
+ const visit = (node: {
+ typNode?: string
+ nodFold?: boolean
+ nodKids?: unknown[]
+ }): boolean =>
+ (node.typNode === 'row' && node.nodFold === true) ||
+ (node.nodKids ?? []).some((kid) => visit(kid as typeof node))
+ return visit(saved as Parameters[0])
+}
+
+/**
+ * When every unfolded child of a row has weight 0, v1 skips the row and its
+ * children keep stale positions, sometimes outside the row (audit B28). v2
+ * shares the space equally, so these states are compared by structure only.
+ */
+export function hasZeroWeightRow(saved: unknown): boolean {
+ type V1 = {
+ typNode?: string
+ nodPart?: number
+ nodFold?: boolean
+ nodKids?: V1[]
+ }
+ const visit = (node: V1): boolean => {
+ const kids = node.typNode === 'row' ? (node.nodKids ?? []) : []
+ const open = kids.filter(
+ (kid) => !(kid.typNode === 'tabset' && kid.nodFold)
+ )
+ if (open.length > 0 && open.every((kid) => kid.nodPart === 0))
+ return true
+ return kids.some(visit)
+ }
+ return visit(saved as V1)
+}
diff --git a/packages/core/test/characterization/scenario.ts b/packages/core/test/characterization/scenario.ts
new file mode 100644
index 0000000..895fa93
--- /dev/null
+++ b/packages/core/test/characterization/scenario.ts
@@ -0,0 +1,128 @@
+/**
+ * Engine-neutral description of layout behaviour.
+ *
+ * The scenarios in `../fixtures/v1` were recorded from the v1 engine (see the
+ * README there). The v2 engine replays the same operations and must produce
+ * the same canonical structure and pixel-identical rects. Everything here is
+ * keyed by tab ids so it does not depend on either engine's internal node ids.
+ */
+
+export type Direction = 'horizontal' | 'vertical'
+export type Side = 'top' | 'bottom' | 'left' | 'right'
+export type RectTuple = [x: number, y: number, width: number, height: number]
+
+/** A tabset is addressed by any tab inside it. */
+export type TabsetRef = { tabsetOf: string }
+
+export type Operation =
+ | {
+ op: 'move'
+ source: { tab: string } | TabsetRef
+ target:
+ | { tab: string; position: 'left' | 'right' }
+ | (TabsetRef & { area: Side | 'contain' })
+ | { root: Side }
+ }
+ | {
+ op: 'splitter'
+ before: string
+ after: string
+ point: { x: number; y: number }
+ }
+ | ({ op: 'fold' } & TabsetRef)
+ | ({ op: 'maximize' } & TabsetRef)
+ | { op: 'resize'; width: number; height: number }
+
+export interface CanonicalTabset {
+ tabs: string[]
+ active: string
+ folded?: true
+}
+export interface CanonicalRow {
+ direction: Direction
+ children: CanonicalNode[]
+}
+export type CanonicalNode = CanonicalRow | CanonicalTabset
+
+export interface Observation {
+ tree: CanonicalRow
+ /** Tabset rects keyed by `tabs.join('|')`. */
+ tabsets: Record
+ /** Splitter rects keyed by `firstTab(before) + '>' + firstTab(after)`. */
+ splitters: Record
+ /** Key of the maximized tabset, if any. */
+ maximized: string | null
+}
+
+export interface Step {
+ operation: Operation
+ /** Whether the engine accepted the operation (moves, fold, maximize). */
+ accepted?: boolean
+ observation: Observation
+ /** v1 saved layout after this step (every few steps), for migration checks. */
+ savedV1?: unknown
+}
+
+export interface Scenario {
+ name: string
+ tabs: string[]
+ container: { width: number; height: number }
+ initial: Observation
+ initialSavedV1: unknown
+ steps: Step[]
+}
+
+export interface Driver {
+ create(
+ tabs: string[],
+ container: { width: number; height: number },
+ saved?: unknown
+ ): void
+ apply(operation: Operation): boolean | undefined
+ observe(): Observation
+ save(): unknown
+}
+
+export const isRow = (node: CanonicalNode): node is CanonicalRow =>
+ 'direction' in node
+
+export const firstTab = (node: CanonicalNode): string =>
+ isRow(node) ? firstTab(node.children[0]) : node.tabs[0]
+
+export const tabsetKey = (tabs: readonly string[]) => tabs.join('|')
+
+/**
+ * Flattens single-child rows and same-direction nesting, which v2 does on
+ * every change and v1 sometimes leaves behind (audit B7). Both engines are
+ * compared in this normalized form.
+ */
+export function normalizeCanonical(root: CanonicalRow): CanonicalRow {
+ const normalizeRow = (row: CanonicalRow): CanonicalRow => {
+ const children: CanonicalNode[] = []
+ for (const child of row.children) {
+ if (!isRow(child)) {
+ if (child.tabs.length > 0) children.push(child)
+ continue
+ }
+ let next: CanonicalNode = normalizeRow(child)
+ if (isRow(next) && next.children.length === 1)
+ next = next.children[0]
+ if (isRow(next) && next.children.length === 0) continue
+ if (isRow(next) && next.direction === row.direction) {
+ children.push(...next.children)
+ } else {
+ children.push(next)
+ }
+ }
+ return { direction: row.direction, children }
+ }
+
+ let result = normalizeRow(root)
+ while (result.children.length === 1 && isRow(result.children[0])) {
+ result = result.children[0]
+ }
+ return result
+}
+
+/** Settings the fixtures were recorded with (the v1 React adapter's defaults). */
+export const V1_SETTINGS = { bond: 10, minW: 40, minH: 40, collapsedSize: 40 }
diff --git a/packages/core/test/characterization/v2-driver.ts b/packages/core/test/characterization/v2-driver.ts
new file mode 100644
index 0000000..f50ab08
--- /dev/null
+++ b/packages/core/test/characterization/v2-driver.ts
@@ -0,0 +1,109 @@
+import { createLayout, type Layout } from '../../src/create-layout'
+import type { DropTarget, Side } from '../../src/model/types'
+import type { LayoutTreeV1 } from '../../src/serialize/schema'
+import { findSplitterPair } from '../../src/geometry/splitter'
+import { firstTabId } from '../../src/tree/find'
+import { observeV2 } from './v2-observe'
+import { V1_SETTINGS, type Driver, type Operation } from './scenario'
+
+/**
+ * Drives the v2 `createLayout` through the public API. v1's center drop
+ * inserted the tab before the target's last tab (audit B26); to compare like
+ * with like, `contain` is replayed as exactly that.
+ */
+export function createV2Driver(): Driver {
+ let layout: Layout
+
+ const tabsetOf = (tab: string) =>
+ layout.getSnapshot().tabs.get(tab)?.tabsetId
+
+ const toTarget = (
+ target: Extract['target']
+ ): DropTarget | null => {
+ if ('root' in target) return { type: 'root', position: target.root }
+ if ('tab' in target)
+ return {
+ type: 'tab',
+ tabId: target.tab,
+ position: target.position === 'left' ? 'before' : 'after',
+ }
+ const tabsetId = tabsetOf(target.tabsetOf)
+ if (!tabsetId) return null
+ if (target.area !== 'contain')
+ return { type: 'tabset', tabsetId, position: target.area as Side }
+ const tabIds = layout.getSnapshot().tabsets.get(tabsetId)!.tabIds
+ return {
+ type: 'tab',
+ tabId: tabIds[tabIds.length - 1],
+ position: 'before',
+ }
+ }
+
+ return {
+ create(tabs, size, saved) {
+ layout = createLayout({
+ tabs: tabs.map((id) => ({ id })),
+ initialLayout: (saved as LayoutTreeV1 | undefined) ?? null,
+ minPanelSize: {
+ width: V1_SETTINGS.minW,
+ height: V1_SETTINGS.minH,
+ },
+ splitterSize: V1_SETTINGS.bond,
+ foldedSize: V1_SETTINGS.collapsedSize,
+ onWarning: () => {},
+ })
+ layout.setContainerRect({ x: 0, y: 0, ...size })
+ },
+ apply(operation) {
+ switch (operation.op) {
+ case 'resize':
+ layout.setContainerRect({
+ x: 0,
+ y: 0,
+ width: operation.width,
+ height: operation.height,
+ })
+ return undefined
+ case 'fold':
+ return layout.toggleFold(tabsetOf(operation.tabsetOf)!)
+ case 'maximize':
+ return layout.toggleMaximize(tabsetOf(operation.tabsetOf)!)
+ case 'splitter': {
+ const { root } = layout.getSnapshot()
+ const id = [...layout.getSnapshot().splitters.keys()].find(
+ (key) => {
+ const pair = findSplitterPair(root, key)!
+ return (
+ firstTabId(pair.before) === operation.before &&
+ firstTabId(pair.after) === operation.after
+ )
+ }
+ )
+ if (id) layout.resizeSplitter(id, operation.point)
+ return undefined
+ }
+ case 'move': {
+ const target = toTarget(operation.target)
+ if (!target) return false
+ const { source } = operation
+ return 'tab' in source
+ ? layout.moveTab(source.tab, target)
+ : layout.moveTabset(tabsetOf(source.tabsetOf)!, target)
+ }
+ }
+ },
+ observe() {
+ const snapshot = layout.getSnapshot()
+ return observeV2(
+ {
+ root: snapshot.root,
+ maximizedTabsetId: snapshot.maximizedTabsetId,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+ },
+ snapshot.rects
+ )
+ },
+ save: () => layout.toJSON(),
+ }
+}
diff --git a/packages/core/test/characterization/v2-observe.ts b/packages/core/test/characterization/v2-observe.ts
new file mode 100644
index 0000000..9990fbd
--- /dev/null
+++ b/packages/core/test/characterization/v2-observe.ts
@@ -0,0 +1,63 @@
+import { isRow } from '../../src/model/guards'
+import type {
+ LayoutModel,
+ Rect,
+ RowChild,
+ RowNode,
+} from '../../src/model/types'
+import type { LayoutRects } from '../../src/geometry/compute-rects'
+import { splitterId } from '../../src/geometry/compute-rects'
+import { firstTabId } from '../../src/tree/find'
+import {
+ normalizeCanonical,
+ tabsetKey,
+ type CanonicalNode,
+ type CanonicalRow,
+ type Observation,
+ type RectTuple,
+} from './scenario'
+
+const tuple = (r: Rect): RectTuple => [r.x, r.y, r.width, r.height]
+
+/** Describes a v2 model and its rects in the engine-neutral fixture format. */
+export function observeV2(model: LayoutModel, rects: LayoutRects): Observation {
+ const toCanonical = (node: RowChild): CanonicalNode => {
+ if (isRow(node))
+ return {
+ direction: node.direction,
+ children: node.children.map(toCanonical),
+ }
+ const tabset = {
+ tabs: node.children.map((t) => t.id),
+ active: node.activeTabId,
+ }
+ return node.isFolded ? { ...tabset, folded: true } : tabset
+ }
+
+ const tabsets: Record = {}
+ const splitters: Record = {}
+ let maximized: string | null = null
+ const visit = (row: RowNode) => {
+ row.children.forEach((child, i) => {
+ if (isRow(child)) visit(child)
+ else {
+ const key = tabsetKey(child.children.map((t) => t.id))
+ tabsets[key] = tuple(rects.tabsets.get(child.id)!)
+ if (child.id === model.maximizedTabsetId) maximized = key
+ }
+ const next = row.children[i + 1]
+ if (next)
+ splitters[`${firstTabId(child)}>${firstTabId(next)}`] = tuple(
+ rects.splitters.get(splitterId(child.id, next.id))!
+ )
+ })
+ }
+ visit(model.root)
+
+ return {
+ tree: normalizeCanonical(toCanonical(model.root) as CanonicalRow),
+ tabsets,
+ splitters,
+ maximized,
+ }
+}
diff --git a/packages/core/test/characterization/v2.test.ts b/packages/core/test/characterization/v2.test.ts
new file mode 100644
index 0000000..50f6e2c
--- /dev/null
+++ b/packages/core/test/characterization/v2.test.ts
@@ -0,0 +1,105 @@
+import { describe, it, expect } from 'vitest'
+import { isDeepStrictEqual } from 'node:util'
+import { loadScenarios } from './fixtures'
+import type { Observation, Operation } from './scenario'
+import { createV2Driver } from './v2-driver'
+
+/**
+ * Replays every v1 fixture through the v2 public API. Each step must match
+ * v1 exactly (structure, every rect, accepted or refused), except where v1
+ * hits one of its known bugs. There the engines' states legitimately differ,
+ * so the replay checks the structure still matches and stops that scenario.
+ */
+
+const FOLDED_SIZE = 40
+
+/** Why v1 diverges at this step, or `null` when it should match exactly. */
+function knownV1Bug(
+ operation: Operation,
+ before: Observation,
+ after: Observation,
+ container: { width: number; height: number }
+): string | null {
+ const overflows = Object.values(after.tabsets).some(
+ ([x, y, w, h]) => x + w > container.width || y + h > container.height
+ )
+ if (overflows)
+ return 'B29: v1 keeps a grown root after the container shrank'
+
+ if (
+ operation.op === 'move' &&
+ 'tabsetOf' in operation.target &&
+ operation.target.area !== 'contain'
+ ) {
+ const tab = operation.target.tabsetOf
+ const folded = findTabset(before.tree, tab)?.folded
+ if (folded)
+ return 'B27: splitting beside a folded tabset leaves a row stuck at its minimum'
+ }
+
+ const stale = Object.entries(after.tabsets).some(([key, [, , w, h]]) => {
+ const tabset = findTabset(after.tree, key.split('|')[0])
+ return tabset?.folded && w !== FOLDED_SIZE && h !== FOLDED_SIZE
+ })
+ if (stale)
+ return 'B28: a row whose open children all weigh 0 keeps stale sizes'
+ return null
+}
+
+type Tree = Observation['tree']
+function findTabset(
+ node: Tree | Tree['children'][number],
+ tab: string
+): { folded?: true } | undefined {
+ if ('tabs' in node) return node.tabs.includes(tab) ? node : undefined
+ for (const child of node.children) {
+ const found = findTabset(child, tab)
+ if (found) return found
+ }
+ return undefined
+}
+
+describe('v2 replays the v1 fixtures', () => {
+ const scenarios = loadScenarios()
+ const stops: string[] = []
+ let compared = 0
+
+ it.each(scenarios.map((s) => [s.name, s] as const))('%s', (_, scenario) => {
+ const driver = createV2Driver()
+ driver.create(scenario.tabs, scenario.container)
+ expect(driver.observe(), 'initial').toEqual(scenario.initial)
+
+ let container = scenario.container
+ let before = scenario.initial
+ for (const [i, step] of scenario.steps.entries()) {
+ const { operation } = step
+ if (operation.op === 'resize')
+ container = { width: operation.width, height: operation.height }
+ const accepted = driver.apply(operation)
+ const observed = driver.observe()
+ const label = `step ${i + 1}: ${JSON.stringify(operation)}`
+ if (step.accepted !== undefined)
+ expect(accepted, label).toBe(step.accepted)
+
+ if (!isDeepStrictEqual(observed, step.observation)) {
+ const bug = knownV1Bug(
+ operation,
+ before,
+ step.observation,
+ container
+ )
+ if (!bug) expect(observed, label).toEqual(step.observation)
+ expect(observed.tree, label).toEqual(step.observation.tree)
+ stops.push(`${scenario.name} ${label} → ${bug}`)
+ return
+ }
+ compared++
+ before = step.observation
+ }
+ })
+
+ it('compares most steps exactly', () => {
+ expect(compared).toBeGreaterThan(400)
+ expect(stops.length).toBeLessThanOrEqual(4)
+ })
+})
diff --git a/packages/core/test/drop-preview.test.ts b/packages/core/test/drop-preview.test.ts
deleted file mode 100644
index 73b42c7..0000000
--- a/packages/core/test/drop-preview.test.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import { describe, it, expect, vi, afterEach } from 'vitest'
-import {
- createFrameScheduler,
- getNavbarDropPreview,
- getRootSplitPreview,
- getTabBodyRect,
- getTabsetDropPreview,
- isSameDropPreview,
-} from '../src'
-
-const rect = (left: number, top: number, width: number, height: number) => ({
- left,
- top,
- width,
- height,
- right: left + width,
- bottom: top + height,
-})
-
-describe('getTabsetDropPreview', () => {
- const tabset = rect(100, 50, 300, 600)
-
- it.each([
- ['left', 120, 300, { left: 100, top: 50, width: 150, height: 600 }],
- ['right', 380, 300, { left: 250, top: 50, width: 150, height: 600 }],
- ['top', 250, 100, { left: 100, top: 50, width: 300, height: 300 }],
- ['bottom', 250, 600, { left: 100, top: 350, width: 300, height: 300 }],
- ['contain', 250, 350, { left: 130, top: 110, width: 240, height: 480 }],
- ])('previews the %s zone', (area, x, y, box) => {
- expect(getTabsetDropPreview(tabset, x, y)).toEqual({ area, ...box })
- })
-})
-
-describe('getRootSplitPreview', () => {
- const container = { x: 0, y: 0, w: 1000, h: 800 }
-
- it.each([
- ['left', { left: 0, top: 0, width: 500, height: 800 }],
- ['right', { left: 500, top: 0, width: 500, height: 800 }],
- ['top', { left: 0, top: 0, width: 1000, height: 400 }],
- ['bottom', { left: 0, top: 400, width: 1000, height: 400 }],
- ] as const)('previews the %s half', (side, box) => {
- expect(getRootSplitPreview(container, side)).toEqual({
- area: side,
- ...box,
- })
- })
-})
-
-describe('getNavbarDropPreview', () => {
- const navbar = rect(0, 0, 500, 40)
- // Three tabs, 60px wide, 10px apart.
- const tabs = [rect(8, 5, 60, 30), rect(78, 5, 60, 30), rect(148, 5, 60, 30)]
-
- it('returns null outside the bar vertically or without tabs', () => {
- expect(getNavbarDropPreview(navbar, tabs, 100, 60)).toBeNull()
- expect(getNavbarDropPreview(navbar, [], 100, 20)).toBeNull()
- })
-
- it('marks after the last tab past its right edge', () => {
- expect(getNavbarDropPreview(navbar, tabs, 300, 20)).toEqual({
- index: 2,
- area: 'right',
- left: 209,
- top: 5,
- width: 4,
- height: 30,
- })
- })
-
- it('marks before the first tab left of it', () => {
- expect(getNavbarDropPreview(navbar, tabs, 2, 20)).toEqual({
- index: 0,
- area: 'left',
- left: 3,
- top: 5,
- width: 4,
- height: 30,
- })
- })
-
- it('uses the measured gap between neighbouring tabs', () => {
- expect(getNavbarDropPreview(navbar, tabs, 100, 20)).toEqual({
- index: 1,
- area: 'left',
- left: 69,
- top: 5,
- width: 8,
- height: 30,
- })
- })
-})
-
-describe('isSameDropPreview', () => {
- it('compares area and box', () => {
- const a = {
- area: 'left' as const,
- left: 1,
- top: 2,
- width: 3,
- height: 4,
- }
- expect(isSameDropPreview(a, { ...a })).toBe(true)
- expect(isSameDropPreview(a, { ...a, width: 5 })).toBe(false)
- expect(isSameDropPreview({}, a)).toBe(false)
- })
-})
-
-describe('getTabBodyRect', () => {
- it('sits below the tab bar and never goes negative', () => {
- expect(getTabBodyRect({ x: 10, y: 20, w: 300, h: 200 }, 40)).toEqual({
- x: 10,
- y: 60,
- w: 300,
- h: 160,
- })
- expect(getTabBodyRect({ x: 0, y: 0, w: 100, h: 20 }, 40).h).toBe(0)
- })
-})
-
-describe('createFrameScheduler', () => {
- afterEach(() => vi.unstubAllGlobals())
-
- const stubFrames = () => {
- const callbacks = new Map void>()
- let next = 1
- vi.stubGlobal('requestAnimationFrame', (cb: () => void) => {
- callbacks.set(next, cb)
- return next++
- })
- vi.stubGlobal('cancelAnimationFrame', (id: number) =>
- callbacks.delete(id)
- )
- return () => {
- const pending = [...callbacks.values()]
- callbacks.clear()
- pending.forEach((cb) => cb())
- }
- }
-
- it('runs once per frame with the latest value', () => {
- const runFrame = stubFrames()
- const onFlush = vi.fn()
- const scheduler = createFrameScheduler(onFlush)
-
- scheduler.schedule(1)
- scheduler.schedule(2)
- scheduler.schedule(3)
- runFrame()
-
- expect(onFlush).toHaveBeenCalledTimes(1)
- expect(onFlush).toHaveBeenCalledWith(3)
- })
-
- it('flushes immediately and cancels pending work', () => {
- const runFrame = stubFrames()
- const onFlush = vi.fn()
- const scheduler = createFrameScheduler(onFlush)
-
- scheduler.schedule(1)
- scheduler.flush()
- expect(onFlush).toHaveBeenCalledWith(1)
-
- scheduler.schedule(2)
- scheduler.cancel()
- runFrame()
- expect(onFlush).toHaveBeenCalledTimes(1)
- })
-})
diff --git a/packages/core/test/dynamic.test.ts b/packages/core/test/dynamic.test.ts
deleted file mode 100644
index ebbc055..0000000
--- a/packages/core/test/dynamic.test.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { DynamixLayoutCore, LayoutTree, NodeTypeWithBond } from '../src'
-import { assertNodeTreeIsValid } from './utils/test.utils'
-
-/**
- * Extracts all nodes from a LayoutTree and returns them in a flat array.
- * Each node is represented by its id, name, and type.
- * @param tree The root of the LayoutTree to extract nodes from.
- * @returns An array of objects containing id, name, and type of each node.
- */
-const getNodesFromTree = (
- tree: LayoutTree
-): { id: string; name: string; type: NodeTypeWithBond }[] => {
- const nodes = [
- {
- id: tree.uidNode,
- name: tree.nodName,
- type: tree.typNode,
- },
- ]
- if (tree.nodKids) {
- for (const kid of tree.nodKids) {
- nodes.push(...getNodesFromTree(kid))
- }
- }
- return nodes
-}
-
-/**
- * Test suite for dynamic layout management functionality.
- *
- * This suite tests the Layout class's ability to handle various node configurations
- * and relocation operations. It dynamically generates layout structures with an
- * increasing number of tabs (1 to n), converts each layout to JSON, and then
- * performs extensive node movement tests.
- *
- * For each layout configuration, it:
- * 1. Creates a layout with a subset of tabs
- * 2. Extracts all nodes (tabs, tabsets, etc.) from the layout tree
- * 3. Tests every possible movement combination:
- * - Every draggable node (tabs and tabsets)
- * - Every potential drop target (tabs, tabsets, and root)
- * - Every position (top, bottom, left, right, contain)
- * 4. Validates that each successful movement results in a valid tree structure
- *
- * This comprehensive approach ensures the layout engine correctly handles
- * node relocation under a wide variety of scenarios and maintains tree integrity
- * after each operation.
- */
-
-describe('Dynamic Layout Management Test Suite', () => {
- beforeEach(() => {
- const layout = new DynamixLayoutCore()
- layout.clearAllCache()
- })
-
- describe('Layout Initialization', () => {
- it('should create a valid layout structure from tabs list', () => {
- const tabs = ['dashboard', 'profile', 'settings']
- const layoutInstance = new DynamixLayoutCore({ tabs })
- expect(layoutInstance).toBeInstanceOf(DynamixLayoutCore)
- expect(() =>
- assertNodeTreeIsValid(DynamixLayoutCore._root)
- ).not.toThrow()
- })
- })
-
- describe('Node Relocation Operations', () => {
- const tabs = [
- 'dashboard',
- 'profile',
- 'settings',
- 'analytics',
- 'messages',
- 'notifications',
- // 'activity',
- // 'projects',
- // 'tasks',
- // 'calendar',
- // 'files',
- // 'contacts',
- // 'inbox',
- // 'reports',
- // 'billing',
- // 'support',
- // 'integrations',
- // 'api-keys',
- // 'logs',
- // 'themes',
- // 'permissions',
- // 'account',
- // 'devices',
- // 'security',
- // 'feedback'
- ]
-
- const layouts: LayoutTree[] = []
- const tabsets: string[][] = []
- for (let i = 0; i < tabs.length; i++) {
- const subsetTabs = tabs.slice(0, i + 1)
- tabsets.push(subsetTabs)
- new DynamixLayoutCore({
- tabs: subsetTabs,
- })
- layouts.push(DynamixLayoutCore._root.toJSON())
- }
-
- for (let i = 0; i < layouts.length; i++) {
- const initialLayoutTree = layouts[i]
- const tabset = tabsets[i]
- describe(`Layout with ${tabset.length} Tabs: Node Movement Tests`, () => {
- const nodes = getNodesFromTree(initialLayoutTree)
- const draggableNodes = nodes.filter(
- (n) => n.type === 'tab' || n.type === 'tabset'
- )
- const droppableNodes = nodes.filter(
- (n) =>
- n.type === 'tab' ||
- n.type === 'tabset' ||
- n.id === 'dynamix-layout-root'
- )
- const positions: (
- | 'top'
- | 'bottom'
- | 'left'
- | 'right'
- | 'contain'
- )[] = ['top', 'bottom', 'left', 'right', 'contain']
-
- for (const srcNode of draggableNodes) {
- for (const desNode of droppableNodes) {
- for (const position of positions) {
- if (srcNode.id === desNode.id) continue
-
- it(`should handle ${srcNode.type} '${srcNode.id}' moving to '${position}' of ${desNode.type} '${desNode.id}'`, () => {
- const testInstance = new DynamixLayoutCore({
- tree: initialLayoutTree,
- })
- const moveSucceeded = testInstance.updateTree(
- srcNode.id,
- desNode.id,
- position
- )
-
- if (moveSucceeded) {
- expect(() =>
- assertNodeTreeIsValid(
- DynamixLayoutCore._root
- )
- ).not.toThrow()
- } else {
- expect(moveSucceeded).toBe(false)
- }
- })
- }
- }
- }
- })
- }
- })
-})
diff --git a/packages/core/test/fixtures/v1/README.md b/packages/core/test/fixtures/v1/README.md
new file mode 100644
index 0000000..66ad1e6
--- /dev/null
+++ b/packages/core/test/fixtures/v1/README.md
@@ -0,0 +1,18 @@
+# v1 fixtures
+
+Recorded from the v1 engine (`DynamixLayoutCore`, core 1.x) before it was
+removed. They are the reference v2 is checked against and must not be
+regenerated.
+
+- `scenario-*.json`: 16 seeded scenarios of 30 UI-reachable operations each
+ (moves, splitter drags, fold, maximize, resize), with the canonical tree,
+ every tabset and splitter rect, and the saved v1 layout every few steps.
+ Recorded by `test/characterization/v1.test.ts` and `v1-driver.ts`
+ (commit `dc654bd`).
+- `default-layouts.json`: default layouts of 1–9 tabs at five container
+ sizes.
+- `drop-previews.json`: outputs of the v1 drop-preview helpers for sampled
+ pointer positions.
+
+Replayed by `test/characterization/v2.test.ts`, `test/v2/serialize.test.ts`,
+`test/v2/geometry.test.ts` and `test/v2/drop.test.ts`.
diff --git a/packages/core/test/fixtures/v1/default-layouts.json b/packages/core/test/fixtures/v1/default-layouts.json
new file mode 100644
index 0000000..813bc0c
--- /dev/null
+++ b/packages/core/test/fixtures/v1/default-layouts.json
@@ -0,0 +1,2259 @@
+[
+ {
+ "tabs": ["a"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["a"], "active": "a" }]
+ },
+ "tabsets": { "a": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["a"], "active": "a" }]
+ },
+ "tabsets": { "a": [0, 0, 1357, 911] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["a"], "active": "a" }]
+ },
+ "tabsets": { "a": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["a"], "active": "a" }]
+ },
+ "tabsets": { "a": [0, 0, 300, 200] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["a"], "active": "a" }]
+ },
+ "tabsets": { "a": [0, 0, 97, 61] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ { "tabs": ["b"], "active": "b" }
+ ]
+ },
+ "tabsets": { "a": [0, 0, 595, 800], "b": [605, 0, 595, 800] },
+ "splitters": { "a>b": [595, 0, 10, 800] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ { "tabs": ["b"], "active": "b" }
+ ]
+ },
+ "tabsets": { "a": [0, 0, 674, 911], "b": [684, 0, 673, 911] },
+ "splitters": { "a>b": [674, 0, 10, 911] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ { "tabs": ["b"], "active": "b" }
+ ]
+ },
+ "tabsets": { "a": [0, 0, 315, 420], "b": [325, 0, 315, 420] },
+ "splitters": { "a>b": [315, 0, 10, 420] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ { "tabs": ["b"], "active": "b" }
+ ]
+ },
+ "tabsets": { "a": [0, 0, 145, 200], "b": [155, 0, 145, 200] },
+ "splitters": { "a>b": [145, 0, 10, 200] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ { "tabs": ["b"], "active": "b" }
+ ]
+ },
+ "tabsets": { "a": [0, 0, 44, 61], "b": [54, 0, 43, 61] },
+ "splitters": { "a>b": [44, 0, 10, 61] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ { "tabs": ["c"], "active": "c" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 595, 800],
+ "b": [605, 0, 595, 395],
+ "c": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "a>b": [595, 0, 10, 800],
+ "b>c": [605, 395, 595, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ { "tabs": ["c"], "active": "c" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 674, 911],
+ "b": [684, 0, 673, 451],
+ "c": [684, 461, 673, 450]
+ },
+ "splitters": {
+ "a>b": [674, 0, 10, 911],
+ "b>c": [684, 451, 673, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ { "tabs": ["c"], "active": "c" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 315, 420],
+ "b": [325, 0, 315, 205],
+ "c": [325, 215, 315, 205]
+ },
+ "splitters": {
+ "a>b": [315, 0, 10, 420],
+ "b>c": [325, 205, 315, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ { "tabs": ["c"], "active": "c" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 145, 200],
+ "b": [155, 0, 145, 95],
+ "c": [155, 105, 145, 95]
+ },
+ "splitters": {
+ "a>b": [145, 0, 10, 200],
+ "b>c": [155, 95, 145, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ { "tabs": ["c"], "active": "c" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 44, 90],
+ "b": [54, 0, 43, 40],
+ "c": [54, 50, 43, 40]
+ },
+ "splitters": { "a>b": [44, 0, 10, 90], "b>c": [54, 40, 43, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ { "tabs": ["d"], "active": "d" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 570, 800],
+ "b": [580, 0, 620, 395],
+ "c": [580, 405, 305, 395],
+ "d": [895, 405, 305, 395]
+ },
+ "splitters": {
+ "a>b": [570, 0, 10, 800],
+ "b>c": [580, 395, 620, 10],
+ "c>d": [885, 405, 10, 395]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ { "tabs": ["d"], "active": "d" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 649, 911],
+ "b": [659, 0, 698, 451],
+ "c": [659, 461, 344, 450],
+ "d": [1013, 461, 344, 450]
+ },
+ "splitters": {
+ "a>b": [649, 0, 10, 911],
+ "b>c": [659, 451, 698, 10],
+ "c>d": [1003, 461, 10, 450]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ { "tabs": ["d"], "active": "d" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 290, 420],
+ "b": [300, 0, 340, 205],
+ "c": [300, 215, 165, 205],
+ "d": [475, 215, 165, 205]
+ },
+ "splitters": {
+ "a>b": [290, 0, 10, 420],
+ "b>c": [300, 205, 340, 10],
+ "c>d": [465, 215, 10, 205]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ { "tabs": ["d"], "active": "d" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 120, 200],
+ "b": [130, 0, 170, 95],
+ "c": [130, 105, 80, 95],
+ "d": [220, 105, 80, 95]
+ },
+ "splitters": {
+ "a>b": [120, 0, 10, 200],
+ "b>c": [130, 95, 170, 10],
+ "c>d": [210, 105, 10, 95]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ { "tabs": ["d"], "active": "d" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 90],
+ "b": [50, 0, 90, 40],
+ "c": [50, 50, 40, 40],
+ "d": [100, 50, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 90],
+ "b>c": [50, 40, 90, 10],
+ "c>d": [90, 50, 10, 40]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ { "tabs": ["e"], "active": "e" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 570, 800],
+ "b": [580, 0, 620, 370],
+ "c": [580, 380, 305, 420],
+ "d": [895, 380, 305, 205],
+ "e": [895, 595, 305, 205]
+ },
+ "splitters": {
+ "a>b": [570, 0, 10, 800],
+ "b>c": [580, 370, 620, 10],
+ "c>d": [885, 380, 10, 420],
+ "d>e": [895, 585, 305, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ { "tabs": ["e"], "active": "e" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 649, 911],
+ "b": [659, 0, 698, 426],
+ "c": [659, 436, 344, 475],
+ "d": [1013, 436, 344, 233],
+ "e": [1013, 679, 344, 232]
+ },
+ "splitters": {
+ "a>b": [649, 0, 10, 911],
+ "b>c": [659, 426, 698, 10],
+ "c>d": [1003, 436, 10, 475],
+ "d>e": [1013, 669, 344, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ { "tabs": ["e"], "active": "e" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 290, 420],
+ "b": [300, 0, 340, 180],
+ "c": [300, 190, 165, 230],
+ "d": [475, 190, 165, 110],
+ "e": [475, 310, 165, 110]
+ },
+ "splitters": {
+ "a>b": [290, 0, 10, 420],
+ "b>c": [300, 180, 340, 10],
+ "c>d": [465, 190, 10, 230],
+ "d>e": [475, 300, 165, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ { "tabs": ["e"], "active": "e" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 120, 200],
+ "b": [130, 0, 170, 70],
+ "c": [130, 80, 80, 120],
+ "d": [220, 80, 80, 55],
+ "e": [220, 145, 80, 55]
+ },
+ "splitters": {
+ "a>b": [120, 0, 10, 200],
+ "b>c": [130, 70, 170, 10],
+ "c>d": [210, 80, 10, 120],
+ "d>e": [220, 135, 80, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ { "tabs": ["e"], "active": "e" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 140],
+ "b": [50, 0, 90, 40],
+ "c": [50, 50, 40, 90],
+ "d": [100, 50, 40, 40],
+ "e": [100, 100, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 140],
+ "b>c": [50, 40, 90, 10],
+ "c>d": [90, 50, 10, 90],
+ "d>e": [100, 90, 40, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "tabs": ["f"],
+ "active": "f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 545, 800],
+ "b": [555, 0, 645, 370],
+ "c": [555, 380, 293, 420],
+ "d": [858, 380, 342, 205],
+ "e": [858, 595, 166, 205],
+ "f": [1034, 595, 166, 205]
+ },
+ "splitters": {
+ "a>b": [545, 0, 10, 800],
+ "b>c": [555, 370, 645, 10],
+ "c>d": [848, 380, 10, 420],
+ "d>e": [858, 585, 342, 10],
+ "e>f": [1024, 595, 10, 205]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "tabs": ["f"],
+ "active": "f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 624, 911],
+ "b": [634, 0, 723, 426],
+ "c": [634, 436, 332, 475],
+ "d": [976, 436, 381, 233],
+ "e": [976, 679, 186, 232],
+ "f": [1172, 679, 185, 232]
+ },
+ "splitters": {
+ "a>b": [624, 0, 10, 911],
+ "b>c": [634, 426, 723, 10],
+ "c>d": [966, 436, 10, 475],
+ "d>e": [976, 669, 381, 10],
+ "e>f": [1162, 679, 10, 232]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "tabs": ["f"],
+ "active": "f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 265, 420],
+ "b": [275, 0, 365, 180],
+ "c": [275, 190, 153, 230],
+ "d": [438, 190, 202, 110],
+ "e": [438, 310, 96, 110],
+ "f": [544, 310, 96, 110]
+ },
+ "splitters": {
+ "a>b": [265, 0, 10, 420],
+ "b>c": [275, 180, 365, 10],
+ "c>d": [428, 190, 10, 230],
+ "d>e": [438, 300, 202, 10],
+ "e>f": [534, 310, 10, 110]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "tabs": ["f"],
+ "active": "f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 95, 200],
+ "b": [105, 0, 195, 70],
+ "c": [105, 80, 68, 120],
+ "d": [183, 80, 117, 55],
+ "e": [183, 145, 54, 55],
+ "f": [247, 145, 53, 55]
+ },
+ "splitters": {
+ "a>b": [95, 0, 10, 200],
+ "b>c": [105, 70, 195, 10],
+ "c>d": [173, 80, 10, 120],
+ "d>e": [183, 135, 117, 10],
+ "e>f": [237, 145, 10, 55]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "tabs": ["f"],
+ "active": "f"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 140],
+ "b": [50, 0, 140, 40],
+ "c": [50, 50, 40, 90],
+ "d": [100, 50, 90, 40],
+ "e": [100, 100, 40, 40],
+ "f": [150, 100, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 140],
+ "b>c": [50, 40, 140, 10],
+ "c>d": [90, 50, 10, 90],
+ "d>e": [100, 90, 90, 10],
+ "e>f": [140, 100, 10, 40]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "tabs": ["g"],
+ "active": "g"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 545, 800],
+ "b": [555, 0, 645, 345],
+ "c": [555, 355, 293, 445],
+ "d": [858, 355, 342, 193],
+ "e": [858, 558, 166, 242],
+ "f": [1034, 558, 166, 116],
+ "g": [1034, 684, 166, 116]
+ },
+ "splitters": {
+ "a>b": [545, 0, 10, 800],
+ "b>c": [555, 345, 645, 10],
+ "c>d": [848, 355, 10, 445],
+ "d>e": [858, 548, 342, 10],
+ "e>f": [1024, 558, 10, 242],
+ "f>g": [1034, 674, 166, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "tabs": ["g"],
+ "active": "g"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 624, 911],
+ "b": [634, 0, 723, 401],
+ "c": [634, 411, 332, 500],
+ "d": [976, 411, 381, 220],
+ "e": [976, 641, 186, 270],
+ "f": [1172, 641, 185, 130],
+ "g": [1172, 781, 185, 130]
+ },
+ "splitters": {
+ "a>b": [624, 0, 10, 911],
+ "b>c": [634, 401, 723, 10],
+ "c>d": [966, 411, 10, 500],
+ "d>e": [976, 631, 381, 10],
+ "e>f": [1162, 641, 10, 270],
+ "f>g": [1172, 771, 185, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "tabs": ["g"],
+ "active": "g"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 265, 420],
+ "b": [275, 0, 365, 155],
+ "c": [275, 165, 153, 255],
+ "d": [438, 165, 202, 98],
+ "e": [438, 273, 96, 147],
+ "f": [544, 273, 96, 69],
+ "g": [544, 352, 96, 68]
+ },
+ "splitters": {
+ "a>b": [265, 0, 10, 420],
+ "b>c": [275, 155, 365, 10],
+ "c>d": [428, 165, 10, 255],
+ "d>e": [438, 263, 202, 10],
+ "e>f": [534, 273, 10, 147],
+ "f>g": [544, 342, 96, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "tabs": ["g"],
+ "active": "g"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 95, 200],
+ "b": [105, 0, 195, 45],
+ "c": [105, 55, 68, 145],
+ "d": [183, 55, 117, 43],
+ "e": [183, 108, 54, 92],
+ "f": [247, 108, 53, 41],
+ "g": [247, 159, 53, 41]
+ },
+ "splitters": {
+ "a>b": [95, 0, 10, 200],
+ "b>c": [105, 45, 195, 10],
+ "c>d": [173, 55, 10, 145],
+ "d>e": [183, 98, 117, 10],
+ "e>f": [237, 108, 10, 92],
+ "f>g": [247, 149, 53, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "tabs": ["g"],
+ "active": "g"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 190],
+ "b": [50, 0, 140, 40],
+ "c": [50, 50, 40, 140],
+ "d": [100, 50, 90, 40],
+ "e": [100, 100, 40, 90],
+ "f": [150, 100, 40, 40],
+ "g": [150, 150, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 190],
+ "b>c": [50, 40, 140, 10],
+ "c>d": [90, 50, 10, 140],
+ "d>e": [100, 90, 90, 10],
+ "e>f": [140, 100, 10, 90],
+ "f>g": [150, 140, 40, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 520, 800],
+ "b": [530, 0, 670, 345],
+ "c": [530, 355, 280, 445],
+ "d": [820, 355, 380, 193],
+ "e": [820, 558, 160, 242],
+ "f": [990, 558, 210, 116],
+ "g": [990, 684, 100, 116],
+ "h": [1100, 684, 100, 116]
+ },
+ "splitters": {
+ "a>b": [520, 0, 10, 800],
+ "b>c": [530, 345, 670, 10],
+ "c>d": [810, 355, 10, 445],
+ "d>e": [820, 548, 380, 10],
+ "e>f": [980, 558, 10, 242],
+ "f>g": [990, 674, 210, 10],
+ "g>h": [1090, 684, 10, 116]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 599, 911],
+ "b": [609, 0, 748, 401],
+ "c": [609, 411, 319, 500],
+ "d": [938, 411, 419, 220],
+ "e": [938, 641, 180, 270],
+ "f": [1128, 641, 229, 130],
+ "g": [1128, 781, 110, 130],
+ "h": [1248, 781, 109, 130]
+ },
+ "splitters": {
+ "a>b": [599, 0, 10, 911],
+ "b>c": [609, 401, 748, 10],
+ "c>d": [928, 411, 10, 500],
+ "d>e": [938, 631, 419, 10],
+ "e>f": [1118, 641, 10, 270],
+ "f>g": [1128, 771, 229, 10],
+ "g>h": [1238, 781, 10, 130]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 240, 420],
+ "b": [250, 0, 390, 155],
+ "c": [250, 165, 140, 255],
+ "d": [400, 165, 240, 98],
+ "e": [400, 273, 90, 147],
+ "f": [500, 273, 140, 69],
+ "g": [500, 352, 65, 68],
+ "h": [575, 352, 65, 68]
+ },
+ "splitters": {
+ "a>b": [240, 0, 10, 420],
+ "b>c": [250, 155, 390, 10],
+ "c>d": [390, 165, 10, 255],
+ "d>e": [400, 263, 240, 10],
+ "e>f": [490, 273, 10, 147],
+ "f>g": [500, 342, 140, 10],
+ "g>h": [565, 352, 10, 68]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 70, 200],
+ "b": [80, 0, 220, 45],
+ "c": [80, 55, 55, 145],
+ "d": [145, 55, 155, 43],
+ "e": [145, 108, 48, 92],
+ "f": [203, 108, 97, 41],
+ "g": [203, 159, 44, 41],
+ "h": [257, 159, 43, 41]
+ },
+ "splitters": {
+ "a>b": [70, 0, 10, 200],
+ "b>c": [80, 45, 220, 10],
+ "c>d": [135, 55, 10, 145],
+ "d>e": [145, 98, 155, 10],
+ "e>f": [193, 108, 10, 92],
+ "f>g": [203, 149, 97, 10],
+ "g>h": [247, 159, 10, 41]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 190],
+ "b": [50, 0, 190, 40],
+ "c": [50, 50, 40, 140],
+ "d": [100, 50, 140, 40],
+ "e": [100, 100, 40, 90],
+ "f": [150, 100, 90, 40],
+ "g": [150, 150, 40, 40],
+ "h": [200, 150, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 190],
+ "b>c": [50, 40, 190, 10],
+ "c>d": [90, 50, 10, 140],
+ "d>e": [100, 90, 140, 10],
+ "e>f": [140, 100, 10, 90],
+ "f>g": [150, 140, 90, 10],
+ "g>h": [190, 150, 10, 40]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
+ "container": { "width": 1200, "height": 800 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ },
+ {
+ "tabs": [
+ "i"
+ ],
+ "active": "i"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 520, 800],
+ "b": [530, 0, 670, 320],
+ "c": [530, 330, 280, 470],
+ "d": [820, 330, 380, 180],
+ "e": [820, 520, 160, 280],
+ "f": [990, 520, 210, 110],
+ "g": [990, 640, 100, 160],
+ "h": [1100, 640, 100, 75],
+ "i": [1100, 725, 100, 75]
+ },
+ "splitters": {
+ "a>b": [520, 0, 10, 800],
+ "b>c": [530, 320, 670, 10],
+ "c>d": [810, 330, 10, 470],
+ "d>e": [820, 510, 380, 10],
+ "e>f": [980, 520, 10, 280],
+ "f>g": [990, 630, 210, 10],
+ "g>h": [1090, 640, 10, 160],
+ "h>i": [1100, 715, 100, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
+ "container": { "width": 1357, "height": 911 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ },
+ {
+ "tabs": [
+ "i"
+ ],
+ "active": "i"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 599, 911],
+ "b": [609, 0, 748, 376],
+ "c": [609, 386, 319, 525],
+ "d": [938, 386, 419, 208],
+ "e": [938, 604, 180, 307],
+ "f": [1128, 604, 229, 124],
+ "g": [1128, 738, 110, 173],
+ "h": [1248, 738, 109, 82],
+ "i": [1248, 830, 109, 81]
+ },
+ "splitters": {
+ "a>b": [599, 0, 10, 911],
+ "b>c": [609, 376, 748, 10],
+ "c>d": [928, 386, 10, 525],
+ "d>e": [938, 594, 419, 10],
+ "e>f": [1118, 604, 10, 307],
+ "f>g": [1128, 728, 229, 10],
+ "g>h": [1238, 738, 10, 173],
+ "h>i": [1248, 820, 109, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
+ "container": { "width": 640, "height": 420 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ },
+ {
+ "tabs": [
+ "i"
+ ],
+ "active": "i"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 240, 420],
+ "b": [250, 0, 390, 130],
+ "c": [250, 140, 140, 280],
+ "d": [400, 140, 240, 85],
+ "e": [400, 235, 90, 185],
+ "f": [500, 235, 140, 63],
+ "g": [500, 308, 65, 112],
+ "h": [575, 308, 65, 51],
+ "i": [575, 369, 65, 51]
+ },
+ "splitters": {
+ "a>b": [240, 0, 10, 420],
+ "b>c": [250, 130, 390, 10],
+ "c>d": [390, 140, 10, 280],
+ "d>e": [400, 225, 240, 10],
+ "e>f": [490, 235, 10, 185],
+ "f>g": [500, 298, 140, 10],
+ "g>h": [565, 308, 10, 112],
+ "h>i": [575, 359, 65, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
+ "container": { "width": 300, "height": 200 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ },
+ {
+ "tabs": [
+ "i"
+ ],
+ "active": "i"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 70, 240],
+ "b": [80, 0, 220, 40],
+ "c": [80, 50, 55, 190],
+ "d": [145, 50, 155, 40],
+ "e": [145, 100, 48, 140],
+ "f": [203, 100, 97, 40],
+ "g": [203, 150, 44, 90],
+ "h": [257, 150, 43, 40],
+ "i": [257, 200, 43, 40]
+ },
+ "splitters": {
+ "a>b": [70, 0, 10, 240],
+ "b>c": [80, 40, 220, 10],
+ "c>d": [135, 50, 10, 190],
+ "d>e": [145, 90, 155, 10],
+ "e>f": [193, 100, 10, 140],
+ "f>g": [203, 140, 97, 10],
+ "g>h": [247, 150, 10, 90],
+ "h>i": [257, 190, 43, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "tabs": ["a", "b", "c", "d", "e", "f", "g", "h", "i"],
+ "container": { "width": 97, "height": 61 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["a"], "active": "a" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["b"], "active": "b" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["c"], "active": "c" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["d"], "active": "d" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["e"],
+ "active": "e"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["f"],
+ "active": "f"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "g"
+ ],
+ "active": "g"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "h"
+ ],
+ "active": "h"
+ },
+ {
+ "tabs": [
+ "i"
+ ],
+ "active": "i"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "a": [0, 0, 40, 240],
+ "b": [50, 0, 190, 40],
+ "c": [50, 50, 40, 190],
+ "d": [100, 50, 140, 40],
+ "e": [100, 100, 40, 140],
+ "f": [150, 100, 90, 40],
+ "g": [150, 150, 40, 90],
+ "h": [200, 150, 40, 40],
+ "i": [200, 200, 40, 40]
+ },
+ "splitters": {
+ "a>b": [40, 0, 10, 240],
+ "b>c": [50, 40, 190, 10],
+ "c>d": [90, 50, 10, 190],
+ "d>e": [100, 90, 140, 10],
+ "e>f": [140, 100, 10, 140],
+ "f>g": [150, 140, 90, 10],
+ "g>h": [190, 150, 10, 90],
+ "h>i": [200, 190, 40, 10]
+ },
+ "maximized": null
+ }
+ }
+]
diff --git a/packages/core/test/fixtures/v1/drop-previews.json b/packages/core/test/fixtures/v1/drop-previews.json
new file mode 100644
index 0000000..4d50d60
--- /dev/null
+++ b/packages/core/test/fixtures/v1/drop-previews.json
@@ -0,0 +1,3092 @@
+{
+ "tabsetRect": { "x": 610, "y": 0, "width": 600, "height": 800 },
+ "tabset": [
+ {
+ "point": { "x": 631, "y": 217 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 935, "y": 537 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 753, "y": 706 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1042, "y": 350 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1085, "y": 563 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1083, "y": 498 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 992, "y": 136 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1171, "y": 312 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1097, "y": 457 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1083, "y": 766 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 672, "y": 743 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 690, "y": 587 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1196, "y": 582 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 907, "y": 657 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1011, "y": 161 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1007, "y": 314 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 946, "y": 511 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 746, "y": 649 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 900, "y": 623 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 653, "y": 606 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1111, "y": 272 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 695, "y": 584 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 618, "y": 685 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1047, "y": 509 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1192, "y": 594 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 719, "y": 342 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 934, "y": 382 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1167, "y": 760 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 735, "y": 704 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1106, "y": 199 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1082, "y": 141 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 810, "y": 262 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 719, "y": 162 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1141, "y": 94 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 800, "y": 289 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 657, "y": 66 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 715, "y": 635 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1158, "y": 405 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 905, "y": 238 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 791, "y": 443 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1061, "y": 712 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 697, "y": 299 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1067, "y": 588 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1031, "y": 128 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 869, "y": 410 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1174, "y": 199 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 678, "y": 729 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 819, "y": 614 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 711, "y": 767 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 724, "y": 375 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 666, "y": 714 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 823, "y": 34 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1021, "y": 597 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 647, "y": 181 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 645, "y": 435 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 743, "y": 89 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 798, "y": 521 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1068, "y": 50 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1036, "y": 247 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 620, "y": 646 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 790, "y": 181 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1192, "y": 627 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1195, "y": 680 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1055, "y": 700 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 628, "y": 376 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1142, "y": 618 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 856, "y": 392 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 864, "y": 486 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 648, "y": 466 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1176, "y": 179 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 933, "y": 29 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 777, "y": 481 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 764, "y": 633 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 730, "y": 120 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1015, "y": 321 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 879, "y": 350 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 895, "y": 67 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 772, "y": 372 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 877, "y": 243 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 678, "y": 194 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 979, "y": 261 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1002, "y": 640 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 724, "y": 218 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1023, "y": 142 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 614, "y": 705 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 688, "y": 698 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1099, "y": 364 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 742, "y": 573 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1060, "y": 727 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 785, "y": 204 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 918, "y": 654 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 932, "y": 233 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1104, "y": 265 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1004, "y": 551 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 679, "y": 536 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 795, "y": 747 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 768, "y": 521 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 791, "y": 405 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 842, "y": 276 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1200, "y": 32 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 848, "y": 281 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 770, "y": 524 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 992, "y": 295 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 936, "y": 762 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 817, "y": 654 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 822, "y": 197 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1038, "y": 746 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 902, "y": 89 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 740, "y": 407 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1199, "y": 565 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1121, "y": 687 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 948, "y": 178 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 617, "y": 420 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 938, "y": 149 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1078, "y": 359 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 932, "y": 536 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 645, "y": 322 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 828, "y": 143 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 631, "y": 588 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1189, "y": 510 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 956, "y": 168 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1128, "y": 14 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 852, "y": 736 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 944, "y": 214 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1110, "y": 36 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 742, "y": 124 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 740, "y": 167 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 763, "y": 630 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 725, "y": 89 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1003, "y": 458 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 773, "y": 401 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 637, "y": 70 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1103, "y": 372 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 772, "y": 10 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1021, "y": 618 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1067, "y": 671 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 963, "y": 238 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1208, "y": 241 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 860, "y": 368 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 813, "y": 280 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1138, "y": 766 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1072, "y": 287 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 922, "y": 299 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1054, "y": 438 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 897, "y": 547 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 743, "y": 757 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1175, "y": 19 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 751, "y": 211 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1129, "y": 669 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1031, "y": 524 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 875, "y": 428 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 806, "y": 321 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 610, "y": 435 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1120, "y": 505 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 764, "y": 424 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1181, "y": 522 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 834, "y": 111 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1144, "y": 494 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1081, "y": 103 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1197, "y": 758 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 773, "y": 174 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1115, "y": 163 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 970, "y": 789 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 709, "y": 474 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1056, "y": 627 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1177, "y": 162 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1194, "y": 435 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 837, "y": 123 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 772, "y": 70 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 708, "y": 331 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 956, "y": 26 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 828, "y": 554 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1037, "y": 170 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 963, "y": 254 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 715, "y": 54 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 702, "y": 225 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 942, "y": 603 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1178, "y": 184 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1187, "y": 18 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 750, "y": 468 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1205, "y": 16 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 961, "y": 552 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1020, "y": 77 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1051, "y": 383 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1195, "y": 425 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 935, "y": 72 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1160, "y": 385 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 784, "y": 713 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 734, "y": 33 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1035, "y": 512 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 635, "y": 125 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 901, "y": 476 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1060, "y": 403 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1203, "y": 559 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 910, "y": 176 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1115, "y": 230 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 972, "y": 418 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 813, "y": 465 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1150, "y": 780 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1059, "y": 232 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 810, "y": 189 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1110, "y": 149 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 693, "y": 67 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 665, "y": 333 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 836, "y": 499 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 760, "y": 103 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1050, "y": 770 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1048, "y": 67 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 700, "y": 673 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 644, "y": 562 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 742, "y": 537 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 651, "y": 36 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1155, "y": 10 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1204, "y": 29 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1136, "y": 12 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1169, "y": 733 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 625, "y": 416 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1017, "y": 412 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 863, "y": 109 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 723, "y": 618 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 999, "y": 65 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 680, "y": 294 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 634, "y": 100 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1088, "y": 655 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1134, "y": 636 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 633, "y": 568 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 993, "y": 95 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 822, "y": 131 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1196, "y": 256 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 619, "y": 219 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1037, "y": 359 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 940, "y": 116 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 713, "y": 624 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1123, "y": 444 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 652, "y": 685 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 733, "y": 174 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1054, "y": 496 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 708, "y": 765 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 745, "y": 575 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1023, "y": 509 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 727, "y": 578 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 925, "y": 397 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1122, "y": 83 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 614, "y": 478 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 675, "y": 476 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 657, "y": 572 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1076, "y": 764 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 656, "y": 211 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1066, "y": 667 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 657, "y": 666 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 996, "y": 531 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 883, "y": 163 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 747, "y": 163 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 899, "y": 85 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 970, "y": 783 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1084, "y": 755 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1044, "y": 693 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1049, "y": 559 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 945, "y": 275 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 744, "y": 511 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 948, "y": 696 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 842, "y": 418 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 981, "y": 701 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 735, "y": 133 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 654, "y": 719 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 919, "y": 502 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 687, "y": 764 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 959, "y": 535 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 940, "y": 358 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 678, "y": 229 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 906, "y": 550 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 689, "y": 150 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1158, "y": 151 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1124, "y": 267 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1091, "y": 308 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 617, "y": 23 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1191, "y": 614 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 891, "y": 441 },
+ "preview": {
+ "area": "contain",
+ "left": 670,
+ "top": 80,
+ "width": 480,
+ "height": 640
+ }
+ },
+ {
+ "point": { "x": 1164, "y": 123 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 915, "y": 583 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 640, "y": 657 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 996, "y": 262 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 964, "y": 604 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 1118, "y": 558 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 617, "y": 263 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 664, "y": 458 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 685, "y": 117 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 694, "y": 450 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 712, "y": 257 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1083, "y": 232 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1030, "y": 111 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 768, "y": 619 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 860, "y": 265 },
+ "preview": {
+ "area": "top",
+ "left": 610,
+ "top": 0,
+ "width": 600,
+ "height": 400
+ }
+ },
+ {
+ "point": { "x": 749, "y": 392 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 802, "y": 291 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 721, "y": 408 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 745, "y": 573 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 761, "y": 752 },
+ "preview": {
+ "area": "left",
+ "left": 610,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 1046, "y": 631 },
+ "preview": {
+ "area": "right",
+ "left": 910,
+ "top": 0,
+ "width": 300,
+ "height": 800
+ }
+ },
+ {
+ "point": { "x": 827, "y": 600 },
+ "preview": {
+ "area": "bottom",
+ "left": 610,
+ "top": 400,
+ "width": 600,
+ "height": 400
+ }
+ }
+ ],
+ "root": [
+ {
+ "side": "left",
+ "preview": {
+ "area": "left",
+ "left": 0,
+ "top": 0,
+ "width": 605,
+ "height": 800
+ }
+ },
+ {
+ "side": "right",
+ "preview": {
+ "area": "right",
+ "left": 605,
+ "top": 0,
+ "width": 605,
+ "height": 800
+ }
+ },
+ {
+ "side": "top",
+ "preview": {
+ "area": "top",
+ "left": 0,
+ "top": 0,
+ "width": 1210,
+ "height": 400
+ }
+ },
+ {
+ "side": "bottom",
+ "preview": {
+ "area": "bottom",
+ "left": 0,
+ "top": 400,
+ "width": 1210,
+ "height": 400
+ }
+ }
+ ],
+ "navbar": [
+ { "x": 0, "index": 0, "area": "left" },
+ { "x": 7, "index": 0, "area": "left" },
+ { "x": 14, "index": 0, "area": "left" },
+ { "x": 21, "index": 0, "area": "left" },
+ { "x": 28, "index": 0, "area": "left" },
+ { "x": 35, "index": 0, "area": "left" },
+ { "x": 42, "index": 0, "area": "right" },
+ { "x": 49, "index": 0, "area": "right" },
+ { "x": 56, "index": 0, "area": "right" },
+ { "x": 63, "index": 0, "area": "right" },
+ { "x": 70, "index": 0, "area": "right" },
+ { "x": 77, "index": 1, "area": "left" },
+ { "x": 84, "index": 1, "area": "left" },
+ { "x": 91, "index": 1, "area": "left" },
+ { "x": 98, "index": 1, "area": "left" },
+ { "x": 105, "index": 1, "area": "left" },
+ { "x": 112, "index": 1, "area": "right" },
+ { "x": 119, "index": 1, "area": "right" },
+ { "x": 126, "index": 1, "area": "right" },
+ { "x": 133, "index": 1, "area": "right" },
+ { "x": 140, "index": 1, "area": "right" },
+ { "x": 147, "index": 2, "area": "left" },
+ { "x": 154, "index": 2, "area": "left" },
+ { "x": 161, "index": 2, "area": "left" },
+ { "x": 168, "index": 2, "area": "left" },
+ { "x": 175, "index": 2, "area": "left" },
+ { "x": 182, "index": 2, "area": "right" },
+ { "x": 189, "index": 2, "area": "right" },
+ { "x": 196, "index": 2, "area": "right" },
+ { "x": 203, "index": 2, "area": "right" },
+ { "x": 210, "index": 2, "area": "right" },
+ { "x": 217, "index": 2, "area": "right" },
+ { "x": 224, "index": 2, "area": "right" },
+ { "x": 231, "index": 2, "area": "right" },
+ { "x": 238, "index": 2, "area": "right" },
+ { "x": 245, "index": 2, "area": "right" },
+ { "x": 252, "index": 2, "area": "right" },
+ { "x": 259, "index": 2, "area": "right" },
+ { "x": 266, "index": 2, "area": "right" },
+ { "x": 273, "index": 2, "area": "right" },
+ { "x": 280, "index": 2, "area": "right" },
+ { "x": 287, "index": 2, "area": "right" },
+ { "x": 294, "index": 2, "area": "right" }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-01.json b/packages/core/test/fixtures/v1/scenario-01.json
new file mode 100644
index 0000000..6322110
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-01.json
@@ -0,0 +1,637 @@
+{
+ "name": "1 tabs, seed 1",
+ "tabs": ["editor"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 224, "height": 851 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 224, 851] },
+ "splitters": {},
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 224, 851] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 224, 851] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 224, 851] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 501, "height": 467 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 501, 467] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-02.json b/packages/core/test/fixtures/v1/scenario-02.json
new file mode 100644
index 0000000..231d9e4
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-02.json
@@ -0,0 +1,920 @@
+{
+ "name": "2 tabs, seed 2",
+ "tabs": ["editor", "terminal"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "terminal": [605, 0, 595, 800]
+ },
+ "splitters": { "editor>terminal": [595, 0, 10, 800] },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tab": "terminal", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "terminal": [605, 0, 595, 800]
+ },
+ "splitters": { "editor>terminal": [595, 0, 10, 800] },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "terminal": [0, 0, 1200, 800]
+ },
+ "splitters": { "editor>terminal": [595, 0, 10, 800] },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 971, "height": 330 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 481, 330],
+ "terminal": [0, 0, 971, 330]
+ },
+ "splitters": { "editor>terminal": [481, 0, 10, 330] },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 481, 330],
+ "terminal": [491, 0, 480, 330]
+ },
+ "splitters": { "editor>terminal": [481, 0, 10, 330] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 971, 330],
+ "terminal": [491, 0, 480, 330]
+ },
+ "splitters": { "editor>terminal": [481, 0, 10, 330] },
+ "maximized": "editor"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-9",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-9",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 971, 330],
+ "terminal": [491, 0, 480, 330]
+ },
+ "splitters": { "editor>terminal": [481, 0, 10, 330] },
+ "maximized": "editor"
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 1598, "height": 959 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1598, 959],
+ "terminal": [804, 0, 794, 959]
+ },
+ "splitters": { "editor>terminal": [794, 0, 10, 959] },
+ "maximized": "editor"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 475],
+ "editor": [0, 485, 1598, 474]
+ },
+ "splitters": { "terminal>editor": [0, 475, 1598, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1598, 959] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 794, 959],
+ "terminal": [804, 0, 794, 959]
+ },
+ "splitters": { "editor>terminal": [794, 0, 10, 959] },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-9",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-16",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 475],
+ "editor": [0, 485, 1598, 474]
+ },
+ "splitters": { "terminal>editor": [0, 475, 1598, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 799, "y": 568 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 563],
+ "editor": [0, 573, 1598, 386]
+ },
+ "splitters": { "terminal>editor": [0, 563, 1598, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 799, "y": 616 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 611],
+ "editor": [0, 621, 1598, 338]
+ },
+ "splitters": { "terminal>editor": [0, 611, 1598, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 799, "y": 622 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 617],
+ "editor": [0, 627, 1598, 332]
+ },
+ "splitters": { "terminal>editor": [0, 617, 1598, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1598, 909],
+ "editor": [0, 919, 1598, 40]
+ },
+ "splitters": { "terminal>editor": [0, 909, 1598, 10] },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-9",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 132.7963176064442,
+ "nodName": "",
+ "uidNode": "id-16",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 67.20368239355581,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "terminal", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1598, 959] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1466, "height": 499 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "editor|terminal": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 132.7963176064442,
+ "nodName": "",
+ "uidNode": "id-16",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1466, 273],
+ "terminal": [0, 283, 1466, 216]
+ },
+ "splitters": { "editor>terminal": [0, 273, 1466, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 733, "y": 288 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1466, 283],
+ "terminal": [0, 293, 1466, 206]
+ },
+ "splitters": { "editor>terminal": [0, 283, 1466, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1466, 449],
+ "terminal": [0, 459, 1466, 40]
+ },
+ "splitters": { "editor>terminal": [0, 449, 1466, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 733, "y": 481 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1466, 449],
+ "terminal": [0, 459, 1466, 40]
+ },
+ "splitters": { "editor>terminal": [0, 449, 1466, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 733, "y": 409 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1466, 449],
+ "terminal": [0, 459, 1466, 40]
+ },
+ "splitters": { "editor>terminal": [0, 449, 1466, 10] },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 138.31174860236172,
+ "nodName": "",
+ "uidNode": "id-16",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 94.48456900408249,
+ "nodName": "",
+ "uidNode": "id-21",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1416, 499],
+ "terminal": [1426, 0, 40, 499]
+ },
+ "splitters": { "editor>terminal": [1416, 0, 10, 499] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "terminal|editor": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "terminal|editor": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "terminal|editor": [0, 0, 1466, 499] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 379, "height": 1010 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": { "terminal|editor": [0, 0, 379, 1010] },
+ "splitters": {},
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 94.48456900408249,
+ "nodName": "",
+ "uidNode": "id-21",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-03.json b/packages/core/test/fixtures/v1/scenario-03.json
new file mode 100644
index 0000000..39e049b
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-03.json
@@ -0,0 +1,1155 @@
+{
+ "name": "3 tabs, seed 3",
+ "tabs": ["editor", "terminal", "preview"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "terminal": [605, 0, 595, 395],
+ "preview": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "editor>terminal": [595, 0, 10, 800],
+ "terminal>preview": [605, 395, 595, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "preview", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "preview": [605, 0, 595, 395],
+ "terminal": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "editor>preview": [595, 0, 10, 800],
+ "preview>terminal": [605, 395, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 393, 800],
+ "terminal": [403, 0, 394, 800],
+ "preview": [807, 0, 393, 800]
+ },
+ "splitters": {
+ "editor>terminal": [393, 0, 10, 800],
+ "terminal>preview": [797, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 393, 800],
+ "terminal": [403, 0, 394, 800],
+ "preview": [807, 0, 393, 800]
+ },
+ "splitters": {
+ "editor>terminal": [393, 0, 10, 800],
+ "terminal>preview": [797, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "preview", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 393, 800],
+ "terminal": [403, 0, 394, 800],
+ "preview": [807, 0, 393, 800]
+ },
+ "splitters": {
+ "editor>terminal": [393, 0, 10, 800],
+ "terminal>preview": [797, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 295, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 290, 800],
+ "terminal": [300, 0, 497, 800],
+ "preview": [807, 0, 393, 800]
+ },
+ "splitters": {
+ "editor>terminal": [290, 0, 10, 800],
+ "terminal>preview": [797, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 70.72135785007072,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 129.27864214992928,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 415, 800],
+ "terminal": [425, 0, 725, 800],
+ "preview": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "editor>terminal": [415, 0, 10, 800],
+ "terminal>preview": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 1150, 800],
+ "preview": [1160, 0, 40, 800]
+ },
+ "splitters": { "terminal>preview": [1150, 0, 10, 800] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 290, "height": 403 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 240, 403],
+ "preview": [250, 0, 40, 403]
+ },
+ "splitters": { "terminal>preview": [240, 0, 10, 403] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 240, 403],
+ "preview": [250, 0, 40, 403]
+ },
+ "splitters": { "terminal>preview": [240, 0, 10, 403] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 290, 403],
+ "preview": [250, 0, 40, 403]
+ },
+ "splitters": { "terminal>preview": [240, 0, 10, 403] },
+ "maximized": "terminal|editor"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-2",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 70.72135785007072,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 40, 403],
+ "preview": [50, 0, 240, 403]
+ },
+ "splitters": { "terminal>preview": [40, 0, 10, 403] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 115, 403],
+ "terminal": [125, 0, 40, 403],
+ "preview": [175, 0, 115, 403]
+ },
+ "splitters": {
+ "editor>terminal": [115, 0, 10, 403],
+ "terminal>preview": [165, 0, 10, 403]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 403],
+ "terminal": [50, 0, 40, 403],
+ "preview": [100, 0, 190, 403]
+ },
+ "splitters": {
+ "editor>terminal": [40, 0, 10, 403],
+ "terminal>preview": [90, 0, 10, 403]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": -59, "y": 201.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 403],
+ "terminal": [50, 0, 40, 403],
+ "preview": [100, 0, 190, 403]
+ },
+ "splitters": {
+ "editor>terminal": [40, 0, 10, 403],
+ "terminal>preview": [90, 0, 10, 403]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 472, "height": 701 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 701],
+ "terminal": [50, 0, 40, 701],
+ "preview": [100, 0, 372, 701]
+ },
+ "splitters": {
+ "editor>terminal": [40, 0, 10, 701],
+ "terminal>preview": [90, 0, 10, 701]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-24",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 70.72135785007072,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 231, 701],
+ "preview": [241, 0, 231, 701]
+ },
+ "splitters": { "terminal>preview": [231, 0, 10, 701] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ "tabsets": { "terminal|preview|editor": [0, 0, 472, 701] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ "tabsets": { "terminal|preview|editor": [0, 0, 472, 701] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ "tabsets": { "terminal|preview|editor": [0, 0, 472, 701] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ "tabsets": { "terminal|preview|editor": [0, 0, 472, 701] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-24",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ "tabsets": { "terminal|preview|editor": [0, 0, 472, 701] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal", "editor"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 472, 346],
+ "terminal|editor": [0, 356, 472, 345]
+ },
+ "splitters": { "preview>terminal": [0, 346, 472, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1268, "height": 449 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal", "editor"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 1268, 220],
+ "terminal|editor": [0, 230, 1268, 219]
+ },
+ "splitters": { "preview>terminal": [0, 220, 1268, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 804, "height": 251 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal", "editor"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 804, 121],
+ "terminal|editor": [0, 131, 804, 120]
+ },
+ "splitters": { "preview>terminal": [0, 121, 804, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["terminal", "editor"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 804, 40],
+ "terminal|editor": [0, 50, 804, 201]
+ },
+ "splitters": { "preview>terminal": [0, 40, 804, 10] },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-35",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-24",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "preview",
+ "after": "terminal",
+ "point": { "x": 402, "y": 89 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["terminal", "editor"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 804, 40],
+ "terminal|editor": [0, 50, 804, 201]
+ },
+ "splitters": { "preview>terminal": [0, 40, 804, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 804, 121],
+ "preview": [0, 131, 804, 120]
+ },
+ "splitters": { "terminal>preview": [0, 121, 804, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 402, "y": 259 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 804, 201],
+ "preview": [0, 211, 804, 40]
+ },
+ "splitters": { "terminal>preview": [0, 201, 804, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1188, "height": 481 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 1188, 431],
+ "preview": [0, 441, 1188, 40]
+ },
+ "splitters": { "terminal>preview": [0, 431, 1188, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal", "editor"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal|editor": [0, 0, 1188, 481],
+ "preview": [0, 441, 1188, 40]
+ },
+ "splitters": { "terminal>preview": [0, 431, 1188, 10] },
+ "maximized": "terminal|editor"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-37",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-24",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-04.json b/packages/core/test/fixtures/v1/scenario-04.json
new file mode 100644
index 0000000..a6d2c7f
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-04.json
@@ -0,0 +1,1681 @@
+{
+ "name": "4 tabs, seed 4",
+ "tabs": ["editor", "terminal", "preview", "console"],
+ "container": { "width": 640, "height": 420 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 290, 420],
+ "terminal": [300, 0, 340, 205],
+ "preview": [300, 215, 165, 205],
+ "console": [475, 215, 165, 205]
+ },
+ "splitters": {
+ "editor>terminal": [290, 0, 10, 420],
+ "terminal>preview": [300, 205, 340, 10],
+ "preview>console": [465, 215, 10, 205]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 640, 420],
+ "terminal": [300, 0, 340, 205],
+ "preview": [300, 215, 165, 205],
+ "console": [475, 215, 165, 205]
+ },
+ "splitters": {
+ "editor>terminal": [290, 0, 10, 420],
+ "terminal>preview": [300, 205, 340, 10],
+ "preview>console": [465, 215, 10, 205]
+ },
+ "maximized": "editor"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 207, 420],
+ "preview": [217, 0, 206, 420],
+ "terminal": [433, 0, 207, 205],
+ "console": [433, 215, 207, 205]
+ },
+ "splitters": {
+ "editor>preview": [207, 0, 10, 420],
+ "preview>terminal": [423, 0, 10, 420],
+ "terminal>console": [433, 205, 207, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 207, 420],
+ "preview": [217, 0, 206, 420],
+ "terminal": [433, 0, 207, 205],
+ "console": [0, 0, 640, 420]
+ },
+ "splitters": {
+ "editor>preview": [207, 0, 10, 420],
+ "preview>terminal": [423, 0, 10, 420],
+ "terminal>console": [433, 205, 207, 10]
+ },
+ "maximized": "console"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 153, 420],
+ "console": [163, 0, 152, 420],
+ "preview": [325, 0, 153, 420],
+ "terminal": [488, 0, 152, 420]
+ },
+ "splitters": {
+ "editor>console": [153, 0, 10, 420],
+ "console>preview": [315, 0, 10, 420],
+ "preview>terminal": [478, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "console", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 153, 420],
+ "console": [163, 0, 152, 420],
+ "preview": [325, 0, 153, 420],
+ "terminal": [488, 0, 152, 420]
+ },
+ "splitters": {
+ "editor>console": [153, 0, 10, 420],
+ "console>preview": [315, 0, 10, 420],
+ "preview>terminal": [478, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-20",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 153, 420],
+ "terminal": [163, 0, 152, 420],
+ "console": [325, 0, 153, 420],
+ "preview": [488, 0, 152, 420]
+ },
+ "splitters": {
+ "editor>terminal": [153, 0, 10, 420],
+ "terminal>console": [315, 0, 10, 420],
+ "console>preview": [478, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 190, 420],
+ "terminal": [200, 0, 190, 420],
+ "console": [400, 0, 40, 420],
+ "preview": [450, 0, 190, 420]
+ },
+ "splitters": {
+ "editor>terminal": [190, 0, 10, 420],
+ "terminal>console": [390, 0, 10, 420],
+ "console>preview": [440, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 265, 420],
+ "terminal": [275, 0, 265, 420],
+ "console": [550, 0, 40, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "editor>terminal": [265, 0, 10, 420],
+ "terminal>console": [540, 0, 10, 420],
+ "console>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 190, 420],
+ "editor": [200, 0, 190, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "console>editor": [190, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "editor",
+ "point": { "x": 32, "y": 210 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 420],
+ "editor": [50, 0, 340, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-20",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 420],
+ "editor": [50, 0, 340, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [0, 0, 640, 420]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 420],
+ "editor": [50, 0, 340, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 413, "y": 210 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 420],
+ "editor": [50, 0, 340, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 420],
+ "editor": [50, 0, 340, 420],
+ "terminal": [400, 0, 190, 420],
+ "preview": [600, 0, 40, 420]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 420],
+ "editor>terminal": [390, 0, 10, 420],
+ "terminal>preview": [590, 0, 10, 420]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 1025, "height": 710 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 710],
+ "editor": [50, 0, 597, 710],
+ "terminal": [657, 0, 318, 710],
+ "preview": [985, 0, 40, 710]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 710],
+ "editor>terminal": [647, 0, 10, 710],
+ "terminal>preview": [975, 0, 10, 710]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-20",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1443, "height": 429 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 429],
+ "editor": [50, 0, 875, 429],
+ "terminal": [935, 0, 458, 429],
+ "preview": [1403, 0, 40, 429]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>terminal": [925, 0, 10, 429],
+ "terminal>preview": [1393, 0, 10, 429]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 429],
+ "editor": [50, 0, 875, 429],
+ "terminal": [935, 0, 458, 429],
+ "preview": [1403, 0, 40, 429]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>terminal": [925, 0, 10, 429],
+ "terminal>preview": [1393, 0, 10, 429]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 1230, "y": 214.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 429],
+ "editor": [50, 0, 875, 429],
+ "terminal": [935, 0, 458, 429],
+ "preview": [1403, 0, 40, 429]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>terminal": [925, 0, 10, 429],
+ "terminal>preview": [1393, 0, 10, 429]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 429],
+ "preview": [1403, 0, 40, 429],
+ "editor": [50, 0, 1343, 210],
+ "terminal": [50, 220, 1343, 209]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>preview": [1393, 0, 10, 429],
+ "editor>terminal": [50, 210, 1343, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "editor",
+ "point": { "x": -44, "y": 214.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 429],
+ "preview": [1403, 0, 40, 429],
+ "editor": [50, 0, 1343, 210],
+ "terminal": [50, 220, 1343, 209]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>preview": [1393, 0, 10, 429],
+ "editor>terminal": [50, 210, 1343, 10]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-20",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "console", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [50, 0, 1343, 429],
+ "preview": [1403, 0, 40, 429],
+ "console": [0, 0, 40, 210],
+ "terminal": [0, 220, 40, 209]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>preview": [1393, 0, 10, 429],
+ "console>terminal": [0, 210, 40, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tab": "console", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [50, 0, 1343, 429],
+ "preview": [1403, 0, 40, 429],
+ "console": [0, 0, 40, 210],
+ "terminal": [0, 220, 40, 209]
+ },
+ "splitters": {
+ "console>editor": [40, 0, 10, 429],
+ "editor>preview": [1393, 0, 10, 429],
+ "console>terminal": [0, 210, 40, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "console", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "console"],
+ "active": "preview"
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [50, 0, 1393, 429],
+ "preview|console": [0, 0, 40, 210],
+ "terminal": [0, 220, 40, 209]
+ },
+ "splitters": {
+ "preview>editor": [40, 0, 10, 429],
+ "preview>terminal": [0, 210, 40, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "console"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 220, 1443, 209],
+ "preview|console": [0, 0, 717, 210],
+ "editor": [727, 0, 716, 210]
+ },
+ "splitters": {
+ "preview>terminal": [0, 210, 1443, 10],
+ "preview>editor": [717, 0, 10, 210]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 717, 210],
+ "editor": [727, 0, 716, 210],
+ "terminal": [0, 220, 717, 209],
+ "preview": [727, 220, 716, 209]
+ },
+ "splitters": {
+ "console>terminal": [0, 210, 1443, 10],
+ "console>editor": [717, 0, 10, 210],
+ "terminal>preview": [717, 220, 10, 209]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-50",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-54",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 40, 210],
+ "editor": [50, 0, 1393, 210],
+ "terminal": [0, 220, 717, 209],
+ "preview": [727, 220, 716, 209]
+ },
+ "splitters": {
+ "console>terminal": [0, 210, 1443, 10],
+ "console>editor": [40, 0, 10, 210],
+ "terminal>preview": [717, 220, 10, 209]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 692, 429],
+ "terminal": [702, 220, 741, 209],
+ "console": [702, 0, 40, 210],
+ "editor": [752, 0, 691, 210]
+ },
+ "splitters": {
+ "preview>console": [692, 0, 10, 429],
+ "console>terminal": [702, 210, 741, 10],
+ "console>editor": [742, 0, 10, 210]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1246, "height": 401 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 593, 401],
+ "terminal": [603, 206, 643, 195],
+ "console": [603, 0, 40, 196],
+ "editor": [653, 0, 593, 196]
+ },
+ "splitters": {
+ "preview>console": [593, 0, 10, 401],
+ "console>terminal": [603, 196, 643, 10],
+ "console>editor": [643, 0, 10, 196]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 1246, 401],
+ "terminal": [603, 206, 643, 195],
+ "console": [603, 0, 40, 196],
+ "editor": [653, 0, 593, 196]
+ },
+ "splitters": {
+ "preview>console": [593, 0, 10, 401],
+ "console>terminal": [603, 196, 643, 10],
+ "console>editor": [643, 0, 10, 196]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 593, 401],
+ "terminal": [603, 206, 643, 195],
+ "console": [603, 0, 40, 196],
+ "editor": [653, 0, 593, 196]
+ },
+ "splitters": {
+ "preview>console": [593, 0, 10, 401],
+ "console>terminal": [603, 196, 643, 10],
+ "console>editor": [643, 0, 10, 196]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-57",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-50",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-54",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-05.json b/packages/core/test/fixtures/v1/scenario-05.json
new file mode 100644
index 0000000..ea0259c
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-05.json
@@ -0,0 +1,2016 @@
+{
+ "name": "5 tabs, seed 5",
+ "tabs": ["editor", "terminal", "preview", "console", "files"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 370],
+ "preview": [580, 380, 305, 420],
+ "console": [895, 380, 305, 205],
+ "files": [895, 595, 305, 205]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 370, 620, 10],
+ "preview>console": [885, 380, 10, 420],
+ "console>files": [895, 585, 305, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 890, "y": 399 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 394],
+ "preview": [580, 404, 305, 396],
+ "console": [895, 404, 305, 193],
+ "files": [895, 607, 305, 193]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 394, 620, 10],
+ "preview>console": [885, 404, 10, 396],
+ "console>files": [895, 597, 305, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 688, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 683, 800],
+ "terminal": [693, 0, 507, 394],
+ "preview": [693, 404, 249, 396],
+ "console": [952, 404, 248, 193],
+ "files": [952, 607, 248, 193]
+ },
+ "splitters": {
+ "editor>terminal": [683, 0, 10, 800],
+ "terminal>preview": [693, 394, 507, 10],
+ "preview>console": [942, 404, 10, 396],
+ "console>files": [952, 597, 248, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tabsetOf": "console", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 683, 800],
+ "terminal": [693, 0, 507, 394],
+ "preview": [693, 404, 249, 396],
+ "console": [952, 404, 248, 193],
+ "files": [952, 607, 248, 193]
+ },
+ "splitters": {
+ "editor>terminal": [683, 0, 10, 800],
+ "terminal>preview": [693, 394, 507, 10],
+ "preview>console": [942, 404, 10, 396],
+ "console>files": [952, 597, 248, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "files", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 394],
+ "preview": [0, 404, 595, 396],
+ "console": [605, 404, 595, 193],
+ "files|editor": [605, 607, 595, 193]
+ },
+ "splitters": {
+ "terminal>preview": [0, 394, 1200, 10],
+ "preview>console": [595, 404, 10, 396],
+ "console>files": [605, 597, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "preview", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 394],
+ "preview": [0, 404, 595, 396],
+ "console": [605, 404, 595, 193],
+ "files|editor": [605, 607, 595, 193]
+ },
+ "splitters": {
+ "terminal>preview": [0, 394, 1200, 10],
+ "preview>console": [595, 404, 10, 396],
+ "console>files": [605, 597, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 78.67924528301887,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 107.27272727272728,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 92.72727272727272,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 595, 800],
+ "console": [605, 0, 595, 395],
+ "files|editor": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "terminal>console": [595, 0, 10, 800],
+ "console>files": [605, 395, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 595, 800],
+ "console": [605, 0, 595, 750],
+ "files|editor": [605, 760, 595, 40]
+ },
+ "splitters": {
+ "terminal>console": [595, 0, 10, 800],
+ "console>files": [605, 750, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 595, 800],
+ "console": [605, 0, 595, 750],
+ "files|editor": [605, 760, 595, 40]
+ },
+ "splitters": {
+ "terminal>console": [595, 0, 10, 800],
+ "console>files": [605, 750, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 595, 800],
+ "console": [605, 0, 595, 395],
+ "files|editor": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "terminal>console": [595, 0, 10, 800],
+ "console>files": [605, 395, 595, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 595, 800],
+ "console": [0, 0, 1200, 800],
+ "files|editor": [605, 405, 595, 395]
+ },
+ "splitters": {
+ "terminal>console": [595, 0, 10, 800],
+ "console>files": [605, 395, 595, 10]
+ },
+ "maximized": "console"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-26",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 393, 800],
+ "console": [403, 0, 394, 800],
+ "files|editor": [807, 0, 393, 800]
+ },
+ "splitters": {
+ "terminal>console": [393, 0, 10, 800],
+ "console>files": [797, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "preview"],
+ "active": "terminal"
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal|preview": [0, 0, 570, 800],
+ "console": [580, 0, 570, 800],
+ "files|editor": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "terminal>console": [570, 0, 10, 800],
+ "console>files": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "files", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files", "editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 377, 800],
+ "console": [387, 0, 376, 800],
+ "files|editor": [773, 0, 40, 800],
+ "preview": [823, 0, 377, 800]
+ },
+ "splitters": {
+ "terminal>console": [377, 0, 10, 800],
+ "console>files": [763, 0, 10, 800],
+ "files>preview": [813, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 280, 800],
+ "terminal": [290, 0, 280, 800],
+ "console": [580, 0, 280, 800],
+ "editor": [870, 0, 40, 800],
+ "preview": [920, 0, 280, 800]
+ },
+ "splitters": {
+ "files>terminal": [280, 0, 10, 800],
+ "terminal>console": [570, 0, 10, 800],
+ "console>editor": [860, 0, 10, 800],
+ "editor>preview": [910, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 788, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 280, 800],
+ "terminal": [290, 0, 280, 800],
+ "console": [580, 0, 280, 800],
+ "editor": [870, 0, 40, 800],
+ "preview": [920, 0, 280, 800]
+ },
+ "splitters": {
+ "files>terminal": [280, 0, 10, 800],
+ "terminal>console": [570, 0, 10, 800],
+ "console>editor": [860, 0, 10, 800],
+ "editor>preview": [910, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 360, 800],
+ "terminal": [370, 0, 40, 800],
+ "console": [420, 0, 360, 800],
+ "editor": [790, 0, 40, 800],
+ "preview": [840, 0, 360, 800]
+ },
+ "splitters": {
+ "files>terminal": [360, 0, 10, 800],
+ "terminal>console": [410, 0, 10, 800],
+ "console>editor": [780, 0, 10, 800],
+ "editor>preview": [830, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1200, 800],
+ "terminal": [370, 0, 40, 800],
+ "console": [420, 0, 360, 800],
+ "editor": [790, 0, 40, 800],
+ "preview": [840, 0, 360, 800]
+ },
+ "splitters": {
+ "files>terminal": [360, 0, 10, 800],
+ "terminal>console": [410, 0, 10, 800],
+ "console>editor": [780, 0, 10, 800],
+ "editor>preview": [830, 0, 10, 800]
+ },
+ "maximized": "files"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 280, 800],
+ "console": [290, 0, 280, 800],
+ "terminal": [580, 0, 280, 800],
+ "editor": [870, 0, 40, 800],
+ "preview": [920, 0, 280, 800]
+ },
+ "splitters": {
+ "files>console": [280, 0, 10, 800],
+ "console>terminal": [570, 0, 10, 800],
+ "terminal>editor": [860, 0, 10, 800],
+ "editor>preview": [910, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 377, 395],
+ "console": [387, 405, 376, 395],
+ "editor": [773, 405, 40, 395],
+ "preview": [823, 405, 377, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>console": [377, 405, 10, 395],
+ "console>editor": [763, 405, 10, 395],
+ "editor>preview": [813, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 377, 395],
+ "console": [387, 405, 376, 395],
+ "editor": [773, 405, 40, 395],
+ "preview": [823, 405, 377, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>console": [377, 405, 10, 395],
+ "console>editor": [763, 405, 10, 395],
+ "editor>preview": [813, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-46",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-50",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-26",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 545, 395],
+ "console": [555, 405, 545, 395],
+ "editor": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>console": [545, 405, 10, 395],
+ "console>editor": [1100, 405, 10, 395],
+ "editor>preview": [1150, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 545, 395],
+ "editor": [555, 405, 40, 395],
+ "console": [605, 405, 545, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [545, 405, 10, 395],
+ "editor>console": [595, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 0, 1200, 800],
+ "editor": [555, 405, 40, 395],
+ "console": [605, 405, 545, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [545, 405, 10, 395],
+ "editor>console": [595, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": "files"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "console",
+ "point": { "x": 686, "y": 602.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 0, 1200, 800],
+ "editor": [555, 405, 40, 395],
+ "console": [605, 405, 545, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [545, 405, 10, 395],
+ "editor>console": [595, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": "files"
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 1050, 395],
+ "editor": [1060, 405, 40, 395],
+ "console": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [1050, 405, 10, 395],
+ "editor>console": [1100, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-46",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-50",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "preview",
+ "point": { "x": 1346, "y": 602.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 395],
+ "files": [0, 405, 1050, 395],
+ "editor": [1060, 405, 40, 395],
+ "console": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [1050, 405, 10, 395],
+ "editor>console": [1100, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 800],
+ "files": [0, 405, 1050, 395],
+ "editor": [1060, 405, 40, 395],
+ "console": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [1050, 405, 10, 395],
+ "editor>console": [1100, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 600, "y": 297 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 800],
+ "files": [0, 405, 1050, 395],
+ "editor": [1060, 405, 40, 395],
+ "console": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [1050, 405, 10, 395],
+ "editor>console": [1100, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 600, "y": 426 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 800],
+ "files": [0, 405, 1050, 395],
+ "editor": [1060, 405, 40, 395],
+ "console": [1110, 405, 40, 395],
+ "preview": [1160, 405, 40, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [1050, 405, 10, 395],
+ "editor>console": [1100, 405, 10, 395],
+ "console>preview": [1150, 405, 10, 395]
+ },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 800],
+ "files": [0, 405, 545, 395],
+ "editor": [555, 405, 40, 395],
+ "console": [605, 405, 40, 395],
+ "preview": [655, 405, 545, 395]
+ },
+ "splitters": {
+ "terminal>files": [0, 395, 1200, 10],
+ "files>editor": [545, 405, 10, 395],
+ "editor>console": [595, 405, 10, 395],
+ "console>preview": [645, 405, 10, 395]
+ },
+ "maximized": "terminal"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-46",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-46",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-50",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-06.json b/packages/core/test/fixtures/v1/scenario-06.json
new file mode 100644
index 0000000..7f010a9
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-06.json
@@ -0,0 +1,2117 @@
+{
+ "name": "6 tabs, seed 6",
+ "tabs": ["editor", "terminal", "preview", "console", "files", "search"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 545, 800],
+ "terminal": [555, 0, 645, 370],
+ "preview": [555, 380, 293, 420],
+ "console": [858, 380, 342, 205],
+ "files": [858, 595, 166, 205],
+ "search": [1034, 595, 166, 205]
+ },
+ "splitters": {
+ "editor>terminal": [545, 0, 10, 800],
+ "terminal>preview": [555, 370, 645, 10],
+ "preview>console": [848, 380, 10, 420],
+ "console>files": [858, 585, 342, 10],
+ "files>search": [1024, 595, 10, 205]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "console", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 193],
+ "preview": [580, 203, 620, 192],
+ "console": [580, 405, 620, 193],
+ "files": [580, 608, 305, 192],
+ "search": [895, 608, 305, 192]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 193, 620, 10],
+ "preview>console": [580, 395, 620, 10],
+ "console>files": [580, 598, 620, 10],
+ "files>search": [885, 608, 10, 192]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "files", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 260],
+ "console": [580, 270, 620, 260],
+ "preview|files": [580, 540, 305, 260],
+ "search": [895, 540, 305, 260]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>console": [580, 260, 620, 10],
+ "console>preview": [580, 530, 620, 10],
+ "preview>search": [885, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "console", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "console"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview"
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 260],
+ "editor|console": [0, 270, 1200, 260],
+ "preview|files": [0, 540, 595, 260],
+ "search": [605, 540, 595, 260]
+ },
+ "splitters": {
+ "terminal>editor": [0, 260, 1200, 10],
+ "editor>preview": [0, 530, 1200, 10],
+ "preview>search": [595, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 600, "y": 189 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "console"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview"
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 184],
+ "editor|console": [0, 194, 1200, 336],
+ "preview|files": [0, 540, 595, 260],
+ "search": [605, 540, 595, 260]
+ },
+ "splitters": {
+ "terminal>editor": [0, 184, 1200, 10],
+ "editor>preview": [0, 530, 1200, 10],
+ "preview>search": [595, 540, 10, 260]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 600, "y": 296 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "console"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview"
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 291],
+ "editor|console": [0, 301, 1200, 229],
+ "preview|files": [0, 540, 595, 260],
+ "search": [605, 540, 595, 260]
+ },
+ "splitters": {
+ "terminal>editor": [0, 291, 1200, 10],
+ "editor>preview": [0, 530, 1200, 10],
+ "preview>search": [595, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 114.0909090909091,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 85.9090909090909,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 600, "y": 290 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "console"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview"
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 285],
+ "editor|console": [0, 295, 1200, 235],
+ "preview|files": [0, 540, 595, 260],
+ "search": [605, 540, 595, 260]
+ },
+ "splitters": {
+ "terminal>editor": [0, 285, 1200, 10],
+ "editor>preview": [0, 530, 1200, 10],
+ "preview>search": [595, 540, 10, 260]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "console"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1200, 285],
+ "editor|console": [0, 295, 1200, 235],
+ "preview|files": [0, 540, 40, 260],
+ "search": [50, 540, 1150, 260]
+ },
+ "splitters": {
+ "terminal>editor": [0, 285, 1200, 10],
+ "editor>preview": [0, 530, 1200, 10],
+ "preview>search": [40, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "files"],
+ "active": "preview",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 285],
+ "editor": [580, 295, 620, 235],
+ "preview|files": [580, 540, 40, 260],
+ "search": [630, 540, 570, 260]
+ },
+ "splitters": {
+ "console>terminal": [570, 0, 10, 800],
+ "terminal>editor": [580, 285, 620, 10],
+ "editor>preview": [580, 530, 620, 10],
+ "preview>search": [620, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tab": "terminal", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 570, 800],
+ "preview|terminal": [580, 0, 620, 285],
+ "editor": [580, 295, 620, 235],
+ "files": [580, 540, 40, 260],
+ "search": [630, 540, 570, 260]
+ },
+ "splitters": {
+ "console>preview": [570, 0, 10, 800],
+ "preview>editor": [580, 285, 620, 10],
+ "editor>files": [580, 530, 620, 10],
+ "files>search": [620, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "preview",
+ "after": "editor",
+ "point": { "x": 890, "y": 260 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 570, 800],
+ "preview|terminal": [580, 0, 620, 255],
+ "editor": [580, 265, 620, 265],
+ "files": [580, 540, 40, 260],
+ "search": [630, 540, 570, 260]
+ },
+ "splitters": {
+ "console>preview": [570, 0, 10, 800],
+ "preview>editor": [580, 255, 620, 10],
+ "editor>files": [580, 530, 620, 10],
+ "files>search": [620, 540, 10, 260]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-35",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 97.72727272727273,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 102.27272727272727,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 570, 800],
+ "preview|terminal": [580, 0, 620, 255],
+ "editor": [0, 0, 1200, 800],
+ "files": [580, 540, 40, 260],
+ "search": [630, 540, 570, 260]
+ },
+ "splitters": {
+ "console>preview": [570, 0, 10, 800],
+ "preview>editor": [580, 255, 620, 10],
+ "editor>files": [580, 530, 620, 10],
+ "files>search": [620, 540, 10, 260]
+ },
+ "maximized": "editor"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "files", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 545, 800],
+ "editor": [555, 0, 645, 399],
+ "files": [555, 409, 40, 391],
+ "preview|terminal": [605, 409, 290, 391],
+ "search": [905, 409, 295, 391]
+ },
+ "splitters": {
+ "console>editor": [545, 0, 10, 800],
+ "editor>files": [555, 399, 645, 10],
+ "files>preview": [595, 409, 10, 391],
+ "preview>search": [895, 409, 10, 391]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "console", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["search"], "active": "search" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "search": [0, 0, 570, 395],
+ "console": [0, 405, 570, 395],
+ "editor": [580, 0, 620, 399],
+ "files": [580, 409, 40, 391],
+ "preview|terminal": [630, 409, 570, 391]
+ },
+ "splitters": {
+ "search>editor": [570, 0, 10, 800],
+ "search>console": [0, 395, 570, 10],
+ "editor>files": [580, 399, 620, 10],
+ "files>preview": [620, 409, 10, 391]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "files", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 545, 800],
+ "editor": [555, 0, 645, 399],
+ "files": [555, 409, 40, 391],
+ "search": [605, 409, 295, 391],
+ "preview|terminal": [910, 409, 290, 391]
+ },
+ "splitters": {
+ "console>editor": [545, 0, 10, 800],
+ "editor>files": [555, 399, 645, 10],
+ "files>search": [595, 409, 10, 391],
+ "search>preview": [900, 409, 10, 391]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 545, 800],
+ "editor": [555, 0, 645, 40],
+ "files": [555, 50, 40, 750],
+ "search": [605, 50, 295, 750],
+ "preview|terminal": [910, 50, 290, 750]
+ },
+ "splitters": {
+ "console>editor": [545, 0, 10, 800],
+ "editor>files": [555, 40, 645, 10],
+ "files>search": [595, 50, 10, 750],
+ "search>preview": [900, 50, 10, 750]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-41",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 102.27272727272727,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 97.72727272727273,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "console", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 545, 800],
+ "editor": [555, 0, 645, 40],
+ "files": [555, 50, 40, 750],
+ "search": [605, 50, 295, 750],
+ "preview|terminal": [910, 50, 290, 750]
+ },
+ "splitters": {
+ "console>editor": [545, 0, 10, 800],
+ "editor>files": [555, 40, 645, 10],
+ "files>search": [595, 50, 10, 750],
+ "search>preview": [900, 50, 10, 750]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "preview", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 281, 800],
+ "files": [291, 0, 40, 800],
+ "search": [341, 0, 282, 800],
+ "preview|terminal": [633, 0, 276, 800],
+ "editor": [919, 0, 281, 800]
+ },
+ "splitters": {
+ "console>files": [281, 0, 10, 800],
+ "files>search": [331, 0, 10, 800],
+ "search>preview": [623, 0, 10, 800],
+ "preview>editor": [909, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "search",
+ "after": "preview",
+ "point": { "x": 654, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 281, 800],
+ "files": [291, 0, 40, 800],
+ "search": [341, 0, 308, 800],
+ "preview|terminal": [659, 0, 250, 800],
+ "editor": [919, 0, 281, 800]
+ },
+ "splitters": {
+ "console>files": [281, 0, 10, 800],
+ "files>search": [331, 0, 10, 800],
+ "search>preview": [649, 0, 10, 800],
+ "preview>editor": [909, 0, 10, 800]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tabsetOf": "files", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console", "files"], "active": "console" },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "console|files": [0, 0, 294, 800],
+ "search": [304, 0, 321, 800],
+ "preview|terminal": [635, 0, 261, 800],
+ "editor": [906, 0, 294, 800]
+ },
+ "splitters": {
+ "console>search": [294, 0, 10, 800],
+ "search>preview": [625, 0, 10, 800],
+ "preview>editor": [896, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 294, 800],
+ "search": [304, 0, 321, 800],
+ "preview|terminal": [635, 0, 261, 800],
+ "editor": [906, 0, 294, 395],
+ "files": [906, 405, 294, 395]
+ },
+ "splitters": {
+ "console>search": [294, 0, 10, 800],
+ "search>preview": [625, 0, 10, 800],
+ "preview>editor": [896, 0, 10, 800],
+ "editor>files": [906, 395, 294, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 110.85964244960063,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.86763027767212,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-52",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "files",
+ "point": { "x": 1053, "y": 571 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 294, 800],
+ "search": [304, 0, 321, 800],
+ "preview|terminal": [635, 0, 261, 800],
+ "editor": [906, 0, 294, 566],
+ "files": [906, 576, 294, 224]
+ },
+ "splitters": {
+ "console>search": [294, 0, 10, 800],
+ "search>preview": [625, 0, 10, 800],
+ "preview>editor": [896, 0, 10, 800],
+ "editor>files": [906, 566, 294, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 410, 800],
+ "preview|terminal": [420, 0, 360, 800],
+ "editor|search": [790, 0, 410, 566],
+ "files": [790, 576, 410, 224]
+ },
+ "splitters": {
+ "console>preview": [410, 0, 10, 800],
+ "preview>editor": [780, 0, 10, 800],
+ "editor>files": [790, 566, 410, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 410, 800],
+ "preview|terminal": [420, 0, 360, 800],
+ "editor|search": [790, 0, 410, 566],
+ "files": [0, 0, 1200, 800]
+ },
+ "splitters": {
+ "console>preview": [410, 0, 10, 800],
+ "preview>editor": [780, 0, 10, 800],
+ "editor>files": [790, 566, 410, 10]
+ },
+ "maximized": "files"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "console", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor", "search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 301, 800],
+ "editor|search": [311, 0, 427, 800],
+ "preview|terminal": [748, 0, 267, 800],
+ "files": [1025, 0, 175, 800]
+ },
+ "splitters": {
+ "console>editor": [301, 0, 10, 800],
+ "editor>preview": [738, 0, 10, 800],
+ "preview>files": [1015, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor", "search"], "active": "search" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 301, 800],
+ "editor|search": [311, 0, 427, 800],
+ "preview|terminal": [748, 0, 267, 800],
+ "files": [1025, 0, 175, 800]
+ },
+ "splitters": {
+ "console>editor": [301, 0, 10, 800],
+ "editor>preview": [738, 0, 10, 800],
+ "preview>files": [1015, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 148.16901408450704,
+ "nodName": "",
+ "uidNode": "id-52",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.86763027767212,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 51.83098591549295,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "files" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1200, 282],
+ "console": [0, 292, 356, 508],
+ "editor|search": [366, 292, 509, 508],
+ "preview|terminal": [885, 292, 315, 508]
+ },
+ "splitters": {
+ "files>console": [0, 282, 1200, 10],
+ "console>editor": [356, 292, 10, 508],
+ "editor>preview": [875, 292, 10, 508]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tabsetOf": "files", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1200, 282],
+ "console": [0, 292, 356, 508],
+ "editor|search": [366, 292, 509, 508],
+ "preview|terminal": [885, 292, 315, 508]
+ },
+ "splitters": {
+ "files>console": [0, 282, 1200, 10],
+ "console>editor": [356, 292, 10, 508],
+ "editor>preview": [875, 292, 10, 508]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1200, 176],
+ "terminal": [0, 498, 1200, 302],
+ "console": [0, 186, 356, 302],
+ "editor|search": [366, 186, 509, 302],
+ "preview": [885, 186, 315, 302]
+ },
+ "splitters": {
+ "files>console": [0, 176, 1200, 10],
+ "console>terminal": [0, 488, 1200, 10],
+ "console>editor": [356, 186, 10, 302],
+ "editor>preview": [875, 186, 10, 302]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1492, "height": 674 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1492, 150],
+ "terminal": [0, 422, 1492, 252],
+ "console": [0, 160, 444, 252],
+ "editor|search": [454, 160, 637, 252],
+ "preview": [1101, 160, 391, 252]
+ },
+ "splitters": {
+ "files>console": [0, 150, 1492, 10],
+ "console>terminal": [0, 412, 1492, 10],
+ "console>editor": [444, 160, 10, 252],
+ "editor>preview": [1091, 160, 10, 252]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "search"],
+ "active": "search"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 1492, 150],
+ "terminal": [0, 422, 1492, 252],
+ "console": [0, 160, 444, 252],
+ "editor|search": [0, 0, 1492, 674],
+ "preview": [1101, 160, 391, 252]
+ },
+ "splitters": {
+ "files>console": [0, 150, 1492, 10],
+ "console>terminal": [0, 412, 1492, 10],
+ "console>editor": [444, 160, 10, 252],
+ "editor>preview": [1091, 160, 10, 252]
+ },
+ "maximized": "editor|search"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-52",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-61",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 51.83098591549295,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-62",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 148.16901408450704,
+ "nodName": "",
+ "uidNode": "id-52",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.86763027767212,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-64",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-07.json b/packages/core/test/fixtures/v1/scenario-07.json
new file mode 100644
index 0000000..97ebaf7
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-07.json
@@ -0,0 +1,2699 @@
+{
+ "name": "7 tabs, seed 7",
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview",
+ "console",
+ "files",
+ "search",
+ "git"
+ ],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 545, 800],
+ "terminal": [555, 0, 645, 345],
+ "preview": [555, 355, 293, 445],
+ "console": [858, 355, 342, 193],
+ "files": [858, 558, 166, 242],
+ "search": [1034, 558, 166, 116],
+ "git": [1034, 684, 166, 116]
+ },
+ "splitters": {
+ "editor>terminal": [545, 0, 10, 800],
+ "terminal>preview": [555, 345, 645, 10],
+ "preview>console": [848, 355, 10, 445],
+ "console>files": [858, 548, 342, 10],
+ "files>search": [1024, 558, 10, 242],
+ "search>git": [1034, 674, 166, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": { "op": "resize", "width": 543, "height": 1026 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "search"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 217, 1026],
+ "terminal": [227, 0, 316, 458],
+ "preview": [227, 468, 128, 558],
+ "console": [365, 468, 178, 249],
+ "files": [365, 727, 84, 299],
+ "search": [459, 727, 84, 145],
+ "git": [459, 882, 84, 144]
+ },
+ "splitters": {
+ "editor>terminal": [217, 0, 10, 1026],
+ "terminal>preview": [227, 458, 316, 10],
+ "preview>console": [355, 468, 10, 558],
+ "console>files": [365, 717, 178, 10],
+ "files>search": [449, 727, 10, 299],
+ "search>git": [459, 872, 84, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 385, "y": 320 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "search"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 217, 1026],
+ "terminal": [227, 0, 316, 315],
+ "preview": [227, 325, 128, 701],
+ "console": [365, 325, 178, 321],
+ "files": [365, 656, 84, 370],
+ "search": [459, 656, 84, 180],
+ "git": [459, 846, 84, 180]
+ },
+ "splitters": {
+ "editor>terminal": [217, 0, 10, 1026],
+ "terminal>preview": [227, 315, 316, 10],
+ "preview>console": [355, 325, 10, 701],
+ "console>files": [365, 646, 178, 10],
+ "files>search": [449, 656, 10, 370],
+ "search>git": [459, 836, 84, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "search"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 217, 1026],
+ "terminal": [227, 0, 316, 315],
+ "preview": [227, 325, 128, 701],
+ "console": [0, 0, 543, 1026],
+ "files": [365, 656, 84, 370],
+ "search": [459, 656, 84, 180],
+ "git": [459, 846, 84, 180]
+ },
+ "splitters": {
+ "editor>terminal": [217, 0, 10, 1026],
+ "terminal>preview": [227, 315, 316, 10],
+ "preview>console": [355, 325, 10, 701],
+ "console>files": [365, 646, 178, 10],
+ "files>search": [449, 656, 10, 370],
+ "search>git": [459, 836, 84, 10]
+ },
+ "maximized": "console"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 217, 1026],
+ "terminal": [227, 0, 316, 331],
+ "search|preview": [227, 341, 128, 685],
+ "console": [365, 341, 178, 338],
+ "files": [365, 689, 84, 337],
+ "git": [459, 689, 84, 337]
+ },
+ "splitters": {
+ "editor>terminal": [217, 0, 10, 1026],
+ "terminal>search": [227, 331, 316, 10],
+ "search>console": [355, 341, 10, 685],
+ "console>files": [365, 679, 178, 10],
+ "files>git": [449, 689, 10, 337]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1380, "height": 951 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 635, 951],
+ "terminal": [645, 0, 735, 307],
+ "search|preview": [645, 317, 338, 634],
+ "console": [993, 317, 387, 312],
+ "files": [993, 639, 189, 312],
+ "git": [1192, 639, 188, 312]
+ },
+ "splitters": {
+ "editor>terminal": [635, 0, 10, 951],
+ "terminal>search": [645, 307, 735, 10],
+ "search>console": [983, 317, 10, 634],
+ "console>files": [993, 629, 387, 10],
+ "files>git": [1182, 639, 10, 312]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 65.78947368421053,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 134.21052631578948,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "preview", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1380, 307],
+ "editor": [0, 317, 660, 312],
+ "search|preview": [0, 639, 660, 312],
+ "console": [670, 317, 710, 312],
+ "files": [670, 639, 350, 312],
+ "git": [1030, 639, 350, 312]
+ },
+ "splitters": {
+ "terminal>editor": [0, 307, 1380, 10],
+ "editor>console": [660, 317, 10, 634],
+ "editor>search": [0, 629, 660, 10],
+ "console>files": [670, 629, 710, 10],
+ "files>git": [1020, 639, 10, 312]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1380, 307],
+ "editor": [0, 317, 660, 584],
+ "search|preview": [0, 911, 660, 40],
+ "console": [670, 317, 710, 312],
+ "files": [670, 639, 350, 312],
+ "git": [1030, 639, 350, 312]
+ },
+ "splitters": {
+ "terminal>editor": [0, 307, 1380, 10],
+ "editor>console": [660, 317, 10, 634],
+ "editor>search": [0, 901, 660, 10],
+ "console>files": [670, 629, 710, 10],
+ "files>git": [1020, 639, 10, 312]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "preview", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1380, 307],
+ "editor": [0, 317, 660, 584],
+ "search|preview": [0, 911, 660, 40],
+ "console": [670, 317, 710, 312],
+ "files": [670, 639, 350, 312],
+ "git": [1030, 639, 350, 312]
+ },
+ "splitters": {
+ "terminal>editor": [0, 307, 1380, 10],
+ "editor>console": [660, 317, 10, 634],
+ "editor>search": [0, 901, 660, 10],
+ "console>files": [670, 629, 710, 10],
+ "files>git": [1020, 639, 10, 312]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "git" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1380, 307],
+ "editor": [0, 317, 660, 584],
+ "search|preview": [0, 911, 660, 40],
+ "console": [670, 317, 710, 312],
+ "files": [670, 639, 660, 312],
+ "git": [1340, 639, 40, 312]
+ },
+ "splitters": {
+ "terminal>editor": [0, 307, 1380, 10],
+ "editor>console": [660, 317, 10, 634],
+ "editor>search": [0, 901, 660, 10],
+ "console>files": [670, 629, 710, 10],
+ "files>git": [1330, 639, 10, 312]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1035, "height": 787 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 253],
+ "editor": [0, 263, 488, 474],
+ "search|preview": [0, 747, 488, 40],
+ "console": [498, 263, 537, 257],
+ "files": [498, 530, 487, 257],
+ "git": [995, 530, 40, 257]
+ },
+ "splitters": {
+ "terminal>editor": [0, 253, 1035, 10],
+ "editor>console": [488, 263, 10, 524],
+ "editor>search": [0, 737, 488, 10],
+ "console>files": [498, 520, 537, 10],
+ "files>git": [985, 530, 10, 257]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 65.78947368421053,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 134.21052631578948,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "search",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "git",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "files",
+ "point": { "x": 766.5, "y": 496 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 253],
+ "editor": [0, 263, 488, 474],
+ "search|preview": [0, 747, 488, 40],
+ "console": [498, 263, 537, 228],
+ "files": [498, 501, 487, 286],
+ "git": [995, 501, 40, 286]
+ },
+ "splitters": {
+ "terminal>editor": [0, 253, 1035, 10],
+ "editor>console": [488, 263, 10, 524],
+ "editor>search": [0, 737, 488, 10],
+ "console>files": [498, 491, 537, 10],
+ "files>git": [985, 501, 10, 286]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tab": "search", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "editor", "preview"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 253],
+ "search|editor|preview": [0, 263, 488, 524],
+ "console": [498, 263, 537, 228],
+ "files": [498, 501, 487, 286],
+ "git": [995, 501, 40, 286]
+ },
+ "splitters": {
+ "terminal>search": [0, 253, 1035, 10],
+ "search>console": [488, 263, 10, 524],
+ "console>files": [498, 491, 537, 10],
+ "files>git": [985, 501, 10, 286]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "search",
+ "point": { "x": 517.5, "y": 413 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "editor", "preview"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 408],
+ "search|editor|preview": [0, 418, 488, 369],
+ "console": [498, 418, 537, 161],
+ "files": [498, 589, 487, 198],
+ "git": [995, 589, 40, 198]
+ },
+ "splitters": {
+ "terminal>search": [0, 408, 1035, 10],
+ "search>console": [488, 418, 10, 369],
+ "console>files": [498, 579, 537, 10],
+ "files>git": [985, 589, 10, 198]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 266],
+ "editor": [0, 548, 1035, 239],
+ "search|preview": [0, 276, 488, 262],
+ "console": [498, 276, 537, 115],
+ "files": [498, 401, 487, 137],
+ "git": [995, 401, 40, 137]
+ },
+ "splitters": {
+ "terminal>search": [0, 266, 1035, 10],
+ "search>editor": [0, 538, 1035, 10],
+ "search>console": [488, 276, 10, 262],
+ "console>files": [498, 391, 537, 10],
+ "files>git": [985, 401, 10, 137]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "git" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["git"], "active": "git" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 266],
+ "search|preview": [0, 276, 513, 262],
+ "editor": [0, 548, 513, 239],
+ "git": [523, 548, 512, 239],
+ "console": [523, 276, 512, 120],
+ "files": [523, 406, 512, 132]
+ },
+ "splitters": {
+ "terminal>search": [0, 266, 1035, 10],
+ "search>editor": [0, 538, 1035, 10],
+ "search>console": [513, 276, 10, 262],
+ "editor>git": [513, 548, 10, 239],
+ "console>files": [523, 396, 512, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 113.75579598145286,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 86.24420401854714,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 86.63594470046083,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-42",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-47",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "git" },
+ "target": { "tabsetOf": "console", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 247],
+ "editor": [0, 565, 1035, 222],
+ "search|preview": [0, 257, 513, 298],
+ "git": [523, 257, 512, 95],
+ "console": [523, 362, 512, 88],
+ "files": [523, 460, 512, 95]
+ },
+ "splitters": {
+ "terminal>search": [0, 247, 1035, 10],
+ "search>editor": [0, 555, 1035, 10],
+ "search>git": [513, 257, 10, 298],
+ "git>console": [523, 352, 512, 10],
+ "console>files": [523, 450, 512, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 247],
+ "editor": [0, 565, 1035, 222],
+ "search|preview": [0, 257, 513, 298],
+ "git": [523, 257, 512, 95],
+ "console": [523, 362, 512, 88],
+ "files": [523, 460, 512, 95]
+ },
+ "splitters": {
+ "terminal>search": [0, 247, 1035, 10],
+ "search>editor": [0, 555, 1035, 10],
+ "search>git": [513, 257, 10, 298],
+ "git>console": [523, 352, 512, 10],
+ "console>files": [523, 450, 512, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "git", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "git"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 1035, 380],
+ "search|preview": [0, 390, 513, 397],
+ "editor|git": [523, 390, 512, 130],
+ "console": [523, 530, 512, 117],
+ "files": [523, 657, 512, 130]
+ },
+ "splitters": {
+ "terminal>search": [0, 380, 1035, 10],
+ "search>editor": [513, 390, 10, 397],
+ "editor>console": [523, 520, 512, 10],
+ "console>files": [523, 647, 512, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["search", "preview"], "active": "search" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["editor", "git"],
+ "active": "editor"
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "search|preview": [0, 0, 513, 787],
+ "terminal": [523, 0, 512, 210],
+ "editor|git": [523, 220, 512, 189],
+ "console": [523, 419, 512, 169],
+ "files": [523, 598, 512, 189]
+ },
+ "splitters": {
+ "search>terminal": [513, 0, 10, 787],
+ "terminal>editor": [523, 210, 512, 10],
+ "editor>console": [523, 409, 512, 10],
+ "console>files": [523, 588, 512, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 220, 1035, 189],
+ "console": [0, 419, 1035, 169],
+ "files": [0, 598, 1035, 189],
+ "terminal": [0, 0, 513, 210],
+ "search|preview": [523, 0, 512, 210]
+ },
+ "splitters": {
+ "terminal>editor": [0, 210, 1035, 10],
+ "editor>console": [0, 409, 1035, 10],
+ "console>files": [0, 588, 1035, 10],
+ "terminal>search": [513, 0, 10, 210]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 113.75579598145286,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.63594470046083,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "editor",
+ "point": { "x": 517.5, "y": 84 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 89, 1035, 320],
+ "console": [0, 419, 1035, 169],
+ "files": [0, 598, 1035, 189],
+ "terminal": [0, 0, 513, 79],
+ "search|preview": [523, 0, 512, 79]
+ },
+ "splitters": {
+ "terminal>editor": [0, 79, 1035, 10],
+ "editor>console": [0, 409, 1035, 10],
+ "console>files": [0, 588, 1035, 10],
+ "terminal>search": [513, 0, 10, 79]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 89, 1035, 320],
+ "console": [0, 419, 1035, 169],
+ "files": [0, 598, 1035, 189],
+ "terminal": [0, 0, 985, 79],
+ "search|preview": [995, 0, 40, 79]
+ },
+ "splitters": {
+ "terminal>editor": [0, 79, 1035, 10],
+ "editor>console": [0, 409, 1035, 10],
+ "console>files": [0, 588, 1035, 10],
+ "terminal>search": [985, 0, 10, 79]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "console", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 89, 1035, 320],
+ "console": [0, 419, 1035, 169],
+ "files": [0, 598, 1035, 189],
+ "terminal": [0, 0, 985, 79],
+ "search|preview": [995, 0, 40, 79]
+ },
+ "splitters": {
+ "terminal>editor": [0, 79, 1035, 10],
+ "editor>console": [0, 409, 1035, 10],
+ "console>files": [0, 588, 1035, 10],
+ "terminal>search": [985, 0, 10, 79]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "preview", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 50, 1035, 339],
+ "console": [0, 399, 1035, 178],
+ "files": [0, 587, 1035, 200],
+ "terminal": [0, 0, 513, 40],
+ "search|preview": [523, 0, 512, 40]
+ },
+ "splitters": {
+ "terminal>editor": [0, 40, 1035, 10],
+ "editor>console": [0, 389, 1035, 10],
+ "console>files": [0, 577, 1035, 10],
+ "terminal>search": [513, 0, 10, 40]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 50, 1035, 339],
+ "console": [0, 0, 1035, 787],
+ "files": [0, 587, 1035, 200],
+ "terminal": [0, 0, 513, 40],
+ "search|preview": [523, 0, 512, 40]
+ },
+ "splitters": {
+ "terminal>editor": [0, 40, 1035, 10],
+ "editor>console": [0, 389, 1035, 10],
+ "console>files": [0, 577, 1035, 10],
+ "terminal>search": [513, 0, 10, 40]
+ },
+ "maximized": "console"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-17",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "search",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 187.6226422407737,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.63594470046083,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1317, "height": 537 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "tabs": ["search", "preview"],
+ "active": "search"
+ }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 50, 1317, 214],
+ "console": [0, 0, 1317, 537],
+ "files": [0, 404, 1317, 133],
+ "terminal": [0, 0, 654, 40],
+ "search|preview": [664, 0, 653, 40]
+ },
+ "splitters": {
+ "terminal>editor": [0, 40, 1317, 10],
+ "editor>console": [0, 264, 1317, 10],
+ "console>files": [0, 394, 1317, 10],
+ "terminal>search": [654, 0, 10, 40]
+ },
+ "maximized": "console"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "files", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["search", "preview"], "active": "search" },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "search|preview": [0, 0, 1317, 92],
+ "editor|git": [0, 102, 1317, 137],
+ "console": [0, 249, 1317, 85],
+ "files": [0, 344, 1317, 91],
+ "terminal": [0, 445, 1317, 92]
+ },
+ "splitters": {
+ "search>editor": [0, 92, 1317, 10],
+ "editor>console": [0, 239, 1317, 10],
+ "console>files": [0, 334, 1317, 10],
+ "files>terminal": [0, 435, 1317, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["search", "preview"], "active": "search" },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "search|preview": [0, 0, 1317, 103],
+ "editor|git": [0, 113, 1317, 157],
+ "console": [0, 280, 1317, 94],
+ "files": [0, 384, 1317, 103],
+ "terminal": [0, 497, 1317, 40]
+ },
+ "splitters": {
+ "search>editor": [0, 103, 1317, 10],
+ "editor>console": [0, 270, 1317, 10],
+ "console>files": [0, 374, 1317, 10],
+ "files>terminal": [0, 487, 1317, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "search", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["search"], "active": "search" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 113, 1317, 157],
+ "console": [0, 280, 1317, 94],
+ "files": [0, 384, 1317, 103],
+ "terminal": [0, 497, 1317, 40],
+ "search": [0, 0, 654, 103],
+ "preview": [664, 0, 653, 103]
+ },
+ "splitters": {
+ "search>editor": [0, 103, 1317, 10],
+ "editor>console": [0, 270, 1317, 10],
+ "console>files": [0, 374, 1317, 10],
+ "files>terminal": [0, 487, 1317, 10],
+ "search>preview": [654, 0, 10, 103]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "files",
+ "after": "terminal",
+ "point": { "x": 658.5, "y": 601 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["search"], "active": "search" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["editor", "git"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|git": [0, 113, 1317, 157],
+ "console": [0, 280, 1317, 94],
+ "files": [0, 384, 1317, 103],
+ "terminal": [0, 497, 1317, 40],
+ "search": [0, 0, 654, 103],
+ "preview": [664, 0, 653, 103]
+ },
+ "splitters": {
+ "search>editor": [0, 103, 1317, 10],
+ "editor>console": [0, 270, 1317, 10],
+ "console>files": [0, 374, 1317, 10],
+ "files>terminal": [0, 487, 1317, 10],
+ "search>preview": [654, 0, 10, 103]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-63",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-64",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 187.6226422407737,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 86.63594470046083,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-08.json b/packages/core/test/fixtures/v1/scenario-08.json
new file mode 100644
index 0000000..e9dbce5
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-08.json
@@ -0,0 +1,3020 @@
+{
+ "name": "8 tabs, seed 8",
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview",
+ "console",
+ "files",
+ "search",
+ "git",
+ "output"
+ ],
+ "container": { "width": 640, "height": 420 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 240, 420],
+ "terminal": [250, 0, 390, 155],
+ "preview": [250, 165, 140, 255],
+ "console": [400, 165, 240, 98],
+ "files": [400, 273, 90, 147],
+ "search": [500, 273, 140, 69],
+ "git": [500, 352, 65, 68],
+ "output": [575, 352, 65, 68]
+ },
+ "splitters": {
+ "editor>terminal": [240, 0, 10, 420],
+ "terminal>preview": [250, 155, 390, 10],
+ "preview>console": [390, 165, 10, 255],
+ "console>files": [400, 263, 240, 10],
+ "files>search": [490, 273, 10, 147],
+ "search>git": [500, 342, 140, 10],
+ "git>output": [565, 352, 10, 68]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "git",
+ "after": "output",
+ "point": { "x": 672, "y": 386 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "search"
+ ],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 240, 420],
+ "terminal": [250, 0, 390, 155],
+ "preview": [250, 165, 140, 255],
+ "console": [400, 165, 240, 98],
+ "files": [400, 273, 90, 147],
+ "search": [500, 273, 140, 69],
+ "git": [500, 352, 90, 68],
+ "output": [600, 352, 40, 68]
+ },
+ "splitters": {
+ "editor>terminal": [240, 0, 10, 420],
+ "terminal>preview": [250, 155, 390, 10],
+ "preview>console": [390, 165, 10, 255],
+ "console>files": [400, 263, 240, 10],
+ "files>search": [490, 273, 10, 147],
+ "search>git": [500, 342, 140, 10],
+ "git>output": [590, 352, 10, 68]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "search", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "terminal",
+ "search"
+ ],
+ "active": "terminal"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 173, 420],
+ "preview": [183, 0, 174, 420],
+ "console": [367, 0, 273, 180],
+ "files": [367, 190, 107, 230],
+ "terminal|search": [484, 190, 156, 110],
+ "git": [484, 310, 106, 110],
+ "output": [600, 310, 40, 110]
+ },
+ "splitters": {
+ "editor>preview": [173, 0, 10, 420],
+ "preview>console": [357, 0, 10, 420],
+ "console>files": [367, 180, 273, 10],
+ "files>terminal": [474, 190, 10, 230],
+ "terminal>git": [484, 300, 156, 10],
+ "git>output": [590, 310, 10, 110]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [183, 0, 174, 420],
+ "editor": [0, 0, 173, 205],
+ "terminal|search": [0, 215, 173, 205],
+ "console": [367, 0, 273, 205],
+ "files": [367, 215, 84, 205],
+ "git": [461, 215, 129, 205],
+ "output": [600, 215, 40, 205]
+ },
+ "splitters": {
+ "editor>preview": [173, 0, 10, 420],
+ "preview>console": [357, 0, 10, 420],
+ "editor>terminal": [0, 205, 173, 10],
+ "console>files": [367, 205, 273, 10],
+ "files>git": [451, 215, 10, 205],
+ "git>output": [590, 215, 10, 205]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "files",
+ "point": { "x": 503.5, "y": 242 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [183, 0, 174, 420],
+ "editor": [0, 0, 173, 205],
+ "terminal|search": [0, 215, 173, 205],
+ "console": [367, 0, 273, 237],
+ "files": [367, 247, 84, 173],
+ "git": [461, 247, 129, 173],
+ "output": [600, 247, 40, 173]
+ },
+ "splitters": {
+ "editor>preview": [173, 0, 10, 420],
+ "preview>console": [357, 0, 10, 420],
+ "editor>terminal": [0, 205, 173, 10],
+ "console>files": [367, 237, 273, 10],
+ "files>git": [451, 247, 10, 173],
+ "git>output": [590, 247, 10, 173]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 896, "height": 933 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [269, 0, 258, 933],
+ "editor": [0, 0, 259, 462],
+ "terminal|search": [0, 472, 259, 461],
+ "console": [537, 0, 359, 543],
+ "files": [537, 553, 113, 380],
+ "git": [660, 553, 186, 380],
+ "output": [856, 553, 40, 380]
+ },
+ "splitters": {
+ "editor>preview": [259, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 259, 10],
+ "console>files": [537, 543, 359, 10],
+ "files>git": [650, 553, 10, 380],
+ "git>output": [846, 553, 10, 380]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-42",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 119.39393939393939,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 80.60606060606061,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [269, 0, 258, 933],
+ "editor": [0, 0, 259, 462],
+ "terminal|search": [0, 472, 259, 461],
+ "console": [537, 0, 359, 40],
+ "files": [537, 50, 113, 883],
+ "git": [660, 50, 186, 883],
+ "output": [856, 50, 40, 883]
+ },
+ "splitters": {
+ "editor>preview": [259, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 259, 10],
+ "console>files": [537, 40, 359, 10],
+ "files>git": [650, 50, 10, 883],
+ "git>output": [846, 50, 10, 883]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "git" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [269, 0, 258, 933],
+ "editor": [0, 0, 259, 462],
+ "terminal|search": [0, 472, 259, 461],
+ "console": [537, 0, 359, 40],
+ "files": [537, 50, 259, 883],
+ "git": [806, 50, 40, 883],
+ "output": [856, 50, 40, 883]
+ },
+ "splitters": {
+ "editor>preview": [259, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 259, 10],
+ "console>files": [537, 40, 359, 10],
+ "files>git": [796, 50, 10, 883],
+ "git>output": [846, 50, 10, 883]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 453, "y": 466.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [458, 0, 69, 933],
+ "editor": [0, 0, 448, 462],
+ "terminal|search": [0, 472, 448, 461],
+ "console": [537, 0, 359, 40],
+ "files": [537, 50, 259, 883],
+ "git": [806, 50, 40, 883],
+ "output": [856, 50, 40, 883]
+ },
+ "splitters": {
+ "editor>preview": [448, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 448, 10],
+ "console>files": [537, 40, 359, 10],
+ "files>git": [796, 50, 10, 883],
+ "git>output": [846, 50, 10, 883]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [458, 0, 69, 933],
+ "editor": [0, 0, 448, 462],
+ "terminal|search": [0, 472, 448, 461],
+ "console": [537, 0, 359, 40],
+ "files": [537, 50, 259, 883],
+ "git": [806, 50, 40, 883],
+ "output": [856, 50, 40, 883]
+ },
+ "splitters": {
+ "editor>preview": [448, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 448, 10],
+ "console>files": [537, 40, 359, 10],
+ "files>git": [796, 50, 10, 883],
+ "git>output": [846, 50, 10, 883]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "git" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [458, 0, 69, 933],
+ "editor": [0, 0, 448, 462],
+ "terminal|search": [0, 472, 448, 461],
+ "console": [537, 0, 359, 40],
+ "files": [537, 50, 40, 883],
+ "git": [587, 50, 259, 883],
+ "output": [856, 50, 40, 883]
+ },
+ "splitters": {
+ "editor>preview": [448, 0, 10, 933],
+ "preview>console": [527, 0, 10, 933],
+ "editor>terminal": [0, 462, 448, 10],
+ "console>files": [537, 40, 359, 10],
+ "files>git": [577, 50, 10, 883],
+ "git>output": [846, 50, 10, 883]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 186.72768878718534,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-42",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 13.272311212814644,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 119.39393939393939,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 80.60606060606061,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 497, 896, 436],
+ "preview": [489, 0, 72, 487],
+ "editor": [0, 0, 479, 239],
+ "terminal|search": [0, 249, 479, 238],
+ "console": [571, 0, 325, 40],
+ "git": [571, 50, 275, 437],
+ "output": [856, 50, 40, 437]
+ },
+ "splitters": {
+ "editor>files": [0, 487, 896, 10],
+ "editor>preview": [479, 0, 10, 487],
+ "preview>console": [561, 0, 10, 487],
+ "editor>terminal": [0, 239, 479, 10],
+ "console>git": [571, 40, 325, 10],
+ "git>output": [846, 50, 10, 437]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "output", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "output"
+ ],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 497, 896, 436],
+ "terminal|search": [0, 0, 371, 487],
+ "preview": [381, 0, 84, 487],
+ "console": [475, 0, 421, 40],
+ "git": [475, 50, 371, 437],
+ "editor|output": [856, 50, 40, 437]
+ },
+ "splitters": {
+ "terminal>files": [0, 487, 896, 10],
+ "terminal>preview": [371, 0, 10, 487],
+ "preview>console": [465, 0, 10, 487],
+ "console>git": [475, 40, 421, 10],
+ "git>editor": [846, 50, 10, 437]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 448, "y": 652 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "output"
+ ],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 657, 896, 276],
+ "terminal|search": [0, 0, 371, 647],
+ "preview": [381, 0, 84, 647],
+ "console": [475, 0, 421, 40],
+ "git": [475, 50, 371, 597],
+ "editor|output": [856, 50, 40, 597]
+ },
+ "splitters": {
+ "terminal>files": [0, 647, 896, 10],
+ "terminal>preview": [371, 0, 10, 647],
+ "preview>console": [465, 0, 10, 647],
+ "console>git": [475, 40, 421, 10],
+ "git>editor": [846, 50, 10, 597]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 799, "height": 808 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "output"
+ ],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|search": [0, 0, 326, 559],
+ "preview": [336, 0, 77, 559],
+ "console": [423, 0, 376, 40],
+ "git": [423, 50, 326, 509],
+ "editor|output": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>preview": [326, 0, 10, 559],
+ "preview>console": [413, 0, 10, 559],
+ "console>git": [423, 40, 376, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 414, "y": 279.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal"
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "output"
+ ],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|search": [0, 0, 363, 559],
+ "preview": [373, 0, 40, 559],
+ "console": [423, 0, 376, 40],
+ "git": [423, 50, 326, 509],
+ "editor|output": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>preview": [363, 0, 10, 559],
+ "preview>console": [413, 0, 10, 559],
+ "console>git": [423, 40, 376, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-47",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 140.47919293820934,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 113.27231121281464,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 119.39393939393939,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 80.60606060606061,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 59.52080706179067,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "output"
+ ],
+ "active": "editor"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|search": [0, 0, 40, 559],
+ "preview": [50, 0, 40, 559],
+ "console": [100, 0, 699, 40],
+ "git": [100, 50, 649, 509],
+ "editor|output": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>preview": [40, 0, 10, 559],
+ "preview>console": [90, 0, 10, 559],
+ "console>git": [100, 40, 699, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "output", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal",
+ "folded": true
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview",
+ "output"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|search": [0, 0, 40, 559],
+ "console": [50, 0, 749, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview|output": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>console": [40, 0, 10, 559],
+ "console>git": [50, 40, 749, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "git" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "terminal",
+ "folded": true
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview",
+ "output"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|search": [0, 0, 40, 559],
+ "console": [50, 0, 749, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview|output": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>console": [40, 0, 10, 559],
+ "console>git": [50, 40, 749, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "output" },
+ "target": { "tabsetOf": "search", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "search"],
+ "active": "output"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 569, 799, 239],
+ "terminal|output|search": [0, 0, 390, 559],
+ "console": [400, 0, 399, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 559, 799, 10],
+ "terminal>console": [390, 0, 10, 559],
+ "console>git": [400, 40, 399, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 399.5, "y": 560 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "search"],
+ "active": "output"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 565, 799, 243],
+ "terminal|output|search": [0, 0, 390, 555],
+ "console": [400, 0, 399, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 555, 799, 10],
+ "terminal>console": [390, 0, 10, 555],
+ "console>git": [400, 40, 399, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-47",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 139.22155688622755,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 113.27231121281464,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 119.39393939393939,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 80.60606060606061,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 60.778443113772454,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "output" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "search"],
+ "active": "output",
+ "folded": true
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 565, 799, 243],
+ "terminal|output|search": [0, 0, 40, 555],
+ "console": [50, 0, 749, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 555, 799, 10],
+ "terminal>console": [40, 0, 10, 555],
+ "console>git": [50, 40, 749, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 399.5, "y": 731 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "search"],
+ "active": "output",
+ "folded": true
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 736, 799, 72],
+ "terminal|output|search": [0, 0, 40, 726],
+ "console": [50, 0, 749, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 726, 799, 10],
+ "terminal>console": [40, 0, 10, 726],
+ "console>git": [50, 40, 749, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 399.5, "y": 849 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "search"],
+ "active": "output",
+ "folded": true
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git",
+ "folded": true
+ },
+ {
+ "tabs": [
+ "editor",
+ "preview"
+ ],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 768, 799, 40],
+ "terminal|output|search": [0, 0, 40, 758],
+ "console": [50, 0, 749, 40],
+ "git": [50, 50, 699, 509],
+ "editor|preview": [759, 50, 40, 509]
+ },
+ "splitters": {
+ "terminal>files": [0, 758, 799, 10],
+ "terminal>console": [40, 0, 10, 758],
+ "console>git": [50, 40, 749, 10],
+ "git>editor": [749, 50, 10, 509]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "git", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "terminal",
+ "output",
+ "search",
+ "git"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["editor", "preview"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 799, 40],
+ "files": [0, 768, 799, 40],
+ "terminal|output|search|git": [0, 50, 749, 708],
+ "editor|preview": [759, 50, 40, 708]
+ },
+ "splitters": {
+ "console>terminal": [0, 40, 799, 10],
+ "terminal>files": [0, 758, 799, 10],
+ "terminal>editor": [749, 50, 10, 708]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "files",
+ "point": { "x": 399.5, "y": 760 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "terminal",
+ "output",
+ "search",
+ "git"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["editor", "preview"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 799, 40],
+ "files": [0, 765, 799, 43],
+ "terminal|output|search|git": [0, 50, 749, 705],
+ "editor|preview": [759, 50, 40, 705]
+ },
+ "splitters": {
+ "console>terminal": [0, 40, 799, 10],
+ "terminal>files": [0, 755, 799, 10],
+ "terminal>editor": [749, 50, 10, 705]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-47",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 119.39393939393939,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 80.24405733986573,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0.362003266194883,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 455, "height": 407 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "terminal",
+ "output",
+ "search",
+ "git"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["editor", "preview"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 455, 40],
+ "files": [0, 366, 455, 41],
+ "terminal|output|search|git": [0, 50, 405, 306],
+ "editor|preview": [415, 50, 40, 306]
+ },
+ "splitters": {
+ "console>terminal": [0, 40, 455, 10],
+ "terminal>files": [0, 356, 455, 10],
+ "terminal>editor": [405, 50, 10, 306]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "output", "git"],
+ "active": "terminal"
+ },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["editor", "preview"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 455, 40],
+ "files": [0, 366, 455, 41],
+ "terminal|output|git": [0, 50, 250, 306],
+ "search": [260, 50, 145, 306],
+ "editor|preview": [415, 50, 40, 306]
+ },
+ "splitters": {
+ "console>terminal": [0, 40, 455, 10],
+ "terminal>files": [0, 356, 455, 10],
+ "terminal>search": [250, 50, 10, 306],
+ "search>editor": [405, 50, 10, 306]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "output" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "git"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["editor", "preview"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ { "tabs": ["output"], "active": "output" }
+ ]
+ },
+ "tabsets": {
+ "output": [283, 0, 172, 407],
+ "console": [0, 0, 273, 40],
+ "files": [0, 366, 273, 41],
+ "terminal|git": [0, 50, 129, 306],
+ "search": [139, 50, 84, 306],
+ "editor|preview": [233, 50, 40, 306]
+ },
+ "splitters": {
+ "console>output": [273, 0, 10, 407],
+ "console>terminal": [0, 40, 273, 10],
+ "terminal>files": [0, 356, 273, 10],
+ "terminal>search": [129, 50, 10, 306],
+ "search>editor": [223, 50, 10, 306]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "git"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": [
+ "console",
+ "editor",
+ "preview"
+ ],
+ "active": "console"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ { "tabs": ["output"], "active": "output" }
+ ]
+ },
+ "tabsets": {
+ "output": [283, 0, 172, 407],
+ "files": [0, 366, 273, 41],
+ "terminal|git": [0, 0, 129, 356],
+ "search": [139, 0, 84, 356],
+ "console|editor|preview": [233, 0, 40, 356]
+ },
+ "splitters": {
+ "terminal>output": [273, 0, 10, 407],
+ "terminal>files": [0, 356, 273, 10],
+ "terminal>search": [129, 0, 10, 356],
+ "search>console": [223, 0, 10, 356]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "search", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal", "git"],
+ "active": "terminal"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": [
+ "console",
+ "editor",
+ "preview"
+ ],
+ "active": "console"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ { "tabs": ["output"], "active": "output" }
+ ]
+ },
+ "tabsets": {
+ "output": [258, 0, 197, 407],
+ "files": [0, 366, 248, 41],
+ "terminal|git": [0, 0, 145, 356],
+ "search": [155, 0, 93, 306],
+ "console|editor|preview": [155, 316, 93, 40]
+ },
+ "splitters": {
+ "terminal>output": [248, 0, 10, 407],
+ "terminal>files": [0, 356, 248, 10],
+ "terminal>search": [145, 0, 10, 356],
+ "search>console": [155, 306, 93, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-47",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 80.24405733986573,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-60",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-67",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0.362003266194883,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-64",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-36"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-09.json b/packages/core/test/fixtures/v1/scenario-09.json
new file mode 100644
index 0000000..da8ae06
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-09.json
@@ -0,0 +1,3699 @@
+{
+ "name": "9 tabs, seed 9",
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview",
+ "console",
+ "files",
+ "search",
+ "git",
+ "output",
+ "debug"
+ ],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 520, 800],
+ "terminal": [530, 0, 670, 320],
+ "preview": [530, 330, 280, 470],
+ "console": [820, 330, 380, 180],
+ "files": [820, 520, 160, 280],
+ "search": [990, 520, 210, 110],
+ "git": [990, 640, 100, 160],
+ "output": [1100, 640, 100, 75],
+ "debug": [1100, 725, 100, 75]
+ },
+ "splitters": {
+ "editor>terminal": [520, 0, 10, 800],
+ "terminal>preview": [530, 320, 670, 10],
+ "preview>console": [810, 330, 10, 470],
+ "console>files": [820, 510, 380, 10],
+ "files>search": [980, 520, 10, 280],
+ "search>git": [990, 630, 210, 10],
+ "git>output": [1090, 640, 10, 160],
+ "output>debug": [1100, 715, 100, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "search", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 520, 800],
+ "terminal": [530, 0, 670, 320],
+ "preview": [530, 330, 200, 470],
+ "files": [740, 330, 200, 470],
+ "search": [950, 330, 250, 133],
+ "console": [950, 473, 250, 134],
+ "git": [950, 617, 120, 183],
+ "output": [1080, 617, 120, 87],
+ "debug": [1080, 714, 120, 86]
+ },
+ "splitters": {
+ "editor>terminal": [520, 0, 10, 800],
+ "terminal>preview": [530, 320, 670, 10],
+ "preview>files": [730, 330, 10, 470],
+ "files>search": [940, 330, 10, 470],
+ "search>console": [950, 463, 250, 10],
+ "console>git": [950, 607, 250, 10],
+ "git>output": [1070, 617, 10, 183],
+ "output>debug": [1080, 704, 120, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "search",
+ "after": "console",
+ "point": { "x": 1075, "y": 493 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 520, 800],
+ "terminal": [530, 0, 670, 320],
+ "preview": [530, 330, 200, 470],
+ "files": [740, 330, 200, 470],
+ "search": [950, 330, 250, 158],
+ "console": [950, 498, 250, 109],
+ "git": [950, 617, 120, 183],
+ "output": [1080, 617, 120, 87],
+ "debug": [1080, 714, 120, 86]
+ },
+ "splitters": {
+ "editor>terminal": [520, 0, 10, 800],
+ "terminal>preview": [530, 320, 670, 10],
+ "preview>files": [730, 330, 10, 470],
+ "files>search": [940, 330, 10, 470],
+ "search>console": [950, 488, 250, 10],
+ "console>git": [950, 607, 250, 10],
+ "git>output": [1070, 617, 10, 183],
+ "output>debug": [1080, 704, 120, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "files",
+ "after": "search",
+ "point": { "x": 842, "y": 565 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 520, 800],
+ "terminal": [530, 0, 670, 320],
+ "preview": [530, 330, 200, 470],
+ "files": [740, 330, 97, 470],
+ "search": [847, 330, 353, 158],
+ "console": [847, 498, 353, 109],
+ "git": [847, 617, 172, 183],
+ "output": [1029, 617, 171, 87],
+ "debug": [1029, 714, 171, 86]
+ },
+ "splitters": {
+ "editor>terminal": [520, 0, 10, 800],
+ "terminal>preview": [530, 320, 670, 10],
+ "preview>files": [730, 330, 10, 470],
+ "files>search": [837, 330, 10, 470],
+ "search>console": [847, 488, 353, 10],
+ "console>git": [847, 607, 353, 10],
+ "git>output": [1019, 617, 10, 183],
+ "output>debug": [1029, 704, 171, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1178, "height": 933 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 509, 933],
+ "terminal": [519, 0, 659, 387],
+ "preview": [519, 397, 196, 536],
+ "files": [725, 397, 96, 536],
+ "search": [831, 397, 347, 186],
+ "console": [831, 593, 347, 125],
+ "git": [831, 728, 169, 205],
+ "output": [1010, 728, 168, 98],
+ "debug": [1010, 836, 168, 97]
+ },
+ "splitters": {
+ "editor>terminal": [509, 0, 10, 933],
+ "terminal>preview": [519, 387, 659, 10],
+ "preview>files": [715, 397, 10, 536],
+ "files>search": [821, 397, 10, 536],
+ "search>console": [831, 583, 347, 10],
+ "console>git": [831, 718, 347, 10],
+ "git>output": [1000, 728, 10, 205],
+ "output>debug": [1010, 826, 168, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 261, "height": 411 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 51, 411],
+ "terminal": [61, 0, 200, 126],
+ "preview": [61, 136, 43, 275],
+ "files": [114, 136, 42, 275],
+ "search": [166, 136, 95, 76],
+ "console": [166, 222, 95, 61],
+ "git": [166, 293, 43, 118],
+ "output": [219, 293, 42, 54],
+ "debug": [219, 357, 42, 54]
+ },
+ "splitters": {
+ "editor>terminal": [51, 0, 10, 411],
+ "terminal>preview": [61, 126, 200, 10],
+ "preview>files": [104, 136, 10, 275],
+ "files>search": [156, 136, 10, 275],
+ "search>console": [166, 212, 95, 10],
+ "console>git": [166, 283, 95, 10],
+ "git>output": [209, 293, 10, 118],
+ "output>debug": [219, 347, 42, 10]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 164.375,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 126.20320855614973,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 73.79679144385027,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1508, "height": 336 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 674, 336],
+ "terminal": [684, 0, 824, 88],
+ "preview": [684, 98, 251, 238],
+ "files": [945, 98, 116, 238],
+ "search": [1071, 98, 437, 60],
+ "console": [1071, 168, 437, 52],
+ "git": [1071, 230, 214, 106],
+ "output": [1295, 230, 213, 48],
+ "debug": [1295, 288, 213, 48]
+ },
+ "splitters": {
+ "editor>terminal": [674, 0, 10, 336],
+ "terminal>preview": [684, 88, 824, 10],
+ "preview>files": [935, 98, 10, 238],
+ "files>search": [1061, 98, 10, 238],
+ "search>console": [1071, 158, 437, 10],
+ "console>git": [1071, 220, 437, 10],
+ "git>output": [1285, 230, 10, 106],
+ "output>debug": [1295, 278, 213, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 946, "height": 187 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "output"
+ ],
+ "active": "output"
+ },
+ {
+ "tabs": [
+ "debug"
+ ],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 393, 240],
+ "terminal": [403, 0, 543, 40],
+ "preview": [403, 50, 158, 190],
+ "files": [571, 50, 82, 190],
+ "search": [663, 50, 283, 40],
+ "console": [663, 100, 283, 40],
+ "git": [663, 150, 137, 90],
+ "output": [810, 150, 136, 40],
+ "debug": [810, 200, 136, 40]
+ },
+ "splitters": {
+ "editor>terminal": [393, 0, 10, 240],
+ "terminal>preview": [403, 40, 543, 10],
+ "preview>files": [561, 50, 10, 190],
+ "files>search": [653, 50, 10, 190],
+ "search>console": [663, 90, 283, 10],
+ "console>git": [663, 140, 283, 10],
+ "git>output": [800, 150, 10, 90],
+ "output>debug": [810, 190, 136, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "git", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 40],
+ "preview": [0, 50, 275, 190],
+ "files": [285, 50, 124, 190],
+ "search": [419, 50, 527, 40],
+ "console": [419, 100, 527, 40],
+ "git": [419, 150, 169, 90],
+ "editor": [598, 150, 169, 90],
+ "output": [777, 150, 169, 40],
+ "debug": [777, 200, 169, 40]
+ },
+ "splitters": {
+ "terminal>preview": [0, 40, 946, 10],
+ "preview>files": [275, 50, 10, 190],
+ "files>search": [409, 50, 10, 190],
+ "search>console": [419, 90, 527, 10],
+ "console>git": [419, 140, 527, 10],
+ "git>editor": [588, 150, 10, 90],
+ "editor>output": [767, 150, 10, 90],
+ "output>debug": [777, 190, 169, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 40],
+ "preview": [0, 50, 275, 190],
+ "files": [285, 50, 124, 190],
+ "search": [419, 50, 527, 40],
+ "console": [419, 100, 527, 40],
+ "git": [419, 150, 169, 90],
+ "editor": [598, 150, 169, 90],
+ "output": [777, 150, 169, 40],
+ "debug": [777, 200, 169, 40]
+ },
+ "splitters": {
+ "terminal>preview": [0, 40, 946, 10],
+ "preview>files": [275, 50, 10, 190],
+ "files>search": [409, 50, 10, 190],
+ "search>console": [419, 90, 527, 10],
+ "console>git": [419, 140, 527, 10],
+ "git>editor": [588, 150, 10, 90],
+ "editor>output": [767, 150, 10, 90],
+ "output>debug": [777, 190, 169, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "search" },
+ "target": { "tabsetOf": "console", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search", "console"],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "preview": [0, 75, 275, 165],
+ "files": [285, 75, 124, 165],
+ "search|console": [419, 75, 527, 51],
+ "git": [419, 136, 169, 104],
+ "editor": [598, 136, 169, 104],
+ "output": [777, 136, 169, 47],
+ "debug": [777, 193, 169, 47]
+ },
+ "splitters": {
+ "terminal>preview": [0, 65, 946, 10],
+ "preview>files": [275, 75, 10, 165],
+ "files>search": [409, 75, 10, 165],
+ "search>git": [419, 126, 527, 10],
+ "git>editor": [588, 136, 10, 104],
+ "editor>output": [767, 136, 10, 104],
+ "output>debug": [777, 183, 169, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 164.375,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 73.79679144385027,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "output",
+ "point": { "x": 581, "y": 188 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search", "console"],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "preview": [0, 75, 275, 165],
+ "files": [285, 75, 124, 165],
+ "search|console": [419, 75, 527, 51],
+ "git": [419, 136, 169, 104],
+ "editor": [598, 136, 40, 104],
+ "output": [648, 136, 298, 47],
+ "debug": [648, 193, 298, 47]
+ },
+ "splitters": {
+ "terminal>preview": [0, 65, 946, 10],
+ "preview>files": [275, 75, 10, 165],
+ "files>search": [409, 75, 10, 165],
+ "search>git": [419, 126, 527, 10],
+ "git>editor": [588, 136, 10, 104],
+ "editor>output": [638, 136, 10, 104],
+ "output>debug": [648, 183, 298, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "search" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search", "console"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 58],
+ "search|console": [0, 187, 946, 53],
+ "preview": [0, 68, 202, 109],
+ "files": [212, 68, 98, 109],
+ "git": [320, 68, 202, 109],
+ "editor": [532, 68, 40, 109],
+ "output": [582, 68, 364, 50],
+ "debug": [582, 128, 364, 49]
+ },
+ "splitters": {
+ "terminal>preview": [0, 58, 946, 10],
+ "preview>search": [0, 177, 946, 10],
+ "preview>files": [202, 68, 10, 109],
+ "files>git": [310, 68, 10, 109],
+ "git>editor": [522, 68, 10, 109],
+ "editor>output": [572, 68, 10, 109],
+ "output>debug": [582, 118, 364, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["output"],
+ "active": "output"
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["search", "console"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "search|console": [0, 200, 946, 40],
+ "preview": [0, 75, 202, 115],
+ "files": [212, 75, 98, 115],
+ "git": [320, 75, 202, 115],
+ "editor": [532, 75, 40, 115],
+ "output": [582, 75, 364, 53],
+ "debug": [582, 138, 364, 52]
+ },
+ "splitters": {
+ "terminal>preview": [0, 65, 946, 10],
+ "preview>search": [0, 190, 946, 10],
+ "preview>files": [202, 75, 10, 115],
+ "files>git": [310, 75, 10, 115],
+ "git>editor": [522, 75, 10, 115],
+ "editor>output": [572, 75, 10, 115],
+ "output>debug": [582, 128, 364, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "output", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["search", "console"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "search|console": [0, 200, 946, 40],
+ "files": [0, 75, 115, 115],
+ "git": [125, 75, 250, 115],
+ "editor": [385, 75, 40, 115],
+ "debug": [435, 138, 511, 52],
+ "preview": [435, 75, 251, 53],
+ "output": [696, 75, 250, 53]
+ },
+ "splitters": {
+ "terminal>files": [0, 65, 946, 10],
+ "files>search": [0, 190, 946, 10],
+ "files>git": [115, 75, 10, 115],
+ "git>editor": [375, 75, 10, 115],
+ "editor>preview": [425, 75, 10, 115],
+ "preview>debug": [435, 128, 511, 10],
+ "preview>output": [686, 75, 10, 53]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "search", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["search", "console"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "files": [0, 75, 120, 115],
+ "git": [130, 75, 265, 115],
+ "editor": [0, 200, 468, 40],
+ "search|console": [478, 200, 468, 40],
+ "debug": [405, 138, 541, 52],
+ "preview": [405, 75, 266, 53],
+ "output": [681, 75, 265, 53]
+ },
+ "splitters": {
+ "terminal>files": [0, 65, 946, 10],
+ "files>editor": [0, 190, 946, 10],
+ "files>git": [120, 75, 10, 115],
+ "git>preview": [395, 75, 10, 115],
+ "editor>search": [468, 200, 10, 40],
+ "preview>debug": [405, 128, 541, 10],
+ "preview>output": [671, 75, 10, 53]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 73.79679144385027,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "search",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-60",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "files", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 946, 65],
+ "console|files": [0, 75, 120, 115],
+ "git": [130, 75, 265, 115],
+ "editor": [0, 200, 468, 40],
+ "search": [478, 200, 468, 40],
+ "debug": [405, 138, 541, 52],
+ "preview": [405, 75, 266, 53],
+ "output": [681, 75, 265, 53]
+ },
+ "splitters": {
+ "terminal>console": [0, 65, 946, 10],
+ "console>editor": [0, 190, 946, 10],
+ "console>git": [120, 75, 10, 115],
+ "git>preview": [395, 75, 10, 115],
+ "editor>search": [468, 200, 10, 40],
+ "preview>debug": [405, 128, 541, 10],
+ "preview>output": [671, 75, 10, 53]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 872, "height": 826 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 872, 358],
+ "console|files": [0, 368, 112, 408],
+ "git": [122, 368, 244, 408],
+ "editor": [0, 786, 431, 40],
+ "search": [441, 786, 431, 40],
+ "debug": [376, 577, 496, 199],
+ "preview": [376, 368, 243, 199],
+ "output": [629, 368, 243, 199]
+ },
+ "splitters": {
+ "terminal>console": [0, 358, 872, 10],
+ "console>editor": [0, 776, 872, 10],
+ "console>git": [112, 368, 10, 408],
+ "git>preview": [366, 368, 10, 408],
+ "editor>search": [431, 786, 10, 40],
+ "preview>debug": [376, 567, 496, 10],
+ "preview>output": [619, 368, 10, 199]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["search"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 872, 358],
+ "console|files": [0, 368, 112, 408],
+ "git": [122, 368, 244, 408],
+ "editor": [0, 786, 822, 40],
+ "search": [832, 786, 40, 40],
+ "debug": [376, 577, 496, 199],
+ "preview": [376, 368, 243, 199],
+ "output": [629, 368, 243, 199]
+ },
+ "splitters": {
+ "terminal>console": [0, 358, 872, 10],
+ "console>editor": [0, 776, 872, 10],
+ "console>git": [112, 368, 10, 408],
+ "git>preview": [366, 368, 10, 408],
+ "editor>search": [822, 786, 10, 40],
+ "preview>debug": [376, 567, 496, 10],
+ "preview>output": [619, 368, 10, 199]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["search"],
+ "active": "search",
+ "folded": true
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 872, 358],
+ "console|files": [0, 368, 112, 408],
+ "git": [122, 368, 244, 408],
+ "editor": [0, 786, 822, 40],
+ "search": [832, 786, 40, 40],
+ "debug": [376, 577, 496, 199],
+ "preview": [0, 0, 872, 826],
+ "output": [629, 368, 243, 199]
+ },
+ "splitters": {
+ "terminal>console": [0, 358, 872, 10],
+ "console>editor": [0, 776, 872, 10],
+ "console>git": [112, 368, 10, 408],
+ "git>preview": [366, 368, 10, 408],
+ "editor>search": [822, 786, 10, 40],
+ "preview>debug": [376, 567, 496, 10],
+ "preview>output": [619, 368, 10, 199]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal", "search"], "active": "search" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal|search": [0, 0, 872, 252],
+ "editor": [0, 574, 872, 252],
+ "console|files": [0, 262, 112, 302],
+ "git": [122, 262, 244, 302],
+ "debug": [376, 418, 496, 146],
+ "preview": [376, 262, 243, 146],
+ "output": [629, 262, 243, 146]
+ },
+ "splitters": {
+ "terminal>console": [0, 252, 872, 10],
+ "console>editor": [0, 564, 872, 10],
+ "console>git": [112, 262, 10, 302],
+ "git>preview": [366, 262, 10, 302],
+ "preview>debug": [376, 408, 496, 10],
+ "preview>output": [619, 262, 10, 146]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 506, "height": 1049 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal", "search"], "active": "search" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal|search": [0, 0, 506, 326],
+ "editor": [0, 723, 506, 326],
+ "console|files": [0, 336, 74, 377],
+ "git": [84, 336, 134, 377],
+ "debug": [228, 530, 278, 183],
+ "preview": [228, 336, 134, 184],
+ "output": [372, 336, 134, 184]
+ },
+ "splitters": {
+ "terminal>console": [0, 326, 506, 10],
+ "console>editor": [0, 713, 506, 10],
+ "console>git": [74, 336, 10, 377],
+ "git>preview": [218, 336, 10, 377],
+ "preview>debug": [228, 520, 278, 10],
+ "preview>output": [362, 336, 10, 184]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal", "search"],
+ "active": "search"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [333, 0, 173, 1049],
+ "terminal|search": [0, 0, 323, 495],
+ "console|files": [0, 505, 54, 544],
+ "git": [64, 505, 80, 544],
+ "debug": [154, 782, 169, 267],
+ "preview": [154, 505, 80, 267],
+ "output": [244, 505, 79, 267]
+ },
+ "splitters": {
+ "terminal>editor": [323, 0, 10, 1049],
+ "terminal>console": [0, 495, 323, 10],
+ "console>git": [54, 505, 10, 544],
+ "git>preview": [144, 505, 10, 544],
+ "preview>debug": [154, 772, 169, 10],
+ "preview>output": [234, 505, 10, 267]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [333, 0, 173, 1049],
+ "terminal": [0, 0, 323, 495],
+ "console|files": [0, 505, 54, 544],
+ "git": [64, 505, 80, 544],
+ "debug": [154, 782, 169, 267],
+ "search|preview": [154, 505, 80, 267],
+ "output": [244, 505, 79, 267]
+ },
+ "splitters": {
+ "terminal>editor": [323, 0, 10, 1049],
+ "terminal>console": [0, 495, 323, 10],
+ "console>git": [54, 505, 10, 544],
+ "git>search": [144, 505, 10, 544],
+ "search>debug": [154, 772, 169, 10],
+ "search>output": [234, 505, 10, 267]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "console", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [358, 0, 148, 1049],
+ "terminal": [0, 0, 348, 495],
+ "files": [0, 505, 49, 544],
+ "console": [59, 505, 65, 544],
+ "git": [134, 505, 64, 544],
+ "debug": [208, 782, 140, 267],
+ "search|preview": [208, 505, 65, 267],
+ "output": [283, 505, 65, 267]
+ },
+ "splitters": {
+ "terminal>editor": [348, 0, 10, 1049],
+ "terminal>files": [0, 495, 348, 10],
+ "files>console": [49, 505, 10, 544],
+ "console>git": [124, 505, 10, 544],
+ "git>search": [198, 505, 10, 544],
+ "search>debug": [208, 772, 140, 10],
+ "search>output": [273, 505, 10, 267]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "search",
+ "after": "output",
+ "point": { "x": 363, "y": 638.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [358, 0, 148, 1049],
+ "terminal": [0, 0, 348, 495],
+ "files": [0, 505, 49, 544],
+ "console": [59, 505, 65, 544],
+ "git": [134, 505, 64, 544],
+ "debug": [208, 782, 140, 267],
+ "search|preview": [208, 505, 90, 267],
+ "output": [308, 505, 40, 267]
+ },
+ "splitters": {
+ "terminal>editor": [348, 0, 10, 1049],
+ "terminal>files": [0, 495, 348, 10],
+ "files>console": [49, 505, 10, 544],
+ "console>git": [124, 505, 10, 544],
+ "git>search": [198, 505, 10, 544],
+ "search>debug": [208, 772, 140, 10],
+ "search>output": [298, 505, 10, 267]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-67",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-56",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "search",
+ "after": "debug",
+ "point": { "x": 278, "y": 763 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [358, 0, 148, 1049],
+ "terminal": [0, 0, 348, 495],
+ "files": [0, 505, 49, 544],
+ "console": [59, 505, 65, 544],
+ "git": [134, 505, 64, 544],
+ "debug": [208, 768, 140, 281],
+ "search|preview": [208, 505, 90, 253],
+ "output": [308, 505, 40, 253]
+ },
+ "splitters": {
+ "terminal>editor": [348, 0, 10, 1049],
+ "terminal>files": [0, 495, 348, 10],
+ "files>console": [49, 505, 10, 544],
+ "console>git": [124, 505, 10, 544],
+ "git>search": [198, 505, 10, 544],
+ "search>debug": [208, 758, 140, 10],
+ "search>output": [298, 505, 10, 253]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["debug"],
+ "active": "debug"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [358, 0, 148, 1049],
+ "terminal": [0, 0, 348, 495],
+ "files": [0, 505, 40, 544],
+ "console": [50, 505, 67, 544],
+ "git": [127, 505, 67, 544],
+ "debug": [204, 768, 144, 281],
+ "search|preview": [204, 505, 94, 253],
+ "output": [308, 505, 40, 253]
+ },
+ "splitters": {
+ "terminal>editor": [348, 0, 10, 1049],
+ "terminal>files": [0, 495, 348, 10],
+ "files>console": [40, 505, 10, 544],
+ "console>git": [117, 505, 10, 544],
+ "git>search": [194, 505, 10, 544],
+ "search>debug": [204, 758, 144, 10],
+ "search>output": [298, 505, 10, 253]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "debug" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ },
+ "tabsets": {
+ "debug": [0, 555, 506, 494],
+ "editor": [358, 0, 148, 545],
+ "terminal": [0, 0, 348, 268],
+ "files": [0, 278, 40, 267],
+ "console": [50, 278, 67, 267],
+ "git": [127, 278, 67, 267],
+ "search|preview": [204, 278, 94, 267],
+ "output": [308, 278, 40, 267]
+ },
+ "splitters": {
+ "terminal>debug": [0, 545, 506, 10],
+ "terminal>editor": [348, 0, 10, 545],
+ "terminal>files": [0, 268, 348, 10],
+ "files>console": [40, 278, 10, 267],
+ "console>git": [117, 278, 10, 267],
+ "git>search": [194, 278, 10, 267],
+ "search>output": [298, 278, 10, 267]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files",
+ "folded": true
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ },
+ "tabsets": {
+ "debug": [0, 555, 506, 494],
+ "editor": [466, 0, 40, 545],
+ "terminal": [0, 0, 456, 268],
+ "files": [0, 278, 40, 267],
+ "console": [50, 278, 94, 267],
+ "git": [154, 278, 94, 267],
+ "search|preview": [258, 278, 148, 267],
+ "output": [416, 278, 40, 267]
+ },
+ "splitters": {
+ "terminal>debug": [0, 545, 506, 10],
+ "terminal>editor": [456, 0, 10, 545],
+ "terminal>files": [0, 268, 456, 10],
+ "files>console": [40, 278, 10, 267],
+ "console>git": [144, 278, 10, 267],
+ "git>search": [248, 278, 10, 267],
+ "search>output": [406, 278, 10, 267]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "output" },
+ "target": { "tabsetOf": "files", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["output"],
+ "active": "output"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": [
+ "search",
+ "preview"
+ ],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["debug"], "active": "debug" }
+ ]
+ },
+ "tabsets": {
+ "debug": [0, 580, 506, 469],
+ "editor": [466, 0, 40, 570],
+ "terminal": [0, 0, 456, 255],
+ "console": [50, 265, 107, 305],
+ "git": [167, 265, 106, 305],
+ "search|preview": [283, 265, 173, 305],
+ "files": [0, 265, 40, 148],
+ "output": [0, 423, 40, 147]
+ },
+ "splitters": {
+ "terminal>debug": [0, 570, 506, 10],
+ "terminal>editor": [456, 0, 10, 570],
+ "terminal>files": [0, 255, 456, 10],
+ "files>console": [40, 265, 10, 305],
+ "console>git": [157, 265, 10, 305],
+ "git>search": [273, 265, 10, 305],
+ "files>output": [0, 413, 40, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-74",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-75",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 35.625,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-78",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-77",
+ "nodOpen": "output",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "output",
+ "uidNode": "id-39"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-67",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-34"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-71",
+ "nodOpen": "debug",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "debug",
+ "uidNode": "id-41"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-10.json b/packages/core/test/fixtures/v1/scenario-10.json
new file mode 100644
index 0000000..5231b43
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-10.json
@@ -0,0 +1,616 @@
+{
+ "name": "1 tabs, seed 10",
+ "tabs": ["editor"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1200, 800] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 908, "height": 711 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 908, 711] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 1587, "height": 192 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 1587, 192] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 985, "height": 835 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 985, 835] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 985, 835] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 985, 835] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [{ "tabs": ["editor"], "active": "editor" }]
+ },
+ "tabsets": { "editor": [0, 0, 985, 835] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-3"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-11.json b/packages/core/test/fixtures/v1/scenario-11.json
new file mode 100644
index 0000000..8ad29ec
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-11.json
@@ -0,0 +1,1004 @@
+{
+ "name": "2 tabs, seed 11",
+ "tabs": ["editor", "terminal"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 595, 800],
+ "terminal": [605, 0, 595, 800]
+ },
+ "splitters": { "editor>terminal": [595, 0, 10, 800] },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 800],
+ "terminal": [50, 0, 1150, 800]
+ },
+ "splitters": { "editor>terminal": [40, 0, 10, 800] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1150, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": { "editor>terminal": [1150, 0, 10, 800] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 1139, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1150, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": { "editor>terminal": [1150, 0, 10, 800] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 624, "height": 979 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 574, 979],
+ "terminal": [584, 0, 40, 979]
+ },
+ "splitters": { "editor>terminal": [574, 0, 10, 979] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 574, 979],
+ "terminal": [584, 0, 40, 979]
+ },
+ "splitters": { "editor>terminal": [574, 0, 10, 979] },
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 574, 979],
+ "terminal": [0, 0, 624, 979]
+ },
+ "splitters": { "editor>terminal": [574, 0, 10, 979] },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 624, 485],
+ "terminal": [0, 495, 624, 484]
+ },
+ "splitters": { "editor>terminal": [0, 485, 624, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 624, 929],
+ "terminal": [0, 939, 624, 40]
+ },
+ "splitters": { "editor>terminal": [0, 929, 624, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 624, 979],
+ "terminal": [0, 939, 624, 40]
+ },
+ "splitters": { "editor>terminal": [0, 929, 624, 10] },
+ "maximized": "editor"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 588, "height": 706 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 588, 706],
+ "terminal": [0, 666, 588, 40]
+ },
+ "splitters": { "editor>terminal": [0, 656, 588, 10] },
+ "maximized": "editor"
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-2",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-11",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-9",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 588, 40],
+ "terminal": [0, 50, 588, 656]
+ },
+ "splitters": { "editor>terminal": [0, 40, 588, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 588, 40],
+ "terminal": [0, 0, 588, 706]
+ },
+ "splitters": { "editor>terminal": [0, 40, 588, 10] },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 588, 40],
+ "terminal": [0, 0, 588, 706]
+ },
+ "splitters": { "editor>terminal": [0, 40, 588, 10] },
+ "maximized": "terminal"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 289, 706],
+ "terminal": [299, 0, 289, 706]
+ },
+ "splitters": { "editor>terminal": [289, 0, 10, 706] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 272, "y": 353 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 267, 706],
+ "terminal": [277, 0, 311, 706]
+ },
+ "splitters": { "editor>terminal": [267, 0, 10, 706] },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 91.16465863453816,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 108.83534136546184,
+ "nodName": "",
+ "uidNode": "id-9",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 277, 706],
+ "terminal": [287, 0, 301, 706]
+ },
+ "splitters": { "editor>terminal": [277, 0, 10, 706] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 538, 706],
+ "terminal": [548, 0, 40, 706]
+ },
+ "splitters": { "editor>terminal": [538, 0, 10, 706] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 933, "height": 1010 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 883, 1010],
+ "terminal": [893, 0, 40, 1010]
+ },
+ "splitters": { "editor>terminal": [883, 0, 10, 1010] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 883, 1010],
+ "terminal": [893, 0, 40, 1010]
+ },
+ "splitters": { "editor>terminal": [883, 0, 10, 1010] },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 883, 1010],
+ "terminal": [0, 0, 933, 1010]
+ },
+ "splitters": { "editor>terminal": [883, 0, 10, 1010] },
+ "maximized": "terminal"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-14",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 91.16465863453816,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-14",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 933, 1010],
+ "terminal": [893, 0, 40, 1010]
+ },
+ "splitters": { "editor>terminal": [883, 0, 10, 1010] },
+ "maximized": "editor"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 933, 1010],
+ "terminal": [893, 0, 40, 1010]
+ },
+ "splitters": { "editor>terminal": [883, 0, 10, 1010] },
+ "maximized": "editor"
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 1023, "height": 546 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 1023, 546],
+ "terminal": [983, 0, 40, 546]
+ },
+ "splitters": { "editor>terminal": [973, 0, 10, 546] },
+ "maximized": "editor"
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 583, "height": 792 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [543, 0, 40, 792]
+ },
+ "splitters": { "editor>terminal": [533, 0, 10, 792] },
+ "maximized": "editor"
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-2",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 91.16465863453816,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-14",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 292, "y": 396 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 583, 792],
+ "terminal": [285, 0, 298, 792]
+ },
+ "splitters": { "editor>terminal": [275, 0, 10, 792] },
+ "maximized": "editor"
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-2",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 91.16465863453816,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-14",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-6"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-12.json b/packages/core/test/fixtures/v1/scenario-12.json
new file mode 100644
index 0000000..98ee280
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-12.json
@@ -0,0 +1,1147 @@
+{
+ "name": "3 tabs, seed 12",
+ "tabs": ["editor", "terminal", "preview"],
+ "container": { "width": 640, "height": 420 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 315, 420],
+ "terminal": [325, 0, 315, 205],
+ "preview": [325, 215, 315, 205]
+ },
+ "splitters": {
+ "editor>terminal": [315, 0, 10, 420],
+ "terminal>preview": [325, 205, 315, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 315, 420],
+ "terminal": [325, 0, 315, 205],
+ "preview": [325, 215, 315, 205]
+ },
+ "splitters": {
+ "editor>terminal": [315, 0, 10, 420],
+ "terminal>preview": [325, 205, 315, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 315, 420],
+ "terminal": [0, 0, 640, 420],
+ "preview": [325, 215, 315, 205]
+ },
+ "splitters": {
+ "editor>terminal": [315, 0, 10, 420],
+ "terminal>preview": [325, 205, 315, 10]
+ },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview", "editor"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview|editor": [0, 0, 315, 420],
+ "terminal": [325, 0, 315, 420]
+ },
+ "splitters": { "preview>terminal": [315, 0, 10, 420] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "preview|terminal": [0, 0, 640, 205],
+ "editor": [0, 215, 640, 205]
+ },
+ "splitters": { "preview>editor": [0, 205, 640, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 640, 420] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 801, "height": 952 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 801, 952] },
+ "splitters": {},
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ "tabsets": { "preview|editor|terminal": [0, 0, 801, 952] },
+ "splitters": {},
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview", "editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 801, 471],
+ "preview|editor": [0, 481, 801, 471]
+ },
+ "splitters": { "terminal>preview": [0, 471, 801, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "preview", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview", "editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview|editor": [0, 0, 801, 471],
+ "terminal": [0, 481, 801, 471]
+ },
+ "splitters": { "preview>terminal": [0, 471, 801, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "preview", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "preview"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor|preview": [0, 0, 801, 471],
+ "terminal": [0, 481, 801, 471]
+ },
+ "splitters": { "editor>terminal": [0, 471, 801, 10] },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-24",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 486, "height": 648 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "preview"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor|preview": [0, 0, 486, 319],
+ "terminal": [0, 329, 486, 319]
+ },
+ "splitters": { "editor>terminal": [0, 319, 486, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 329, 486, 319],
+ "editor": [0, 0, 238, 319],
+ "preview": [248, 0, 238, 319]
+ },
+ "splitters": {
+ "editor>terminal": [0, 319, 486, 10],
+ "editor>preview": [238, 0, 10, 319]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "preview"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor|preview": [0, 0, 486, 319],
+ "terminal": [0, 329, 486, 319]
+ },
+ "splitters": { "editor>terminal": [0, 319, 486, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["editor", "preview"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 238, 648],
+ "editor|preview": [248, 0, 238, 648]
+ },
+ "splitters": { "terminal>editor": [238, 0, 10, 648] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "preview", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 238, 648],
+ "preview": [248, 0, 238, 319],
+ "editor": [248, 329, 238, 319]
+ },
+ "splitters": {
+ "terminal>preview": [238, 0, 10, 648],
+ "preview>editor": [248, 319, 238, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-39",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tab": "preview", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 238, 648],
+ "preview": [248, 0, 238, 319],
+ "editor": [248, 329, 238, 319]
+ },
+ "splitters": {
+ "terminal>preview": [238, 0, 10, 648],
+ "preview>editor": [248, 319, 238, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tab": "preview", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "terminal"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "preview|terminal": [0, 0, 486, 319],
+ "editor": [0, 329, 486, 319]
+ },
+ "splitters": { "preview>editor": [0, 319, 486, 10] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "preview",
+ "after": "editor",
+ "point": { "x": 243, "y": 154 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "terminal"
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "preview|terminal": [0, 0, 486, 149],
+ "editor": [0, 159, 486, 489]
+ },
+ "splitters": { "preview>editor": [0, 149, 486, 10] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 325, 648],
+ "preview|terminal": [335, 0, 151, 648]
+ },
+ "splitters": { "editor>preview": [325, 0, 10, 648] },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "editor", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 325, 648],
+ "preview|terminal": [335, 0, 151, 648]
+ },
+ "splitters": { "editor>preview": [325, 0, 10, 648] },
+ "maximized": null
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-42",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 39.06810035842294,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 266, "height": 628 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 167, 628],
+ "preview|terminal": [177, 0, 89, 628]
+ },
+ "splitters": { "editor>preview": [167, 0, 10, 628] },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "preview", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 167, 628],
+ "preview": [177, 0, 89, 309],
+ "terminal": [177, 319, 89, 309]
+ },
+ "splitters": {
+ "editor>preview": [167, 0, 10, 628],
+ "preview>terminal": [177, 309, 89, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 167, 628],
+ "preview": [177, 0, 89, 309],
+ "terminal": [177, 319, 89, 309]
+ },
+ "splitters": {
+ "editor>preview": [167, 0, 10, 628],
+ "preview>terminal": [177, 309, 89, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 209, "y": 314 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 204, 628],
+ "preview": [214, 0, 52, 309],
+ "terminal": [214, 319, 52, 309]
+ },
+ "splitters": {
+ "editor>preview": [204, 0, 10, 628],
+ "preview>terminal": [214, 309, 52, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 42, "y": 314 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 628],
+ "preview": [50, 0, 216, 309],
+ "terminal": [50, 319, 216, 309]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 628],
+ "preview>terminal": [50, 309, 216, 10]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-42",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 139.06810035842292,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-11"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-13.json b/packages/core/test/fixtures/v1/scenario-13.json
new file mode 100644
index 0000000..0bcc365
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-13.json
@@ -0,0 +1,1459 @@
+{
+ "name": "4 tabs, seed 13",
+ "tabs": ["editor", "terminal", "preview", "console"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 395],
+ "preview": [580, 405, 305, 395],
+ "console": [895, 405, 305, 395]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 395, 620, 10],
+ "preview>console": [885, 405, 10, 395]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 393, 800],
+ "editor": [403, 0, 394, 800],
+ "terminal": [807, 0, 393, 395],
+ "console": [807, 405, 393, 395]
+ },
+ "splitters": {
+ "preview>editor": [393, 0, 10, 800],
+ "editor>terminal": [797, 0, 10, 800],
+ "terminal>console": [807, 395, 393, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tabsetOf": "preview", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 293, 800],
+ "console": [303, 0, 292, 800],
+ "editor": [605, 0, 293, 800],
+ "terminal": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "preview>console": [293, 0, 10, 800],
+ "console>editor": [595, 0, 10, 800],
+ "editor>terminal": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 934, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 293, 800],
+ "console": [303, 0, 292, 800],
+ "editor": [605, 0, 324, 800],
+ "terminal": [939, 0, 261, 800]
+ },
+ "splitters": {
+ "preview>console": [293, 0, 10, 800],
+ "console>editor": [595, 0, 10, 800],
+ "editor>terminal": [929, 0, 10, 800]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 363, 800],
+ "console": [373, 0, 363, 800],
+ "editor": [746, 0, 404, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [363, 0, 10, 800],
+ "console>editor": [736, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 40, 800],
+ "console": [50, 0, 515, 800],
+ "editor": [575, 0, 575, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [40, 0, 10, 800],
+ "console>editor": [565, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 112.47524752475249,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 87.52475247524752,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "editor",
+ "point": { "x": 391, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 40, 800],
+ "console": [50, 0, 336, 800],
+ "editor": [396, 0, 754, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [40, 0, 10, 800],
+ "console>editor": [386, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 40, 800],
+ "console": [0, 0, 1200, 800],
+ "editor": [396, 0, 754, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [40, 0, 10, 800],
+ "console>editor": [386, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": "console"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "console", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 363, 800],
+ "console": [373, 0, 241, 800],
+ "editor": [624, 0, 526, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [363, 0, 10, 800],
+ "console>editor": [614, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 363, 800],
+ "console": [373, 0, 241, 800],
+ "editor": [624, 0, 526, 800],
+ "terminal": [1160, 0, 40, 800]
+ },
+ "splitters": {
+ "preview>console": [363, 0, 10, 800],
+ "console>editor": [614, 0, 10, 800],
+ "editor>terminal": [1150, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 285, 800],
+ "preview": [295, 0, 285, 800],
+ "console": [590, 0, 192, 800],
+ "editor": [792, 0, 408, 800]
+ },
+ "splitters": {
+ "terminal>preview": [285, 0, 10, 800],
+ "preview>console": [580, 0, 10, 800],
+ "console>editor": [782, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 62.26997353200667,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 150.20527399274582,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "preview", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 285, 800],
+ "preview": [295, 0, 285, 800],
+ "console": [590, 0, 192, 800],
+ "editor": [792, 0, 408, 800]
+ },
+ "splitters": {
+ "terminal>preview": [285, 0, 10, 800],
+ "preview>console": [580, 0, 10, 800],
+ "console>editor": [782, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 96, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "terminal": [0, 0, 91, 800],
+ "preview": [101, 0, 479, 800],
+ "console": [590, 0, 192, 800],
+ "editor": [792, 0, 408, 800]
+ },
+ "splitters": {
+ "terminal>preview": [91, 0, 10, 800],
+ "preview>console": [580, 0, 10, 800],
+ "console>editor": [782, 0, 10, 800]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "terminal", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 124, 800],
+ "preview": [134, 0, 764, 800],
+ "console": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "editor>preview": [124, 0, 10, 800],
+ "preview>console": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 124, 800],
+ "preview": [134, 0, 764, 800],
+ "console": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "editor>preview": [124, 0, 10, 800],
+ "preview>console": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": -22, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 800],
+ "preview": [50, 0, 848, 800],
+ "console": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 800],
+ "preview>console": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 62.26997353200667,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 800],
+ "preview": [50, 0, 848, 800],
+ "console": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 800],
+ "preview>console": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 800],
+ "preview": [50, 0, 848, 800],
+ "console": [908, 0, 292, 800]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 800],
+ "preview>console": [898, 0, 10, 800]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 992, "height": 615 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [50, 0, 690, 615],
+ "console": [750, 0, 242, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [740, 0, 10, 615]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [50, 0, 608, 615],
+ "console": [668, 0, 324, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [658, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [0, 0, 992, 615],
+ "console": [668, 0, 324, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [658, 0, 10, 615]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-12",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 200,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [50, 0, 608, 615],
+ "console": [668, 0, 324, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [658, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "preview",
+ "after": "console",
+ "point": { "x": 600, "y": 307.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [50, 0, 545, 615],
+ "console": [605, 0, 387, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [595, 0, 10, 615]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "console", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 40, 615],
+ "preview": [50, 0, 545, 615],
+ "console": [605, 0, 387, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [595, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "console", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 615],
+ "preview": [50, 0, 397, 615],
+ "console": [457, 0, 285, 615],
+ "terminal": [752, 0, 240, 615]
+ },
+ "splitters": {
+ "editor>preview": [40, 0, 10, 615],
+ "preview>console": [447, 0, 10, 615],
+ "console>terminal": [742, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["preview", "terminal"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 615],
+ "console": [50, 0, 509, 615],
+ "preview|terminal": [569, 0, 423, 615]
+ },
+ "splitters": {
+ "editor>console": [40, 0, 10, 615],
+ "console>preview": [559, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-25",
+ "nodOpen": "editor",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 122.1830985915493,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor",
+ "folded": true
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["preview", "terminal"], "active": "preview" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 40, 615],
+ "console": [50, 0, 509, 615],
+ "preview|terminal": [569, 0, 423, 615]
+ },
+ "splitters": {
+ "editor>console": [40, 0, 10, 615],
+ "console>preview": [559, 0, 10, 615]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "console": [50, 0, 942, 615],
+ "editor": [0, 0, 40, 303],
+ "preview|terminal": [0, 313, 40, 302]
+ },
+ "splitters": {
+ "editor>console": [40, 0, 10, 615],
+ "editor>preview": [0, 303, 40, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "console": [50, 0, 942, 615],
+ "editor": [0, 0, 40, 303],
+ "preview|terminal": [0, 0, 992, 615]
+ },
+ "splitters": {
+ "editor>console": [40, 0, 10, 615],
+ "editor>preview": [0, 303, 40, 10]
+ },
+ "maximized": "preview|terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tab": "editor", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "console": [50, 0, 942, 615],
+ "editor": [0, 0, 40, 303],
+ "preview|terminal": [0, 0, 992, 615]
+ },
+ "splitters": {
+ "editor>console": [40, 0, 10, 615],
+ "editor>preview": [0, 303, 40, 10]
+ },
+ "maximized": "preview|terminal"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview", "terminal"],
+ "active": "preview"
+ },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 313, 992, 302],
+ "preview|terminal": [0, 0, 446, 303],
+ "console": [456, 0, 536, 303]
+ },
+ "splitters": {
+ "preview>editor": [0, 303, 992, 10],
+ "preview>console": [446, 0, 10, 303]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-44",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-33",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 122.1830985915493,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-16"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-14.json b/packages/core/test/fixtures/v1/scenario-14.json
new file mode 100644
index 0000000..3c8c8f3
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-14.json
@@ -0,0 +1,2064 @@
+{
+ "name": "5 tabs, seed 14",
+ "tabs": ["editor", "terminal", "preview", "console", "files"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 370],
+ "preview": [580, 380, 305, 420],
+ "console": [895, 380, 305, 205],
+ "files": [895, 595, 305, 205]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 370, 620, 10],
+ "preview>console": [885, 380, 10, 420],
+ "console>files": [895, 585, 305, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 370],
+ "preview": [580, 380, 305, 420],
+ "console": [895, 380, 305, 205],
+ "files": [0, 0, 1200, 800]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 370, 620, 10],
+ "preview>console": [885, 380, 10, 420],
+ "console>files": [895, 585, 305, 10]
+ },
+ "maximized": "files"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 570, 800],
+ "terminal": [580, 0, 620, 370],
+ "preview": [580, 380, 305, 420],
+ "console": [895, 380, 305, 205],
+ "files": [0, 0, 1200, 800]
+ },
+ "splitters": {
+ "editor>terminal": [570, 0, 10, 800],
+ "terminal>preview": [580, 370, 620, 10],
+ "preview>console": [885, 380, 10, 420],
+ "console>files": [895, 585, 305, 10]
+ },
+ "maximized": "files"
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 915, "height": 775 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 428, 775],
+ "terminal": [438, 0, 477, 358],
+ "preview": [438, 368, 234, 407],
+ "console": [682, 368, 233, 199],
+ "files": [0, 0, 915, 775]
+ },
+ "splitters": {
+ "editor>terminal": [428, 0, 10, 775],
+ "terminal>preview": [438, 358, 477, 10],
+ "preview>console": [672, 368, 10, 407],
+ "console>files": [682, 567, 233, 10]
+ },
+ "maximized": "files"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "console", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 428, 775],
+ "terminal": [438, 0, 477, 358],
+ "preview": [438, 368, 234, 407],
+ "console": [682, 368, 233, 199],
+ "files": [0, 0, 915, 775]
+ },
+ "splitters": {
+ "editor>terminal": [428, 0, 10, 775],
+ "terminal>preview": [438, 358, 477, 10],
+ "preview>console": [672, 368, 10, 407],
+ "console>files": [682, 567, 233, 10]
+ },
+ "maximized": "files"
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 676.5, "y": 331 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 428, 775],
+ "terminal": [438, 0, 477, 358],
+ "preview": [438, 368, 234, 407],
+ "console": [682, 368, 233, 199],
+ "files": [0, 0, 915, 775]
+ },
+ "splitters": {
+ "editor>terminal": [428, 0, 10, 775],
+ "terminal>preview": [438, 358, 477, 10],
+ "preview>console": [672, 368, 10, 407],
+ "console>files": [682, 567, 233, 10]
+ },
+ "maximized": "files"
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 428, 775],
+ "terminal": [438, 0, 477, 358],
+ "preview": [438, 368, 234, 407],
+ "console": [682, 368, 233, 40],
+ "files": [682, 418, 233, 357]
+ },
+ "splitters": {
+ "editor>terminal": [428, 0, 10, 775],
+ "terminal>preview": [438, 358, 477, 10],
+ "preview>console": [672, 368, 10, 407],
+ "console>files": [682, 408, 233, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tabsetOf": "files", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 428, 775],
+ "terminal": [438, 0, 477, 383],
+ "preview": [438, 393, 234, 382],
+ "console|files": [682, 393, 233, 382]
+ },
+ "splitters": {
+ "editor>terminal": [428, 0, 10, 775],
+ "terminal>preview": [438, 383, 477, 10],
+ "preview>console": [672, 393, 10, 382]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 667, "height": 344 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["console", "files"],
+ "active": "console"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 304, 344],
+ "terminal": [314, 0, 353, 167],
+ "preview": [314, 177, 172, 167],
+ "console|files": [496, 177, 171, 167]
+ },
+ "splitters": {
+ "editor>terminal": [304, 0, 10, 344],
+ "terminal>preview": [314, 167, 353, 10],
+ "preview>console": [486, 177, 10, 167]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 199, 344],
+ "editor": [209, 0, 199, 344],
+ "terminal": [418, 0, 249, 167],
+ "preview": [418, 177, 120, 167],
+ "files": [548, 177, 119, 167]
+ },
+ "splitters": {
+ "console>editor": [199, 0, 10, 344],
+ "editor>terminal": [408, 0, 10, 344],
+ "terminal>preview": [418, 167, 249, 10],
+ "preview>files": [538, 177, 10, 167]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 304, 344],
+ "console": [314, 0, 172, 167],
+ "terminal": [496, 0, 171, 167],
+ "preview": [314, 177, 172, 167],
+ "files": [496, 177, 171, 167]
+ },
+ "splitters": {
+ "editor>console": [304, 0, 10, 344],
+ "console>preview": [314, 167, 353, 10],
+ "console>terminal": [486, 0, 10, 167],
+ "preview>files": [486, 177, 10, 167]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-29",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "console", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 304, 344],
+ "terminal": [314, 0, 353, 108],
+ "console": [314, 118, 353, 108],
+ "preview": [314, 236, 172, 108],
+ "files": [496, 236, 171, 108]
+ },
+ "splitters": {
+ "editor>terminal": [304, 0, 10, 344],
+ "terminal>console": [314, 108, 353, 10],
+ "console>preview": [314, 226, 353, 10],
+ "preview>files": [486, 236, 10, 108]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 453, "y": 172 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 448, 344],
+ "terminal": [458, 0, 209, 108],
+ "console": [458, 118, 209, 108],
+ "preview": [458, 236, 100, 108],
+ "files": [568, 236, 99, 108]
+ },
+ "splitters": {
+ "editor>terminal": [448, 0, 10, 344],
+ "terminal>console": [458, 108, 209, 10],
+ "console>preview": [458, 226, 209, 10],
+ "preview>files": [558, 236, 10, 108]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 667, 108],
+ "console": [0, 118, 667, 108],
+ "preview": [0, 236, 329, 108],
+ "files": [339, 236, 328, 108]
+ },
+ "splitters": {
+ "editor>console": [0, 108, 667, 10],
+ "console>preview": [0, 226, 667, 10],
+ "preview>files": [329, 236, 10, 108]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tabsetOf": "editor", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 329, 167],
+ "editor|terminal": [339, 0, 328, 167],
+ "preview": [0, 177, 329, 167],
+ "files": [339, 177, 328, 167]
+ },
+ "splitters": {
+ "console>preview": [0, 167, 667, 10],
+ "console>editor": [329, 0, 10, 167],
+ "preview>files": [329, 177, 10, 167]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 329, 167],
+ "editor|terminal": [339, 0, 328, 167],
+ "preview": [0, 0, 667, 344],
+ "files": [339, 177, 328, 167]
+ },
+ "splitters": {
+ "console>preview": [0, 167, 667, 10],
+ "console>editor": [329, 0, 10, 167],
+ "preview>files": [329, 177, 10, 167]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-12",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 45.16129032258064,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tab": "console", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 329, 167],
+ "editor|terminal": [339, 0, 328, 167],
+ "preview": [0, 0, 667, 344],
+ "files": [339, 177, 328, 167]
+ },
+ "splitters": {
+ "console>preview": [0, 167, 667, 10],
+ "console>editor": [329, 0, 10, 167],
+ "preview>files": [329, 177, 10, 167]
+ },
+ "maximized": "preview"
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "resize", "width": 1149, "height": 668 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 0, 570, 329],
+ "editor|terminal": [580, 0, 569, 329],
+ "preview": [0, 0, 1149, 668],
+ "files": [580, 339, 569, 329]
+ },
+ "splitters": {
+ "console>preview": [0, 329, 1149, 10],
+ "console>editor": [570, 0, 10, 329],
+ "preview>files": [570, 339, 10, 329]
+ },
+ "maximized": "preview"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "console", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "terminal": [580, 0, 569, 354],
+ "preview": [0, 364, 570, 304],
+ "files": [580, 364, 569, 304],
+ "console": [0, 0, 570, 172],
+ "editor": [0, 182, 570, 172]
+ },
+ "splitters": {
+ "console>preview": [0, 354, 1149, 10],
+ "console>terminal": [570, 0, 10, 354],
+ "preview>files": [570, 364, 10, 304],
+ "console>editor": [0, 172, 570, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tab": "editor", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["preview", "editor"],
+ "active": "preview"
+ }
+ ]
+ },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 364, 1149, 304],
+ "terminal": [580, 0, 569, 354],
+ "console": [0, 0, 570, 172],
+ "preview|editor": [0, 182, 570, 172]
+ },
+ "splitters": {
+ "console>files": [0, 354, 1149, 10],
+ "console>terminal": [570, 0, 10, 354],
+ "console>preview": [0, 172, 570, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview", "editor", "terminal"],
+ "active": "editor"
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 339, 1149, 329],
+ "console": [0, 0, 570, 329],
+ "preview|editor|terminal": [580, 0, 569, 329]
+ },
+ "splitters": {
+ "console>files": [0, 329, 1149, 10],
+ "console>preview": [570, 0, 10, 329]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 45.16129032258064,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-32",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-39",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 364, 1149, 304],
+ "console": [0, 0, 570, 354],
+ "editor|terminal": [580, 0, 569, 172],
+ "preview": [580, 182, 569, 172]
+ },
+ "splitters": {
+ "console>files": [0, 354, 1149, 10],
+ "console>editor": [570, 0, 10, 354],
+ "editor>preview": [580, 172, 569, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tab": "console", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files", "console"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files|console": [0, 0, 570, 668],
+ "editor|terminal": [580, 0, 569, 329],
+ "preview": [580, 339, 569, 329]
+ },
+ "splitters": {
+ "files>editor": [570, 0, 10, 668],
+ "editor>preview": [580, 329, 569, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "editor", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 570, 668],
+ "editor|console|terminal": [580, 0, 569, 329],
+ "preview": [580, 339, 569, 329]
+ },
+ "splitters": {
+ "files>editor": [570, 0, 10, 668],
+ "editor>preview": [580, 329, 569, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "files",
+ "after": "editor",
+ "point": { "x": 476, "y": 334 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 471, 668],
+ "editor|console|terminal": [481, 0, 668, 329],
+ "preview": [481, 339, 668, 329]
+ },
+ "splitters": {
+ "files>editor": [471, 0, 10, 668],
+ "editor>preview": [481, 329, 668, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "files",
+ "after": "editor",
+ "point": { "x": 331, "y": 334 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 326, 668],
+ "editor|console|terminal": [336, 0, 813, 329],
+ "preview": [336, 339, 813, 329]
+ },
+ "splitters": {
+ "files>editor": [326, 0, 10, 668],
+ "editor>preview": [336, 329, 813, 10]
+ },
+ "maximized": null
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 54.01322001888575,
+ "nodName": "",
+ "uidNode": "id-39",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 145.98677998111427,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-46",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "tab": "terminal", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 326, 668],
+ "editor|console|terminal": [336, 0, 813, 329],
+ "preview": [336, 339, 813, 329]
+ },
+ "splitters": {
+ "files>editor": [326, 0, 10, 668],
+ "editor>preview": [336, 329, 813, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 742.5, "y": 464 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 326, 668],
+ "editor|console|terminal": [336, 0, 813, 459],
+ "preview": [336, 469, 813, 199]
+ },
+ "splitters": {
+ "files>editor": [326, 0, 10, 668],
+ "editor>preview": [336, 459, 813, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 971, "height": 929 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 278, 929],
+ "editor|console|terminal": [288, 0, 683, 648],
+ "preview": [288, 658, 683, 271]
+ },
+ "splitters": {
+ "files>editor": [278, 0, 10, 929],
+ "editor>preview": [288, 648, 683, 10]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "preview", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "console", "terminal"],
+ "active": "console"
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 278, 929],
+ "editor|console|terminal": [288, 0, 683, 648],
+ "preview": [288, 658, 683, 271]
+ },
+ "splitters": {
+ "files>editor": [278, 0, 10, 929],
+ "editor>preview": [288, 648, 683, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["editor", "console"],
+ "active": "console"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "files": [0, 0, 264, 929],
+ "preview": [274, 658, 697, 271],
+ "terminal": [274, 0, 344, 648],
+ "editor|console": [628, 0, 343, 648]
+ },
+ "splitters": {
+ "files>terminal": [264, 0, 10, 929],
+ "terminal>preview": [274, 648, 697, 10],
+ "terminal>editor": [618, 0, 10, 648]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 54.01322001888575,
+ "nodName": "",
+ "uidNode": "id-39",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-21"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 145.98677998111427,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 144.98269896193773,
+ "nodName": "",
+ "uidNode": "id-46",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-51",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-52",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 55.017301038062286,
+ "nodName": "",
+ "uidNode": "id-45",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-15.json b/packages/core/test/fixtures/v1/scenario-15.json
new file mode 100644
index 0000000..edeed43
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-15.json
@@ -0,0 +1,2767 @@
+{
+ "name": "6 tabs, seed 15",
+ "tabs": ["editor", "terminal", "preview", "console", "files", "search"],
+ "container": { "width": 1200, "height": 800 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 545, 800],
+ "terminal": [555, 0, 645, 370],
+ "preview": [555, 380, 293, 420],
+ "console": [858, 380, 342, 205],
+ "files": [858, 595, 166, 205],
+ "search": [1034, 595, 166, 205]
+ },
+ "splitters": {
+ "editor>terminal": [545, 0, 10, 800],
+ "terminal>preview": [555, 370, 645, 10],
+ "preview>console": [848, 380, 10, 420],
+ "console>files": [858, 585, 342, 10],
+ "files>search": [1024, 595, 10, 205]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "console",
+ "after": "files",
+ "point": { "x": 1029, "y": 568 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 545, 800],
+ "terminal": [555, 0, 645, 370],
+ "preview": [555, 380, 293, 420],
+ "console": [858, 380, 342, 183],
+ "files": [858, 573, 166, 227],
+ "search": [1034, 573, 166, 227]
+ },
+ "splitters": {
+ "editor>terminal": [545, 0, 10, 800],
+ "terminal>preview": [555, 370, 645, 10],
+ "preview>console": [848, 380, 10, 420],
+ "console>files": [858, 563, 342, 10],
+ "files>search": [1024, 573, 10, 227]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 545, 800],
+ "terminal": [555, 0, 645, 370],
+ "preview": [555, 380, 293, 420],
+ "console": [858, 380, 342, 183],
+ "files": [858, 573, 166, 227],
+ "search": [0, 0, 1200, 800]
+ },
+ "splitters": {
+ "editor>terminal": [545, 0, 10, 800],
+ "terminal>preview": [555, 370, 645, 10],
+ "preview>console": [848, 380, 10, 420],
+ "console>files": [858, 563, 342, 10],
+ "files>search": [1024, 573, 10, 227]
+ },
+ "maximized": "search"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1560, "height": 350 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 725, 350],
+ "terminal": [735, 0, 825, 145],
+ "preview": [735, 155, 383, 195],
+ "console": [1128, 155, 432, 86],
+ "files": [1128, 251, 211, 99],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>terminal": [725, 0, 10, 350],
+ "terminal>preview": [735, 145, 825, 10],
+ "preview>console": [1118, 155, 10, 195],
+ "console>files": [1128, 241, 432, 10],
+ "files>search": [1339, 251, 10, 99]
+ },
+ "maximized": "search"
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "preview",
+ "after": "console",
+ "point": { "x": 1288, "y": 252.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 725, 350],
+ "terminal": [735, 0, 825, 145],
+ "preview": [735, 155, 383, 195],
+ "console": [1128, 155, 432, 86],
+ "files": [1128, 251, 211, 99],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>terminal": [725, 0, 10, 350],
+ "terminal>preview": [735, 145, 825, 10],
+ "preview>console": [1118, 155, 10, 195],
+ "console>files": [1128, 241, 432, 10],
+ "files>search": [1339, 251, 10, 99]
+ },
+ "maximized": "search"
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 1147.5, "y": -20 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 725, 350],
+ "terminal": [735, 0, 825, 145],
+ "preview": [735, 155, 383, 195],
+ "console": [1128, 155, 432, 86],
+ "files": [1128, 251, 211, 99],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>terminal": [725, 0, 10, 350],
+ "terminal>preview": [735, 145, 825, 10],
+ "preview>console": [1118, 155, 10, 195],
+ "console>files": [1128, 241, 432, 10],
+ "files>search": [1339, 251, 10, 99]
+ },
+ "maximized": "search"
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 86.66666666666667,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 113.33333333333333,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "preview",
+ "point": { "x": 1147.5, "y": -15 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 725, 350],
+ "terminal": [735, 0, 825, 145],
+ "preview": [735, 155, 383, 195],
+ "console": [1128, 155, 432, 86],
+ "files": [1128, 251, 211, 99],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>terminal": [725, 0, 10, 350],
+ "terminal>preview": [735, 145, 825, 10],
+ "preview>console": [1118, 155, 10, 195],
+ "console>files": [1128, 241, 432, 10],
+ "files>search": [1339, 251, 10, 99]
+ },
+ "maximized": "search"
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "terminal",
+ "point": { "x": 863, "y": 175 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 725, 350],
+ "terminal": [735, 0, 825, 145],
+ "preview": [735, 155, 383, 195],
+ "console": [1128, 155, 432, 86],
+ "files": [1128, 251, 211, 99],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>terminal": [725, 0, 10, 350],
+ "terminal>preview": [735, 145, 825, 10],
+ "preview>console": [1118, 155, 10, 195],
+ "console>files": [1128, 241, 432, 10],
+ "files>search": [1339, 251, 10, 99]
+ },
+ "maximized": "search"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "editor", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [507, 0, 496, 350],
+ "editor": [0, 0, 497, 170],
+ "terminal": [0, 180, 497, 170],
+ "console": [1013, 0, 547, 153],
+ "files": [1013, 163, 269, 187],
+ "search": [1292, 163, 268, 187]
+ },
+ "splitters": {
+ "editor>preview": [497, 0, 10, 350],
+ "preview>console": [1003, 0, 10, 350],
+ "editor>terminal": [0, 170, 497, 10],
+ "console>files": [1013, 153, 547, 10],
+ "files>search": [1282, 163, 10, 187]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [507, 0, 496, 350],
+ "editor": [0, 0, 497, 170],
+ "terminal": [0, 180, 497, 170],
+ "console": [0, 0, 1560, 350],
+ "files": [1013, 163, 269, 187],
+ "search": [1292, 163, 268, 187]
+ },
+ "splitters": {
+ "editor>preview": [497, 0, 10, 350],
+ "preview>console": [1003, 0, 10, 350],
+ "editor>terminal": [0, 170, 497, 10],
+ "console>files": [1013, 153, 547, 10],
+ "files>search": [1282, 163, 10, 187]
+ },
+ "maximized": "console"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "search" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["terminal"], "active": "terminal" }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["console"], "active": "console" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "preview": [507, 0, 496, 350],
+ "editor": [0, 0, 497, 170],
+ "terminal": [0, 180, 497, 170],
+ "console": [1013, 0, 547, 153],
+ "files": [1013, 163, 269, 187],
+ "search": [0, 0, 1560, 350]
+ },
+ "splitters": {
+ "editor>preview": [497, 0, 10, 350],
+ "preview>console": [1003, 0, 10, 350],
+ "editor>terminal": [0, 170, 497, 10],
+ "console>files": [1013, 153, 547, 10],
+ "files>search": [1282, 163, 10, 187]
+ },
+ "maximized": "search"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-23",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-31",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 86.66666666666667,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 113.33333333333333,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "root": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 205, 1560, 145],
+ "preview": [393, 0, 382, 195],
+ "files": [785, 0, 383, 195],
+ "search": [1178, 0, 382, 195],
+ "editor": [0, 0, 383, 93],
+ "terminal": [0, 103, 383, 92]
+ },
+ "splitters": {
+ "editor>console": [0, 195, 1560, 10],
+ "editor>preview": [383, 0, 10, 195],
+ "preview>files": [775, 0, 10, 195],
+ "files>search": [1168, 0, 10, 195],
+ "editor>terminal": [0, 93, 383, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 310, 1560, 40],
+ "preview": [393, 0, 382, 300],
+ "files": [785, 0, 383, 300],
+ "search": [1178, 0, 382, 300],
+ "editor": [0, 0, 383, 145],
+ "terminal": [0, 155, 383, 145]
+ },
+ "splitters": {
+ "editor>console": [0, 300, 1560, 10],
+ "editor>preview": [383, 0, 10, 300],
+ "preview>files": [775, 0, 10, 300],
+ "files>search": [1168, 0, 10, 300],
+ "editor>terminal": [0, 145, 383, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [0, 310, 1560, 40],
+ "preview": [0, 0, 1560, 350],
+ "files": [785, 0, 383, 300],
+ "search": [1178, 0, 382, 300],
+ "editor": [0, 0, 383, 145],
+ "terminal": [0, 155, 383, 145]
+ },
+ "splitters": {
+ "editor>console": [0, 300, 1560, 10],
+ "editor>preview": [383, 0, 10, 300],
+ "preview>files": [775, 0, 10, 300],
+ "files>search": [1168, 0, 10, 300],
+ "editor>terminal": [0, 145, 383, 10]
+ },
+ "maximized": "preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [860, 0, 700, 350],
+ "console": [0, 310, 850, 40],
+ "terminal": [0, 0, 205, 300],
+ "preview": [215, 0, 205, 300],
+ "files": [430, 0, 205, 300],
+ "search": [645, 0, 205, 300]
+ },
+ "splitters": {
+ "terminal>editor": [850, 0, 10, 350],
+ "terminal>console": [0, 300, 850, 10],
+ "terminal>preview": [205, 0, 10, 300],
+ "preview>files": [420, 0, 10, 300],
+ "files>search": [635, 0, 10, 300]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [860, 0, 700, 350],
+ "console": [0, 310, 850, 40],
+ "terminal": [0, 0, 205, 300],
+ "preview": [215, 0, 205, 300],
+ "files": [430, 0, 205, 300],
+ "search": [645, 0, 205, 300]
+ },
+ "splitters": {
+ "terminal>editor": [850, 0, 10, 350],
+ "terminal>console": [0, 300, 850, 10],
+ "terminal>preview": [205, 0, 10, 300],
+ "preview>files": [420, 0, 10, 300],
+ "files>search": [635, 0, 10, 300]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-31",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [860, 0, 700, 350],
+ "console": [0, 310, 850, 40],
+ "terminal": [0, 0, 1560, 350],
+ "preview": [215, 0, 205, 300],
+ "files": [430, 0, 205, 300],
+ "search": [645, 0, 205, 300]
+ },
+ "splitters": {
+ "terminal>editor": [850, 0, 10, 350],
+ "terminal>console": [0, 300, 850, 10],
+ "terminal>preview": [205, 0, 10, 300],
+ "preview>files": [420, 0, 10, 300],
+ "files>search": [635, 0, 10, 300]
+ },
+ "maximized": "terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 372, "height": 835 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [266, 0, 106, 835],
+ "console": [0, 795, 256, 40],
+ "terminal": [0, 0, 372, 835],
+ "preview": [67, 0, 56, 785],
+ "files": [133, 0, 57, 785],
+ "search": [200, 0, 56, 785]
+ },
+ "splitters": {
+ "terminal>editor": [256, 0, 10, 835],
+ "terminal>console": [0, 785, 256, 10],
+ "terminal>preview": [57, 0, 10, 785],
+ "preview>files": [123, 0, 10, 785],
+ "files>search": [190, 0, 10, 785]
+ },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "terminal",
+ "after": "console",
+ "point": { "x": 128, "y": 919 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [266, 0, 106, 835],
+ "console": [0, 795, 256, 40],
+ "terminal": [0, 0, 372, 835],
+ "preview": [67, 0, 56, 785],
+ "files": [133, 0, 57, 785],
+ "search": [200, 0, 56, 785]
+ },
+ "splitters": {
+ "terminal>editor": [256, 0, 10, 835],
+ "terminal>console": [0, 785, 256, 10],
+ "terminal>preview": [57, 0, 10, 785],
+ "preview>files": [123, 0, 10, 785],
+ "files>search": [190, 0, 10, 785]
+ },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 1145, "height": 269 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [653, 0, 492, 269],
+ "console": [0, 229, 643, 40],
+ "terminal": [0, 0, 1145, 269],
+ "preview": [163, 0, 154, 219],
+ "files": [327, 0, 153, 219],
+ "search": [490, 0, 153, 219]
+ },
+ "splitters": {
+ "terminal>editor": [643, 0, 10, 269],
+ "terminal>console": [0, 219, 643, 10],
+ "terminal>preview": [153, 0, 10, 219],
+ "preview>files": [317, 0, 10, 219],
+ "files>search": [480, 0, 10, 219]
+ },
+ "maximized": "terminal"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [653, 0, 492, 269],
+ "console": [0, 229, 643, 40],
+ "terminal": [0, 0, 1145, 269],
+ "preview": [163, 0, 154, 219],
+ "files": [327, 0, 153, 219],
+ "search": [490, 0, 153, 219]
+ },
+ "splitters": {
+ "terminal>editor": [643, 0, 10, 269],
+ "terminal>console": [0, 219, 643, 10],
+ "terminal>preview": [153, 0, 10, 219],
+ "preview>files": [317, 0, 10, 219],
+ "files>search": [480, 0, 10, 219]
+ },
+ "maximized": "terminal"
+ },
+ "accepted": false,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-30",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-30",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-31",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tab": "files", "position": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files", "terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [628, 0, 517, 269],
+ "console": [0, 229, 618, 40],
+ "preview": [0, 0, 199, 219],
+ "files|terminal": [209, 0, 200, 219],
+ "search": [419, 0, 199, 219]
+ },
+ "splitters": {
+ "preview>editor": [618, 0, 10, 269],
+ "preview>console": [0, 219, 618, 10],
+ "preview>files": [199, 0, 10, 219],
+ "files>search": [409, 0, 10, 219]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files", "terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [628, 0, 517, 269],
+ "console": [0, 229, 618, 40],
+ "preview": [0, 0, 199, 219],
+ "files|terminal": [209, 0, 200, 219],
+ "search": [419, 0, 199, 219]
+ },
+ "splitters": {
+ "preview>editor": [618, 0, 10, 269],
+ "preview>console": [0, 219, 618, 10],
+ "preview>files": [199, 0, 10, 219],
+ "files>search": [409, 0, 10, 219]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files", "terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" }
+ ]
+ },
+ "tabsets": {
+ "editor": [628, 0, 517, 269],
+ "console": [0, 229, 618, 40],
+ "preview": [0, 0, 199, 219],
+ "files|terminal": [0, 0, 1145, 269],
+ "search": [419, 0, 199, 219]
+ },
+ "splitters": {
+ "preview>editor": [618, 0, 10, 269],
+ "preview>console": [0, 219, 618, 10],
+ "preview>files": [199, 0, 10, 219],
+ "files>search": [409, 0, 10, 219]
+ },
+ "maximized": "files|terminal"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "search" },
+ "target": { "tabsetOf": "editor", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "tabs": ["files", "terminal"],
+ "active": "terminal"
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "editor": [418, 0, 359, 269],
+ "search": [787, 0, 358, 269],
+ "console": [0, 229, 408, 40],
+ "preview": [0, 0, 199, 219],
+ "files|terminal": [209, 0, 199, 219]
+ },
+ "splitters": {
+ "preview>editor": [408, 0, 10, 269],
+ "editor>search": [777, 0, 10, 269],
+ "preview>console": [0, 219, 408, 10],
+ "preview>files": [199, 0, 10, 219]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "terminal", "area": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["editor"], "active": "editor" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "editor": [418, 0, 359, 269],
+ "search": [787, 0, 358, 269],
+ "console": [0, 229, 408, 40],
+ "preview": [0, 0, 199, 219],
+ "terminal": [209, 0, 199, 105],
+ "files": [209, 115, 199, 104]
+ },
+ "splitters": {
+ "preview>editor": [408, 0, 10, 269],
+ "editor>search": [777, 0, 10, 269],
+ "preview>console": [0, 219, 408, 10],
+ "preview>terminal": [199, 0, 10, 219],
+ "terminal>files": [209, 105, 199, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-48",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-31",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "preview", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [628, 0, 517, 269],
+ "console": [0, 229, 618, 40],
+ "editor": [0, 0, 199, 219],
+ "preview": [209, 0, 200, 219],
+ "terminal": [419, 0, 199, 105],
+ "files": [419, 115, 199, 104]
+ },
+ "splitters": {
+ "editor>search": [618, 0, 10, 269],
+ "editor>console": [0, 219, 618, 10],
+ "editor>preview": [199, 0, 10, 219],
+ "preview>terminal": [409, 0, 10, 219],
+ "terminal>files": [419, 105, 199, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [628, 0, 517, 269],
+ "console": [0, 0, 1145, 269],
+ "editor": [0, 0, 199, 219],
+ "preview": [209, 0, 200, 219],
+ "terminal": [419, 0, 199, 105],
+ "files": [419, 115, 199, 104]
+ },
+ "splitters": {
+ "editor>search": [618, 0, 10, 269],
+ "editor>console": [0, 219, 618, 10],
+ "editor>preview": [199, 0, 10, 219],
+ "preview>terminal": [409, 0, 10, 219],
+ "terminal>files": [419, 105, 199, 10]
+ },
+ "maximized": "console"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 130, "y": 109.5 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [628, 0, 517, 269],
+ "console": [0, 0, 1145, 269],
+ "editor": [0, 0, 199, 219],
+ "preview": [209, 0, 200, 219],
+ "terminal": [419, 0, 199, 105],
+ "files": [419, 115, 199, 104]
+ },
+ "splitters": {
+ "editor>search": [618, 0, 10, 269],
+ "editor>console": [0, 219, 618, 10],
+ "editor>preview": [199, 0, 10, 219],
+ "preview>terminal": [409, 0, 10, 219],
+ "terminal>files": [419, 105, 199, 10]
+ },
+ "maximized": "console"
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "root": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 358, 269],
+ "search": [787, 0, 358, 269],
+ "console": [368, 229, 409, 40],
+ "editor": [368, 0, 200, 219],
+ "terminal": [578, 0, 199, 105],
+ "files": [578, 115, 199, 104]
+ },
+ "splitters": {
+ "preview>editor": [358, 0, 10, 269],
+ "editor>search": [777, 0, 10, 269],
+ "editor>console": [368, 219, 409, 10],
+ "editor>terminal": [568, 0, 10, 219],
+ "terminal>files": [578, 105, 199, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "terminal" },
+ "target": { "tabsetOf": "console", "area": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["editor"],
+ "active": "editor"
+ },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["terminal"],
+ "active": "terminal"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "preview": [0, 0, 358, 269],
+ "search": [787, 0, 358, 269],
+ "editor": [368, 0, 200, 219],
+ "files": [578, 0, 199, 219],
+ "terminal": [368, 229, 200, 40],
+ "console": [578, 229, 199, 40]
+ },
+ "splitters": {
+ "preview>editor": [358, 0, 10, 269],
+ "editor>search": [777, 0, 10, 269],
+ "editor>terminal": [368, 219, 409, 10],
+ "editor>files": [568, 0, 10, 219],
+ "terminal>console": [568, 229, 10, 40]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-55",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-37",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-38",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-52",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-34",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-58",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-59",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-26"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/fixtures/v1/scenario-16.json b/packages/core/test/fixtures/v1/scenario-16.json
new file mode 100644
index 0000000..b55a67d
--- /dev/null
+++ b/packages/core/test/fixtures/v1/scenario-16.json
@@ -0,0 +1,2577 @@
+{
+ "name": "7 tabs, seed 16",
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview",
+ "console",
+ "files",
+ "search",
+ "git"
+ ],
+ "container": { "width": 640, "height": 420 },
+ "initial": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["search"],
+ "active": "search"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 265, 420],
+ "terminal": [275, 0, 365, 155],
+ "preview": [275, 165, 153, 255],
+ "console": [438, 165, 202, 98],
+ "files": [438, 273, 96, 147],
+ "search": [544, 273, 96, 69],
+ "git": [544, 352, 96, 68]
+ },
+ "splitters": {
+ "editor>terminal": [265, 0, 10, 420],
+ "terminal>preview": [275, 155, 365, 10],
+ "preview>console": [428, 165, 10, 255],
+ "console>files": [438, 263, 202, 10],
+ "files>search": [534, 273, 10, 147],
+ "search>git": [544, 342, 96, 10]
+ },
+ "maximized": null
+ },
+ "initialSavedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-2",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-23",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-28",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "steps": [
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "git" },
+ "target": { "tabsetOf": "files", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "files"
+ ],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ }
+ ]
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 265, 420],
+ "terminal": [275, 0, 365, 155],
+ "preview": [275, 165, 153, 255],
+ "console": [438, 165, 202, 98],
+ "search": [544, 273, 96, 147],
+ "files": [438, 273, 96, 69],
+ "git": [438, 352, 96, 68]
+ },
+ "splitters": {
+ "editor>terminal": [265, 0, 10, 420],
+ "terminal>preview": [275, 155, 365, 10],
+ "preview>console": [428, 165, 10, 255],
+ "console>files": [438, 263, 202, 10],
+ "files>search": [534, 273, 10, 147],
+ "files>git": [438, 342, 96, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "git" },
+ "target": { "tab": "git", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["editor"], "active": "editor" },
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["terminal"], "active": "terminal" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": [
+ "files"
+ ],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "git"
+ ],
+ "active": "git"
+ }
+ ]
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor": [0, 0, 265, 420],
+ "terminal": [275, 0, 365, 155],
+ "preview": [275, 165, 153, 255],
+ "console": [438, 165, 202, 98],
+ "search": [544, 273, 96, 147],
+ "files": [438, 273, 96, 69],
+ "git": [438, 352, 96, 68]
+ },
+ "splitters": {
+ "editor>terminal": [265, 0, 10, 420],
+ "terminal>preview": [275, 155, 365, 10],
+ "preview>console": [428, 165, 10, 255],
+ "console>files": [438, 263, 202, 10],
+ "files>search": [534, 273, 10, 147],
+ "files>git": [438, 342, 96, 10]
+ },
+ "maximized": null
+ },
+ "accepted": false
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "editor" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 155],
+ "preview": [0, 165, 290, 255],
+ "console": [300, 165, 340, 98],
+ "search": [475, 273, 165, 147],
+ "files": [300, 273, 165, 69],
+ "git": [300, 352, 165, 68]
+ },
+ "splitters": {
+ "editor>preview": [0, 155, 640, 10],
+ "preview>console": [290, 165, 10, 255],
+ "console>files": [300, 263, 340, 10],
+ "files>search": [465, 273, 10, 147],
+ "files>git": [300, 342, 165, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "files" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 155],
+ "preview": [0, 165, 290, 255],
+ "console": [300, 165, 340, 98],
+ "search": [475, 273, 165, 147],
+ "files": [0, 0, 640, 420],
+ "git": [300, 352, 165, 68]
+ },
+ "splitters": {
+ "editor>preview": [0, 155, 640, 10],
+ "preview>console": [290, 165, 10, 255],
+ "console>files": [300, 263, 340, 10],
+ "files>search": [465, 273, 10, 147],
+ "files>git": [300, 342, 165, 10]
+ },
+ "maximized": "files"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "files",
+ "after": "git",
+ "point": { "x": 382.5, "y": 268 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["git"],
+ "active": "git"
+ }
+ ]
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 155],
+ "preview": [0, 165, 290, 255],
+ "console": [300, 165, 340, 98],
+ "search": [475, 273, 165, 147],
+ "files": [0, 0, 640, 420],
+ "git": [300, 352, 165, 68]
+ },
+ "splitters": {
+ "editor>preview": [0, 155, 640, 10],
+ "preview>console": [290, 165, 10, 255],
+ "console>files": [300, 263, 340, 10],
+ "files>search": [465, 273, 10, 147],
+ "files>git": [300, 342, 165, 10]
+ },
+ "maximized": "files"
+ },
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-36",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-8",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-12",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-13",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-22",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-36",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-35",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "files" },
+ "target": { "tabsetOf": "terminal", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["editor", "files", "terminal"],
+ "active": "files"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|files|terminal": [0, 0, 640, 180],
+ "preview": [0, 190, 290, 230],
+ "console": [300, 190, 340, 110],
+ "git": [300, 310, 165, 110],
+ "search": [475, 310, 165, 110]
+ },
+ "splitters": {
+ "editor>preview": [0, 180, 640, 10],
+ "preview>console": [290, 190, 10, 230],
+ "console>git": [300, 300, 340, 10],
+ "git>search": [465, 310, 10, 110]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "files" },
+ "target": { "tabsetOf": "git", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["git"],
+ "active": "git"
+ },
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 180],
+ "preview": [0, 190, 265, 230],
+ "console": [275, 190, 365, 110],
+ "git": [275, 310, 115, 110],
+ "files": [400, 310, 115, 110],
+ "search": [525, 310, 115, 110]
+ },
+ "splitters": {
+ "editor>preview": [0, 180, 640, 10],
+ "preview>console": [265, 190, 10, 230],
+ "console>git": [275, 300, 365, 10],
+ "git>files": [390, 310, 10, 110],
+ "files>search": [515, 310, 10, 110]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "git" },
+ "target": { "tabsetOf": "console", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 155],
+ "preview": [0, 165, 290, 255],
+ "console": [300, 165, 340, 78],
+ "git": [300, 253, 340, 79],
+ "files": [300, 342, 165, 78],
+ "search": [475, 342, 165, 78]
+ },
+ "splitters": {
+ "editor>preview": [0, 155, 640, 10],
+ "preview>console": [290, 165, 10, 255],
+ "console>git": [300, 243, 340, 10],
+ "git>files": [300, 332, 340, 10],
+ "files>search": [465, 342, 10, 78]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "fold", "tabsetOf": "console" },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "vertical",
+ "children": [
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["search"],
+ "active": "search"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 155],
+ "preview": [0, 165, 290, 255],
+ "console": [300, 165, 340, 40],
+ "git": [300, 215, 340, 98],
+ "files": [300, 323, 165, 97],
+ "search": [475, 323, 165, 97]
+ },
+ "splitters": {
+ "editor>preview": [0, 155, 640, 10],
+ "preview>console": [290, 165, 10, 255],
+ "console>git": [300, 205, 340, 10],
+ "git>files": [300, 313, 340, 10],
+ "files>search": [465, 323, 10, 97]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tabsetOf": "terminal", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "tabs": ["console"],
+ "active": "console",
+ "folded": true
+ },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 85],
+ "preview": [0, 95, 640, 85],
+ "console": [0, 190, 640, 40],
+ "git": [0, 240, 640, 85],
+ "files": [0, 335, 315, 85],
+ "search": [325, 335, 315, 85]
+ },
+ "splitters": {
+ "editor>preview": [0, 85, 640, 10],
+ "preview>console": [0, 180, 640, 10],
+ "console>git": [0, 230, 640, 10],
+ "git>files": [0, 325, 640, 10],
+ "files>search": [315, 335, 10, 85]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-49",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-17",
+ "nodOpen": "console",
+ "nodFold": true,
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-35",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "console" },
+ "target": { "tabsetOf": "files", "area": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 98],
+ "preview": [0, 108, 640, 97],
+ "git": [0, 215, 640, 98],
+ "files": [0, 323, 207, 97],
+ "console": [217, 323, 206, 97],
+ "search": [433, 323, 207, 97]
+ },
+ "splitters": {
+ "editor>preview": [0, 98, 640, 10],
+ "preview>git": [0, 205, 640, 10],
+ "git>files": [0, 313, 640, 10],
+ "files>console": [207, 323, 10, 97],
+ "console>search": [423, 323, 10, 97]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "editor",
+ "after": "preview",
+ "point": { "x": 320, "y": -19 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "editor|terminal": [0, 0, 640, 40],
+ "preview": [0, 50, 640, 155],
+ "git": [0, 215, 640, 98],
+ "files": [0, 323, 207, 97],
+ "console": [217, 323, 206, 97],
+ "search": [433, 323, 207, 97]
+ },
+ "splitters": {
+ "editor>preview": [0, 40, 640, 10],
+ "preview>git": [0, 205, 640, 10],
+ "git>files": [0, 313, 640, 10],
+ "files>console": [207, 323, 10, 97],
+ "console>search": [423, 323, 10, 97]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "git" },
+ "target": { "root": "top" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "git": [0, 0, 640, 98],
+ "editor|terminal": [0, 108, 640, 40],
+ "preview": [0, 158, 640, 155],
+ "files": [0, 323, 207, 97],
+ "console": [217, 323, 206, 97],
+ "search": [433, 323, 207, 97]
+ },
+ "splitters": {
+ "git>editor": [0, 98, 640, 10],
+ "editor>preview": [0, 148, 640, 10],
+ "preview>files": [0, 313, 640, 10],
+ "files>console": [207, 323, 10, 97],
+ "console>search": [423, 323, 10, 97]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1111, "height": 969 },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ { "tabs": ["preview"], "active": "preview" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "git": [0, 0, 1111, 235],
+ "editor|terminal": [0, 245, 1111, 40],
+ "preview": [0, 295, 1111, 429],
+ "files": [0, 734, 364, 235],
+ "console": [374, 734, 363, 235],
+ "search": [747, 734, 364, 235]
+ },
+ "splitters": {
+ "git>editor": [0, 235, 1111, 10],
+ "editor>preview": [0, 285, 1111, 10],
+ "preview>files": [0, 724, 1111, 10],
+ "files>console": [364, 734, 10, 235],
+ "console>search": [737, 734, 10, 235]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "preview" },
+ "target": { "tab": "search", "position": "left" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["editor", "terminal"], "active": "editor" },
+ {
+ "direction": "horizontal",
+ "children": [
+ { "tabs": ["files"], "active": "files" },
+ { "tabs": ["console"], "active": "console" },
+ {
+ "tabs": ["preview", "search"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ "tabsets": {
+ "git": [0, 0, 1111, 455],
+ "editor|terminal": [0, 465, 1111, 40],
+ "files": [0, 515, 364, 454],
+ "console": [374, 515, 363, 454],
+ "preview|search": [747, 515, 364, 454]
+ },
+ "splitters": {
+ "git>editor": [0, 455, 1111, 10],
+ "editor>files": [0, 505, 1111, 10],
+ "files>console": [364, 515, 10, 454],
+ "console>preview": [737, 515, 10, 454]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-57",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 0,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [611, 0, 500, 969],
+ "git": [0, 0, 601, 455],
+ "editor|terminal": [0, 465, 601, 40],
+ "files": [0, 515, 194, 454],
+ "console": [204, 515, 193, 454],
+ "preview": [407, 515, 194, 454]
+ },
+ "splitters": {
+ "git>search": [601, 0, 10, 969],
+ "git>editor": [0, 455, 601, 10],
+ "editor>files": [0, 505, 601, 10],
+ "files>console": [194, 515, 10, 454],
+ "console>preview": [397, 515, 10, 454]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "splitter",
+ "before": "git",
+ "after": "editor",
+ "point": { "x": 300.5, "y": 400 }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [611, 0, 500, 969],
+ "git": [0, 0, 601, 395],
+ "editor|terminal": [0, 405, 601, 99],
+ "files": [0, 514, 194, 455],
+ "console": [204, 514, 193, 455],
+ "preview": [407, 514, 194, 455]
+ },
+ "splitters": {
+ "git>search": [601, 0, 10, 969],
+ "git>editor": [0, 395, 601, 10],
+ "editor>files": [0, 504, 601, 10],
+ "files>console": [194, 514, 10, 455],
+ "console>preview": [397, 514, 10, 455]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 610, "height": 1014 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [360, 0, 250, 1014],
+ "git": [0, 0, 350, 414],
+ "editor|terminal": [0, 424, 350, 103],
+ "files": [0, 537, 110, 477],
+ "console": [120, 537, 110, 477],
+ "preview": [240, 537, 110, 477]
+ },
+ "splitters": {
+ "git>search": [350, 0, 10, 1014],
+ "git>editor": [0, 414, 350, 10],
+ "editor>files": [0, 527, 350, 10],
+ "files>console": [110, 537, 10, 477],
+ "console>preview": [230, 537, 10, 477]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": { "op": "resize", "width": 861, "height": 400 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["console"],
+ "active": "console"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "search": [486, 0, 375, 400],
+ "git": [0, 0, 476, 151],
+ "editor|terminal": [0, 161, 476, 59],
+ "files": [0, 230, 152, 170],
+ "console": [162, 230, 152, 170],
+ "preview": [324, 230, 152, 170]
+ },
+ "splitters": {
+ "git>search": [476, 0, 10, 400],
+ "git>editor": [0, 151, 476, 10],
+ "editor>files": [0, 220, 476, 10],
+ "files>console": [152, 230, 10, 170],
+ "console>preview": [314, 230, 10, 170]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "console" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "tabs": ["editor", "terminal"],
+ "active": "editor"
+ },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": ["preview"],
+ "active": "preview"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "search": [324, 0, 263, 400],
+ "console": [597, 0, 264, 400],
+ "git": [0, 0, 314, 151],
+ "editor|terminal": [0, 161, 314, 59],
+ "files": [0, 230, 152, 170],
+ "preview": [162, 230, 152, 170]
+ },
+ "splitters": {
+ "git>search": [314, 0, 10, 400],
+ "search>console": [587, 0, 10, 400],
+ "git>editor": [0, 151, 314, 10],
+ "editor>files": [0, 220, 314, 10],
+ "files>preview": [152, 230, 10, 170]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 85.54216867469879,
+ "nodName": "",
+ "uidNode": "id-57",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 14.457831325301203,
+ "nodName": "",
+ "uidNode": "id-7",
+ "nodOpen": "editor",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "preview",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-63",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "editor" },
+ "target": { "tabsetOf": "preview", "area": "contain" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["search"], "active": "search" },
+ { "tabs": ["console"], "active": "console" }
+ ]
+ },
+ "tabsets": {
+ "search": [324, 0, 263, 400],
+ "console": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [162, 193, 152, 207]
+ },
+ "splitters": {
+ "git>search": [314, 0, 10, 400],
+ "search>console": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tab": "search" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [324, 0, 263, 400],
+ "search": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [162, 193, 152, 207]
+ },
+ "splitters": {
+ "git>console": [314, 0, 10, 400],
+ "console>search": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [324, 0, 263, 400],
+ "search": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [0, 0, 861, 400]
+ },
+ "splitters": {
+ "git>console": [314, 0, 10, 400],
+ "console>search": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": "editor|terminal|preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [324, 0, 263, 400],
+ "search": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [162, 193, 152, 207]
+ },
+ "splitters": {
+ "git>console": [314, 0, 10, 400],
+ "console>search": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "preview" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [324, 0, 263, 400],
+ "search": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [0, 0, 861, 400]
+ },
+ "splitters": {
+ "git>console": [314, 0, 10, 400],
+ "console>search": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": "editor|terminal|preview"
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodMaxd": "id-27",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 85.54216867469879,
+ "nodName": "",
+ "uidNode": "id-57",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-18",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-70",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "terminal" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [324, 0, 263, 400],
+ "search": [597, 0, 264, 400],
+ "git": [0, 0, 314, 183],
+ "files": [0, 193, 152, 207],
+ "editor|terminal|preview": [162, 193, 152, 207]
+ },
+ "splitters": {
+ "git>console": [314, 0, 10, 400],
+ "console>search": [587, 0, 10, 400],
+ "git>files": [0, 183, 314, 10],
+ "files>editor": [152, 193, 10, 207]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "resize", "width": 1579, "height": 294 },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ {
+ "direction": "horizontal",
+ "children": [
+ {
+ "tabs": ["files"],
+ "active": "files"
+ },
+ {
+ "tabs": [
+ "editor",
+ "terminal",
+ "preview"
+ ],
+ "active": "terminal"
+ }
+ ]
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [563, 0, 503, 294],
+ "search": [1076, 0, 503, 294],
+ "git": [0, 0, 553, 134],
+ "files": [0, 144, 272, 150],
+ "editor|terminal|preview": [282, 144, 271, 150]
+ },
+ "splitters": {
+ "git>console": [553, 0, 10, 294],
+ "console>search": [1066, 0, 10, 294],
+ "git>files": [0, 134, 553, 10],
+ "files>editor": [272, 144, 10, 150]
+ },
+ "maximized": null
+ }
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "tabsetOf": "files", "area": "bottom" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor", "terminal", "preview"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [530, 0, 519, 294],
+ "search": [1059, 0, 520, 294],
+ "git": [0, 0, 520, 86],
+ "files": [0, 96, 520, 94],
+ "editor|terminal|preview": [0, 200, 520, 94]
+ },
+ "splitters": {
+ "git>console": [520, 0, 10, 294],
+ "console>search": [1049, 0, 10, 294],
+ "git>files": [0, 86, 520, 10],
+ "files>editor": [0, 190, 520, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true
+ },
+ {
+ "operation": { "op": "maximize", "tabsetOf": "editor" },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["files"], "active": "files" },
+ {
+ "tabs": ["editor", "terminal", "preview"],
+ "active": "terminal"
+ }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" }
+ ]
+ },
+ "tabsets": {
+ "console": [530, 0, 519, 294],
+ "search": [1059, 0, 520, 294],
+ "git": [0, 0, 520, 86],
+ "files": [0, 96, 520, 94],
+ "editor|terminal|preview": [0, 0, 1579, 294]
+ },
+ "splitters": {
+ "git>console": [520, 0, 10, 294],
+ "console>search": [1049, 0, 10, 294],
+ "git>files": [0, 86, 520, 10],
+ "files>editor": [0, 190, 520, 10]
+ },
+ "maximized": "editor|terminal|preview"
+ },
+ "accepted": true
+ },
+ {
+ "operation": {
+ "op": "move",
+ "source": { "tabsetOf": "preview" },
+ "target": { "root": "right" }
+ },
+ "observation": {
+ "tree": {
+ "direction": "horizontal",
+ "children": [
+ {
+ "direction": "vertical",
+ "children": [
+ { "tabs": ["git"], "active": "git" },
+ { "tabs": ["files"], "active": "files" }
+ ]
+ },
+ { "tabs": ["console"], "active": "console" },
+ { "tabs": ["search"], "active": "search" },
+ {
+ "tabs": ["editor", "terminal", "preview"],
+ "active": "terminal"
+ }
+ ]
+ },
+ "tabsets": {
+ "console": [397, 0, 388, 294],
+ "search": [795, 0, 387, 294],
+ "editor|terminal|preview": [1192, 0, 387, 294],
+ "git": [0, 0, 387, 134],
+ "files": [0, 144, 387, 150]
+ },
+ "splitters": {
+ "git>console": [387, 0, 10, 294],
+ "console>search": [785, 0, 10, 294],
+ "search>editor": [1182, 0, 10, 294],
+ "git>files": [0, 134, 387, 10]
+ },
+ "maximized": null
+ },
+ "accepted": true,
+ "savedV1": {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "dynamix-layout-root",
+ "uidNode": "dynamix-layout-root",
+ "nodKids": [
+ {
+ "typNode": "row",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-3",
+ "nodKids": [
+ {
+ "typNode": "tabset",
+ "nodPart": 85.54216867469879,
+ "nodName": "",
+ "uidNode": "id-57",
+ "nodOpen": "git",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "git",
+ "uidNode": "id-31"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-43",
+ "nodOpen": "files",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "files",
+ "uidNode": "id-24"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-53",
+ "nodOpen": "console",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "console",
+ "uidNode": "id-19"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-70",
+ "nodOpen": "search",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "search",
+ "uidNode": "id-29"
+ }
+ ]
+ },
+ {
+ "typNode": "tabset",
+ "nodPart": 100,
+ "nodName": "",
+ "uidNode": "id-27",
+ "nodOpen": "terminal",
+ "nodKids": [
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "editor",
+ "uidNode": "id-4"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "terminal",
+ "uidNode": "id-9"
+ },
+ {
+ "typNode": "tab",
+ "nodPart": 100,
+ "nodName": "preview",
+ "uidNode": "id-14"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/core/test/geometry.test.ts b/packages/core/test/geometry.test.ts
deleted file mode 100644
index 3315668..0000000
--- a/packages/core/test/geometry.test.ts
+++ /dev/null
@@ -1,319 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { DynamixLayoutCore, Node } from '../src'
-import type { LayoutTree } from '../src'
-
-const BOND = 10
-
-/** Every row's kids (plus bonds) must tile the row exactly along its axis and
- * span it fully across the other axis. Returns human readable problems. */
-const geometryProblems = (root: Node): string[] => {
- const problems: string[] = []
- const walk = (n: Node, path: string) => {
- const kids = [...n.kids]
- if (n.type === 'row' && kids.length) {
- const dir = Node.cache.mapDirs.get(n.unId)
- const [pos, size, cpos, csize] = dir
- ? (['x', 'w', 'y', 'h'] as const)
- : (['y', 'h', 'x', 'w'] as const)
- let cursor = n.dims[pos]
- kids.forEach((k, i) => {
- if (k.dims[pos] !== cursor)
- problems.push(
- `${path}/${i} ${pos}=${k.dims[pos]} want ${cursor}`
- )
- if (
- k.dims[cpos] !== n.dims[cpos] ||
- k.dims[csize] !== n.dims[csize]
- )
- problems.push(`${path}/${i} cross-axis mismatch`)
- if (k.dims[size] < 0)
- problems.push(`${path}/${i} negative ${size}`)
- cursor = k.dims[pos] + k.dims[size] + BOND
- })
- const end = cursor - BOND
- const parentEnd = n.dims[pos] + n.dims[size]
- if (end !== parentEnd)
- problems.push(
- `${path} kids end at ${end}, row ends at ${parentEnd}`
- )
- }
- kids.forEach((k, i) => walk(k, `${path}/${i}`))
- }
- walk(root, 'root')
- return problems
-}
-
-/** Tabs must survive every move and the tree/caches must stay consistent. */
-const structureProblems = (root: Node, expectedTabs: string[]): string[] => {
- const problems: string[] = []
- const tabs: string[] = []
- let live = 0
- const walk = (n: Node) => {
- live++
- if (n.next) live++
- if (n.type === 'tab') tabs.push(n.name)
- if (n.type === 'tabset' && n.kids.size() === 0)
- problems.push('empty tabset')
- for (const k of n.kids) {
- if (k.host !== n) problems.push(`${k.type} has wrong host`)
- if (n.type === 'tabset' && k.type !== 'tab')
- problems.push(`tabset contains ${k.type}`)
- walk(k)
- }
- }
- walk(root)
- if (tabs.sort().join() !== [...expectedTabs].sort().join())
- problems.push(`tabs changed: ${tabs.sort().join()}`)
- if (Node.cache.mapDirs.size > live)
- problems.push(
- `mapDirs has ${Node.cache.mapDirs.size} entries for ${live} live nodes`
- )
- return problems
-}
-
-const depthOf = (n: Node): number =>
- n.kids.size() ? 1 + Math.max(...[...n.kids].map(depthOf)) : 0
-
-describe('Layout geometry', () => {
- beforeEach(() => {
- new DynamixLayoutCore().clearAllCache()
- })
-
- it.each([7, 11, 99, 1, 2, 3, 42, 777, 2024, 31337])(
- 'keeps tabs and exact tiling after random moves (seed %i)',
- (initialSeed) => {
- const tabNames = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
- const layout = new DynamixLayoutCore({ tabs: tabNames, bond: BOND })
- layout.updateDimension({ w: 1440, h: 900, x: 0, y: 0 }, true)
-
- let seed = initialSeed
- const rnd = (n: number) => {
- seed = (seed * 16807) % 2147483647
- return seed % n
- }
- const areas = ['top', 'bottom', 'left', 'right', 'contain'] as const
- const failures: string[] = []
- let maxDepth = 0
-
- for (let i = 0; i < 400 && failures.length < 5; i++) {
- const nodes = [...Node.cache.mapElem.values()].filter(
- (n): n is Node => n instanceof Node
- )
- const srcs = nodes.filter(
- (n) => n.type === 'tab' || n.type === 'tabset'
- )
- const src = srcs[rnd(srcs.length)]
- // Drop targets the UI can produce: tabs, tabsets and the root.
- const targets = nodes.filter(
- (n) => n.type !== 'row' || n === DynamixLayoutCore._root
- )
- const des = targets[rnd(targets.length)]
- if (!src || !des) continue
-
- const area = areas[rnd(areas.length)]
- let ok: boolean
- try {
- ok = layout.updateTree(src.unId, des.unId, area)
- } catch (e) {
- failures.push(
- `move ${i} (${src.type} -> ${des.type} ${area}) threw: ${(e as Error).message}`
- )
- break
- }
- if (!ok) continue
-
- layout.updateDimension({ w: 1440, h: 900, x: 0, y: 0 }, true)
- maxDepth = Math.max(maxDepth, depthOf(DynamixLayoutCore._root))
- const p = [
- ...structureProblems(DynamixLayoutCore._root, tabNames),
- ...geometryProblems(DynamixLayoutCore._root),
- ]
- if (p.length)
- failures.push(
- `move ${i} (${src.type} -> ${des.type} ${area}), depth ${depthOf(DynamixLayoutCore._root)}: ${p.slice(0, 3).join('; ')}`
- )
- }
-
- expect(maxDepth).toBeGreaterThan(3)
- expect(failures).toEqual([])
- }
- )
-
- it('moves every edge smoothly while the container resizes', () => {
- // Shape of the React demo: a three-column row (the last column split in
- // two) above a terminal, with uneven parts left behind by slider drags.
- const tabset = (name: string, part: number): LayoutTree => ({
- typNode: 'tabset',
- nodName: '',
- uidNode: `ts-${name}`,
- nodPart: part,
- nodOpen: name,
- nodKids: [
- {
- typNode: 'tab',
- nodName: name,
- uidNode: `tab-${name}`,
- nodPart: 100,
- },
- ],
- })
- const row = (
- uid: string,
- part: number,
- kids: LayoutTree[]
- ): LayoutTree => ({
- typNode: 'row',
- nodName: '',
- uidNode: uid,
- nodPart: part,
- nodKids: kids,
- })
- const layout = new DynamixLayoutCore({
- tree: row('dynamix-layout-root', 100, [
- row('main', 95.14, [
- row('columns', 253.64, [
- tabset('ts', 95.68),
- tabset('preview', 102.47),
- row('side', 101.85, [
- tabset('css', 100),
- tabset('html', 100),
- ]),
- ]),
- tabset('terminal', 97.76),
- ]),
- ]),
- bond: BOND,
- })
-
- let backward = 0
- let maxJump = 0
- let last: number[] | null = null
-
- for (let w = 600; w <= 2560; w++) {
- layout.updateDimension({ w, h: 900, x: 0, y: 0 }, true)
- const edges: number[] = []
- for (const [, o] of Node.cache.nodOpts.get())
- edges.push(o.nodDims.x, o.nodDims.x + o.nodDims.w)
- if (last)
- edges.forEach((e, i) => {
- const d = e - last![i]
- if (d < 0) backward++
- maxJump = Math.max(maxJump, Math.abs(d))
- })
- last = edges
- }
-
- expect(backward).toBe(0)
- expect(maxJump).toBeLessThanOrEqual(1)
- })
-
- it('does not move unrelated panels while dragging a splitter', () => {
- const layout = new DynamixLayoutCore({
- tree: {
- typNode: 'row',
- nodName: 'dynamix-layout-root',
- uidNode: 'dynamix-layout-root',
- nodPart: 100,
- nodKids: ['a', 'b', 'c'].map((name) => ({
- typNode: 'tabset' as const,
- nodName: '',
- uidNode: `ts-${name}`,
- nodPart: 100,
- nodOpen: name,
- nodKids: [
- {
- typNode: 'tab' as const,
- nodName: name,
- uidNode: `tab-${name}`,
- nodPart: 100,
- },
- ],
- })),
- },
- bond: BOND,
- })
- layout.updateDimension({ w: 1440, h: 900, x: 0, y: 0 }, true)
-
- const first = Node.cache.mapElem.get('ts-a') as Node
- const third = Node.cache.mapElem.get('ts-c') as Node
- const bondId = first.next!.unId
- const before = { ...third.dims }
-
- // Real pointers report fractional coordinates on HiDPI screens.
- for (let i = 0; i < 400; i++) {
- const x = first.next!.dims.x + Math.sin(i / 7) * 150 + 0.37
- layout.updateSliderDimension(bondId, { x, y: 0 })
- expect(third.dims).toEqual(before)
- }
- })
-
- it('keeps all tabs when the only tabset is dropped on a root edge', () => {
- const tabNames = ['a', 'b', 'c']
- const layout = new DynamixLayoutCore({ tabs: tabNames, bond: BOND })
- layout.updateDimension({ w: 1200, h: 800, x: 0, y: 0 }, true)
-
- // Gather every tab into one tabset.
- const tabsets = () =>
- [...Node.cache.mapElem.values()].filter(
- (n): n is Node => n instanceof Node && n.type === 'tabset'
- )
- while (tabsets().length > 1) {
- const [target, source] = tabsets()
- const tab = source.kids.peek()!
- expect(layout.updateTree(tab.unId, target.unId, 'contain')).toBe(
- true
- )
- }
-
- const only = tabsets()[0]
- for (const area of ['top', 'bottom', 'left', 'right'] as const) {
- expect(
- layout.updateTree(only.unId, DynamixLayoutCore._root.unId, area)
- ).toBe(false)
- }
- expect(structureProblems(DynamixLayoutCore._root, tabNames)).toEqual([])
- })
-
- it('does not reuse the tree of a previously created layout', () => {
- new DynamixLayoutCore({
- tree: {
- typNode: 'row',
- nodName: 'dynamix-layout-root',
- uidNode: 'dynamix-layout-root',
- nodPart: 100,
- nodKids: [
- {
- typNode: 'tabset',
- nodName: '',
- uidNode: 'ts-old',
- nodPart: 100,
- nodOpen: 'old',
- nodKids: [
- {
- typNode: 'tab',
- nodName: 'old',
- uidNode: 'tab-old',
- nodPart: 100,
- },
- ],
- },
- ],
- },
- })
-
- new DynamixLayoutCore({ tabs: ['x', 'y'] })
-
- expect(structureProblems(DynamixLayoutCore._root, ['x', 'y'])).toEqual(
- []
- )
- })
-
- it('gives a single tab the whole layout', () => {
- const layout = new DynamixLayoutCore({ tabs: ['only'], bond: BOND })
- layout.updateDimension({ w: 1200, h: 800, x: 0, y: 0 }, true)
-
- const root = DynamixLayoutCore._root
- expect(root.kids.size()).toBe(1)
- expect(root.kids.peek()!.dims).toEqual({ w: 1200, h: 800, x: 0, y: 0 })
- })
-})
diff --git a/packages/core/test/manual.test.ts b/packages/core/test/manual.test.ts
deleted file mode 100644
index 3ccd91b..0000000
--- a/packages/core/test/manual.test.ts
+++ /dev/null
@@ -1,444 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { DynamixLayoutCore, LayoutTree, Node, Queue } from '../src'
-
-/**
- * Yields all nodes in the layout tree using breadth-first traversal
- */
-function* nodeLayoutIterator(root: Node): Generator {
- const queue = new Queue()
- if (root) queue.enqueue(root)
-
- while (!queue.isEmpty()) {
- const current = queue.dequeue()!
- yield current
- for (const child of current.kids) {
- queue.enqueue(child)
- }
- }
-}
-
-/**
- * Validates the integrity of the layout tree structure
- *
- * Checks:
- * - Parent-child relationships are bidirectional
- * - Layout directions are opposite between parent and child
- * - Sibling doubly-linked lists are intact
- * - All nodes have proper host references
- *
- * @param node - Root node to validate (defaults to Layout._root)
- * @returns true if tree structure is valid, false otherwise
- */
-const validateTree = (node: Node = DynamixLayoutCore._root): boolean => {
- let isValid = true
- const allNodes = Array.from(nodeLayoutIterator(node))
-
- console.group(`Validating Tree Integrity for ${allNodes.length} nodes...`)
-
- for (const currentNode of allNodes) {
- let nodeIsValid = true
- const id = `Node (uid: ${currentNode.unId}, type: ${currentNode.type}, name: '${currentNode.name}')`
- console.groupCollapsed(`Checking ${id}`)
-
- // Validate parent-child host relationship
- if (currentNode !== DynamixLayoutCore._root) {
- if (!currentNode.host) {
- console.error(`❌ ORPHANED: ${id} has a null host.`)
- nodeIsValid = false
- } else if (currentNode.host.kids.indexOf(currentNode) === -1) {
- console.error(
- `❌ DISOWNED: ${id}'s host (uid: ${currentNode.host.unId}) does not contain it in its kids queue.`
- )
- nodeIsValid = false
- }
- }
-
- // Validate children's host relationship
- for (const kid of currentNode.kids) {
- if (kid.host !== currentNode) {
- console.error(
- `❌ WRONG PARENT: Child (uid: ${kid.unId}) has host (uid: ${kid.host?.unId}), but should be ${currentNode.unId}.`
- )
- nodeIsValid = false
- }
- }
-
- // Validate layout direction
- if (currentNode !== DynamixLayoutCore._root && currentNode.host) {
- const parentDir = Node.cache.mapDirs.get(currentNode.host.unId)
- const currentDir = Node.cache.mapDirs.get(currentNode.unId)
-
- if (parentDir === undefined) {
- console.error(
- `❌ MISSING PARENT DIR: Host (uid: ${currentNode.host.unId}) of ${id} has no direction set in the cache.`
- )
- nodeIsValid = false
- } else if (currentDir === undefined) {
- console.error(
- `❌ MISSING OWN DIR: ${id} has no direction set in the cache.`
- )
- nodeIsValid = false
- } else if (currentDir === parentDir) {
- console.error(
- `❌ DIRECTION CONFLICT: ${id} has the same direction (${currentDir}) as its host. It should be the opposite.`
- )
- nodeIsValid = false
- }
- }
-
- // Validate sibling bonds for container nodes
- if (currentNode.type === 'row' || currentNode.type === 'tabset') {
- const kidsArray = currentNode.kids.toArray()
- if (kidsArray.length > 0) {
- if (kidsArray[0].prev !== null) {
- console.error(
- `❌ BROKEN START: First child (uid: ${kidsArray[0].unId})'s 'prev' link is not null.`
- )
- nodeIsValid = false
- }
- if (kidsArray[kidsArray.length - 1].next !== null) {
- console.error(
- `❌ BROKEN END: Last child (uid: ${kidsArray[kidsArray.length - 1].unId})'s 'next' link is not null.`
- )
- nodeIsValid = false
- }
-
- for (let i = 0; i < kidsArray.length - 1; i++) {
- const currentKid = kidsArray[i]
- const nextKid = kidsArray[i + 1]
-
- if (!currentKid.next) {
- console.error(
- `❌ BROKEN CHAIN: Child (uid: ${currentKid.unId}) is missing its 'next' bond.`
- )
- nodeIsValid = false
- continue
- }
- if (currentKid.next.next !== nextKid) {
- console.error(
- `❌ FORWARD LINK: Child (uid: ${currentKid.unId})'s 'next' bond does not point to the correct next sibling (uid: ${nextKid.unId}).`
- )
- nodeIsValid = false
- }
- if (!nextKid.prev) {
- console.error(
- `❌ BROKEN CHAIN: Child (uid: ${nextKid.unId}) is missing its 'prev' bond.`
- )
- nodeIsValid = false
- continue
- }
- if (nextKid.prev.prev !== currentKid) {
- console.error(
- `❌ BACKWARD LINK: Child (uid: ${nextKid.unId})'s 'prev' bond does not point back to the correct previous sibling (uid: ${currentKid.unId}).`
- )
- nodeIsValid = false
- }
- if (currentKid.next.host !== currentNode) {
- console.error(
- `❌ BOND HOST: Bond between ${currentKid.unId} and ${nextKid.unId} has incorrect host (uid: ${currentKid.next.host?.unId}). Should be ${currentNode.unId}.`
- )
- nodeIsValid = false
- }
- }
- }
- }
-
- if (nodeIsValid) {
- console.log(`✅ OK: ${id} passed all checks.`)
- }
-
- console.groupEnd()
- if (!nodeIsValid) {
- isValid = false
- }
- }
-
- if (isValid) {
- console.log(
- '✅ Validation Complete: Tree integrity and directions are OK.'
- )
- } else {
- console.error(
- '❌ Validation Complete: Found one or more issues in the tree structure.'
- )
- }
- console.groupEnd()
-
- return isValid
-}
-
-describe('Comprehensive Move Test Suite', () => {
- const initialLayoutTree: LayoutTree = {
- typNode: 'row',
- nodPart: 100,
- nodName: 'dynamix-layout-root',
- uidNode: 'dynamix-layout-root',
- nodKids: [
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode: '4fb93a5d-2436-4e1d-b530-d88958b21acd',
- nodOpen: 'sidebar',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'sidebar',
- uidNode: '51f72497-4be6-4e67-95b6-240b3320105a',
- },
- ],
- },
- {
- typNode: 'row',
- nodPart: 100,
- nodName: '',
- uidNode: '534112f9-0566-484d-b3a7-a36a92cda495',
- nodKids: [
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode: '01e53ba1-1456-45b0-a2e7-ccb4a399738d',
- nodOpen: 'codeblock',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'codeblock',
- uidNode: '10685b53-612e-4ad9-9167-74b8237ebce9',
- },
- ],
- },
- {
- typNode: 'row',
- nodPart: 100,
- nodName: '',
- uidNode: '637e68dc-feec-4eb0-a70a-6e4ec9fbf5af',
- nodKids: [
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode: 'ee984aa2-b46c-469e-a24d-43492edacf71',
- nodOpen: 'terminal',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'terminal',
- uidNode:
- '604bc026-018d-40f0-85a6-aa02e65db899',
- },
- ],
- },
- {
- typNode: 'row',
- nodPart: 100,
- nodName: '',
- uidNode: 'fd5e41be-07b1-4caa-a72e-9cc5226e895e',
- nodKids: [
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode:
- 'c0d0d6e5-c4bc-4146-8911-017257757d2a',
- nodOpen: 'editor',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'editor',
- uidNode:
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- },
- ],
- },
- {
- typNode: 'row',
- nodPart: 100,
- nodName: '',
- uidNode:
- '10fbe6c8-18d4-439a-ad90-21fd3241141c',
- nodKids: [
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode:
- 'c379ba53-044e-4f54-812e-f5e2219ece6d',
- nodOpen: 'preview',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'preview',
- uidNode:
- '40f1397b-240a-4ef4-bb5e-0b9f3044fdd6',
- },
- ],
- },
- {
- typNode: 'tabset',
- nodPart: 100,
- nodName: '',
- uidNode:
- '88d41c42-cc22-46ed-89e7-055070f6d9f6',
- nodOpen: 'settings',
- nodKids: [
- {
- typNode: 'tab',
- nodPart: 100,
- nodName: 'settings',
- uidNode:
- '61954f56-03ae-431b-819b-287c0a5ce315',
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- }
-
- let layout: DynamixLayoutCore
-
- beforeEach(() => {
- const layoutTreeCopy = JSON.parse(JSON.stringify(initialLayoutTree))
- layout = new DynamixLayoutCore({ tree: layoutTreeCopy })
- })
-
- describe('Splitting Panels', () => {
- it('should split vertical (drop top)', () => {
- const success = layout.updateTree(
- '604bc026-018d-40f0-85a6-aa02e65db899',
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- 'top'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
-
- it('should split horizontal (drop right)', () => {
- const success = layout.updateTree(
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- '604bc026-018d-40f0-85a6-aa02e65db899',
- 'right'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Containing (Moving Into Tabsets)', () => {
- it('should contain tab (move settings into editor)', () => {
- const success = layout.updateTree(
- '61954f56-03ae-431b-819b-287c0a5ce315',
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- 'contain'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
-
- it('should contain tabset (move terminal into sidebar)', () => {
- const success = layout.updateTree(
- 'ee984aa2-b46c-469e-a24d-43492edacf71',
- '51f72497-4be6-4e67-95b6-240b3320105a',
- 'contain'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Moving Outward (Deep to Shallow)', () => {
- it('should move deepest tab to top level (settings to left of sidebar)', () => {
- const success = layout.updateTree(
- '61954f56-03ae-431b-819b-287c0a5ce315',
- '51f72497-4be6-4e67-95b6-240b3320105a',
- 'left'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Moving Inward (Shallow to Deep)', () => {
- it('should move top level tab to deepest (sidebar to bottom of settings)', () => {
- const success = layout.updateTree(
- '51f72497-4be6-4e67-95b6-240b3320105a',
- '61954f56-03ae-431b-819b-287c0a5ce315',
- 'bottom'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Moving Between Different Branches', () => {
- it('should move across branches (codeblock to right of preview)', () => {
- const success = layout.updateTree(
- '10685b53-612e-4ad9-9167-74b8237ebce9',
- '40f1397b-240a-4ef4-bb5e-0b9f3044fdd6',
- 'right'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Sibling and Root Edge Cases', () => {
- it('should move sibling left (settings to left of preview)', () => {
- const success = layout.updateTree(
- '88d41c42-cc22-46ed-89e7-055070f6d9f6',
- 'c379ba53-044e-4f54-812e-f5e2219ece6d',
- 'left'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
-
- it('should move to root (bottom)', () => {
- const success = layout.updateTree(
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- 'dynamix-layout-root',
- 'bottom'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-
- describe('Validation Function', () => {
- it('should validate tree structure on initial layout', () => {
- expect(validateTree()).toBe(true)
- })
-
- it('should detect invalid tree structure', () => {
- const success = layout.updateTree(
- 'd7586e7b-9735-4745-b6ac-bf739686db79',
- 'dynamix-layout-root',
- 'bottom'
- )
-
- expect(success).toBe(true)
- expect(validateTree()).toBe(true)
- })
- })
-})
diff --git a/packages/core/test/slider.test.ts b/packages/core/test/slider.test.ts
deleted file mode 100644
index 9f85cd8..0000000
--- a/packages/core/test/slider.test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { DynamixLayoutCore, Node, Bond } from '../src'
-
-describe('Slider updates', () => {
- beforeEach(() => {
- const layout = new DynamixLayoutCore()
- layout.clearAllCache()
- })
-
- it('keeps tabsets, bonds and tabs outside the dragged subtree', () => {
- const tabs = ['a', 'b', 'c', 'd', 'e']
- const layout = new DynamixLayoutCore({ tabs })
- layout.updateDimension({ w: 1200, h: 800, x: 0, y: 0 }, true)
-
- const tabsetIds = [...Node.cache.nodOpts.get().keys()]
- const bondIds = [...Node.cache.bndOpts.get().keys()]
- const tabIds = [...Node.cache.tabOpts.get().keys()]
-
- const nestedBondId = bondIds.find((id) => {
- const bond = Node.cache.mapElem.get(id)
- return bond instanceof Bond && bond.host !== DynamixLayoutCore._root
- })
- expect(nestedBondId).toBeDefined()
-
- const bond = Node.cache.mapElem.get(nestedBondId!) as Bond
- layout.updateSliderDimension(nestedBondId!, {
- x: bond.dims.x + 20,
- y: bond.dims.y + 20,
- })
-
- expect([...Node.cache.nodOpts.get().keys()].sort()).toEqual(
- tabsetIds.sort()
- )
- expect([...Node.cache.bndOpts.get().keys()].sort()).toEqual(
- bondIds.sort()
- )
- expect([...Node.cache.tabOpts.get().keys()].sort()).toEqual(
- tabIds.sort()
- )
- })
-
- it('notifies listeners once per update and stops after unsubscribe', () => {
- const layout = new DynamixLayoutCore({ tabs: ['a', 'b', 'c'] })
- layout.updateDimension({ w: 900, h: 600, x: 0, y: 0 }, true)
-
- let calls = 0
- const off = Node.cache.bndOpts.onChange(() => calls++)
-
- const bondId = [...Node.cache.bndOpts.get().keys()][0]
- const bond = Node.cache.mapElem.get(bondId) as Bond
- layout.updateSliderDimension(bondId, {
- x: bond.dims.x + 30,
- y: bond.dims.y + 30,
- })
- expect(calls).toBe(1)
-
- off()
- layout.updateSliderDimension(bondId, {
- x: bond.dims.x + 30,
- y: bond.dims.y + 30,
- })
- expect(calls).toBe(1)
- })
-})
diff --git a/packages/core/test/utils/test.utils.ts b/packages/core/test/utils/test.utils.ts
deleted file mode 100644
index 9ba932d..0000000
--- a/packages/core/test/utils/test.utils.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Node, Queue } from '../../src'
-
-/**
- * A standalone tree iterator that doesn't rely on a Layout instance.
- */
-function* nodeLayoutIterator(root: Node): Generator {
- const queue = new Queue()
- if (root) queue.enqueue(root)
-
- while (!queue.isEmpty()) {
- const current = queue.dequeue()!
- yield current
- for (const child of current.kids) {
- queue.enqueue(child)
- }
- }
-}
-
-/**
- * Asserts that a node tree is structurally and logically intact.
- * Throws a detailed error if any validation check fails.
- * @param rootNode The root node of the tree to validate.
- */
-export function assertNodeTreeIsValid(rootNode: Node): void {
- if (!rootNode) throw new Error('Validation Error: Root node is null.')
-
- const allNodes = Array.from(nodeLayoutIterator(rootNode))
-
- for (const node of allNodes) {
- const nodeId = `Node (uid: ${node.unId}, type: ${node.type})`
-
- // 1. Parent-Child Host Consistency
- if (node !== rootNode) {
- if (!node.host)
- throw new Error(`ORPHANED: ${nodeId} has a null host.`)
- if (node.host.kids.indexOf(node) === -1) {
- throw new Error(
- `DISOWNED: ${nodeId}'s host does not contain it in its kids queue.`
- )
- }
- }
- for (const kid of node.kids) {
- if (kid.host !== node) {
- throw new Error(
- `WRONG PARENT: Child ${kid.unId} does not point back to parent ${node.unId}.`
- )
- }
- }
-
- // 2. Layout Direction Consistency
- if (node !== rootNode && node.host) {
- const parentDir = Node.cache.mapDirs.get(node.host.unId)
- const currentDir = Node.cache.mapDirs.get(node.unId)
- if (currentDir === undefined)
- throw new Error(`MISSING DIR: ${nodeId} has no direction set.`)
- if (parentDir === currentDir) {
- throw new Error(
- `DIRECTION CONFLICT: ${nodeId} has the same direction as its parent.`
- )
- }
- }
-
- // 3. Sibling (Linked List) Consistency
- const kids = node.kids.toArray()
- if (kids.length > 0) {
- if (kids[0].prev !== null)
- throw new Error(
- `BROKEN START: First child ${kids[0].unId} has a non-null 'prev' link.`
- )
- if (kids[kids.length - 1].next !== null)
- throw new Error(
- `BROKEN END: Last child ${kids[kids.length - 1].unId} has a non-null 'next' link.`
- )
-
- for (let i = 0; i < kids.length - 1; i++) {
- if (kids[i].next?.next !== kids[i + 1])
- throw new Error(`BROKEN FORWARD LINK near ${kids[i].unId}.`)
- if (kids[i + 1].prev?.prev !== kids[i])
- throw new Error(
- `BROKEN BACKWARD LINK near ${kids[i + 1].unId}.`
- )
- }
- }
- }
-}
diff --git a/packages/core/test/v2/create-layout.test.ts b/packages/core/test/v2/create-layout.test.ts
new file mode 100644
index 0000000..e75fdbf
--- /dev/null
+++ b/packages/core/test/v2/create-layout.test.ts
@@ -0,0 +1,280 @@
+import { describe, it, expect, vi } from 'vitest'
+import { createLayout } from '../../src/create-layout'
+import { DynamixLayoutError } from '../../src/errors'
+import type { LayoutOptions } from '../../src/layout-types'
+
+const BOX = { x: 0, y: 0, width: 1210, height: 800 }
+const tabs = (...ids: string[]) => ids.map((id) => ({ id }))
+const create = (options: Partial = {}) => {
+ const layout = createLayout({
+ tabs: tabs('a', 'b', 'c'),
+ onWarning: () => {},
+ ...options,
+ })
+ layout.setContainerRect(BOX)
+ return layout
+}
+
+describe('instances', () => {
+ it('are independent of each other', () => {
+ const first = create()
+ const second = create({ tabs: tabs('x', 'y') })
+ first.moveTab('a', { type: 'root', position: 'bottom' })
+ expect(second.toJSON().root.children.map((c) => c.id)).toEqual([
+ 'ts-x',
+ 'ts-y',
+ ])
+ expect([...second.getSnapshot().tabs.keys()]).toEqual(['x', 'y'])
+ expect(first.getSnapshot().tabs.has('x')).toBe(false)
+ })
+
+ it('produce identical output for identical input (SSR-safe ids)', () => {
+ expect(JSON.stringify(create().toJSON())).toBe(
+ JSON.stringify(create().toJSON())
+ )
+ expect(create().toJSON().root.children[0].id).toBe('ts-a')
+ })
+
+ it('throws on duplicate tab ids in development', () => {
+ expect(() => createLayout({ tabs: tabs('a', 'a') })).toThrow(
+ DynamixLayoutError
+ )
+ })
+
+ it('restores a saved layout, v2 or v1, and fills in missing tabs', () => {
+ const saved = create()
+ saved.moveTab('c', { type: 'root', position: 'top' })
+ const restored = create({
+ initialLayout: saved.toJSON(),
+ tabs: tabs('a', 'b', 'c', 'd'),
+ })
+ expect(restored.getSnapshot().tabs.get('d')!.tabsetId).toBe(
+ restored.getSnapshot().tabs.get('c')!.tabsetId
+ )
+ expect(restored.getSnapshot().tabs.get('d')!.isActive).toBe(false)
+ const v1 = {
+ typNode: 'row',
+ nodName: 'r',
+ uidNode: 'r',
+ nodPart: 100,
+ nodKids: [
+ {
+ typNode: 'tabset',
+ nodName: '',
+ uidNode: 't',
+ nodPart: 100,
+ nodOpen: 'a',
+ nodKids: [
+ {
+ typNode: 'tab',
+ nodName: 'a',
+ uidNode: 'u',
+ nodPart: 100,
+ },
+ ],
+ },
+ ],
+ } as const
+ const fromV1 = create({ initialLayout: v1, tabs: tabs('a') })
+ expect(fromV1.toJSON().root.children[0]).toMatchObject({
+ id: 't',
+ activeTabId: 'a',
+ })
+ })
+})
+
+describe('snapshots', () => {
+ it('share unchanged parts by reference', () => {
+ const layout = create()
+ const before = layout.getSnapshot()
+ layout.setContainerRect({ ...BOX, width: 1300 })
+ const resized = layout.getSnapshot()
+ expect(resized).not.toBe(before)
+ expect(resized.tabsets).toBe(before.tabsets)
+ expect(resized.root).toBe(before.root)
+ expect(resized.rects).not.toBe(before.rects)
+
+ layout.selectTab('b')
+ const selected = layout.getSnapshot()
+ expect(selected.tabsets.get('ts-a')).toBe(resized.tabsets.get('ts-a'))
+ layout.setContainerRect({ ...BOX, width: 1300 })
+ expect(layout.getSnapshot()).toBe(selected)
+ })
+
+ it('are frozen in development', () => {
+ const snapshot = create().getSnapshot()
+ expect(Object.isFrozen(snapshot)).toBe(true)
+ expect(Object.isFrozen(snapshot.tabsets.get('ts-a'))).toBe(true)
+ })
+
+ it('notify subscribers until they unsubscribe', () => {
+ const layout = create()
+ const listener = vi.fn()
+ const unsubscribe = layout.subscribe(listener)
+ layout.fold('ts-a')
+ expect(listener).toHaveBeenCalledWith(layout.getSnapshot())
+ unsubscribe()
+ layout.unfold('ts-a')
+ expect(listener).toHaveBeenCalledTimes(1)
+ })
+})
+
+describe('onLayoutChange', () => {
+ it('reports committed changes with a reason and v2 JSON', () => {
+ const onLayoutChange = vi.fn()
+ const layout = create({ onLayoutChange })
+ expect(onLayoutChange).not.toHaveBeenCalled()
+ layout.selectTab('b')
+ layout.moveTab('a', {
+ type: 'tabset',
+ tabsetId: 'ts-b',
+ position: 'center',
+ })
+ layout.moveSplitterBy([...layout.getSnapshot().splitters.keys()][0], 30)
+ layout.toggleMaximize('ts-b')
+ layout.fold('ts-c')
+ layout.addTab({ id: 'd' })
+ expect(onLayoutChange.mock.calls.map(([, reason]) => reason)).toEqual([
+ 'select',
+ 'move',
+ 'resize',
+ 'maximize',
+ 'fold',
+ 'tabs',
+ ])
+ expect(onLayoutChange.mock.calls[0][0].version).toBe(2)
+ })
+
+ it('is not called for resizes, refused actions or load', () => {
+ const onLayoutChange = vi.fn()
+ const layout = create({ onLayoutChange })
+ layout.setContainerRect({ ...BOX, width: 900 })
+ expect(
+ layout.moveTab('a', { type: 'tab', tabId: 'a', position: 'after' })
+ ).toBe(false)
+ layout.load(create().toJSON())
+ expect(onLayoutChange).not.toHaveBeenCalled()
+ })
+
+ it('keeps returning the same JSON object until the layout changes', () => {
+ const layout = create()
+ const json = layout.toJSON()
+ layout.setContainerRect({ ...BOX, width: 900 })
+ expect(layout.toJSON()).toBe(json)
+ layout.selectTab('b')
+ expect(layout.toJSON()).not.toBe(json)
+ })
+})
+
+describe('drags', () => {
+ it('moves a tab on endDrag with one change, showing the target meanwhile', () => {
+ const onLayoutChange = vi.fn()
+ const layout = create({ onLayoutChange })
+ layout.startDrag({ type: 'tab', tabId: 'c' })
+ layout.updateDrag({ x: 100, y: 400 })
+ const drag = layout.getSnapshot().drag!
+ expect(drag.target).toEqual({
+ type: 'tabset',
+ tabsetId: 'ts-a',
+ position: 'left',
+ })
+ expect(drag.indicator).toEqual({ x: 0, y: 0, width: 300, height: 800 })
+ layout.updateDrag({ x: 101, y: 401 })
+ expect(layout.getSnapshot().drag).toBe(drag)
+ expect(layout.endDrag()).toBe(true)
+ expect(layout.getSnapshot().drag).toBeNull()
+ expect(onLayoutChange).toHaveBeenCalledTimes(1)
+ expect(layout.getSnapshot().tabs.get('c')!.tabsetId).not.toBe('ts-c')
+ })
+
+ it('leaves maximized mode when a tab drag starts', () => {
+ const layout = create()
+ layout.maximize('ts-b')
+ layout.startDrag({ type: 'tab', tabId: 'a' })
+ expect(layout.getSnapshot().maximizedTabsetId).toBeNull()
+ layout.cancelDrag()
+ })
+
+ it('drags a splitter transiently, commits once, and can cancel', () => {
+ const onLayoutChange = vi.fn()
+ const layout = create({ onLayoutChange })
+ const [id] = layout.getSnapshot().splitters.keys()
+ const start = layout.getSnapshot().rects.tabsets.get('ts-a')!
+ layout.startDrag({ type: 'splitter', splitterId: id })
+ layout.updateDrag({ x: 400, y: 0 })
+ layout.updateDrag({ x: 500, y: 0 })
+ expect(layout.getSnapshot().rects.tabsets.get('ts-a')!.width).toBe(495)
+ expect(onLayoutChange).not.toHaveBeenCalled()
+ layout.cancelDrag()
+ expect(layout.getSnapshot().rects.tabsets.get('ts-a')).toEqual(start)
+
+ layout.startDrag({ type: 'splitter', splitterId: id })
+ layout.updateDrag({ x: 300, y: 0 })
+ expect(layout.endDrag()).toBe(true)
+ expect(onLayoutChange).toHaveBeenCalledTimes(1)
+ expect(onLayoutChange.mock.calls[0][1]).toBe('resize')
+ })
+
+ it('refuses to drag locked splitters', () => {
+ const layout = create()
+ layout.fold('ts-a')
+ const [id] = layout.getSnapshot().splitters.keys()
+ expect(layout.getSnapshot().splitters.get(id)!.isLocked).toBe(true)
+ expect(layout.startDrag({ type: 'splitter', splitterId: id })).toBe(
+ false
+ )
+ })
+
+ it('reports splitter bounds for ARIA', () => {
+ const layout = create()
+ const [id] = layout.getSnapshot().splitters.keys()
+ expect(layout.getSplitterBounds(id)).toEqual({
+ value: 600,
+ min: 40,
+ max: 1160,
+ })
+ })
+})
+
+describe('tabs, reset and destroy', () => {
+ it('setTabs adds, activates and removes tabs', () => {
+ const layout = create()
+ layout.selectTab('b')
+ layout.setTabs(tabs('a', 'b', 'd'))
+ const snapshot = layout.getSnapshot()
+ expect(snapshot.tabs.has('c')).toBe(false)
+ expect(snapshot.tabs.get('d')).toMatchObject({
+ tabsetId: 'ts-b',
+ isActive: true,
+ })
+ })
+
+ it('reset returns to the initial layout', () => {
+ const layout = create()
+ const initial = layout.toJSON()
+ layout.moveTab('a', { type: 'root', position: 'bottom' })
+ layout.reset()
+ expect(layout.toJSON()).toEqual(initial)
+ })
+
+ it('ignores everything after destroy', () => {
+ const warnings: string[] = []
+ const layout = create({ onWarning: (w) => warnings.push(w.code) })
+ const listener = vi.fn()
+ layout.subscribe(listener)
+ layout.destroy()
+ expect(layout.moveTab('a', { type: 'root', position: 'bottom' })).toBe(
+ false
+ )
+ expect(listener).not.toHaveBeenCalled()
+ expect(warnings).toContain('DESTROYED')
+ })
+
+ it('warns about unknown ids instead of throwing', () => {
+ const warnings: string[] = []
+ const layout = create({ onWarning: (w) => warnings.push(w.code) })
+ expect(layout.selectTab('nope')).toBe(false)
+ expect(layout.toggleFold('nope')).toBe(false)
+ expect(warnings).toEqual(['UNKNOWN_ID', 'UNKNOWN_ID'])
+ })
+})
diff --git a/packages/core/test/v2/drop.test.ts b/packages/core/test/v2/drop.test.ts
new file mode 100644
index 0000000..42e87dc
--- /dev/null
+++ b/packages/core/test/v2/drop.test.ts
@@ -0,0 +1,248 @@
+import { describe, it, expect } from 'vitest'
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import { DEFAULT_GEOMETRY } from '../../src/geometry/config'
+import { computeLayoutRects } from '../../src/geometry/compute-rects'
+import { getDropIndicatorRect } from '../../src/drop/drop-indicator'
+import {
+ getDropTarget,
+ getRootDropZoneRect,
+ getTabsetDropPosition,
+ type DropMeasurements,
+} from '../../src/drop/drop-target'
+import { listDropTargets } from '../../src/drop/drop-targets'
+import { createIdGenerator } from '../../src/ids'
+import { buildDefaultTree } from '../../src/tree/build'
+import { fold, maximize } from '../../src/tree/view-state'
+import type { LayoutModel, Rect } from '../../src/model/types'
+
+const model = (tabs: string[]): LayoutModel => ({
+ root: buildDefaultTree(tabs, createIdGenerator().createId),
+ maximizedTabsetId: null,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+})
+const box: Rect = { x: 0, y: 0, width: 1210, height: 800 }
+type Preview = {
+ area: string
+ left: number
+ top: number
+ width: number
+ height: number
+}
+const fromPreview = (p: {
+ left: number
+ top: number
+ width: number
+ height: number
+}): Rect => ({ x: p.left, y: p.top, width: p.width, height: p.height })
+
+describe('matches v1 drop previews', () => {
+ // Recorded from the v1 preview helpers before they were removed.
+ const recorded = JSON.parse(
+ readFileSync(
+ resolve(__dirname, '../fixtures/v1/drop-previews.json'),
+ 'utf8'
+ )
+ ) as {
+ tabset: { point: { x: number; y: number }; preview: Preview }[]
+ root: { side: 'left' | 'right' | 'top' | 'bottom'; preview: Preview }[]
+ navbar: { x: number; index: number; area: 'left' | 'right' }[]
+ }
+
+ it('tabset thirds and indicator rects', () => {
+ const m = model(['a', 'b'])
+ const rects = computeLayoutRects(m.root, box, DEFAULT_GEOMETRY, null)
+ const rect = rects.tabsets.get('ts-b')!
+ for (const { point, preview } of recorded.tabset) {
+ const position = getTabsetDropPosition(rect, point)
+ expect(position).toBe(
+ preview.area === 'contain' ? 'center' : preview.area
+ )
+ const target = {
+ type: 'tabset',
+ tabsetId: 'ts-b',
+ position,
+ } as const
+ expect(getDropIndicatorRect(m, rects, target)).toEqual(
+ fromPreview(preview)
+ )
+ }
+ })
+
+ it('root halves', () => {
+ const m = model(['a', 'b'])
+ const rects = computeLayoutRects(m.root, box, DEFAULT_GEOMETRY, null)
+ for (const { side, preview } of recorded.root) {
+ expect(
+ getDropIndicatorRect(m, rects, { type: 'root', position: side })
+ ).toEqual(fromPreview(preview))
+ }
+ })
+
+ it('tab bar insertion side', () => {
+ const tabsetOf = (id: string, tabIds: string[]) => ({
+ type: 'tabset' as const,
+ id,
+ weight: 100,
+ activeTabId: tabIds[0],
+ isFolded: false,
+ children: tabIds.map((tabId) => ({
+ type: 'tab' as const,
+ id: tabId,
+ })),
+ })
+ const base = model(['a', 'b'])
+ const m: LayoutModel = {
+ ...base,
+ root: {
+ ...base.root,
+ children: [
+ tabsetOf('ts-a', ['a', 'x', 'y']),
+ tabsetOf('ts-b', ['b']),
+ ],
+ },
+ }
+ const rects = computeLayoutRects(m.root, box, DEFAULT_GEOMETRY, null)
+ const tabs = ['a', 'x', 'y'].map((id, i) => ({
+ id,
+ rect: { x: 8 + i * 70, y: 4, width: 64, height: 32 },
+ }))
+ const measurements: DropMeasurements = {
+ tabBars: new Map([
+ [
+ 'ts-a',
+ { rect: { x: 0, y: 0, width: 600, height: 40 }, tabs },
+ ],
+ ]),
+ }
+ for (const { x, index, area } of recorded.navbar) {
+ const target = getDropTarget(
+ m,
+ rects,
+ { x, y: 20 },
+ { type: 'tab', tabId: 'b' },
+ measurements
+ )
+ expect(target).toEqual({
+ type: 'tab',
+ tabId: tabs[index].id,
+ position: area === 'left' ? 'before' : 'after',
+ })
+ }
+ })
+})
+
+describe('getDropTarget', () => {
+ const m = model(['a', 'b', 'c'])
+ const rects = computeLayoutRects(m.root, box, DEFAULT_GEOMETRY, null)
+
+ it('prefers root zones over tabsets', () => {
+ const zone = getRootDropZoneRect(rects.container, 'left')
+ expect(zone).toEqual({ x: 0, y: 300, width: 8, height: 200 })
+ expect(
+ getDropTarget(
+ m,
+ rects,
+ { x: 2, y: 400 },
+ { type: 'tab', tabId: 'c' }
+ )
+ ).toEqual({ type: 'root', position: 'left' })
+ expect(
+ getDropTarget(
+ m,
+ rects,
+ { x: 2, y: 100 },
+ { type: 'tab', tabId: 'c' }
+ )
+ ).toEqual({ type: 'tabset', tabsetId: 'ts-a', position: 'left' })
+ })
+
+ it('returns null for invalid targets and splitters', () => {
+ const single = model(['a'])
+ const singleRects = computeLayoutRects(
+ single.root,
+ box,
+ DEFAULT_GEOMETRY,
+ null
+ )
+ expect(
+ getDropTarget(
+ single,
+ singleRects,
+ { x: 600, y: 400 },
+ { type: 'tab', tabId: 'a' }
+ )
+ ).toBeNull()
+ const splitter = rects.splitters.get('ts-a~row-1')!
+ expect(
+ getDropTarget(
+ m,
+ rects,
+ { x: splitter.x + 5, y: 100 },
+ { type: 'tab', tabId: 'c' }
+ )
+ ).toBeNull()
+ })
+
+ it('folded tabsets and rotated bars always mean center', () => {
+ const folded = fold(m, 'ts-a')!
+ const foldedRects = computeLayoutRects(
+ folded.root,
+ box,
+ DEFAULT_GEOMETRY,
+ null
+ )
+ expect(
+ getDropTarget(
+ folded,
+ foldedRects,
+ { x: 5, y: 100 },
+ { type: 'tab', tabId: 'c' }
+ )
+ ).toEqual({ type: 'tabset', tabsetId: 'ts-a', position: 'center' })
+ expect(
+ getDropIndicatorRect(folded, foldedRects, {
+ type: 'tabset',
+ tabsetId: 'ts-a',
+ position: 'center',
+ })
+ ).toEqual(foldedRects.tabsets.get('ts-a'))
+ })
+
+ it('only the maximized tabset is a target while maximized', () => {
+ const maxed = maximize(m, 'ts-c')!
+ const maxRects = computeLayoutRects(
+ maxed.root,
+ box,
+ DEFAULT_GEOMETRY,
+ 'ts-c'
+ )
+ expect(
+ getDropTarget(
+ maxed,
+ maxRects,
+ { x: 600, y: 400 },
+ { type: 'tab', tabId: 'a' }
+ )
+ ).toEqual({ type: 'tabset', tabsetId: 'ts-c', position: 'center' })
+ })
+
+ it('lists valid targets in reading order', () => {
+ const targets = listDropTargets(m, { type: 'tab', tabId: 'a' })
+ expect(targets[0]).toEqual({
+ type: 'tabset',
+ tabsetId: 'ts-b',
+ position: 'center',
+ })
+ expect(
+ targets.some((t) => t.type === 'tabset' && t.tabsetId === 'ts-a')
+ ).toBe(false)
+ expect(targets.slice(-4).map((t) => t.position)).toEqual([
+ 'left',
+ 'right',
+ 'top',
+ 'bottom',
+ ])
+ })
+})
diff --git a/packages/core/test/v2/geometry.test.ts b/packages/core/test/v2/geometry.test.ts
new file mode 100644
index 0000000..2159fc4
--- /dev/null
+++ b/packages/core/test/v2/geometry.test.ts
@@ -0,0 +1,171 @@
+import { describe, it, expect } from 'vitest'
+import { DEFAULT_GEOMETRY } from '../../src/geometry/config'
+import { computeLayoutRects } from '../../src/geometry/compute-rects'
+import {
+ getSplitterBounds,
+ resizeSplitterWeights,
+} from '../../src/geometry/splitter'
+import {
+ getTabBarPlacement,
+ getTabContentRect,
+} from '../../src/geometry/tab-bar'
+import { createIdGenerator } from '../../src/ids'
+import { buildDefaultTree } from '../../src/tree/build'
+import { fold } from '../../src/tree/view-state'
+import type { LayoutModel, Rect } from '../../src/model/types'
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+import type { Observation } from '../characterization/scenario'
+import { observeV2 } from '../characterization/v2-observe'
+
+const TABS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
+const model = (n: number): LayoutModel => ({
+ root: buildDefaultTree(TABS.slice(0, n), createIdGenerator().createId),
+ maximizedTabsetId: null,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+})
+const box = (width: number, height: number): Rect => ({
+ x: 0,
+ y: 0,
+ width,
+ height,
+})
+const observe = (m: LayoutModel, width: number, height: number) =>
+ observeV2(
+ m,
+ computeLayoutRects(
+ m.root,
+ box(width, height),
+ DEFAULT_GEOMETRY,
+ m.maximizedTabsetId
+ )
+ )
+
+describe('computeLayoutRects matches v1', () => {
+ // Default layouts of 1–9 tabs at five sizes, recorded from the v1 engine.
+ // Splitter drags, folds and maximize are covered by the scenario replay.
+ const layouts = JSON.parse(
+ readFileSync(
+ resolve(__dirname, '../fixtures/v1/default-layouts.json'),
+ 'utf8'
+ )
+ ) as {
+ tabs: string[]
+ container: { width: number; height: number }
+ observation: Observation
+ }[]
+
+ it.each(
+ layouts.map(
+ (l) =>
+ [
+ l.tabs.length,
+ l.container.width,
+ l.container.height,
+ l,
+ ] as const
+ )
+ )('%i tabs at %ix%i', (_, width, height, layout) => {
+ expect(observe(model(layout.tabs.length), width, height)).toEqual(
+ layout.observation
+ )
+ })
+})
+
+describe('geometry details', () => {
+ it('reuses unchanged rect objects', () => {
+ const m = model(3)
+ const first = computeLayoutRects(
+ m.root,
+ box(1000, 600),
+ DEFAULT_GEOMETRY,
+ null
+ )
+ const same = computeLayoutRects(
+ m.root,
+ box(1000, 600),
+ DEFAULT_GEOMETRY,
+ null,
+ first
+ )
+ expect(same.tabsets.get('ts-a')).toBe(first.tabsets.get('ts-a'))
+ const taller = computeLayoutRects(
+ m.root,
+ box(1000, 700),
+ DEFAULT_GEOMETRY,
+ null,
+ first
+ )
+ expect(taller.tabsets.get('ts-a')).not.toBe(first.tabsets.get('ts-a'))
+ })
+
+ it('offsets everything by the container origin (padding)', () => {
+ const m = model(2)
+ const rects = computeLayoutRects(
+ m.root,
+ { x: 8, y: 12, width: 400, height: 300 },
+ DEFAULT_GEOMETRY,
+ null
+ )
+ expect(rects.tabsets.get('ts-a')).toEqual({
+ x: 8,
+ y: 12,
+ width: 195,
+ height: 300,
+ })
+ })
+
+ it('reports splitter bounds and refuses locked splitters', () => {
+ const m = model(2)
+ const rects = computeLayoutRects(
+ m.root,
+ box(410, 300),
+ DEFAULT_GEOMETRY,
+ null
+ )
+ expect(
+ getSplitterBounds(m.root, rects, DEFAULT_GEOMETRY, 'ts-a~ts-b')
+ ).toEqual({ value: 200, min: 40, max: 360 })
+ const folded = fold(m, 'ts-a')!
+ expect(
+ resizeSplitterWeights(
+ folded.root,
+ rects,
+ DEFAULT_GEOMETRY,
+ 'ts-a~ts-b',
+ { x: 100, y: 0 }
+ )
+ ).toBeNull()
+ expect(
+ resizeSplitterWeights(m.root, rects, DEFAULT_GEOMETRY, 'nope', {
+ x: 100,
+ y: 0,
+ })
+ ).toBeNull()
+ })
+
+ it('places tab bars, rotating folded strips in side-by-side rows', () => {
+ const rect = { x: 10, y: 20, width: 40, height: 500 }
+ expect(
+ getTabBarPlacement(
+ rect,
+ { isFolded: true, parentDirection: 'horizontal' },
+ 40
+ )
+ ).toEqual({
+ rect: { x: 50, y: 20, width: 500, height: 40 },
+ isRotated: true,
+ })
+ expect(
+ getTabBarPlacement(
+ rect,
+ { isFolded: true, parentDirection: 'vertical' },
+ 40
+ ).isRotated
+ ).toBe(false)
+ expect(
+ getTabContentRect({ x: 0, y: 0, width: 100, height: 30 }, 40)
+ ).toEqual({ x: 0, y: 40, width: 100, height: 0 })
+ })
+})
diff --git a/packages/core/test/v2/public-api.test.ts b/packages/core/test/v2/public-api.test.ts
new file mode 100644
index 0000000..412b895
--- /dev/null
+++ b/packages/core/test/v2/public-api.test.ts
@@ -0,0 +1,173 @@
+import { afterEach, describe, it, expect, vi } from 'vitest'
+import * as api from '../../src'
+import { createLayout, applyRect, version } from '../../src'
+import { isRow, isTab, isTabset } from '../../src/model/guards'
+import { createIdGenerator } from '../../src/ids'
+
+const BOX = { x: 0, y: 0, width: 1210, height: 800 }
+const create = (extra: Partial[0]> = {}) => {
+ const layout = createLayout({
+ tabs: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
+ onWarning: () => {},
+ ...extra,
+ })
+ layout.setContainerRect(BOX)
+ return layout
+}
+
+describe('public exports', () => {
+ it('exposes the v2 API and nothing from v1', () => {
+ for (const name of [
+ 'createLayout',
+ 'migrateLayoutFromV1',
+ 'isLayoutV1',
+ 'DynamixLayoutError',
+ 'LAYOUT_VERSION',
+ 'getTabBarPlacement',
+ 'getTabContentRect',
+ 'getRootDropZoneRect',
+ 'applyRect',
+ 'createFrameScheduler',
+ 'version',
+ ])
+ expect(api).toHaveProperty(name)
+ for (const name of [
+ 'DynamixLayoutCore',
+ 'Node',
+ 'Bond',
+ 'Queue',
+ 'createReactiveState',
+ 'getTabsetDropPreview',
+ ])
+ expect(api).not.toHaveProperty(name)
+ expect(typeof version).toBe('string')
+ })
+
+ it('has no import-time side effects on window', async () => {
+ expect(
+ (globalThis as { __DYNAMIX_LAYOUT__?: unknown }).__DYNAMIX_LAYOUT__
+ ).toBeUndefined()
+ })
+
+ it('applyRect positions an element', () => {
+ const style: Record = {}
+ applyRect({ style } as unknown as HTMLElement, {
+ x: 1,
+ y: 2,
+ width: 3,
+ height: 4,
+ })
+ expect(style).toEqual({
+ left: '1px',
+ top: '2px',
+ width: '3px',
+ height: '4px',
+ })
+ })
+
+ it('node guards', () => {
+ const tab = { type: 'tab', id: 't' } as const
+ expect([isTab(tab), isTabset(tab), isRow(tab)]).toEqual([
+ true,
+ false,
+ false,
+ ])
+ })
+})
+
+describe('createLayout options and less common actions', () => {
+ afterEach(() => vi.unstubAllEnvs())
+
+ it('uses a custom createId', () => {
+ let n = 0
+ const layout = create({ createId: (kind) => `${kind}#${++n}` })
+ expect([...layout.getSnapshot().tabsets.keys()]).toEqual([
+ 'tabset#1',
+ 'tabset#3',
+ 'tabset#4',
+ ])
+ const ids = createIdGenerator((kind, hint) => `${kind}:${hint ?? 'x'}`)
+ expect(ids.createId('tabset', 'q')).toBe('tabset:q')
+ })
+
+ it('setOptions resizes without rebuilding', () => {
+ const layout = create()
+ const root = layout.getSnapshot().root
+ layout.setOptions({
+ splitterSize: 20,
+ minPanelSize: { width: 100, height: 60 },
+ })
+ expect(layout.getSnapshot().root).toBe(root)
+ expect(
+ [...layout.getSnapshot().rects.splitters.values()][0].width
+ ).toBe(20)
+ })
+
+ it('moves tabsets, adds tabs at a target, and removes tabs', () => {
+ const layout = create()
+ expect(
+ layout.moveTabset('ts-c', {
+ type: 'tab',
+ tabId: 'a',
+ position: 'after',
+ })
+ ).toBe(true)
+ expect(layout.getSnapshot().tabsets.get('ts-a')!.tabIds).toEqual([
+ 'a',
+ 'c',
+ ])
+ expect(
+ layout.addTab({ id: 'd' }, { type: 'root', position: 'bottom' })
+ ).toBe(true)
+ expect(layout.addTab({ id: 'd' })).toBe(false)
+ expect(layout.getSnapshot().root.direction).toBe('vertical')
+ expect(layout.removeTab('d')).toBe(true)
+ expect(layout.removeTab('nope')).toBe(false)
+ })
+
+ it('drop helpers ignore splitter sources', () => {
+ const layout = create()
+ const [id] = layout.getSnapshot().splitters.keys()
+ expect(
+ layout.getDropTarget(
+ { x: 10, y: 10 },
+ { type: 'splitter', splitterId: id }
+ )
+ ).toBeNull()
+ expect(
+ layout.listDropTargets({ type: 'splitter', splitterId: id })
+ ).toEqual([])
+ const target = layout.getDropTarget(
+ { x: 100, y: 400 },
+ { type: 'tab', tabId: 'c' }
+ )!
+ expect(layout.getDropIndicatorRect(target)).toEqual({
+ x: 0,
+ y: 0,
+ width: 300,
+ height: 800,
+ })
+ })
+
+ it('keeps the first duplicate tab in production and warns', () => {
+ vi.stubEnv('NODE_ENV', 'production')
+ const warnings: string[] = []
+ const layout = createLayout({
+ tabs: [{ id: 'a' }, { id: 'a' }],
+ onWarning: (w) => warnings.push(w.code),
+ })
+ expect([...layout.getSnapshot().tabs.keys()]).toEqual(['a'])
+ expect(warnings).toEqual(['DUPLICATE_TAB_ID'])
+ })
+
+ it('the default warning handler logs only in development', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+ createLayout({ tabs: [{ id: 'a' }] }).selectTab('nope')
+ expect(warn).toHaveBeenCalledWith('[dynamix-layout] Unknown tab "nope"')
+ warn.mockClear()
+ vi.stubEnv('NODE_ENV', 'production')
+ createLayout({ tabs: [{ id: 'a' }] }).selectTab('nope')
+ expect(warn).not.toHaveBeenCalled()
+ warn.mockRestore()
+ })
+})
diff --git a/packages/core/test/v2/serialize.test.ts b/packages/core/test/v2/serialize.test.ts
new file mode 100644
index 0000000..9399b1d
--- /dev/null
+++ b/packages/core/test/v2/serialize.test.ts
@@ -0,0 +1,377 @@
+import { describe, it, expect } from 'vitest'
+import { DynamixLayoutError, type LayoutWarning } from '../../src/errors'
+import { DEFAULT_GEOMETRY } from '../../src/geometry/config'
+import { computeLayoutRects } from '../../src/geometry/compute-rects'
+import { createIdGenerator } from '../../src/ids'
+import { layoutFromJSON } from '../../src/serialize/from-json'
+import { isLayoutV1, migrateLayoutFromV1 } from '../../src/serialize/migrate-v1'
+import { parseLayout } from '../../src/serialize/parse'
+import type { LayoutJSON, LayoutTreeV1 } from '../../src/serialize/schema'
+import { toLayoutJSON } from '../../src/serialize/to-json'
+import { buildDefaultTree } from '../../src/tree/build'
+import { fold } from '../../src/tree/view-state'
+import type { LayoutModel } from '../../src/model/types'
+import {
+ hasFoldedRow,
+ hasZeroWeightRow,
+ loadScenarios,
+ overflows,
+ savedStates,
+} from '../characterization/fixtures'
+import { observeV2 } from '../characterization/v2-observe'
+
+const ignore = () => {}
+const collect = () => {
+ const warnings: LayoutWarning[] = []
+ return { warnings, onWarning: (w: LayoutWarning) => warnings.push(w) }
+}
+const v1Tab = (name: string): LayoutTreeV1 => ({
+ typNode: 'tab',
+ nodName: name,
+ uidNode: `u-${name}`,
+ nodPart: 100,
+})
+const v1Tabset = (
+ id: string,
+ tabs: string[],
+ extra: Partial = {}
+): LayoutTreeV1 => ({
+ typNode: 'tabset',
+ nodName: '',
+ uidNode: id,
+ nodPart: 100,
+ nodOpen: tabs[0],
+ nodKids: tabs.map(v1Tab),
+ ...extra,
+})
+const v1Row = (
+ id: string,
+ kids: LayoutTreeV1[],
+ extra: Partial = {}
+): LayoutTreeV1 => ({
+ typNode: 'row',
+ nodName: '',
+ uidNode: id,
+ nodPart: 100,
+ nodKids: kids,
+ ...extra,
+})
+
+describe('toLayoutJSON / layoutFromJSON', () => {
+ it('round-trips a model, including fold and maximize', () => {
+ const model: LayoutModel = {
+ root: buildDefaultTree(
+ ['a', 'b', 'c'],
+ createIdGenerator().createId
+ ),
+ maximizedTabsetId: null,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+ }
+ const folded = { ...fold(model, 'ts-a')!, maximizedTabsetId: 'ts-b' }
+ const json = toLayoutJSON(folded)
+ expect(json.version).toBe(2)
+ expect(json.maximizedTabsetId).toBe('ts-b')
+ expect(json.root.children[0]).toEqual({
+ type: 'tabset',
+ id: 'ts-a',
+ weight: 100,
+ activeTabId: 'a',
+ isFolded: true,
+ children: [{ type: 'tab', id: 'a' }],
+ })
+ const back = layoutFromJSON(json, ignore)
+ expect(back.root).toEqual(folded.root)
+ expect(back.maximizedTabsetId).toBe('ts-b')
+ })
+
+ it('throws with a path for malformed input', () => {
+ const bad = {
+ version: 2,
+ root: {
+ type: 'row',
+ id: 'r',
+ weight: 1,
+ direction: 'horizontal',
+ children: [
+ {
+ type: 'tabset',
+ id: 't',
+ weight: 1,
+ children: [{ type: 'tab' }],
+ },
+ ],
+ },
+ }
+ expect(() => parseLayout(bad, ignore)).toThrowError(
+ /at root\.children\[0\]\.children\[0\]/
+ )
+ try {
+ parseLayout({ version: 3, root: {} }, ignore)
+ } catch (error) {
+ expect(error).toBeInstanceOf(DynamixLayoutError)
+ expect((error as DynamixLayoutError).code).toBe(
+ 'UNSUPPORTED_VERSION'
+ )
+ }
+ expect(() => parseLayout('nope', ignore)).toThrow(DynamixLayoutError)
+ expect(() =>
+ parseLayout(
+ {
+ version: 2,
+ root: {
+ type: 'row',
+ id: 'r',
+ weight: 1,
+ direction: 'diagonal',
+ children: [],
+ },
+ },
+ ignore
+ )
+ ).toThrowError(/root\.direction/)
+ })
+
+ it('repairs weights, active tabs and duplicate tabs with warnings', () => {
+ const { warnings, onWarning } = collect()
+ const json = {
+ version: 2,
+ root: {
+ type: 'row',
+ id: 'r',
+ weight: 100,
+ direction: 'horizontal',
+ children: [
+ {
+ type: 'tabset',
+ id: 't1',
+ weight: -5,
+ activeTabId: 'zz',
+ children: [{ type: 'tab', id: 'a' }],
+ },
+ {
+ type: 'tabset',
+ id: 't2',
+ weight: 100,
+ children: [
+ { type: 'tab', id: 'a' },
+ { type: 'tab', id: 'b' },
+ ],
+ },
+ ],
+ },
+ } as unknown as LayoutJSON
+ const { root } = layoutFromJSON(json, onWarning)
+ expect(warnings.map((w) => w.code).sort()).toEqual([
+ 'DUPLICATE_TAB_ID',
+ 'INVALID_ACTIVE_TAB',
+ 'INVALID_WEIGHT',
+ ])
+ expect(
+ toLayoutJSON({ root, maximizedTabsetId: null }).root.children
+ ).toEqual([
+ {
+ type: 'tabset',
+ id: 't1',
+ weight: 100,
+ activeTabId: 'a',
+ children: [{ type: 'tab', id: 'a' }],
+ },
+ {
+ type: 'tabset',
+ id: 't2',
+ weight: 100,
+ activeTabId: 'b',
+ children: [{ type: 'tab', id: 'b' }],
+ },
+ ])
+ })
+})
+
+describe('migrateLayoutFromV1', () => {
+ it('detects v1 layouts', () => {
+ expect(isLayoutV1(v1Row('root', []))).toBe(true)
+ expect(isLayoutV1({ version: 2, root: {} })).toBe(false)
+ expect(isLayoutV1(null)).toBe(false)
+ })
+
+ it('maps names, weights, directions and view state', () => {
+ const tree = v1Row(
+ 'dynamix-layout-root',
+ [
+ v1Tabset('t1', ['editor', 'notes'], {
+ nodPart: 63.5,
+ nodOpen: 'notes',
+ nodFold: true,
+ }),
+ v1Row(
+ 'r1',
+ [v1Tabset('t2', ['terminal']), v1Tabset('t3', ['preview'])],
+ { nodPart: 136.5 }
+ ),
+ ],
+ { nodMaxd: 't3' }
+ )
+ expect(migrateLayoutFromV1(tree)).toEqual({
+ version: 2,
+ maximizedTabsetId: 't3',
+ root: {
+ type: 'row',
+ id: 'dynamix-layout-root',
+ weight: 100,
+ direction: 'horizontal',
+ children: [
+ {
+ type: 'tabset',
+ id: 't1',
+ weight: 63.5,
+ activeTabId: 'notes',
+ isFolded: true,
+ children: [
+ { type: 'tab', id: 'editor' },
+ { type: 'tab', id: 'notes' },
+ ],
+ },
+ {
+ type: 'row',
+ id: 'r1',
+ weight: 136.5,
+ direction: 'vertical',
+ children: [
+ {
+ type: 'tabset',
+ id: 't2',
+ weight: 100,
+ activeTabId: 'terminal',
+ children: [{ type: 'tab', id: 'terminal' }],
+ },
+ {
+ type: 'tabset',
+ id: 't3',
+ weight: 100,
+ activeTabId: 'preview',
+ children: [{ type: 'tab', id: 'preview' }],
+ },
+ ],
+ },
+ ],
+ },
+ })
+ })
+
+ it('flattens a root with a single nested row (v1 B7) into a vertical root', () => {
+ const tree = v1Row('root', [
+ v1Row('r1', [v1Tabset('t1', ['a']), v1Tabset('t2', ['b'])], {
+ nodPart: 77,
+ }),
+ ])
+ const json = migrateLayoutFromV1(tree)
+ expect(json.root.direction).toBe('vertical')
+ expect(json.root.weight).toBe(77)
+ expect(json.root.children.map((c) => c.id)).toEqual(['t1', 't2'])
+ })
+
+ it('repairs damaged trees', () => {
+ const { warnings, onWarning } = collect()
+ const tree = v1Row(
+ 'root',
+ [
+ v1Tabset('t1', ['a', 'b'], {
+ nodOpen: 'missing',
+ nodPart: NaN,
+ }),
+ v1Tabset('t2', ['a']),
+ v1Tabset('empty', []),
+ v1Row('emptyRow', []),
+ v1Tab('stray'),
+ { typNode: 'bond', nodName: '', uidNode: 'b', nodPart: 0 },
+ ],
+ { nodMaxd: 'gone' }
+ )
+ const json = migrateLayoutFromV1(tree, { onWarning })
+ expect(json.maximizedTabsetId).toBeUndefined()
+ expect(json.root.children).toEqual([
+ {
+ type: 'tabset',
+ id: 't1',
+ weight: 100,
+ activeTabId: 'a',
+ children: [
+ { type: 'tab', id: 'a' },
+ { type: 'tab', id: 'b' },
+ ],
+ },
+ {
+ type: 'tabset',
+ id: 'ts-stray',
+ weight: 100,
+ activeTabId: 'stray',
+ children: [{ type: 'tab', id: 'stray' }],
+ },
+ ])
+ expect(warnings.map((w) => w.code)).toEqual([
+ 'INVALID_ACTIVE_TAB',
+ 'INVALID_WEIGHT',
+ 'DUPLICATE_TAB_ID',
+ ])
+ })
+
+ it('wraps a tabset root and rejects non-layouts', () => {
+ expect(
+ migrateLayoutFromV1(v1Tabset('t', ['a'])).root.children[0].id
+ ).toBe('t')
+ expect(() =>
+ migrateLayoutFromV1({
+ typNode: 'tab',
+ nodName: 'a',
+ uidNode: 'x',
+ nodPart: 1,
+ })
+ ).toThrow(DynamixLayoutError)
+ expect(() => migrateLayoutFromV1({} as LayoutTreeV1)).toThrow(
+ DynamixLayoutError
+ )
+ })
+
+ it('parseLayout migrates v1 with a warning', () => {
+ const { warnings, onWarning } = collect()
+ parseLayout(v1Row('root', [v1Tabset('t', ['a'])]), onWarning)
+ expect(warnings[0].code).toBe('MIGRATED_FROM_V1')
+ })
+})
+
+describe('every saved v1 layout in the fixtures', () => {
+ const scenarios = loadScenarios()
+ it.each(scenarios.map((s) => [s.name, s] as const))(
+ '%s renders identically after migration',
+ (_, scenario) => {
+ let compared = 0
+ for (const state of savedStates(scenario)) {
+ const model = parseLayout(state.saved, ignore)
+ const rects = computeLayoutRects(
+ model.root,
+ { x: 0, y: 0, ...state.container },
+ DEFAULT_GEOMETRY,
+ model.maximizedTabsetId
+ )
+ const observed = observeV2(
+ { ...model, lastActiveTabsetId: null, foldOrder: [] },
+ rects
+ )
+ if (
+ overflows(state.observation, state.container) ||
+ hasFoldedRow(state.saved) ||
+ hasZeroWeightRow(state.saved)
+ ) {
+ expect(observed.tree, state.label).toEqual(
+ state.observation.tree
+ )
+ } else {
+ expect(observed, state.label).toEqual(state.observation)
+ compared++
+ }
+ }
+ expect(compared).toBeGreaterThan(0)
+ }
+ )
+})
diff --git a/packages/core/test/v2/tree.test.ts b/packages/core/test/v2/tree.test.ts
new file mode 100644
index 0000000..574c8c7
--- /dev/null
+++ b/packages/core/test/v2/tree.test.ts
@@ -0,0 +1,449 @@
+import { describe, it, expect } from 'vitest'
+import { createIdGenerator } from '../../src/ids'
+import { buildDefaultTree } from '../../src/tree/build'
+import {
+ collectTabIds,
+ collectTabsets,
+ findTabsetOfTab,
+} from '../../src/tree/find'
+import { moveNode, canMove } from '../../src/tree/move'
+import { normalizeTree } from '../../src/tree/normalize'
+import { removeTab } from '../../src/tree/remove'
+import { reconcileTabs } from '../../src/tree/reconcile-tabs'
+import {
+ fold,
+ maximize,
+ normalizeViewState,
+ restore,
+ unfold,
+} from '../../src/tree/view-state'
+import type {
+ LayoutModel,
+ RowChild,
+ RowNode,
+ TabsetNode,
+} from '../../src/model/types'
+
+const ids = () => createIdGenerator().createId
+
+/** Compact shape: `[direction, ...children]` rows and `'a,b'` tabsets (`*` marks active, `~` folded). */
+type Shape = string | [string, ...Shape[]]
+const shape = (node: RowChild): Shape =>
+ node.type === 'row'
+ ? [node.direction[0], ...node.children.map(shape)]
+ : (node.isFolded ? '~' : '') +
+ node.children
+ .map((t) => (t.id === node.activeTabId ? `*${t.id}` : t.id))
+ .join(',')
+
+const model = (root: RowNode): LayoutModel => ({
+ root,
+ maximizedTabsetId: null,
+ lastActiveTabsetId: null,
+ foldOrder: [],
+})
+
+const tabset = (
+ tabIds: string[],
+ weight = 100,
+ extra: Partial = {}
+): TabsetNode => ({
+ type: 'tabset',
+ id: `ts-${tabIds[0]}`,
+ weight,
+ activeTabId: tabIds[0],
+ isFolded: false,
+ children: tabIds.map((id) => ({ type: 'tab', id })),
+ ...extra,
+})
+const row = (
+ direction: 'horizontal' | 'vertical',
+ children: RowChild[],
+ weight = 100,
+ id = `row-${Math.random()}`
+): RowNode => ({
+ type: 'row',
+ id,
+ weight,
+ direction,
+ children,
+})
+
+describe('buildDefaultTree', () => {
+ it('matches the v1 default layout', () => {
+ expect(buildDefaultTree([], ids()).children).toEqual([])
+ expect(shape(buildDefaultTree(['a'], ids()))).toEqual(['h', '*a'])
+ expect(shape(buildDefaultTree(['a', 'b'], ids()))).toEqual([
+ 'h',
+ '*a',
+ '*b',
+ ])
+ expect(shape(buildDefaultTree(['a', 'b', 'c', 'd'], ids()))).toEqual([
+ 'h',
+ '*a',
+ ['v', '*b', ['h', '*c', '*d']],
+ ])
+ })
+
+ it('creates deterministic ids derived from tab ids', () => {
+ const first = buildDefaultTree(['a', 'b', 'c'], ids())
+ const second = buildDefaultTree(['a', 'b', 'c'], ids())
+ expect(first).toEqual(second)
+ expect(collectTabsets(first).map((t) => t.id)).toEqual([
+ 'ts-a',
+ 'ts-b',
+ 'ts-c',
+ ])
+ })
+})
+
+describe('normalizeTree', () => {
+ it('dissolves single-child rows, keeping the child weight', () => {
+ const root = row('horizontal', [
+ tabset(['a'], 70),
+ row('vertical', [tabset(['b'], 30)], 200),
+ ])
+ const result = normalizeTree(root)
+ expect(shape(result)).toEqual(['h', '*a', '*b'])
+ expect(result.children.map((c) => c.weight)).toEqual([70, 30])
+ })
+
+ it('splices same-direction rows and drops empty nodes', () => {
+ const root = row('horizontal', [
+ tabset(['a']),
+ row('horizontal', [tabset(['b']), tabset(['c'])]),
+ tabset([]),
+ row('vertical', []),
+ ])
+ expect(shape(normalizeTree(root))).toEqual(['h', '*a', '*b', '*c'])
+ })
+
+ it('lets the root take over a single child row, including its weight', () => {
+ const inner = row('vertical', [tabset(['a']), tabset(['b'])], 42)
+ const result = normalizeTree(row('horizontal', [inner], 100, 'root'))
+ expect(result.id).toBe('root')
+ expect(result.direction).toBe('vertical')
+ expect(result.weight).toBe(42)
+ })
+
+ it('fixes invalid active tabs and weights', () => {
+ const result = normalizeTree(
+ row('horizontal', [
+ tabset(['a', 'b'], -1, { activeTabId: 'zz' }),
+ tabset(['c'], NaN),
+ ])
+ )
+ const [first, second] = result.children as TabsetNode[]
+ expect(first.activeTabId).toBe('a')
+ expect(first.weight).toBe(100)
+ expect(second.weight).toBe(100)
+ })
+
+ it('returns the same object when nothing changes', () => {
+ const root = buildDefaultTree(['a', 'b', 'c'], ids())
+ expect(normalizeTree(root)).toBe(root)
+ })
+})
+
+describe('removeTab', () => {
+ it('activates the first remaining tab and removes emptied tabsets', () => {
+ let root = normalizeTree(
+ row('horizontal', [
+ tabset(['a', 'b'], 100, { activeTabId: 'b' }),
+ tabset(['c']),
+ ])
+ )
+ root = removeTab(root, 'b')
+ expect(shape(root)).toEqual(['h', '*a', '*c'])
+ root = removeTab(root, 'c')
+ expect(shape(root)).toEqual(['h', '*a'])
+ })
+
+ it('lifts the remaining sibling into the grandparent', () => {
+ const root = buildDefaultTree(['a', 'b', 'c', 'd'], ids())
+ expect(shape(removeTab(root, 'c'))).toEqual([
+ 'h',
+ '*a',
+ ['v', '*b', '*d'],
+ ])
+ expect(shape(removeTab(root, 'b'))).toEqual(['h', '*a', '*c', '*d'])
+ })
+
+ it('removing the last tab leaves an empty root', () => {
+ expect(removeTab(buildDefaultTree(['a'], ids()), 'a').children).toEqual(
+ []
+ )
+ })
+})
+
+describe('moveNode', () => {
+ const start = () => model(buildDefaultTree(['a', 'b', 'c'], ids()))
+ const move = (
+ m: LayoutModel,
+ source: Parameters[1],
+ target: Parameters[2]
+ ) => moveNode(m, source, target, ids())
+
+ it('moves a tab into another tabset, before or after a tab', () => {
+ const m = move(
+ start(),
+ { type: 'tab', tabId: 'a' },
+ { type: 'tab', tabId: 'c', position: 'before' }
+ )!
+ expect(shape(m.root)).toEqual(['v', '*b', '*a,c'])
+ expect(m.lastActiveTabsetId).toBe(findTabsetOfTab(m.root, 'a')!.id)
+ })
+
+ it('appends on center drops (v1 inserted before the last tab)', () => {
+ const m = move(
+ start(),
+ { type: 'tab', tabId: 'a' },
+ { type: 'tabset', tabsetId: 'ts-c', position: 'center' }
+ )!
+ expect(shape(m.root)).toEqual(['v', '*b', 'c,*a'])
+ })
+
+ it('splits along and across rows', () => {
+ const along = move(
+ start(),
+ { type: 'tab', tabId: 'a' },
+ { type: 'tabset', tabsetId: 'ts-b', position: 'bottom' }
+ )!
+ expect(shape(along.root)).toEqual(['v', '*b', '*a', '*c'])
+ const across = move(
+ start(),
+ { type: 'tab', tabId: 'c' },
+ { type: 'tabset', tabsetId: 'ts-a', position: 'top' }
+ )!
+ expect(shape(across.root)).toEqual(['h', ['v', '*c', '*a'], '*b'])
+ })
+
+ it('docks at the root, wrapping content when crossing the root direction', () => {
+ const left = move(
+ start(),
+ { type: 'tab', tabId: 'c' },
+ { type: 'root', position: 'left' }
+ )!
+ expect(shape(left.root)).toEqual(['h', '*c', '*a', '*b'])
+ const top = move(
+ start(),
+ { type: 'tab', tabId: 'c' },
+ { type: 'root', position: 'top' }
+ )!
+ expect(shape(top.root)).toEqual(['v', '*c', ['h', '*a', '*b']])
+ })
+
+ it('moves whole tabsets and merges them into tabs', () => {
+ const m = move(
+ start(),
+ { type: 'tabset', tabsetId: 'ts-a' },
+ { type: 'tab', tabId: 'b', position: 'after' }
+ )!
+ expect(shape(m.root)).toEqual(['v', 'b,*a', '*c'])
+ })
+
+ it('refuses moves v1 refused', () => {
+ const m = start()
+ expect(
+ canMove(
+ m.root,
+ { type: 'tab', tabId: 'a' },
+ { type: 'tabset', tabsetId: 'ts-a', position: 'left' }
+ )
+ ).toBe(false)
+ expect(
+ canMove(
+ m.root,
+ { type: 'tab', tabId: 'a' },
+ { type: 'tab', tabId: 'a', position: 'after' }
+ )
+ ).toBe(false)
+ expect(
+ canMove(
+ m.root,
+ { type: 'tabset', tabsetId: 'ts-a' },
+ { type: 'tabset', tabsetId: 'ts-a', position: 'center' }
+ )
+ ).toBe(false)
+ const single = model(buildDefaultTree(['a'], ids()))
+ expect(
+ canMove(
+ single.root,
+ { type: 'tab', tabId: 'a' },
+ { type: 'root', position: 'top' }
+ )
+ ).toBe(false)
+ expect(
+ move(
+ m,
+ { type: 'tab', tabId: 'zz' },
+ { type: 'root', position: 'top' }
+ )
+ ).toBeNull()
+ })
+
+ it('allows splitting a tab out of its own tabset', () => {
+ const m = model(
+ normalizeTree(row('horizontal', [tabset(['a', 'b'])], 100, 'root'))
+ )
+ const result = move(
+ m,
+ { type: 'tab', tabId: 'b' },
+ { type: 'tabset', tabsetId: 'ts-a', position: 'right' }
+ )!
+ expect(shape(result.root)).toEqual(['h', '*a', '*b'])
+ })
+
+ it('ends maximize and unfolds the receiving tabset', () => {
+ let m = fold(start(), 'ts-c')!
+ m = maximize(m, 'ts-a')!
+ m = move(
+ m,
+ { type: 'tab', tabId: 'a' },
+ { type: 'tabset', tabsetId: 'ts-c', position: 'center' }
+ )!
+ expect(m.maximizedTabsetId).toBeNull()
+ expect(findTabsetOfTab(m.root, 'a')!.isFolded).toBe(false)
+ })
+
+ it('keeps every tab under random moves', () => {
+ let m = model(buildDefaultTree(['a', 'b', 'c', 'd', 'e', 'f'], ids()))
+ let seed = 7
+ const random = (n: number) => (seed = (seed * 16807) % 2147483647) % n
+ const createId = ids()
+ const positions = ['top', 'bottom', 'left', 'right', 'center'] as const
+ for (let i = 0; i < 500; i++) {
+ const tabs = collectTabIds(m.root)
+ const tabsets = collectTabsets(m.root)
+ const source =
+ random(3) === 0
+ ? {
+ type: 'tabset' as const,
+ tabsetId: tabsets[random(tabsets.length)].id,
+ }
+ : { type: 'tab' as const, tabId: tabs[random(tabs.length)] }
+ const target =
+ random(4) === 0
+ ? {
+ type: 'root' as const,
+ position: positions[random(4)] as 'top',
+ }
+ : {
+ type: 'tabset' as const,
+ tabsetId: tabsets[random(tabsets.length)].id,
+ position: positions[random(5)],
+ }
+ m = moveNode(m, source, target, createId) ?? m
+ expect([...collectTabIds(m.root)].sort()).toEqual([
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ ])
+ expect(normalizeTree(m.root)).toBe(m.root)
+ }
+ })
+})
+
+describe('view state', () => {
+ const start = () => model(buildDefaultTree(['a', 'b', 'c'], ids()))
+
+ it('folds and unfolds, refusing a tabset alone in its row', () => {
+ const m = fold(start(), 'ts-a')!
+ expect(shape(m.root)).toEqual(['h', '~*a', ['v', '*b', '*c']])
+ expect(unfold(m, 'ts-a')!.root).toEqual(start().root)
+ expect(fold(model(buildDefaultTree(['a'], ids())), 'ts-a')).toBeNull()
+ expect(fold(m, 'ts-a')).toBeNull()
+ })
+
+ it('folding the last open child unfolds the most recently folded sibling', () => {
+ let m = fold(start(), 'ts-b')!
+ m = fold(m, 'ts-c')!
+ expect(shape(m.root)).toEqual(['h', '*a', ['v', '*b', '~*c']])
+ })
+
+ it('maximize needs two tabsets and restore clears it', () => {
+ expect(
+ maximize(model(buildDefaultTree(['a'], ids())), 'ts-a')
+ ).toBeNull()
+ const m = maximize(start(), 'ts-b')!
+ expect(m.maximizedTabsetId).toBe('ts-b')
+ expect(maximize(m, 'ts-b')).toBeNull()
+ expect(restore(m)!.maximizedTabsetId).toBeNull()
+ expect(restore(start())).toBeNull()
+ })
+
+ it('normalizes stale view state', () => {
+ const m = normalizeViewState({
+ ...model(
+ normalizeTree(
+ row(
+ 'horizontal',
+ [tabset(['a'], 100, { isFolded: true })],
+ 100,
+ 'root'
+ )
+ )
+ ),
+ maximizedTabsetId: 'gone',
+ foldOrder: ['gone', 'ts-a'],
+ })
+ expect(shape(m.root)).toEqual(['h', '*a'])
+ expect(m.maximizedTabsetId).toBeNull()
+ expect(m.foldOrder).toEqual(['ts-a'])
+ })
+})
+
+describe('reconcileTabs', () => {
+ it('removes unlisted tabs and appends new ones to the last active tabset', () => {
+ const createId = ids()
+ let m = {
+ ...model(buildDefaultTree(['a', 'b', 'c'], createId)),
+ lastActiveTabsetId: 'ts-b',
+ }
+ m = reconcileTabs(m, [{ id: 'a' }, { id: 'b' }, { id: 'd' }], createId)
+ expect(shape(m.root)).toEqual(['h', '*a', 'b,*d'])
+ })
+
+ it('honours a target, and does not activate when asked not to', () => {
+ const createId = ids()
+ let m = model(buildDefaultTree(['a', 'b'], createId))
+ m = reconcileTabs(
+ m,
+ [
+ { id: 'a' },
+ { id: 'b' },
+ { id: 'c', target: { type: 'root', position: 'bottom' } },
+ ],
+ createId
+ )
+ expect(shape(m.root)).toEqual(['v', ['h', '*a', '*b'], '*c'])
+ m = reconcileTabs(
+ m,
+ [
+ { id: 'a' },
+ { id: 'b' },
+ { id: 'c' },
+ {
+ id: 'd',
+ target: { type: 'tab', tabId: 'a', position: 'after' },
+ },
+ ],
+ createId,
+ false
+ )
+ expect(shape(m.root)).toEqual(['v', ['h', '*a,d', '*b'], '*c'])
+ })
+
+ it('builds a tabset when the layout is empty', () => {
+ const createId = ids()
+ const m = reconcileTabs(
+ model(buildDefaultTree([], createId)),
+ [{ id: 'x' }],
+ createId
+ )
+ expect(shape(m.root)).toEqual(['h', '*x'])
+ })
+})
diff --git a/packages/core/test/view-state.test.ts b/packages/core/test/view-state.test.ts
deleted file mode 100644
index d0fa847..0000000
--- a/packages/core/test/view-state.test.ts
+++ /dev/null
@@ -1,315 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest'
-import { DynamixLayoutCore, Node, Bond } from '../src'
-import type { LayoutTree, NodeOptions } from '../src'
-
-const BOND = 10
-const STRIP = 36
-const SIZE = { w: 1200, h: 800, x: 0, y: 0 }
-
-const tabset = (name: string, part = 100): LayoutTree => ({
- typNode: 'tabset',
- nodName: '',
- uidNode: `ts-${name}`,
- nodPart: part,
- nodOpen: name,
- nodKids: [
- { typNode: 'tab', nodName: name, uidNode: `tab-${name}`, nodPart: 100 },
- ],
-})
-
-/** Root (side by side): a | b | column(c over d). */
-const tree = (): LayoutTree => ({
- typNode: 'row',
- nodName: 'dynamix-layout-root',
- uidNode: 'dynamix-layout-root',
- nodPart: 100,
- nodKids: [
- tabset('a', 120),
- tabset('b', 80),
- {
- typNode: 'row',
- nodName: '',
- uidNode: 'col',
- nodPart: 100,
- nodKids: [tabset('c'), tabset('d')],
- },
- ],
-})
-
-const create = (layoutTree: LayoutTree = tree()) => {
- const layout = new DynamixLayoutCore({
- tree: layoutTree,
- bond: BOND,
- minW: 40,
- minH: 40,
- collapsedSize: STRIP,
- })
- layout.updateDimension(SIZE, true)
- return layout
-}
-
-const opts = (id: string) => Node.cache.nodOpts.get().get(id) as NodeOptions
-const dims = (id: string) => ({ ...opts(id).nodDims })
-const node = (id: string) => Node.cache.mapElem.get(id) as Node
-
-/** Kids (plus bonds) must tile every row exactly. */
-const tilingProblems = (): string[] => {
- const problems: string[] = []
- const walk = (n: Node) => {
- const kids = [...n.kids]
- if (n.type === 'row' && kids.length) {
- const horizontal = Node.cache.mapDirs.get(n.unId)
- const [pos, size] = horizontal
- ? (['x', 'w'] as const)
- : (['y', 'h'] as const)
- let cursor = n.dims[pos]
- for (const k of kids) {
- if (k.dims[pos] !== cursor)
- problems.push(`${k.unId} at ${k.dims[pos]}, want ${cursor}`)
- cursor = k.dims[pos] + k.dims[size] + BOND
- }
- if (cursor - BOND !== n.dims[pos] + n.dims[size])
- problems.push(`${n.unId} not filled`)
- }
- kids.forEach(walk)
- }
- walk(DynamixLayoutCore._root)
- return problems
-}
-
-describe('Maximize', () => {
- beforeEach(() => new DynamixLayoutCore().clearAllCache())
-
- it('shows one tabset over the whole layout and hides the rest', () => {
- const layout = create()
- expect(layout.maximize('ts-c')).toBe(true)
-
- expect(layout.maximizedId).toBe('ts-c')
- expect(opts('ts-c').nodMaxd).toBe(true)
- expect(dims('ts-c')).toEqual(SIZE)
- expect(Node.cache.tabOpts.get().get('tab-c')!.nodDims).toEqual(SIZE)
-
- for (const id of ['ts-a', 'ts-b', 'ts-d'])
- expect(opts(id).nodHidden).toBe(true)
- for (const bond of Node.cache.bndOpts.get().values())
- expect(bond.nodHidden).toBe(true)
- })
-
- it('restores the previous split exactly', () => {
- const layout = create()
- const before = ['ts-a', 'ts-b', 'ts-c', 'ts-d'].map(dims)
-
- layout.maximize('ts-b')
- layout.restore()
-
- expect(['ts-a', 'ts-b', 'ts-c', 'ts-d'].map(dims)).toEqual(before)
- expect(layout.maximizedId).toBeNull()
- for (const o of Node.cache.nodOpts.get().values())
- expect(o.nodHidden).toBe(false)
- })
-
- it('toggles and refuses a layout with a single tabset', () => {
- const layout = create()
- expect(layout.toggleMaximize('ts-a')).toBe(true)
- expect(layout.toggleMaximize('ts-a')).toBe(true)
- expect(layout.maximizedId).toBeNull()
-
- new DynamixLayoutCore().clearAllCache()
- const single = new DynamixLayoutCore({ tabs: ['only'] })
- single.updateDimension(SIZE, true)
- const [only] = Node.cache.nodOpts.get().keys()
- expect(opts(only).nodMaximizable).toBe(false)
- expect(single.maximize(only)).toBe(false)
- })
-})
-
-describe('Fold', () => {
- beforeEach(() => new DynamixLayoutCore().clearAllCache())
-
- it('folds to a vertical strip in a horizontal row', () => {
- const layout = create()
- expect(layout.collapse('ts-a')).toBe(true)
-
- expect(opts('ts-a').nodFold).toBe(true)
- expect(dims('ts-a').w).toBe(STRIP)
- expect(dims('ts-a').h).toBe(SIZE.h)
- expect(tilingProblems()).toEqual([])
- })
-
- it('folds to a horizontal strip in a vertical row', () => {
- const layout = create()
- expect(layout.collapse('ts-c')).toBe(true)
-
- expect(dims('ts-c').h).toBe(STRIP)
- expect(dims('ts-d').h).toBe(SIZE.h - STRIP - BOND)
- expect(tilingProblems()).toEqual([])
- })
-
- it('unfolds back to the exact previous size', () => {
- const layout = create()
- const before = ['ts-a', 'ts-b', 'ts-c', 'ts-d'].map(dims)
-
- layout.collapse('ts-b')
- expect(dims('ts-b').w).toBe(STRIP)
- layout.expand('ts-b')
-
- expect(['ts-a', 'ts-b', 'ts-c', 'ts-d'].map(dims)).toEqual(before)
- })
-
- it('folding the last open child unfolds the most recently folded sibling', () => {
- const layout = create()
- layout.collapse('ts-c')
- expect(layout.collapse('ts-d')).toBe(true)
-
- expect(node('ts-d').collapsed).toBe(true)
- expect(node('ts-c').collapsed).toBe(false)
- expect(tilingProblems()).toEqual([])
- })
-
- it('cannot fold a tabset that is alone in its row', () => {
- const single = new DynamixLayoutCore({ tabs: ['only'] })
- single.updateDimension(SIZE, true)
- const [only] = Node.cache.nodOpts.get().keys()
-
- expect(opts(only).nodFoldable).toBe(false)
- expect(single.collapse(only)).toBe(false)
- })
-
- it('locks the bonds next to a folded tabset', () => {
- const layout = create()
- layout.collapse('ts-b')
-
- const bond = node('ts-b').prev as Bond
- expect(Node.cache.bndOpts.get().get(bond.unId)!.nodLocked).toBe(true)
-
- const before = dims('ts-a')
- layout.updateSliderDimension(bond.unId, { x: bond.dims.x + 100, y: 0 })
- expect(dims('ts-a')).toEqual(before)
- })
-
- it('keeps the strip size while the container resizes', () => {
- const layout = create()
- layout.collapse('ts-a')
-
- for (let w = 600; w <= 1600; w += 37) {
- layout.updateDimension({ ...SIZE, w }, true)
- expect(dims('ts-a').w).toBe(STRIP)
- expect(tilingProblems()).toEqual([])
- }
- })
-
- it('folding a maximized tabset leaves maximized mode', () => {
- const layout = create()
- layout.maximize('ts-a')
- layout.collapse('ts-a')
-
- expect(layout.maximizedId).toBeNull()
- expect(dims('ts-a').w).toBe(STRIP)
- })
-})
-
-describe('View state with saved layouts and drag and drop', () => {
- beforeEach(() => new DynamixLayoutCore().clearAllCache())
-
- it('round-trips fold and maximize through toJSON', () => {
- const layout = create()
- layout.collapse('ts-a')
- layout.maximize('ts-c')
- const saved = DynamixLayoutCore._root.toJSON()
-
- expect(saved.nodMaxd).toBe('ts-c')
- expect(saved.nodKids![0].nodFold).toBe(true)
-
- new DynamixLayoutCore().clearAllCache()
- const restored = create(saved)
- expect(restored.maximizedId).toBe('ts-c')
- expect(node('ts-a').collapsed).toBe(true)
- })
-
- it('ends maximize on any move', () => {
- const layout = create()
- layout.maximize('ts-a')
- expect(layout.updateTree('tab-b', 'tab-c', 'contain')).toBe(true)
- expect(layout.maximizedId).toBeNull()
- })
-
- it('unfolds a tabset that receives a dropped tab', () => {
- const layout = create()
- layout.collapse('ts-a')
- expect(layout.updateTree('tab-b', 'tab-a', 'contain')).toBe(true)
- expect(node('ts-a').collapsed).toBe(false)
- })
-
- it('unfolds a tabset that is dragged elsewhere', () => {
- const layout = create()
- layout.collapse('ts-c')
- expect(layout.updateTree('ts-c', 'ts-a', 'left')).toBe(true)
- expect(node('ts-c').collapsed).toBe(false)
- })
-
- it('stays consistent under random moves, folds and maximizes', () => {
- const tabs = ['a', 'b', 'c', 'd', 'e', 'f']
- const layout = new DynamixLayoutCore({
- tabs,
- bond: BOND,
- collapsedSize: STRIP,
- })
- layout.updateDimension(SIZE, true)
-
- let seed = 42
- const rnd = (n: number) => {
- seed = (seed * 16807) % 2147483647
- return seed % n
- }
- const areas = ['top', 'bottom', 'left', 'right', 'contain'] as const
- const problems: string[] = []
-
- for (let i = 0; i < 400 && problems.length < 5; i++) {
- const nodes = [...Node.cache.mapElem.values()].filter(
- (n): n is Node => n instanceof Node
- )
- const tabsets = nodes.filter((n) => n.type === 'tabset')
- const action = rnd(4)
- if (action === 0) {
- layout.toggleCollapse(tabsets[rnd(tabsets.length)].unId)
- } else if (action === 1) {
- layout.toggleMaximize(tabsets[rnd(tabsets.length)].unId)
- } else {
- const srcs = nodes.filter(
- (n) => n.type === 'tab' || n.type === 'tabset'
- )
- const targets = nodes.filter(
- (n) => n.type !== 'row' || n === DynamixLayoutCore._root
- )
- layout.updateTree(
- srcs[rnd(srcs.length)].unId,
- targets[rnd(targets.length)].unId,
- areas[rnd(areas.length)]
- )
- }
- layout.updateDimension(SIZE, true)
-
- const found: string[] = []
- const walk = (n: Node) => {
- if (n.type === 'tab') found.push(n.name)
- if (
- n.type !== 'tab' &&
- n.kids.size() &&
- [...n.kids].every((k) => k.collapsed)
- )
- problems.push(
- `move ${i}: every child of ${n.unId} is folded`
- )
- if (n.collapsed && n.host && n.host.kids.size() < 2)
- problems.push(`move ${i}: ${n.unId} folded alone`)
- for (const kid of n.kids) walk(kid)
- }
- walk(DynamixLayoutCore._root)
- if (found.sort().join() !== tabs.join())
- problems.push(`move ${i}: tabs changed`)
- problems.push(...tilingProblems().map((p) => `move ${i}: ${p}`))
- }
-
- expect(problems).toEqual([])
- })
-})
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
index 043975b..a228d36 100644
--- a/packages/core/tsconfig.json
+++ b/packages/core/tsconfig.json
@@ -7,7 +7,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
- "outDir": "./dist/types",
+ "outDir": "./dist",
"moduleResolution": "Node",
"rootDir": "./src",
"composite": true
diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts
index 81ceec5..2820d47 100644
--- a/packages/core/vite.config.ts
+++ b/packages/core/vite.config.ts
@@ -3,45 +3,35 @@ import { resolve } from 'path'
import dts from 'vite-plugin-dts'
import fs from 'fs'
import { visualizer } from 'rollup-plugin-visualizer'
-import stripComments from 'vite-plugin-strip-comments'
+import { licenseBanner } from '../../scripts/build/license-banner'
-const license = fs.readFileSync(resolve(__dirname, '../../LICENSE'), 'utf-8')
+const { version } = JSON.parse(fs.readFileSync(resolve(__dirname, 'package.json'), 'utf-8'))
export default defineConfig({
define: {
- __LICENSE__: JSON.stringify(license),
+ __VERSION__: JSON.stringify(version),
},
build: {
sourcemap: 'hidden',
lib: {
entry: resolve(__dirname, 'src/index.ts'),
- name: 'dynamix.layout.core',
+ name: 'DynamixLayoutCore',
// `.cjs` so Node loads the CommonJS build as CommonJS in this `"type": "module"` package.
- fileName: (format) => (format === 'cjs' ? 'core.cjs' : `core.${format}.js`),
- formats: ['cjs', 'es', 'iife', 'umd'],
+ fileName: (format) => ({ es: 'index.js', cjs: 'index.cjs', umd: 'index.umd.js' })[format as 'es'],
+ formats: ['es', 'cjs', 'umd'],
},
},
plugins: [
dts({
- outDir: 'dist/types',
+ outDir: 'dist',
// Types-only modules never reach the bundle graph; list sources explicitly.
include: ['src'],
- // One self-contained declaration file (Node16 resolution can't follow
- // extensionless imports), copied to index.d.cts for `require`.
+ exclude: ['node_modules/**', 'test/**'],
+ // One self-contained declaration file, copied to index.d.cts for `require`.
rollupTypes: true,
- afterBuild: () =>
- fs.copyFileSync(
- resolve(__dirname, 'dist/types/index.d.ts'),
- resolve(__dirname, 'dist/types/index.d.cts')
- ),
- exclude: ['node_modules/**', 'src/test/**'],
+ afterBuild: () => fs.copyFileSync(resolve(__dirname, 'dist/index.d.ts'), resolve(__dirname, 'dist/index.d.cts')),
}),
- visualizer({
- filename: 'core.html',
- gzipSize: true,
- brotliSize: true,
- template: 'treemap',
- }),
- stripComments({ type: 'none' }),
+ licenseBanner('@dynamix-layout/core'),
+ visualizer({ filename: 'core.html', gzipSize: true, brotliSize: true, template: 'treemap' }),
],
})
diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts
index de95535..06269fb 100644
--- a/packages/core/vitest.config.ts
+++ b/packages/core/vitest.config.ts
@@ -1,6 +1,13 @@
import { defineConfig } from 'vitest/config'
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+
+const { version } = JSON.parse(
+ readFileSync(resolve(__dirname, 'package.json'), 'utf-8')
+)
export default defineConfig({
+ define: { __VERSION__: JSON.stringify(version) },
test: {
name: 'core',
environment: 'node',
diff --git a/packages/react/README.md b/packages/react/README.md
index b35ab65..4311254 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -1,215 +1,54 @@
-
+# @dynamix-layout/react
-[](https://www.patreon.com/akashaman)
-[](https://www.buymeacoffee.com/akashaman)
-[](mailto:sir.akashaman@gmail.com)
+Dockable, resizable tab layouts for React 18 and 19. Drag tabs to rearrange or split panels, resize them with splitters, maximize or fold a panel, and save the layout as JSON.
-
+
-## 🚀 @dynamix-layout/react
-
-### The official React wrapper for `@dynamix-layout/core`
-
-## Overview
-
-**@dynamix-layout/react** brings the power of the `@dynamix-layout/core` engine to React applications. It provides a flexible ` ` component and an advanced `useDynamixLayout` hook to create fully dynamic, resizable, and draggable tab-based layouts with ease. Build complex UIs like VS Code or JSFiddle in minutes.
-
----
-
-### Made with ❤️ by [Akash Aman](https://linktr.ee/akash_aman)
-
----
-
-
-
----
-
-## 📦 Installation
+## Install
```bash
-npm install @dynamix-layout/react @dynamix-layout/core
+npm install @dynamix-layout/react
```
----
-
-## 🏁 Getting Started
-
-The easiest way to get started is by using the ` ` component. Provide it with an array of `[id, component]` tuples.
+## Quick start
-```jsx
-import React from 'react'
+```tsx
import { DynamixLayout } from '@dynamix-layout/react'
-import '@dynamix-layout/react/style.css' // Don't forget to import the default styles
+import '@dynamix-layout/react/styles.css'
-function App() {
- // 1. Define your tabs as an array of [id, component] tuples
- const myTabs = [
- ['editor', This is my editor!
],
- ['terminal', This is the terminal.
],
- ['preview', Live preview here.
],
- ]
+const tabs = [
+ { id: 'editor', title: 'Editor', content: },
+ { id: 'terminal', title: 'Terminal', content: },
+ { id: 'preview', title: 'Preview', content: , closable: true },
+]
+export function App() {
+ const [saved] = useState(() => JSON.parse(localStorage.getItem('layout') ?? 'null') ?? undefined)
return (
-
- {/* 2. Render the DynamixLayout component with your tabs */}
-
-
- )
-}
-
-export default App
-```
-
----
-
-## 🧩 ` ` Component API
-
-The ` ` component is the primary way to use this package. It's highly configurable through its props.
-
-### Core Functionality
-
-| Prop | Type | Description | Default |
-| ---------------------- | ----------------------- | ------------------------------------------------------------------------------------- | ---------------------------- |
-| **`tabs`** (required) | `[string, ReactNode][]` | An array of `[id, component]` tuples for your tabs. | ` ` |
-| `layoutTree` | `LayoutTree` | A serialized layout object from a previous session to restore a saved layout. | `null` |
-| `tabNames` | `Map` | An optional map to provide friendly display names for your tabs in the tab bar. | `new Map()` |
-| `enableTabbar` | `boolean` | If `true`, renders the draggable tab bar on top of each tab panel. | `true` |
-| `tabHeadHeight` | `number` | The height of the tab bar in pixels. | `40` |
-| `pad` | `{ t, b, l, r }` | Padding for the root layout container in pixels. | `{ t: 0, b: 0, l: 0, r: 0 }` |
-| `minTabWidth` | `number` | Minimum width of a tab panel in pixels. | `40` |
-| `minTabHeight` | `number` | Minimum height of a tab panel in pixels. | `40` |
-| `bondWidth` | `number` | The width/height of the draggable slider between panels in pixels. | `10` |
-| `rootId` | `string` | The HTML `id` for the root `` element of the layout. | `"dynamix-layout-root"` |
-| `disableResizeTimeout` | `boolean` | Disables debounce on window resize for faster updates. Can impact performance. | `true` |
-| `disableSliderTimeout` | `boolean` | Disables debounce when dragging a slider. | `true` |
-| `windowResizeTimeout` | `number` | Debounce timeout in milliseconds for window resize events. | `2` |
-| `sliderUpdateTimeout` | `number` | Debounce timeout in milliseconds for slider drag events. | `2` |
-| `...props` | `HTMLAttributes` | Standard HTML attributes like `style` and `className` are passed to the root `
`. | ` ` |
-
-### 🎨 Customization with Wrapper Components
-
-You can completely change the look and feel of the layout by providing your own React components for rendering different parts of the UI.
-
-| Prop | Description |
-| --------------- | ----------------------------------------------------------------------------- |
-| `WrapTabPanel` | A component to wrap the entire tab panel (tab bar + tab content area). |
-| `WrapTabHead` | A component to wrap the tab bar that contains the tab labels. |
-| `WrapTabLabel` | A component for an individual, clickable tab label in the tab bar. |
-| `WrapTabBody` | A component to wrap the content of a single tab. |
-| `SliderElement` | A component for the draggable slider used to resize panels. |
-| `HoverElement` | A component that displays the visual drop zone indicator when dragging a tab. |
-
-**Example: Custom Slider**
-
-```jsx
-import { DynamixLayout } from '@dynamix-layout/react'
-import React from 'react'
-
-const MyCustomSlider = React.forwardRef((props, ref) => (
-
-))
-
-function App() {
- // ... (myTabs definition)
- return
-}
-```
-
-### Styling Props
-
-For finer-grained control, you can pass `style` objects or `className` strings to the default wrapper components without replacing them entirely.
-
-| Prop | Type | Target Element |
-| --------------------------- | --------------------- | -------------------- |
-| `tabPanelElementStyles` | `React.CSSProperties` | `WrapTabPanel` |
-| `tabPanelElementClass` | `string` | `WrapTabPanel` |
-| `tabHeadElementStyles` | `React.CSSProperties` | `WrapTabHead` |
-| `tabHeadElementClass` | `string` | `WrapTabHead` |
-| `tabLabelElementStyles` | `React.CSSProperties` | `WrapTabLabel` |
-| `tabLabelElementClass` | `string` | `WrapTabLabel` |
-| `tabBodyElementStyles` | `React.CSSProperties` | `WrapTabBody` |
-| `tabBodyElementClass` | `string` | `WrapTabBody` |
-| `sliderElementStyles` | `React.CSSProperties` | `SliderElement` |
-| `sliderElementClass` | `string` | `SliderElement` |
-| `hoverElementStyles` | `React.CSSProperties` | `HoverElement` |
-| `hoverElementClass` | `string` | `HoverElement` |
-| `RootSplitterHoverElStyles` | `React.CSSProperties` | Root edge drop zones |
-| `RootSplitterHoverElClass` | `string` | Root edge drop zones |
-
----
-
-## ⚙️ Advanced Usage: The `useDynamixLayout` Hook
-
-For ultimate control, you can use the `useDynamixLayout` hook to build your own layout renderer from scratch. It contains all the state, refs, and event handlers needed to power the layout.
-
-### Options
-
-The hook accepts an object with the same props as the `
` component, with two key differences:
-
-1. It requires a `tabOutput` prop, generated from your `tabs` array.
-2. It requires a `dimensions` function that returns the layout container's size and position.
-
-### Return Value
-
-The hook returns an object with everything you need to build your UI, including:
-
-- **State**: `tabsets` and `sliders` (Maps containing the layout data to render).
-- **Refs**: `tabsetsRef`, `slidersRef`, `hoverElementRef`, etc., to connect to your DOM elements.
-- **Event Handlers**: `onDragStart`, `onDragEnd`, `onDragOver`, `onPointerDown`, `updateActiveTab`, etc.
-- **Instance**: `layoutInstance` (The raw instance of `@dynamix-layout/core` for advanced actions like saving state).
-
-### Example: Saving a Layout
-
-- `updateJSON`: A callback function that receives the complete layout state object (LayoutTree) every time a change occurs (like dragging a tab or resizing a panel). You can use this to save the state to localStorage, a database, or anywhere else.
-
-```jsx
-import { tabs } from './comp'
-import { DynamixLayout } from '@dynamix-layout/react'
-import type { LayoutTree } from '@dynamix-layout/core'
-import '@dynamix-layout/react/style.css'
-
-function App() {
-
- const UpdateJSON = (layoutTree: LayoutTree) => {
- console.log(layoutTree);
- };
-
- return (
- <>
+
localStorage.setItem('layout', JSON.stringify(layout))}
/>
- >
+
)
}
-
-export default App
-
```
----
+The layout fills its parent, so give the parent a size.
+
+## Three levels of API
-
+- **``**: one component. Customize it with `components`, `classNames`, `styles` and CSS variables.
+- **Headless hooks**: `useDynamixLayout`, `useTabset`, `useTab` and `useSplitter` return prop getters for a fully custom UI.
+- **State and actions anywhere**: `useLayoutState(selector)` and `useLayoutActions()`.
-[](https://www.patreon.com/akashaman)
-[](https://www.buymeacoffee.com/akashaman)
-[](mailto:sir.akashaman@gmail.com)
+## Documentation
-### Made with ❤️ by [Akash Aman](https://linktr.ee/akash_aman)
+- [Getting started](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/getting-started.md)
+- [API reference](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/api/README.md)
+- [Guides](https://github.com/xcode-studio/dynamix-layout/blob/main/README.md#documentation)
+- [Migrating from v1](https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md)
-
+MIT © [Akash Aman](https://linktr.ee/akash_aman)
diff --git a/packages/react/eslint.config.mjs b/packages/react/eslint.config.mjs
index 19170f8..447a2c5 100644
--- a/packages/react/eslint.config.mjs
+++ b/packages/react/eslint.config.mjs
@@ -1,4 +1,15 @@
-import { config } from "@repo/eslint-config/react-internal";
+import { config } from '@repo/eslint-config/react-internal'
/** @type {import("eslint").Linter.Config} */
-export default config;
+export default [
+ ...config,
+ {
+ rules: {
+ // Slot components destructure state props so they don't reach the DOM.
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ { ignoreRestSiblings: true },
+ ],
+ },
+ },
+]
diff --git a/packages/react/package.json b/packages/react/package.json
index 64e5b31..ba0d8b8 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -34,26 +34,29 @@
"url": "https://github.com/xcode-studio/dynamix-layout/issues"
},
"homepage": "https://github.com/xcode-studio/dynamix-layout#readme",
+ "type": "module",
"main": "./dist/index.cjs",
- "module": "./dist/index.es.js",
- "types": "./dist/types/index.d.ts",
- "typings": "./dist/types/index.d.ts",
- "style": "./dist/index.css",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "style": "./dist/styles.css",
"exports": {
".": {
"import": {
- "types": "./dist/types/index.d.ts",
- "default": "./dist/index.es.js"
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
},
"require": {
- "types": "./dist/types/index.d.cts",
+ "types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
- "./style.css": "./dist/index.css",
- "./styles.css": "./dist/index.css",
+ "./styles.css": "./dist/styles.css",
+ "./style.css": "./dist/styles.css",
"./package.json": "./package.json"
},
+ "sideEffects": [
+ "**/*.css"
+ ],
"files": [
"dist",
"LICENSE",
@@ -61,13 +64,9 @@
"README.md",
"!**/*.map"
],
- "sideEffects": [
- "./dist/index.css"
- ],
"publishConfig": {
"access": "public"
},
- "type": "module",
"engines": {
"node": ">=18"
},
@@ -78,10 +77,11 @@
"build": "pnpm run clean && vite build && cp ../../LICENSE dist/LICENSE",
"dev:local": "vite",
"preview": "vite preview",
- "lint": "eslint . --ext ts,tsx"
+ "lint": "eslint . --ext ts,tsx --max-warnings 0",
+ "test": "vitest"
},
"dependencies": {
- "@dynamix-layout/core": "workspace:*"
+ "@dynamix-layout/core": "workspace:^"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
@@ -96,7 +96,6 @@
"react-dom": "^19.3.0",
"typescript": "^5.9.3",
"vite": "^7.3.6",
- "vite-plugin-dts": "^4.5.4",
- "vite-plugin-lib-inject-css": "^2.2.2"
+ "vite-plugin-dts": "^4.5.4"
}
}
diff --git a/packages/react/src/components/Default.tsx b/packages/react/src/components/Default.tsx
deleted file mode 100644
index ef74389..0000000
--- a/packages/react/src/components/Default.tsx
+++ /dev/null
@@ -1,413 +0,0 @@
-import React, {
- forwardRef,
- HTMLAttributes,
- ReactNode,
- useEffect,
- useRef,
-} from 'react'
-import type { TabsetToolbarProps } from '../types'
-
-type DefaultWrapTabLabelProps = HTMLAttributes
& {
- active?: boolean
- children?: ReactNode
-}
-
-/** @deprecated Renamed in 2.0 to `Tab` (pass it as `components.Tab`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultWrapTabLabel = forwardRef<
- HTMLDivElement,
- DefaultWrapTabLabelProps
->(({ children, active, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabLabel.displayName = 'DefaultWrapTabLabel'
-
-/** @deprecated Renamed in 2.0 to `TabContent` (pass it as `components.TabContent`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultWrapTabBody = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabBody.displayName = 'DefaultWrapTabBody'
-
-/** @deprecated Renamed in 2.0 to `TabBar` (pass it as `components.TabBar`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultWrapTabHead = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabHead.displayName = 'DefaultWrapTabHead'
-
-/** @deprecated Renamed in 2.0 to `Panel` (pass it as `components.Panel`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultWrapTabPanel = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultWrapTabPanel.displayName = 'DefaultWrapTabPanel'
-
-/** @deprecated Renamed in 2.0 to `DropIndicator` (pass it as `components.DropIndicator`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultHoverElement = forwardRef<
- HTMLDivElement,
- { children?: ReactNode } & HTMLAttributes
->(({ children, ...props }, ref) => (
-
- {children}
-
-))
-DefaultHoverElement.displayName = 'DefaultHoverElement'
-
-type DefaultSliderElementProps = HTMLAttributes & {
- children?: ReactNode
- direction?: boolean
-}
-
-/** @deprecated Renamed in 2.0 to `Splitter` (pass it as `components.Splitter`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const DefaultSliderElement = forwardRef<
- HTMLDivElement,
- DefaultSliderElementProps
->(({ children, direction, ...props }, ref) => (
-
-
- {!direction ? (
-
-
-
-
-
-
-
-
- ) : (
-
-
-
-
-
-
-
-
- )}
-
- {children}
-
-))
-DefaultSliderElement.displayName = 'DefaultSliderElement'
-
-/** @deprecated Renamed in 2.0 to `RootDropZone` (pass it as `components.RootDropZone`). See https://github.com/xcode-studio/dynamix-layout/blob/main/docs/migration-v1-to-v2.md */
-export const RootSplitterHoverEl = forwardRef<
- HTMLDivElement,
- {
- children?: ReactNode
- area: 'left' | 'right' | 'top' | 'bottom'
- size?: { w: string; h: string }
- gap?: string
- } & HTMLAttributes
->(
- (
- {
- children,
- area,
- size = { h: '20%', w: '6px' },
- gap = '0px',
- ...props
- },
- ref
- ) => {
- let dynamicStyle: React.CSSProperties = {}
-
- if (area === 'left') {
- dynamicStyle = {
- borderTopRightRadius: '16px',
- borderBottomRightRadius: '16px',
- height: size.h,
- left: gap,
- width: size.w,
- }
- dynamicStyle.top = `calc(50% - ${dynamicStyle.height} / 2)`
- } else if (area === 'top') {
- dynamicStyle = {
- borderBottomLeftRadius: '16px',
- borderBottomRightRadius: '16px',
- width: size.h,
- top: gap,
- height: size.w,
- }
- dynamicStyle.left = `calc(50% - ${dynamicStyle.width} / 2)`
- } else if (area === 'right') {
- dynamicStyle = {
- borderTopLeftRadius: '16px',
- borderBottomLeftRadius: '16px',
- height: size.h,
- right: gap,
- width: size.w,
- }
- dynamicStyle.top = `calc(50% - ${dynamicStyle.height} / 2)`
- } else if (area === 'bottom') {
- dynamicStyle = {
- borderTopLeftRadius: '16px',
- borderTopRightRadius: '16px',
- width: size.h,
- bottom: gap,
- height: size.w,
- }
- dynamicStyle.left = `calc(50% - ${dynamicStyle.width} / 2)`
- }
-
- return (
-
- {children}
-
- )
- }
-)
-RootSplitterHoverEl.displayName = 'RootSplitterHoverEl'
-
-const ToolbarIcon = ({
- path,
- rotate = 0,
-}: {
- path: string
- rotate?: number
-}) => (
-
-
-
-)
-
-const ICON_MAXIMIZE =
- 'M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3'
-const ICON_RESTORE =
- 'M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3'
-const ICON_CHEVRON_LEFT = 'm15 18-6-6 6-6'
-
-export const DefaultTabsetToolbar = ({
- maximized,
- folded,
- rotated,
- rowIsHorizontal,
- showMaximize,
- showFold,
- onToggleMaximize,
- onToggleFold,
-}: TabsetToolbarProps) => {
- // Fold points back along the row (left or up); unfold points the other way.
- // Inside a rotated strip the icons are turned back so they read upright.
- const chevronTurn = (rowIsHorizontal ? 0 : 90) + (folded ? 180 : 0)
- const upright = rotated ? -90 : 0
- const toolbarRef = useRef(null)
-
- // Stay pinned to the visible end of the tab bar when its tabs overflow
- // and it scrolls.
- useEffect(() => {
- const toolbar = toolbarRef.current
- const bar = toolbar?.parentElement
- if (!toolbar || !bar) return
- const pin = () => {
- toolbar.style.transform = `translateX(${bar.scrollLeft}px)`
- }
- pin()
- bar.addEventListener('scroll', pin, { passive: true })
- return () => bar.removeEventListener('scroll', pin)
- }, [])
-
- return (
- e.stopPropagation()}
- >
- {showMaximize && (
-
-
-
- )}
- {showFold && (
-
-
-
- )}
-
- )
-}
diff --git a/packages/react/src/components/DropIndicator.tsx b/packages/react/src/components/DropIndicator.tsx
new file mode 100644
index 0000000..11ea191
--- /dev/null
+++ b/packages/react/src/components/DropIndicator.tsx
@@ -0,0 +1,15 @@
+import { forwardRef } from 'react'
+import type { DropIndicatorProps } from '../types'
+
+/**
+ * Default `DropIndicator`: highlights where a dragged tab would land.
+ * @example
+ * const MyIndicator = forwardRef(function MyIndicator({ target, ...props }, ref) {
+ * return {target.type === 'root' ? 'Dock here' : null}
+ * })
+ */
+export const DropIndicator = forwardRef(
+ function DropIndicator({ target, ...props }, ref) {
+ return
+ }
+)
diff --git a/packages/react/src/components/DynamixLayout.tsx b/packages/react/src/components/DynamixLayout.tsx
new file mode 100644
index 0000000..60aa04c
--- /dev/null
+++ b/packages/react/src/components/DynamixLayout.tsx
@@ -0,0 +1,237 @@
+import {
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+ useState,
+ type ForwardedRef,
+ type ForwardRefExoticComponent,
+ type RefAttributes,
+ type ReactElement,
+ type Ref,
+} from 'react'
+import { LayoutContext, fromPublicController } from '../context/layout-context'
+import { useDynamixLayout } from '../hooks/use-dynamix-layout'
+import type {
+ DynamixLayoutHandle,
+ DynamixLayoutProps,
+ LayoutComponents,
+} from '../types'
+import {
+ ContentView,
+ sameParts,
+ slot,
+ SplitterView,
+ TabsetView,
+ type Parts,
+} from './LayoutParts'
+import { DropIndicator } from './DropIndicator'
+import { LiveRegion } from './LiveRegion'
+import { Panel } from './Panel'
+import { RootDropZone } from './RootDropZone'
+import { Splitter } from './Splitter'
+import { Tab } from './Tab'
+import { TabBar } from './TabBar'
+import { TabContent } from './TabContent'
+import { TabsetToolbar } from './TabsetToolbar'
+
+const DEFAULT_COMPONENTS: LayoutComponents = {
+ Panel,
+ TabBar,
+ Tab,
+ TabContent,
+ Splitter,
+ DropIndicator,
+ RootDropZone,
+ TabsetToolbar,
+}
+
+function DynamixLayoutImpl(
+ props: DynamixLayoutProps,
+ ref: ForwardedRef
+): ReactElement {
+ const {
+ tabs,
+ defaultLayout,
+ layout,
+ onLayoutChange,
+ onTabClose,
+ minPanelSize,
+ splitterSize,
+ tabBarHeight,
+ showTabBar = true,
+ padding,
+ allowMaximize = true,
+ allowFold = true,
+ maximizeOnDoubleClick,
+ keyboardShortcuts,
+ tabActivation,
+ resizeThrottleMs,
+ components,
+ classNames = {},
+ styles = {},
+ className,
+ style,
+ ...rest
+ } = props
+ const result = useDynamixLayout({
+ tabs,
+ defaultLayout,
+ layout,
+ onLayoutChange,
+ onTabClose,
+ minPanelSize,
+ splitterSize,
+ tabBarHeight,
+ showTabBar,
+ padding,
+ allowMaximize,
+ allowFold,
+ maximizeOnDoubleClick,
+ keyboardShortcuts,
+ tabActivation,
+ resizeThrottleMs,
+ id: rest.id,
+ })
+ const {
+ controller,
+ getRootProps,
+ tabsets,
+ splitters,
+ tabIds,
+ dropIndicator,
+ rootDropZones,
+ actions,
+ } = result
+
+ // `components`, `classNames` and `styles` are usually inline objects; only their contents matter.
+ const [parts, setParts] = useState(() => ({
+ components: { ...DEFAULT_COMPONENTS, ...components },
+ classNames,
+ styles,
+ showTabBar,
+ allowMaximize,
+ allowFold: allowFold && showTabBar,
+ }))
+ const nextParts: Parts = {
+ components: { ...DEFAULT_COMPONENTS, ...components },
+ classNames,
+ styles,
+ showTabBar,
+ allowMaximize,
+ allowFold: allowFold && showTabBar,
+ }
+ if (!sameParts(parts, nextParts)) setParts(nextParts)
+
+ const rootElement = useRef(null)
+ useImperativeHandle(
+ ref,
+ () => ({
+ ...actions,
+ get element() {
+ return rootElement.current
+ },
+ focusTab(tabId: string) {
+ document
+ .getElementById(
+ fromPublicController(controller).core.ids.tab(tabId)
+ )
+ ?.focus()
+ },
+ }),
+ [actions, controller]
+ )
+
+ const context = fromPublicController(controller)
+ const rootProps = getRootProps({
+ ...rest,
+ className:
+ [className, classNames.root].filter(Boolean).join(' ') || undefined,
+ style: { ...style, ...styles.root },
+ ref: rootElement as Ref