Repository navigation
v2.0.0: pure core engine, new React API and headless hooks, Solid port - #87
Merged
Merged
Conversation
Records 16 seeded scenarios of UI-reachable operations (moves, splitter drags, fold, maximize, resize) against the v1 engine: canonical structure, tabset and splitter rects, and saved v1 layouts. The v2 engine will replay the same operations and must match pixel for pixel.
Adds the v2 model (rows, tabsets and tabs as immutable data with explicit directions and weights) and pure tree functions: default layout, find, normalize, remove, insert, move, maximize/fold view state, tab reconciliation and a deterministic id generator. Nothing is exported yet; the v1 engine is untouched. Behaviour follows v1, including how weights survive when rows dissolve, with two intended differences: center drops append the tab (v1 inserted it before the last tab), and splitting beside a folded tabset unfolds it (v1 left the new row stuck at its minimum size).
Ports v1 min-size propagation, weighted distribution with cumulative boundary rounding, root overflow, fold and maximize overrides and the splitter clamp-and-reweight to pure functions over the immutable model. Rects are in layout-root coordinates and unchanged rects keep their identity. Tests compare the results with the v1 engine pixel for pixel.
Pure hit testing against engine rects: root-edge zones, tab bar insertion markers from measured tab rects, folded strips (always into the tabset) and tabset thirds. Invalid targets return null instead of showing an indicator that the drop then refuses. Indicator rects and positions are checked against the v1 preview helpers.
Adds the versioned LayoutJSON format, a validating parser that reports the path of malformed input and repairs what it safely can, and migrateLayoutFromV1. Migration keeps v1 semantics (tab identity is the label, nodPart becomes weight, directions become explicit), so 106 of the 112 saved v1 layouts in the fixtures render pixel-identically. The other six capture v1 bugs v2 does not reproduce and are compared by structure. Weights of 0 are valid (v1 writes them when a splitter is dragged to a neighbour's minimum); a row whose open children all weigh 0 now shares its space equally instead of keeping stale sizes.
createLayout() returns an independent instance: no module state, no timers, no DOM. It publishes immutable snapshots that share unchanged parts by reference, reports committed changes through onLayoutChange with a reason, and drives tabs, tabsets and splitters through one drag model (startDrag/updateDrag/endDrag/cancelDrag). Saved layouts load as v2 or v1. The v1 fixtures replay through the public API: over 400 steps match v1 exactly, and the four places they stop are classified v1 bugs (a row stuck at its minimum next to a folded tabset, stale sizes when every open child weighs 0, and a root that stays oversized after the container shrinks). A root that is horizontal or has one child is now always a plain root, which keeps its JSON stable. The v2 API is exported alongside v1 until the adapters move over.
…in implementation
vite-plugin-dts only emitted files that reach the bundle graph, so modules imported with `import type` alone (layout-types, model/types, the React types) got no .d.ts, and the types they declare resolved to any for consumers. List the sources explicitly.
Adapters measure tab bars into DropMeasurements; give the entry a public name. LayoutTreeV1 accepts readonly child arrays, so typed literals and frozen data can be passed for migration.
…d slot components Rebuilds @dynamix-layout/react on the v2 core instance API. - <DynamixLayout> takes `tabs` as objects, `defaultLayout` or `layout` (v1 or v2 JSON) and reports changes through `onLayoutChange`. Sizes, slots, classNames and styles replace the 46 v1 props. The engine is created once per component and never rebuilt by prop changes; an inline `tabs` array with unchanged ids does no engine work and remounts nothing. - Headless API: useDynamixLayout with DynamixLayoutProvider, plus useTabset, useTab, useSplitter, useLayoutState, useLayoutActions and useDragState. Prop getters merge refs, classes, styles and handlers. - Pointer events drive tab, tabset and splitter drags (touch long press, Escape cancels). A ResizeObserver follows the container, and rects are relative to the root, so a layout no longer needs to sit at the viewport's top-left. - Pointer moves and resizes never re-render React: an internal fast path writes positions computed by the same function the render uses. - Tabs follow the WAI-ARIA tabs pattern, splitters the window splitter pattern, and a keyboard move mode (Mod+Shift+M) moves tabs without a mouse. Shortcuts only act when focus is inside the layout. - SSR-safe and deterministic: the Next.js example now renders on the server. The React and Next.js examples move to the new API. BREAKING CHANGE: The v1 props, wrapper components (WrapTab*, SliderElement, HoverElement, RootSplitterHoverEl), useDynamixLayout return shape, getTabOutput and the tuple `tabs` format are removed. `ref` returns a DynamixLayoutHandle (`handle.element` is the root). `onLayoutChange` replaces `updateJSON`, receives v2 JSON and is not called on mount. Default class names change to `.dx-*`, and data attributes to `data-dx-*`. See docs/v2/02-design.md for the full mapping.
A minimal port: <DynamixLayout> keeps its props, default components and HTML5 tab drag-and-drop, but runs on one createLayout instance per component instead of the v1 global engine. Drop targets come from the core hit testing, positions follow a snapshot signal, and rects are relative to the root, so two Solid layouts on a page no longer share state and a layout away from the viewport's top-left is placed correctly. BREAKING CHANGE: `layoutTree` accepts v2 JSON (v1 trees are migrated) and `updateJSON` receives v2 `LayoutJSON`. `getTabOutput` and the `TabEntry`/`TabInput`/`TabOutput` types are removed, tab ids are the tab labels, `useDynamixLayout` returns a new shape, and the slider timeout options no longer apply (splitters update once per frame).
…er and window global The v1 engine is gone: DynamixLayoutCore with its static accessors, Node, Bond and Node.cache, the module-level layoutState, Queue, createReactiveState, the comparators, the v1 drop-preview and DOM helpers, and the LayoutTree/NodeOptions types. Importing the package no longer logs a banner or sets window.__DYNAMIX_LAYOUT__; `version` is exported instead. createFrameScheduler stays, moved to dom/. The v1 test suites go with the engine. What they checked is covered by the v2 tests and by fixtures recorded from v1 (scenario replays, default layouts and drop previews), which stay as the reference. BREAKING CHANGE: use createLayout() instead of new DynamixLayoutCore(), layout.toJSON() instead of DynamixLayoutCore._root.toJSON(), and the snapshot from layout.getSnapshot()/subscribe() instead of Node.cache. Saved v1 trees load through migrateLayoutFromV1 (automatic in createLayout and layout.load). See docs/v2/02-design.md for the full rename table.
vite-plugin-dts rewrote the tsconfig path alias for @dynamix-layout/core
into a relative import of the core sources ('../../../core/src'), which
doesn't exist in the published package, so every core type resolved to
any for users. The published react 1.0.2 has the same problem. Keep the
package specifier, and emit declarations for types-only Solid modules.
Prop getters now set `position: absolute` (and the root `position: relative`) inline next to the rects, and hidden slots get the native `hidden` attribute as well as `data-dx-hidden`, so headless layouts work without importing styles.css.
The React example app now has one page per level, picked by URL hash: custom slot components with a saved v1 layout and live size controls (the existing demo), the basic component, headless hooks with no library styles, and controlled mode persisted to localStorage.
…act 18 check - Every package ships ESM (`index.js`) and CommonJS (`index.cjs`). The CommonJS build used a `.js` extension in a `"type": "module"` package, so Node loaded it as ESM and `require()` failed. - `exports` maps put `types` first in each condition; CommonJS gets `index.d.cts`. Declarations are rolled into one file, built against the published core instead of its sources. - React: `react/jsx-runtime` and `react-dom/*` are external (the bundle inlined React 19's runtime), peers are `^18.0.0 || ^19.0.0`, the bundle starts with 'use client', IIFE/UMD builds are gone, and the stylesheet is `styles.css` (`style.css` stays as an alias). - Solid: `solid-js` is a peer dependency, with the same exports layout. - Core: one UMD build for CDNs (`unpkg`), no `browser` field, and a one-line licence banner instead of the full text. - `sideEffects` lists only CSS. publint and attw report no problems. - A `react-18` CI job installs the packed packages into a project that only has React 18 and renders on the server and in jsdom.
…age tooling DynamixLayout.tsx keeps the root component and LayoutParts.tsx the memoized per-tabset, tab, content and splitter parts; keyboard move mode gets its own module. rootDir moves to the declaration-only tsconfig so the dev typecheck, which reads core from source, is clean again. Adds @vitest/coverage-v8 and tests for the public core exports (no v1 leftovers, no window global), custom id factories, setOptions, moveTabset, targeted addTab, splitter sources in drop helpers and the production duplicate-id path. Core coverage: 93.8% statements, 89.5% branches, 96.1% functions, 96.1% lines.
Rewrites the root and package READMEs for v2 (pitch, demo, install, quick start, links to the docs), points every repository link at xcode-studio/dynamix-layout, fixes the documented stylesheet path, and updates the site's install snippets to the v2 API.
migrateLayoutFromV1 replaced a negative or non-numeric nodPart with 100 without telling anyone. It now reports INVALID_WEIGHT with the path, like the v2 parser.
…on guide - getting-started: concepts, the first layout, saving and restoring, and adding, closing and selecting tabs. - api/: one page per export (DynamixLayout, useDynamixLayout and the provider, useTabset, useTab, useSplitter, useLayoutState, useLayoutActions, useDragState, createLayout, LayoutJSON, migrateLayoutFromV1), each with the signature, a table of every option, the return value, examples and common pitfalls. - guides/: custom components, theming, persisting layouts, Next.js and SSR, keyboard and accessibility, multiple layouts, the core without React, and performance (with measured bundle sizes). - migration-v1-to-v2: steps, the full rename table, before/after code for each breaking change, behaviour changes and saved layouts.
getRootProps listed the measured state as a dependency, so its identity changed once after mount, contrary to the documented guarantee. The flag now lives on the controller, which getters read at call time, and a test checks that every function from useDynamixLayout and useTab keeps its identity across renders.
Adds @example to the slot components, useTabset, useTab, useSplitter, useDragState and the core helpers (isLayoutV1, getRootDropZoneRect, getTabBarPlacement, getTabContentRect, applyRect, createFrameScheduler, DynamixLayoutError, version), with @PARAM and @returns where they were missing. Slot examples forward refs, as React 18 requires.
…rbo env var The published packages required node >=22.16.0 and pnpm >=10.13.1, which made installs fail (or warn) for apps on Node 18 or 20 with strict engine checks, although the ES2020 builds run there. They now declare node >=18; the repo's own Node and pnpm versions stay in the root package.json. Also removes UPDATE_V1_FIXTURES from turbo.json, left over from the v1 fixture recorder.
# Conflicts: # README.md # packages/core/README.md # packages/core/package.json # packages/core/src/app/comparator.ts # packages/core/src/app/dom.ts # packages/core/src/app/drop-preview.ts # packages/core/src/app/dynamix.ts # packages/core/src/app/node.ts # packages/core/src/app/queue.ts # packages/core/src/app/reactive-state.ts # packages/core/src/index.ts # packages/core/src/type/index.ts # packages/core/vite.config.ts # packages/react/README.md # packages/react/package.json # packages/react/src/components/Default.tsx # packages/react/src/components/Layout.tsx # packages/react/src/hooks/useLayout.ts # packages/react/src/types/index.ts # packages/react/tsconfig.build.json # packages/react/vite.config.ts # packages/solid/package.json # packages/solid/src/types/index.ts # packages/solid/tsconfig.build.json # packages/solid/vite.config.ts
`tabs` is now reactive (new or removed entries update the same engine;
existing content stays mounted). A third tuple element { title,
closable } adds a close button that calls onTabClose, and onReady hands
over the engine for actions from code. All additive.
Drag, split, resize, close, add (anywhere or at a target), fold, maximize and restore (toolbar, double-click, shortcuts and API buttons), select and move from code, keyboard move mode, localStorage persistence and reset, with a live log of onLayoutChange reasons. It's the default page of the React example.
A small DOM renderer (examples/vanilla/src/dom-layout.ts) over @dynamix-layout/core with the same feature set as the React and Solid showcases: tab and tab bar drags, splitters, closable tabs, add, fold, maximize/restore, select, move, persistence and reset.
Client-only layout restore keeps server and client markup identical.
Rollup's output.banner is added before Vite minifies, and the minifier dropped it, so core shipped without the comment the design called for and react and solid had none. A shared plugin now adds it after minification, on line 1 (after 'use client') so source maps stay aligned.
The second argument is the core's LayoutChangeReason, or 'mount' for the call on mount. Existing one-argument callbacks keep working.
The saved layout lists every open tab, so each showcase rebuilds its tabs from it. The Solid showcases log change reasons instead of "saved".
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Merged
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Ships v2.0.0 of
@dynamix-layout/core,@dynamix-layout/reactand@dynamix-layout/solid(major for all three via changesets). Plan and reasoning:docs/v2/01-audit.mdanddocs/v2/02-design.md.createLayout(options)returns an independent, pure instance: immutable snapshots with structural sharing, typed actions (move, select, add/remove, resize, maximize/restore, fold/unfold, one drag model, hit testing),onLayoutChange(layout, reason), a versionedLayoutJSONformat, and automatic migration of v1 layouts (pixel-identical against recorded v1 fixtures). No module state, timers, DOM access or import-time side effects.<DynamixLayout tabs defaultLayout|layout onLayoutChange onTabClose ref>plus headless hooks (useDynamixLayout,useTabset,useTab,useSplitter,useLayoutState,useLayoutActions,useDragState). Pointer-event drags that never re-render React, ResizeObserver, WAI-ARIA tabs and splitters, keyboard move mode, SSR-safe with a'use client'entry. React 18 and 19.tabs, closable tabs,onReady(layout)andupdateJSON(layout, reason).require()(real CommonJS), exports maps with per-condition types, Node 18+, licence comment kept in every bundle./showcase), Solid, SolidStart (/showcase), and a new plain-JavaScript example on the core (examples/vanilla).Full change lists, including every breaking change, are in
.changeset/v2-core.md,v2-react.mdandv2-solid.md; they become the 2.0.0 changelogs.Related issues
Follows the final 1.x releases (#84, #83, #86: core 1.2.0, react 1.1.0, solid 1.1.0), which added deprecation notes pointing at the v2 APIs.
mainis merged into this branch.Testing done
pnpm build,pnpm lintandvitest run: 21 files, 349 tests passing (characterization tests written against v1 behaviour first, then the v2 suites).pnpm changeset status: core, react and solid at major.Additional context
Merging this makes the release workflow open a "Version Packages" PR for 2.0.0; merging that publishes to npm.