quilt-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 niko-dellic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # quilt-react
2
+
3
+ React components and hooks over the shared Quilt engine and DOM renderer. React 18.3 and 19 are supported peers. MIT licensed; ESM, declarations, and sources included.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install quilt-react react react-dom
9
+ ```
10
+
11
+ Core and DOM are installed automatically. TypeScript apps also need matching `@types/react` and `@types/react-dom`. Use an ESM-capable bundler with CSS imports. For local archives, see [packaging instructions](https://github.com/niko-dellic/quilt/blob/main/docs/packaging.md).
12
+
13
+ ## Use
14
+
15
+ Provide `<div id="app"></div>` in your HTML.
16
+
17
+ ```tsx
18
+ import { createRoot } from 'react-dom/client';
19
+ import { Layout, LayoutStore, createLayout } from 'quilt-react';
20
+ import type { PaneProps } from 'quilt-react';
21
+ import 'quilt-react/styles.css';
22
+
23
+ const store = new LayoutStore(
24
+ createLayout({
25
+ pane: { id: 'notes', type: 'notes', title: 'Notes' },
26
+ }),
27
+ );
28
+ const data = { text: 'Hello' };
29
+ const getPaneState = () => data;
30
+ function Notes({ state }: PaneProps) {
31
+ const model = state as typeof data;
32
+ return (
33
+ <textarea
34
+ defaultValue={model.text}
35
+ onChange={(e) => {
36
+ model.text = e.target.value;
37
+ }}
38
+ />
39
+ );
40
+ }
41
+ const components = { notes: Notes };
42
+ const root = createRoot(document.getElementById('app')!);
43
+ root.render(
44
+ <Layout
45
+ store={store}
46
+ components={components}
47
+ getPaneState={getPaneState}
48
+ style={{ width: '100%', height: 600 }}
49
+ />,
50
+ );
51
+ // Call when removing the workspace:
52
+ function disposeWorkspace() {
53
+ root.unmount();
54
+ // The binding queues pane disposal beyond the parent React commit.
55
+ queueMicrotask(() => store.dispose());
56
+ }
57
+ ```
58
+
59
+ Keep the store, components, registry, and adapter callbacks stable. `useLayoutSnapshot(store)` observes layout changes. The model type is `LayoutSnapshot`; `Layout` is the component. `MountedLayout`, `TabRegistry`, theme presets, and DOM configuration types are available here too. Attach a `MountedLayout` ref to call `popout` from a user gesture; before mounting completes its methods return false/no-op.
60
+
61
+ Panes are separate React roots and do not inherit your app's context providers. Wrap pane components with required providers. React-local state does not survive crossing documents: retain data in an application-owned store, and subscribe inside each view when live synchronization is needed. Companion windows are same-origin and depend on the main session.
62
+
63
+ The stylesheet is identical to `quilt-vanilla/styles.css`; import either once. See [API](https://github.com/niko-dellic/quilt/blob/main/docs/api.md), [lifecycle](https://github.com/niko-dellic/quilt/blob/main/docs/lifecycle.md), and [migration](https://github.com/niko-dellic/quilt/blob/main/docs/migration.md).
@@ -0,0 +1,20 @@
1
+ export type { TabBarOptions, TabBarStyle } from 'quilt-vanilla';
2
+ import type { ComponentType, CSSProperties } from 'react';
3
+ import type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';
4
+ import type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';
5
+ export type PaneProps = Omit<PaneContext, 'element'>;
6
+ /** A separate root is created for each pane view, including companion documents. */
7
+ export declare function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer;
8
+ export interface LayoutProps extends Omit<LayoutOptions, 'renderers'> {
9
+ components: Record<string, ComponentType<PaneProps>>;
10
+ className?: string;
11
+ style?: CSSProperties;
12
+ }
13
+ /** Keep store, components, and adapter callbacks stable to avoid remounting the workspace. */
14
+ export declare const Layout: import("react").ForwardRefExoticComponent<LayoutProps & import("react").RefAttributes<MountedLayout>>;
15
+ export declare function useLayoutSnapshot(store: LayoutStore): LayoutSnapshot;
16
+ export { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';
17
+ export type { AutoCollapse, LayoutStoreOptions, Json, Capability, Axis, Pane, Group, Split, Node, WindowPlacement, Popout, Layout as LayoutSnapshot, Issue, CommandOptions, Change, Bounds, JoinOptions, } from 'quilt-core';
18
+ export { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';
19
+ export type { TabRegistration, LayoutTheme, LayoutOptions, MountedLayout, PaneContext, PaneView, PaneRenderer, } from 'quilt-vanilla';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAWhE,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAIrE,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,IAAI,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAgBrD,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,YAAY,CAqC/E;AACD,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC;IACnE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AACD,8FAA8F;AAC9F,eAAO,MAAM,MAAM,uGAoFjB,CAAC;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,cAAc,CAEpE;AACD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3F,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,IAAI,EACJ,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,KAAK,EACL,IAAI,EACJ,eAAe,EACf,MAAM,EACN,MAAM,IAAI,cAAc,EACxB,KAAK,EACL,cAAc,EACd,MAAM,EACN,MAAM,EACN,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnE,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,EACX,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Component, createElement, forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { flushSync } from 'react-dom';
5
+ import { mountLayout } from 'quilt-vanilla';
6
+ class Boundary extends Component {
7
+ state = { failed: false };
8
+ static getDerivedStateFromError() {
9
+ return { failed: true };
10
+ }
11
+ componentDidCatch(error) {
12
+ this.props.onError(error);
13
+ }
14
+ render() {
15
+ return this.state.failed ? null : this.props.children;
16
+ }
17
+ }
18
+ /** A separate root is created for each pane view, including companion documents. */
19
+ export function reactRenderer(component) {
20
+ return (context) => {
21
+ const root = createRoot(context.element);
22
+ let failure;
23
+ const { element: _element, ...props } = context;
24
+ try {
25
+ flushSync(() => root.render(createElement(Boundary, {
26
+ onError: (error) => {
27
+ failure = error;
28
+ },
29
+ children: createElement(component, props),
30
+ })));
31
+ if (failure)
32
+ throw failure;
33
+ }
34
+ catch (error) {
35
+ root.unmount();
36
+ throw error;
37
+ }
38
+ return {
39
+ dispose() {
40
+ root.unmount();
41
+ },
42
+ update(pane) {
43
+ root.render(createElement(Boundary, {
44
+ onError: (error) => {
45
+ context.element.textContent = error instanceof Error ? error.message : String(error);
46
+ },
47
+ children: createElement(component, { ...props, pane }),
48
+ }));
49
+ },
50
+ };
51
+ };
52
+ }
53
+ /** Keep store, components, and adapter callbacks stable to avoid remounting the workspace. */
54
+ export const Layout = forwardRef(function Layout(props, ref) {
55
+ const { store, tabs, theme, tabBar, components, getPaneState, onError, prepareWindow, openWindow, renderIcon, shortcuts, className, style, } = props;
56
+ const host = useRef(null), mounted = useRef(null);
57
+ const currentTabBar = useRef(tabBar);
58
+ currentTabBar.current = tabBar;
59
+ useLayoutEffect(() => {
60
+ mounted.current?.setTabBar(tabBar ?? {});
61
+ }, [tabBar]);
62
+ const currentTheme = useRef(theme);
63
+ currentTheme.current = theme;
64
+ useLayoutEffect(() => {
65
+ mounted.current?.setTheme(theme ?? {});
66
+ }, [theme]);
67
+ const renderers = useMemo(() => Object.fromEntries(Object.entries(components).map(([id, c]) => [id, reactRenderer(c)])), [components]);
68
+ useImperativeHandle(ref, () => ({
69
+ setTabBar: (options) => mounted.current?.setTabBar(options),
70
+ setTheme: (theme) => mounted.current?.setTheme(theme),
71
+ popout: (...args) => mounted.current?.popout(...args) ?? false,
72
+ returnPane: (id) => mounted.current?.returnPane(id),
73
+ dispose: () => mounted.current?.dispose(),
74
+ }), []);
75
+ useLayoutEffect(() => {
76
+ let cancelled = false;
77
+ let instance;
78
+ // Pane roots must mount outside the parent React commit (flushSync is forbidden there).
79
+ queueMicrotask(() => {
80
+ if (cancelled || !host.current)
81
+ return;
82
+ instance = mountLayout(host.current, {
83
+ store,
84
+ renderers,
85
+ ...(currentTabBar.current ? { tabBar: currentTabBar.current } : {}),
86
+ ...(tabs ? { tabs } : {}),
87
+ ...(currentTheme.current ? { theme: currentTheme.current } : {}),
88
+ ...(getPaneState ? { getPaneState } : {}),
89
+ ...(onError ? { onError } : {}),
90
+ ...(prepareWindow ? { prepareWindow } : {}),
91
+ ...(openWindow ? { openWindow } : {}),
92
+ ...(renderIcon ? { renderIcon } : {}),
93
+ ...(shortcuts !== undefined ? { shortcuts } : {}),
94
+ });
95
+ mounted.current = instance;
96
+ });
97
+ return () => {
98
+ cancelled = true;
99
+ const previous = instance;
100
+ queueMicrotask(() => previous?.dispose());
101
+ if (mounted.current === previous)
102
+ mounted.current = null;
103
+ };
104
+ }, [
105
+ store,
106
+ renderers,
107
+ tabs,
108
+ getPaneState,
109
+ onError,
110
+ prepareWindow,
111
+ openWindow,
112
+ renderIcon,
113
+ shortcuts,
114
+ ]);
115
+ return (_jsx("div", { ref: host, className: className, style: { width: '100%', height: '100%', ...style } }));
116
+ });
117
+ export function useLayoutSnapshot(store) {
118
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
119
+ }
120
+ export { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';
121
+ export { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["export type { TabBarOptions, TabBarStyle } from 'quilt-vanilla';\nimport {\n Component,\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { ComponentType, CSSProperties, ReactNode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { flushSync } from 'react-dom';\nimport { mountLayout } from 'quilt-vanilla';\nimport type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';\nimport type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';\nexport type PaneProps = Omit<PaneContext, 'element'>;\nclass Boundary extends Component<\n { children: ReactNode; onError: (error: unknown) => void },\n { failed: boolean }\n> {\n override state = { failed: false };\n static getDerivedStateFromError() {\n return { failed: true };\n }\n override componentDidCatch(error: unknown) {\n this.props.onError(error);\n }\n override render() {\n return this.state.failed ? null : this.props.children;\n }\n}\n/** A separate root is created for each pane view, including companion documents. */\nexport function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer {\n return (context) => {\n const root = createRoot(context.element);\n let failure: unknown;\n const { element: _element, ...props } = context;\n try {\n flushSync(() =>\n root.render(\n createElement(Boundary, {\n onError: (error) => {\n failure = error;\n },\n children: createElement(component, props),\n }),\n ),\n );\n if (failure) throw failure;\n } catch (error) {\n root.unmount();\n throw error;\n }\n return {\n dispose() {\n root.unmount();\n },\n update(pane) {\n root.render(\n createElement(Boundary, {\n onError: (error) => {\n context.element.textContent = error instanceof Error ? error.message : String(error);\n },\n children: createElement(component, { ...props, pane }),\n }),\n );\n },\n };\n };\n}\nexport interface LayoutProps extends Omit<LayoutOptions, 'renderers'> {\n components: Record<string, ComponentType<PaneProps>>;\n className?: string;\n style?: CSSProperties;\n}\n/** Keep store, components, and adapter callbacks stable to avoid remounting the workspace. */\nexport const Layout = forwardRef<MountedLayout, LayoutProps>(function Layout(props, ref) {\n const {\n store,\n tabs,\n theme,\n tabBar,\n components,\n getPaneState,\n onError,\n prepareWindow,\n openWindow,\n renderIcon,\n shortcuts,\n className,\n style,\n } = props;\n const host = useRef<HTMLDivElement>(null),\n mounted = useRef<MountedLayout | null>(null);\n const currentTabBar = useRef(tabBar);\n currentTabBar.current = tabBar;\n useLayoutEffect(() => {\n mounted.current?.setTabBar(tabBar ?? {});\n }, [tabBar]);\n const currentTheme = useRef(theme);\n currentTheme.current = theme;\n useLayoutEffect(() => {\n mounted.current?.setTheme(theme ?? {});\n }, [theme]);\n const renderers = useMemo(\n () => Object.fromEntries(Object.entries(components).map(([id, c]) => [id, reactRenderer(c)])),\n [components],\n );\n useImperativeHandle(\n ref,\n () => ({\n setTabBar: (options) => mounted.current?.setTabBar(options),\n setTheme: (theme) => mounted.current?.setTheme(theme),\n popout: (...args) => mounted.current?.popout(...args) ?? false,\n returnPane: (id) => mounted.current?.returnPane(id),\n dispose: () => mounted.current?.dispose(),\n }),\n [],\n );\n useLayoutEffect(() => {\n let cancelled = false;\n let instance: MountedLayout | undefined;\n // Pane roots must mount outside the parent React commit (flushSync is forbidden there).\n queueMicrotask(() => {\n if (cancelled || !host.current) return;\n instance = mountLayout(host.current, {\n store,\n renderers,\n ...(currentTabBar.current ? { tabBar: currentTabBar.current } : {}),\n ...(tabs ? { tabs } : {}),\n ...(currentTheme.current ? { theme: currentTheme.current } : {}),\n ...(getPaneState ? { getPaneState } : {}),\n ...(onError ? { onError } : {}),\n ...(prepareWindow ? { prepareWindow } : {}),\n ...(openWindow ? { openWindow } : {}),\n ...(renderIcon ? { renderIcon } : {}),\n ...(shortcuts !== undefined ? { shortcuts } : {}),\n });\n mounted.current = instance;\n });\n return () => {\n cancelled = true;\n const previous = instance;\n queueMicrotask(() => previous?.dispose());\n if (mounted.current === previous) mounted.current = null;\n };\n }, [\n store,\n renderers,\n tabs,\n getPaneState,\n onError,\n prepareWindow,\n openWindow,\n renderIcon,\n shortcuts,\n ]);\n return (\n <div ref={host} className={className} style={{ width: '100%', height: '100%', ...style }} />\n );\n});\nexport function useLayoutSnapshot(store: LayoutStore): LayoutSnapshot {\n return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n}\nexport { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';\nexport type {\n AutoCollapse,\n LayoutStoreOptions,\n Json,\n Capability,\n Axis,\n Pane,\n Group,\n Split,\n Node,\n WindowPlacement,\n Popout,\n Layout as LayoutSnapshot,\n Issue,\n CommandOptions,\n Change,\n Bounds,\n JoinOptions,\n} from 'quilt-core';\nexport { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';\nexport type {\n TabRegistration,\n LayoutTheme,\n LayoutOptions,\n MountedLayout,\n PaneContext,\n PaneView,\n PaneRenderer,\n} from 'quilt-vanilla';\n"]}
@@ -0,0 +1,842 @@
1
+ .layouts {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 100%;
5
+ min-height: 0;
6
+ color: var(--layouts-text, #e5e9f0);
7
+ background: var(--layouts-bg, #161a20);
8
+ font-family: var(--layouts-font-family, system-ui, sans-serif);
9
+ font-size: var(--layouts-font-size, 13px);
10
+ line-height: 1.45;
11
+ isolation: isolate;
12
+ }
13
+ .layouts *,
14
+ .layouts *::before,
15
+ .layouts *::after {
16
+ box-sizing: border-box;
17
+ }
18
+ .layouts [hidden] {
19
+ display: none !important;
20
+ }
21
+ .layouts-stage {
22
+ position: absolute;
23
+ inset: 0;
24
+ overflow: auto;
25
+ }
26
+ .layouts-node {
27
+ position: absolute;
28
+ min-width: 0;
29
+ min-height: 0;
30
+ }
31
+ .layouts-group {
32
+ display: flex;
33
+ flex-direction: column;
34
+ background: var(--layouts-panel, #1d222a);
35
+ }
36
+ .layouts-group::before {
37
+ content: '';
38
+ position: absolute;
39
+ inset: 0;
40
+ z-index: 3;
41
+ pointer-events: none;
42
+ border: 1px solid;
43
+ border-color: var(--layouts-edge-top, var(--layouts-line, #39414e))
44
+ var(--layouts-edge-right, var(--layouts-line, #39414e))
45
+ var(--layouts-edge-bottom, var(--layouts-line, #39414e))
46
+ var(--layouts-edge-left, var(--layouts-line, #39414e));
47
+ }
48
+ .layouts-header {
49
+ height: var(--layouts-header-height, 34px);
50
+ flex: 0 0 var(--layouts-header-height, 34px);
51
+ display: flex;
52
+ align-items: center;
53
+ background: var(--layouts-header, #242a34);
54
+ border-bottom: 1px solid var(--layouts-line, #39414e);
55
+ padding: 4px 6px;
56
+ gap: 2px;
57
+ }
58
+ .layouts-tabs {
59
+ display: flex;
60
+ flex: 1;
61
+ min-width: 0;
62
+ overflow: auto;
63
+ scrollbar-width: thin;
64
+ height: 100%;
65
+ gap: 3px;
66
+ }
67
+ .layouts-button,
68
+ .layouts-tab {
69
+ appearance: none;
70
+ border: 0;
71
+ background: transparent;
72
+ color: var(--layouts-muted, #a2acba);
73
+ font: inherit;
74
+ padding: 5px 9px;
75
+ cursor: pointer;
76
+ border-radius: var(--layouts-radius, 5px);
77
+ white-space: nowrap;
78
+ }
79
+ .layouts-tab {
80
+ border-radius: 0;
81
+ border-bottom: 2px solid transparent;
82
+ text-overflow: ellipsis;
83
+ max-width: 240px;
84
+ overflow: hidden;
85
+ }
86
+ .layouts-tab[aria-selected='true'] {
87
+ color: var(--layouts-text, #e5e9f0);
88
+ background: var(--layouts-panel, #1d222a);
89
+ }
90
+ .layouts-button:hover:not(:disabled),
91
+ .layouts-tab:hover {
92
+ color: var(--layouts-text, #e5e9f0);
93
+ background: color-mix(in srgb, var(--layouts-text, #e5e9f0) 8%, transparent);
94
+ }
95
+ .layouts-button:disabled {
96
+ opacity: 0.4;
97
+ cursor: default;
98
+ }
99
+ .layouts :focus-visible {
100
+ outline: 2px solid var(--layouts-focus, #b3decf);
101
+ outline-offset: -2px;
102
+ }
103
+ .layouts .layouts-menu:focus-visible {
104
+ outline: none;
105
+ }
106
+ /* Arrow-key navigation activates the focused tab; its background marks selection. */
107
+ .layouts .layouts-tab[aria-selected='true']:focus-visible {
108
+ outline: none;
109
+ }
110
+ .layouts-body {
111
+ position: relative;
112
+ flex: 1;
113
+ min-height: 0;
114
+ min-width: 0;
115
+ overflow: hidden;
116
+ }
117
+ .layouts-pane {
118
+ width: 100%;
119
+ height: 100%;
120
+ overflow: auto;
121
+ }
122
+ .layouts-divider {
123
+ position: absolute;
124
+ z-index: 3;
125
+ touch-action: none;
126
+ background: var(--layouts-bg, #161a20);
127
+ }
128
+ .layouts-divider[data-axis='horizontal'] {
129
+ cursor: col-resize;
130
+ }
131
+ .layouts-divider[data-axis='vertical'] {
132
+ cursor: row-resize;
133
+ }
134
+ .layouts-divider:hover:not([aria-disabled='true']),
135
+ .layouts-divider:focus-visible {
136
+ background: var(--layouts-accent, #8bbaaa);
137
+ }
138
+ .layouts-divider[aria-disabled='true'] {
139
+ cursor: default;
140
+ }
141
+ .layouts-resizing {
142
+ user-select: none;
143
+ }
144
+ .layouts-placeholder {
145
+ padding: 24px;
146
+ color: var(--layouts-muted, #a2acba);
147
+ }
148
+ .layouts-status {
149
+ position: absolute;
150
+ bottom: 12px;
151
+ left: 12px;
152
+ max-width: min(560px, 90%);
153
+ padding: 10px 14px;
154
+ background: var(--layouts-header, #242a34);
155
+ border: 1px solid var(--layouts-line, #39414e);
156
+ border-radius: var(--layouts-radius, 5px);
157
+ z-index: 20;
158
+ }
159
+ .layouts-status:empty {
160
+ display: none;
161
+ }
162
+ .layouts-menu {
163
+ position: fixed;
164
+ margin: 0;
165
+ padding: 5px;
166
+ width: 240px;
167
+ max-height: min(520px, 70vh);
168
+ overflow: auto;
169
+ background: var(--layouts-header, #242a34);
170
+ color: var(--layouts-text, #e5e9f0);
171
+ border: 1px solid var(--layouts-line, #39414e);
172
+ border-radius: var(--layouts-radius, 7px);
173
+ box-shadow: 0 12px 40px #0005;
174
+ }
175
+ .layouts-menu .layouts-button {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 10px;
179
+ width: 100%;
180
+ text-align: left;
181
+ white-space: normal;
182
+ }
183
+ .layouts-menu::backdrop {
184
+ background: transparent;
185
+ }
186
+ .layouts-group[data-drop]::after {
187
+ content: '';
188
+ position: absolute;
189
+ inset: 0;
190
+ z-index: 5;
191
+ pointer-events: none;
192
+ background: color-mix(in srgb, var(--layouts-accent, #8bbaaa) 22%, transparent);
193
+ border: 2px solid var(--layouts-accent, #8bbaaa);
194
+ }
195
+ .layouts-group[data-drop='left']::after {
196
+ right: 50%;
197
+ }
198
+ .layouts-group[data-drop='right']::after {
199
+ left: 50%;
200
+ }
201
+ .layouts-group[data-drop='top']::after {
202
+ bottom: 50%;
203
+ }
204
+ .layouts-group[data-drop='bottom']::after {
205
+ top: 50%;
206
+ }
207
+ .layouts-companion {
208
+ position: fixed;
209
+ inset: 0;
210
+ display: flex;
211
+ flex-direction: column;
212
+ }
213
+ .layouts-companion-bar {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: space-between;
217
+ flex: 0 0 38px;
218
+ background: var(--layouts-header, #242a34);
219
+ border-bottom: 1px solid var(--layouts-line, #39414e);
220
+ padding: 0 10px;
221
+ }
222
+ .layouts-companion > .layouts-pane {
223
+ flex: 1;
224
+ min-height: 0;
225
+ height: auto;
226
+ }
227
+
228
+ /* Each tab responds to its allocated width, not the whole viewport. */
229
+ .layouts-tab-item {
230
+ position: relative;
231
+ display: flex;
232
+ flex: 0 1 180px;
233
+ min-width: 32px;
234
+ container: layouts-tab / inline-size;
235
+ border-radius: var(--layouts-radius, 5px);
236
+ }
237
+ .layouts-tab-item[data-active='true'] {
238
+ min-width: 56px;
239
+ background: var(--layouts-panel, #1d222a);
240
+ }
241
+ .layouts-tab-item .layouts-tab {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 7px;
245
+ flex: 1;
246
+ min-width: 0;
247
+ border: 0;
248
+ background: transparent;
249
+ padding: 0 8px;
250
+ }
251
+ .layouts-tab-icon {
252
+ flex: 0 0 var(--layouts-icon-size, 16px);
253
+ width: var(--layouts-icon-size, 16px);
254
+ height: var(--layouts-icon-size, 16px);
255
+ display: inline-flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ }
259
+ .layouts-tab-icon > * {
260
+ width: var(--layouts-icon-size, 16px);
261
+ height: var(--layouts-icon-size, 16px);
262
+ }
263
+ .layouts-tab-label {
264
+ overflow: hidden;
265
+ text-overflow: ellipsis;
266
+ }
267
+ .layouts-tab-item .layouts-tab-close {
268
+ flex: 0 0 22px;
269
+ align-self: center;
270
+ width: 22px;
271
+ height: 100%;
272
+ max-height: 24px;
273
+ padding: 0;
274
+ margin-right: 3px;
275
+ }
276
+ @container layouts-tab (max-width: 130px) {
277
+ .layouts-tab-close {
278
+ display: none;
279
+ }
280
+ .layouts-tab-item[data-active='true'] > .layouts-tab-close {
281
+ display: block;
282
+ }
283
+ }
284
+ @container layouts-tab (max-width: 95px) {
285
+ .layouts-tab-label {
286
+ display: none;
287
+ }
288
+ .layouts-tab-item .layouts-tab {
289
+ justify-content: center;
290
+ padding-inline: 4px;
291
+ }
292
+ }
293
+ .layouts-tab-item[data-tab-drop]::after {
294
+ content: '';
295
+ position: absolute;
296
+ top: 2px;
297
+ bottom: 2px;
298
+ left: 0;
299
+ width: 2px;
300
+ background: var(--layouts-accent, #8bbaaa);
301
+ pointer-events: none;
302
+ }
303
+ .layouts-tab-item[data-tab-drop='after']::after {
304
+ left: auto;
305
+ right: 0;
306
+ }
307
+
308
+ .layouts-picker {
309
+ width: min(340px, calc(100vw - 16px));
310
+ }
311
+ .layouts-picker-search {
312
+ width: 100%;
313
+ padding: 10px;
314
+ border: 1px solid var(--layouts-line, #39414e);
315
+ border-radius: var(--layouts-radius, 5px);
316
+ background: var(--layouts-panel, #1d222a);
317
+ color: var(--layouts-text, #e5e9f0);
318
+ font: inherit;
319
+ }
320
+ .layouts-picker-list {
321
+ max-height: 280px;
322
+ overflow: auto;
323
+ margin-top: 5px;
324
+ }
325
+ .layouts-picker-option {
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 10px;
329
+ padding: 10px;
330
+ cursor: pointer;
331
+ border-radius: var(--layouts-radius, 5px);
332
+ }
333
+ .layouts-picker-option[aria-selected='true'],
334
+ .layouts-picker-option:hover {
335
+ background: color-mix(in srgb, var(--layouts-accent, #8bbaaa) 20%, transparent);
336
+ }
337
+ .layouts-picker-option strong,
338
+ .layouts-picker-option small {
339
+ display: block;
340
+ }
341
+ .layouts-picker-option small,
342
+ .layouts-picker-status {
343
+ color: var(--layouts-muted, #a2acba);
344
+ }
345
+ .layouts-picker-status {
346
+ padding: 10px;
347
+ }
348
+
349
+ /* Consumer-owned scrollbar roles also apply to nested application content. */
350
+ .layouts,
351
+ .layouts * {
352
+ scrollbar-width: thin;
353
+ scrollbar-color: var(--layouts-scrollbar-thumb, var(--layouts-line, #39414e))
354
+ var(--layouts-scrollbar-track, transparent);
355
+ }
356
+ .layouts ::-webkit-scrollbar {
357
+ width: var(--layouts-scrollbar-size, 6px);
358
+ height: var(--layouts-scrollbar-size, 6px);
359
+ }
360
+ .layouts ::-webkit-scrollbar-thumb {
361
+ background: var(--layouts-scrollbar-thumb, var(--layouts-line, #39414e));
362
+ border-radius: var(--layouts-radius, 5px);
363
+ }
364
+ .layouts ::-webkit-scrollbar-track {
365
+ background: var(--layouts-scrollbar-track, transparent);
366
+ }
367
+ .layouts-submenu {
368
+ position: relative;
369
+ }
370
+ .layouts-submenu > button {
371
+ width: 100%;
372
+ text-align: left;
373
+ }
374
+ .layouts-submenu-content {
375
+ display: flex;
376
+ flex-direction: column;
377
+ padding: 4px;
378
+ border: 1px solid var(--layouts-line);
379
+ background: var(--layouts-panel);
380
+ }
381
+ .layouts-menu:has(.layouts-submenu-content:not([hidden])) {
382
+ min-width: 300px;
383
+ }
384
+ .layouts-corner {
385
+ position: absolute;
386
+ display: var(--layouts-corner-handle-display, block);
387
+ width: var(--layouts-corner-handle-size, 8px);
388
+ height: var(--layouts-corner-handle-size, 8px);
389
+ padding: 0;
390
+ border: 0;
391
+ background: transparent;
392
+ z-index: 3;
393
+ touch-action: none;
394
+ cursor: crosshair;
395
+ }
396
+ .layouts-corner::after {
397
+ content: '';
398
+ position: absolute;
399
+ inset: 2px;
400
+ border-style: solid;
401
+ border-width: var(--layouts-corner-handle-border-width, 2px 0 0 2px);
402
+ border-color: var(--layouts-corner-handle-color, var(--layouts-muted));
403
+ border-radius: var(--layouts-corner-handle-radius, 0px);
404
+ background: var(--layouts-corner-handle-fill, transparent);
405
+ opacity: var(--layouts-corner-handle-opacity, 0.35);
406
+ }
407
+ .layouts-corner:hover::after {
408
+ opacity: 1;
409
+ }
410
+ .layouts-corner[data-corner='tl'] {
411
+ top: var(--layouts-corner-handle-inset, 0px);
412
+ left: var(--layouts-corner-handle-inset, 0px);
413
+ }
414
+ .layouts-corner[data-corner='tr'] {
415
+ top: var(--layouts-corner-handle-inset, 0px);
416
+ right: var(--layouts-corner-handle-inset, 0px);
417
+ transform: rotate(90deg);
418
+ }
419
+ .layouts-corner[data-corner='br'] {
420
+ bottom: var(--layouts-corner-handle-inset, 0px);
421
+ right: var(--layouts-corner-handle-inset, 0px);
422
+ transform: rotate(180deg);
423
+ }
424
+ .layouts-corner[data-corner='bl'] {
425
+ bottom: var(--layouts-corner-handle-inset, 0px);
426
+ left: var(--layouts-corner-handle-inset, 0px);
427
+ transform: rotate(270deg);
428
+ }
429
+ .layouts-group[data-corner-preview] {
430
+ outline: 2px solid var(--layouts-accent);
431
+ outline-offset: -2px;
432
+ }
433
+
434
+ .layouts-menu:has(.layouts-submenu) {
435
+ overflow: visible;
436
+ }
437
+ .layouts-submenu-content {
438
+ position: absolute;
439
+ top: 0;
440
+ left: calc(100% - 1px);
441
+ width: 200px;
442
+ box-shadow: 0 8px 24px #0005;
443
+ border-radius: var(--layouts-radius, 5px);
444
+ }
445
+ .layouts-submenu[data-side='left'] .layouts-submenu-content {
446
+ left: auto;
447
+ right: calc(100% - 1px);
448
+ }
449
+ .layouts-menu:has(.layouts-submenu-content:not([hidden])) {
450
+ min-width: 0;
451
+ }
452
+
453
+ .layouts-corner-overlay {
454
+ position: fixed;
455
+ inset: 0;
456
+ pointer-events: none;
457
+ z-index: 100;
458
+ }
459
+ .layouts-corner-preview {
460
+ position: fixed;
461
+ box-sizing: border-box;
462
+ border: 2px solid var(--layouts-accent);
463
+ display: grid;
464
+ place-items: center;
465
+ overflow: hidden;
466
+ }
467
+ .layouts-corner-preview[data-corner-preview='split'],
468
+ .layouts-corner-preview[data-corner-preview='join-target'] {
469
+ background: color-mix(in srgb, var(--layouts-accent) 16%, transparent);
470
+ }
471
+ .layouts-corner-preview[data-corner-preview='join-source'] {
472
+ border-style: dashed;
473
+ background: color-mix(in srgb, var(--layouts-bg) 65%, transparent);
474
+ }
475
+ .layouts-corner-preview[data-corner-preview='join'] {
476
+ border-width: 3px;
477
+ }
478
+ .layouts-corner-label {
479
+ background: var(--layouts-panel);
480
+ color: var(--layouts-text);
481
+ padding: 6px 10px;
482
+ border-radius: var(--layouts-radius);
483
+ text-align: center;
484
+ }
485
+ .layouts-corner-label:empty {
486
+ display: none;
487
+ }
488
+
489
+ /* Themeable density affects chrome only; structural divider gaps stay in layout JSON. */
490
+ .layouts-menu button,
491
+ .layouts-picker button {
492
+ min-height: var(--layouts-control-height, 28px);
493
+ }
494
+ .layouts-menu {
495
+ padding: var(--layouts-spacing, 5px);
496
+ }
497
+
498
+ /* Tapered chrome overlays the pane; only its painted footprint intercepts input. */
499
+ .layouts-group[data-tab-bar='tapered'] > .layouts-header {
500
+ position: absolute;
501
+ inset: 0 auto auto 0;
502
+ z-index: 2;
503
+ max-width: 100%;
504
+ border-bottom-color: transparent;
505
+ }
506
+ .layouts-group[data-tab-bar='tapered'] .layouts-tab-item {
507
+ flex-basis: var(--layouts-tab-preferred-width, 180px);
508
+ }
509
+ .layouts-header > .layouts-button {
510
+ display: inline-flex;
511
+ align-items: center;
512
+ justify-content: center;
513
+ flex-shrink: 0;
514
+ height: 100%;
515
+ padding: 0 7px;
516
+ }
517
+ .layouts-tab-cap {
518
+ position: absolute;
519
+ left: 100%;
520
+ top: 0;
521
+ height: calc(100% + 1px);
522
+ overflow: visible;
523
+ pointer-events: none;
524
+ }
525
+ .layouts-tab-cap path {
526
+ fill: var(--layouts-header, #242a34);
527
+ pointer-events: visiblePainted;
528
+ }
529
+ .layouts-header.layouts-tab-measure {
530
+ position: fixed !important;
531
+ inset: -10000px auto auto -10000px !important;
532
+ width: max-content !important;
533
+ max-width: none !important;
534
+ visibility: hidden;
535
+ pointer-events: none;
536
+ }
537
+ .layouts-tab-measure .layouts-tabs {
538
+ flex: none;
539
+ overflow: visible;
540
+ }
541
+ .layouts-tab-measure .layouts-tab-item {
542
+ container: none;
543
+ flex: none;
544
+ width: max-content;
545
+ min-width: 0;
546
+ max-width: 180px;
547
+ }
548
+ .layouts-tab-measure .layouts-tab-label {
549
+ display: block;
550
+ }
551
+ .layouts-tab-measure .layouts-tab-close {
552
+ display: block;
553
+ }
554
+ .layouts-group[data-tab-bar-filled='false'] .layouts-tab-label {
555
+ display: block;
556
+ }
557
+ .layouts-group[data-tab-bar-filled='false'] .layouts-tab-close {
558
+ display: block;
559
+ }
560
+ .layouts-group[data-tab-bar-filled='false'] .layouts-tab {
561
+ padding: 0 8px;
562
+ justify-content: flex-start;
563
+ }
564
+
565
+ .layouts-tab-measure .layouts-tab {
566
+ flex: none;
567
+ max-width: none;
568
+ }
569
+ .layouts-group[data-tab-bar-filled='false'] .layouts-tab-item {
570
+ flex-shrink: 0;
571
+ }
572
+
573
+ .layouts-tab-outline {
574
+ position: absolute;
575
+ left: 0;
576
+ top: 0;
577
+ z-index: 3;
578
+ pointer-events: none;
579
+ }
580
+ .layouts-tab-outline path {
581
+ fill: none;
582
+ stroke: var(--layouts-line, #39414e);
583
+ stroke-width: 1px;
584
+ vector-effect: non-scaling-stroke;
585
+ pointer-events: none;
586
+ }
587
+
588
+ .layouts-chrome-icon {
589
+ display: inline-block;
590
+ width: var(--layouts-icon-size, 16px);
591
+ height: var(--layouts-icon-size, 16px);
592
+ vertical-align: middle;
593
+ pointer-events: none;
594
+ }
595
+
596
+ /* A reserved left rail keeps pane content clear of icon-only controls. */
597
+ .layouts-group[data-tab-placement='left'] {
598
+ flex-direction: row;
599
+ }
600
+ .layouts-group[data-tab-placement='left'] > .layouts-header {
601
+ position: relative;
602
+ inset: auto;
603
+ width: var(--layouts-header-width, 32px);
604
+ height: 100%;
605
+ flex: 0 0 var(--layouts-header-width, 32px);
606
+ flex-direction: column;
607
+ padding: 4px;
608
+ border-bottom: 0;
609
+ border-right: 1px solid var(--layouts-line, #39414e);
610
+ min-height: 0;
611
+ gap: 4px;
612
+ }
613
+ .layouts-group[data-tab-placement='left'] .layouts-tabs {
614
+ flex-direction: column;
615
+ width: 100%;
616
+ height: auto;
617
+ min-height: 0;
618
+ overflow-x: hidden;
619
+ overflow-y: auto;
620
+ }
621
+ .layouts-group[data-tab-placement='left'] .layouts-tab-item {
622
+ flex: 0 0 30px;
623
+ min-width: 0;
624
+ width: 100%;
625
+ height: 30px;
626
+ }
627
+ .layouts-group[data-tab-placement='left'] .layouts-tab-item .layouts-tab {
628
+ padding: 0;
629
+ justify-content: center;
630
+ border-radius: var(--layouts-radius, 5px);
631
+ }
632
+ .layouts-group[data-tab-placement='left'] .layouts-tab-label,
633
+ .layouts-group[data-tab-placement='left'] .layouts-tab-item > .layouts-tab-close {
634
+ display: none;
635
+ }
636
+ .layouts-group[data-tab-placement='left'] > .layouts-header > .layouts-button {
637
+ flex: 0 0 30px;
638
+ height: 30px;
639
+ width: 100%;
640
+ padding: 0;
641
+ }
642
+ .layouts-group[data-tab-placement='left'] .layouts-tab-item[data-tab-drop]::after {
643
+ inset: 0 2px auto;
644
+ width: auto;
645
+ height: 2px;
646
+ }
647
+ .layouts-group[data-tab-placement='left'] .layouts-tab-item[data-tab-drop='after']::after {
648
+ inset: auto 2px 0;
649
+ }
650
+
651
+ .layouts-group[data-tab-placement='left'][data-tab-bar='tapered'] > .layouts-header {
652
+ position: absolute;
653
+ inset: 0 auto auto 0;
654
+ border-right-color: transparent;
655
+ }
656
+ .layouts-group[data-tab-placement='left'] .layouts-tab-cap {
657
+ left: 0;
658
+ top: 100%;
659
+ }
660
+ .layouts-group[data-tab-placement='left'] > .layouts-header > .layouts-button .layouts-chrome-icon {
661
+ transform: rotate(90deg);
662
+ }
663
+
664
+ .layouts-picker-persistent {
665
+ position: absolute;
666
+ z-index: 3;
667
+ left: 50%;
668
+ top: calc(50% + var(--layouts-header-height, 32px) / 2);
669
+ transform: translate(-50%, -50%);
670
+ width: min(340px, calc(100% - 16px));
671
+ max-height: calc(100% - var(--layouts-header-height, 32px) - 16px);
672
+ box-sizing: border-box;
673
+ }
674
+
675
+ .layouts-tab-tooltip {
676
+ position: fixed;
677
+ z-index: 1000;
678
+ pointer-events: none;
679
+ max-width: min(280px, calc(100vw - 16px));
680
+ padding: 6px 9px;
681
+ border: 1px solid var(--layouts-line, #39414e);
682
+ border-radius: var(--layouts-radius, 5px);
683
+ background: var(--layouts-panel, #1d222a);
684
+ color: var(--layouts-text, #e5e9f0);
685
+ box-shadow: 0 3px 12px #0003;
686
+ font-size: 12px;
687
+ line-height: 1.4;
688
+ overflow-wrap: anywhere;
689
+ }
690
+
691
+ /* Gapless frozen boundaries are decorative and never intercept pane input. */
692
+ [data-frozen-border='horizontal']::after,
693
+ [data-frozen-border='vertical']::after {
694
+ content: '';
695
+ position: absolute;
696
+ z-index: 4;
697
+ pointer-events: none;
698
+ background: var(--layouts-frozen-pane-border, var(--layouts-line, #39414e));
699
+ }
700
+ [data-frozen-border='horizontal']::after {
701
+ left: calc(var(--layouts-frozen-boundary) - 1px);
702
+ top: 0;
703
+ width: 1px;
704
+ height: 100%;
705
+ }
706
+ [data-frozen-border='vertical']::after {
707
+ top: calc(var(--layouts-frozen-boundary) - 1px);
708
+ left: 0;
709
+ height: 1px;
710
+ width: 100%;
711
+ }
712
+
713
+ /* The split owns shared frozen borders; descendants omit those same edges. */
714
+ .layouts-group[data-shared-edges~='top'] {
715
+ --layouts-edge-top: transparent;
716
+ }
717
+ .layouts-group[data-shared-edges~='right'] {
718
+ --layouts-edge-right: transparent;
719
+ }
720
+ .layouts-group[data-shared-edges~='bottom'] {
721
+ --layouts-edge-bottom: transparent;
722
+ }
723
+ .layouts-group[data-shared-edges~='left'] {
724
+ --layouts-edge-left: transparent;
725
+ }
726
+
727
+ .layouts-tabs:empty {
728
+ display: none;
729
+ }
730
+
731
+ .layouts-empty {
732
+ position: absolute;
733
+ inset: 0;
734
+ width: 100%;
735
+ height: 100%;
736
+ padding: 0;
737
+ border: 0;
738
+ border-radius: 0;
739
+ background: transparent;
740
+ color: var(--layouts-line, #39414e);
741
+ cursor: pointer;
742
+ }
743
+ .layouts-empty:disabled {
744
+ cursor: default;
745
+ }
746
+ .layouts-empty:focus-visible {
747
+ outline: 1px solid var(--layouts-accent, #8bbaaa);
748
+ outline-offset: -3px;
749
+ }
750
+ .layouts-empty > svg {
751
+ display: block;
752
+ width: 100%;
753
+ height: 100%;
754
+ pointer-events: none;
755
+ fill: none;
756
+ stroke: currentColor;
757
+ stroke-width: 1;
758
+ }
759
+
760
+ .layouts-picker-search:focus,
761
+ .layouts-picker-search:focus-visible {
762
+ outline: none;
763
+ box-shadow: none;
764
+ }
765
+
766
+ .layouts-group[data-tab-attachment='floating'][data-tab-bar] > .layouts-header {
767
+ position: absolute;
768
+ inset: var(--layouts-floating-inset-y, 8px) auto auto var(--layouts-floating-inset-x, 8px);
769
+ max-width: calc(100% - 2 * var(--layouts-floating-inset-x, 8px));
770
+ max-height: calc(100% - 2 * var(--layouts-floating-inset-y, 8px));
771
+ border-color: transparent;
772
+ box-shadow: inset 0 0 0 1px var(--layouts-line, #39414e);
773
+ }
774
+ .layouts-header[data-tab-bar-shape='rounded'] {
775
+ border-radius: var(--layouts-radius, 5px);
776
+ }
777
+
778
+ .layouts-group[data-tab-attachment='floating'][data-tab-bar]
779
+ > .layouts-header[data-tab-corners='rounded'] {
780
+ border-radius: var(--layouts-radius, 5px);
781
+ box-shadow: inset 0 0 0 1px var(--layouts-line, #39414e);
782
+ }
783
+ .layouts-group[data-tab-attachment='floating'][data-tab-bar]
784
+ > .layouts-header[data-tab-corners='capsule'] {
785
+ border-radius: 999px;
786
+ }
787
+ .layouts-group[data-tab-attachment='floating'][data-tab-bar]
788
+ > .layouts-header[data-tab-corners='fitted'] {
789
+ border-radius: 0;
790
+ }
791
+
792
+ /* Display is independent of orientation; automatic follows each tab's width. */
793
+ @container layouts-tab (min-width: 96px) {
794
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic'] .layouts-tab-label {
795
+ display: block;
796
+ }
797
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic']
798
+ .layouts-tab-item
799
+ .layouts-tab {
800
+ padding: 0 8px;
801
+ justify-content: flex-start;
802
+ }
803
+ }
804
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic'] > .layouts-header {
805
+ min-width: max(32px, calc(var(--layouts-icon-size, 16px) + 10px));
806
+ }
807
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic']
808
+ .layouts-tab-item[data-active='true']:has(> .layouts-tab-close) {
809
+ flex-direction: column;
810
+ flex-basis: 52px;
811
+ height: 52px;
812
+ }
813
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic']
814
+ .layouts-tab-item[data-active='true']:has(> .layouts-tab-close)
815
+ > .layouts-tab {
816
+ flex: 0 0 30px;
817
+ width: 100%;
818
+ }
819
+ .layouts-group[data-tab-placement='left'][data-tab-display='automatic']
820
+ .layouts-tab-item[data-active='true']
821
+ > .layouts-tab-close {
822
+ display: block;
823
+ flex: 0 0 22px;
824
+ height: 22px;
825
+ margin: 0;
826
+ }
827
+ .layouts-group[data-tab-display='compact'] .layouts-tab-label,
828
+ .layouts-group[data-tab-display='compact'] .layouts-tab-close,
829
+ .layouts-header[data-tab-display='compact'] .layouts-tab-label,
830
+ .layouts-header[data-tab-display='compact'] .layouts-tab-close {
831
+ display: none;
832
+ }
833
+ .layouts-group[data-tab-display='compact'] .layouts-tab-item .layouts-tab,
834
+ .layouts-header[data-tab-display='compact'] .layouts-tab-item .layouts-tab {
835
+ justify-content: center;
836
+ padding-inline: 8px;
837
+ }
838
+ .layouts-group[data-tab-placement='top'][data-tab-display='compact'] .layouts-tab-item,
839
+ .layouts-header.layouts-tab-measure[data-tab-display='compact'] .layouts-tab-item {
840
+ flex: 0 0 calc(var(--layouts-icon-size, 16px) + 16px);
841
+ min-width: calc(var(--layouts-icon-size, 16px) + 16px);
842
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "quilt-react",
3
+ "version": "0.1.0",
4
+ "description": "React split panes and tabs with shared DOM chrome and browser window popouts",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/niko-dellic/quilt.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./styles.css": "./dist/styles.css"
24
+ },
25
+ "scripts": {
26
+ "build": "node ../../scripts/build-package.mjs react",
27
+ "prepack": "node ../../scripts/build-package.mjs react --dependencies"
28
+ },
29
+ "dependencies": {
30
+ "quilt-core": "0.1.0",
31
+ "quilt-vanilla": "0.1.0"
32
+ },
33
+ "sideEffects": [
34
+ "**/*.css"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": "^18.3.0 || ^19.0.0",
38
+ "react-dom": "^18.3.0 || ^19.0.0"
39
+ },
40
+ "homepage": "https://github.com/niko-dellic/quilt/tree/main/packages/react#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/niko-dellic/quilt/issues"
43
+ },
44
+ "keywords": [
45
+ "layout",
46
+ "split-pane",
47
+ "tabs",
48
+ "workspace",
49
+ "typescript",
50
+ "react"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ }
56
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,196 @@
1
+ export type { TabBarOptions, TabBarStyle } from 'quilt-vanilla';
2
+ import {
3
+ Component,
4
+ createElement,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useSyncExternalStore,
11
+ } from 'react';
12
+ import type { ComponentType, CSSProperties, ReactNode } from 'react';
13
+ import { createRoot } from 'react-dom/client';
14
+ import { flushSync } from 'react-dom';
15
+ import { mountLayout } from 'quilt-vanilla';
16
+ import type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';
17
+ import type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';
18
+ export type PaneProps = Omit<PaneContext, 'element'>;
19
+ class Boundary extends Component<
20
+ { children: ReactNode; onError: (error: unknown) => void },
21
+ { failed: boolean }
22
+ > {
23
+ override state = { failed: false };
24
+ static getDerivedStateFromError() {
25
+ return { failed: true };
26
+ }
27
+ override componentDidCatch(error: unknown) {
28
+ this.props.onError(error);
29
+ }
30
+ override render() {
31
+ return this.state.failed ? null : this.props.children;
32
+ }
33
+ }
34
+ /** A separate root is created for each pane view, including companion documents. */
35
+ export function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer {
36
+ return (context) => {
37
+ const root = createRoot(context.element);
38
+ let failure: unknown;
39
+ const { element: _element, ...props } = context;
40
+ try {
41
+ flushSync(() =>
42
+ root.render(
43
+ createElement(Boundary, {
44
+ onError: (error) => {
45
+ failure = error;
46
+ },
47
+ children: createElement(component, props),
48
+ }),
49
+ ),
50
+ );
51
+ if (failure) throw failure;
52
+ } catch (error) {
53
+ root.unmount();
54
+ throw error;
55
+ }
56
+ return {
57
+ dispose() {
58
+ root.unmount();
59
+ },
60
+ update(pane) {
61
+ root.render(
62
+ createElement(Boundary, {
63
+ onError: (error) => {
64
+ context.element.textContent = error instanceof Error ? error.message : String(error);
65
+ },
66
+ children: createElement(component, { ...props, pane }),
67
+ }),
68
+ );
69
+ },
70
+ };
71
+ };
72
+ }
73
+ export interface LayoutProps extends Omit<LayoutOptions, 'renderers'> {
74
+ components: Record<string, ComponentType<PaneProps>>;
75
+ className?: string;
76
+ style?: CSSProperties;
77
+ }
78
+ /** Keep store, components, and adapter callbacks stable to avoid remounting the workspace. */
79
+ export const Layout = forwardRef<MountedLayout, LayoutProps>(function Layout(props, ref) {
80
+ const {
81
+ store,
82
+ tabs,
83
+ theme,
84
+ tabBar,
85
+ components,
86
+ getPaneState,
87
+ onError,
88
+ prepareWindow,
89
+ openWindow,
90
+ renderIcon,
91
+ shortcuts,
92
+ className,
93
+ style,
94
+ } = props;
95
+ const host = useRef<HTMLDivElement>(null),
96
+ mounted = useRef<MountedLayout | null>(null);
97
+ const currentTabBar = useRef(tabBar);
98
+ currentTabBar.current = tabBar;
99
+ useLayoutEffect(() => {
100
+ mounted.current?.setTabBar(tabBar ?? {});
101
+ }, [tabBar]);
102
+ const currentTheme = useRef(theme);
103
+ currentTheme.current = theme;
104
+ useLayoutEffect(() => {
105
+ mounted.current?.setTheme(theme ?? {});
106
+ }, [theme]);
107
+ const renderers = useMemo(
108
+ () => Object.fromEntries(Object.entries(components).map(([id, c]) => [id, reactRenderer(c)])),
109
+ [components],
110
+ );
111
+ useImperativeHandle(
112
+ ref,
113
+ () => ({
114
+ setTabBar: (options) => mounted.current?.setTabBar(options),
115
+ setTheme: (theme) => mounted.current?.setTheme(theme),
116
+ popout: (...args) => mounted.current?.popout(...args) ?? false,
117
+ returnPane: (id) => mounted.current?.returnPane(id),
118
+ dispose: () => mounted.current?.dispose(),
119
+ }),
120
+ [],
121
+ );
122
+ useLayoutEffect(() => {
123
+ let cancelled = false;
124
+ let instance: MountedLayout | undefined;
125
+ // Pane roots must mount outside the parent React commit (flushSync is forbidden there).
126
+ queueMicrotask(() => {
127
+ if (cancelled || !host.current) return;
128
+ instance = mountLayout(host.current, {
129
+ store,
130
+ renderers,
131
+ ...(currentTabBar.current ? { tabBar: currentTabBar.current } : {}),
132
+ ...(tabs ? { tabs } : {}),
133
+ ...(currentTheme.current ? { theme: currentTheme.current } : {}),
134
+ ...(getPaneState ? { getPaneState } : {}),
135
+ ...(onError ? { onError } : {}),
136
+ ...(prepareWindow ? { prepareWindow } : {}),
137
+ ...(openWindow ? { openWindow } : {}),
138
+ ...(renderIcon ? { renderIcon } : {}),
139
+ ...(shortcuts !== undefined ? { shortcuts } : {}),
140
+ });
141
+ mounted.current = instance;
142
+ });
143
+ return () => {
144
+ cancelled = true;
145
+ const previous = instance;
146
+ queueMicrotask(() => previous?.dispose());
147
+ if (mounted.current === previous) mounted.current = null;
148
+ };
149
+ }, [
150
+ store,
151
+ renderers,
152
+ tabs,
153
+ getPaneState,
154
+ onError,
155
+ prepareWindow,
156
+ openWindow,
157
+ renderIcon,
158
+ shortcuts,
159
+ ]);
160
+ return (
161
+ <div ref={host} className={className} style={{ width: '100%', height: '100%', ...style }} />
162
+ );
163
+ });
164
+ export function useLayoutSnapshot(store: LayoutStore): LayoutSnapshot {
165
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
166
+ }
167
+ export { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';
168
+ export type {
169
+ AutoCollapse,
170
+ LayoutStoreOptions,
171
+ Json,
172
+ Capability,
173
+ Axis,
174
+ Pane,
175
+ Group,
176
+ Split,
177
+ Node,
178
+ WindowPlacement,
179
+ Popout,
180
+ Layout as LayoutSnapshot,
181
+ Issue,
182
+ CommandOptions,
183
+ Change,
184
+ Bounds,
185
+ JoinOptions,
186
+ } from 'quilt-core';
187
+ export { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';
188
+ export type {
189
+ TabRegistration,
190
+ LayoutTheme,
191
+ LayoutOptions,
192
+ MountedLayout,
193
+ PaneContext,
194
+ PaneView,
195
+ PaneRenderer,
196
+ } from 'quilt-vanilla';