quilt-react 0.1.1 → 0.2.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/README.md CHANGED
@@ -56,8 +56,8 @@ function disposeWorkspace() {
56
56
  }
57
57
  ```
58
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.
59
+ Keep the store stable. Component maps and adapter callbacks can change identity without rebuilding the workspace. Declarative panes inherit application providers through portals. `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, popout/requestClose resolve false; workspace configuration methods throw a clear not-mounted error.
60
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.
61
+ Declarative panes inherit your app's context providers. Only standalone `reactRenderer` creates separate roots requiring explicit provider wrappers. 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
62
 
63
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).
package/dist/index.d.ts CHANGED
@@ -1,20 +1,29 @@
1
- export type { TabBarOptions, TabBarStyle } from 'quilt-vanilla';
2
- import type { ComponentType, CSSProperties } from 'react';
1
+ import type { ComponentType, CSSProperties, RefAttributes, ReactElement } from 'react';
2
+ import { PaneRegistry } from 'quilt-vanilla';
3
3
  import type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';
4
4
  import type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';
5
- export type PaneProps = Omit<PaneContext, 'element'>;
5
+ export type PaneProps<State = unknown> = Omit<PaneContext<State>, 'element'>;
6
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>>;
7
+ export declare function reactRenderer<State = unknown>(component: ComponentType<PaneProps<State>>): PaneRenderer<State>;
8
+ export type LayoutProps<State = unknown> = Omit<LayoutOptions<State>, 'renderers' | 'registry' | 'tabs'> & {
10
9
  className?: string;
11
10
  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>>;
11
+ } & ({
12
+ registry: PaneRegistry<ComponentType<PaneProps<State>>>;
13
+ components?: never;
14
+ tabs?: never;
15
+ } | {
16
+ registry?: undefined;
17
+ components?: Record<string, ComponentType<PaneProps<State>>>;
18
+ tabs?: import('quilt-vanilla').TabRegistry;
19
+ });
20
+ /** State is application-owned and shared by every registered pane in this layout. */
21
+ export declare const Layout: <State = unknown>(props: LayoutProps<State> & RefAttributes<MountedLayout<State>>) => ReactElement | null;
15
22
  export declare function useLayoutSnapshot(store: LayoutStore): LayoutSnapshot;
16
23
  export { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';
17
24
  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
25
  export { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';
19
26
  export type { TabRegistration, LayoutTheme, LayoutOptions, MountedLayout, PaneContext, PaneView, PaneRenderer, } from 'quilt-vanilla';
27
+ export { PaneRegistry, parseWorkspace, dockLayout, themeProperties, defaultMessages, } from 'quilt-vanilla';
28
+ export type { PaneRegistration, WorkspacePreset, Messages, CloseRequest, KeyBinding, TabBarOptions, TabBarStyle, } from 'quilt-vanilla';
20
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAa,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAGlG,OAAO,EAAe,YAAY,EAAE,MAAM,eAAe,CAAC;AAC1D,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,CAAC,KAAK,GAAG,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;AAsB7E,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,KAAK,GAAG,OAAO,EAC3C,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,GACzC,YAAY,CAAC,KAAK,CAAC,CAqCrB;AAmED,MAAM,MAAM,WAAW,CAAC,KAAK,GAAG,OAAO,IAAI,IAAI,CAC7C,aAAa,CAAC,KAAK,CAAC,EACpB,WAAW,GAAG,UAAU,GAAG,MAAM,CAClC,GAAG;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,GAAG,CACE;IAAE,QAAQ,EAAE,YAAY,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAAC,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,GAC7F;IACE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;CAC5C,CACJ,CAAC;AAqIJ,qFAAqF;AACrF,eAAO,MAAM,MAAM,EAAiB,CAAC,KAAK,GAAG,OAAO,EAClD,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,KAC5D,YAAY,GAAG,IAAI,CAAC;AACzB,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;AAEvB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,EACV,eAAe,EACf,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Component, createElement, forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
- import { flushSync } from 'react-dom';
5
- import { mountLayout } from 'quilt-vanilla';
4
+ import { createPortal, flushSync } from 'react-dom';
5
+ import { mountLayout, PaneRegistry } from 'quilt-vanilla';
6
6
  class Boundary extends Component {
7
7
  state = { failed: false };
8
8
  static getDerivedStateFromError() {
@@ -12,7 +12,9 @@ class Boundary extends Component {
12
12
  this.props.onError(error);
13
13
  }
14
14
  render() {
15
- return this.state.failed ? null : this.props.children;
15
+ return this.state.failed
16
+ ? createElement('div', { role: 'alert' }, this.props.fallback ?? 'Pane could not be rendered.')
17
+ : this.props.children;
16
18
  }
17
19
  }
18
20
  /** A separate root is created for each pane view, including companion documents. */
@@ -42,7 +44,7 @@ export function reactRenderer(component) {
42
44
  update(pane) {
43
45
  root.render(createElement(Boundary, {
44
46
  onError: (error) => {
45
- context.element.textContent = error instanceof Error ? error.message : String(error);
47
+ context.reportError(error);
46
48
  },
47
49
  children: createElement(component, { ...props, pane }),
48
50
  }));
@@ -50,73 +52,175 @@ export function reactRenderer(component) {
50
52
  };
51
53
  };
52
54
  }
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
- }), []);
55
+ function Ready({ children, onReady }) {
56
+ useLayoutEffect(onReady, [onReady]);
57
+ return children;
58
+ }
59
+ function createBridge() {
60
+ let sequence = 0;
61
+ let entries = [];
62
+ const listeners = new Set();
63
+ const emit = () => {
64
+ for (const listener of listeners)
65
+ listener();
66
+ };
67
+ const cache = new WeakMap();
68
+ return {
69
+ subscribe: (listener) => {
70
+ listeners.add(listener);
71
+ return () => {
72
+ listeners.delete(listener);
73
+ };
74
+ },
75
+ snapshot: () => entries,
76
+ renderer(component) {
77
+ let renderer = cache.get(component);
78
+ if (renderer)
79
+ return renderer;
80
+ renderer = (context) => {
81
+ let resolve, reject;
82
+ const ready = new Promise((yes, no) => {
83
+ resolve = yes;
84
+ reject = no;
85
+ });
86
+ void ready.catch(() => { });
87
+ const entry = {
88
+ id: ++sequence,
89
+ context,
90
+ component,
91
+ ready: resolve,
92
+ fail: reject,
93
+ };
94
+ entries = [...entries, entry];
95
+ emit();
96
+ return {
97
+ ready,
98
+ update(pane) {
99
+ entry.context = { ...entry.context, pane };
100
+ entries = [...entries];
101
+ emit();
102
+ },
103
+ dispose() {
104
+ entries = entries.filter((value) => value !== entry);
105
+ emit();
106
+ reject(new Error('Pane disposed before readiness'));
107
+ },
108
+ };
109
+ };
110
+ cache.set(component, renderer);
111
+ return renderer;
112
+ },
113
+ };
114
+ }
115
+ const LayoutImpl = forwardRef(function Layout(props, ref) {
116
+ const { store, components, registry, className, style, ...rest } = props;
117
+ if (registry && (components || props.tabs))
118
+ throw new Error('registry cannot be combined with components or tabs');
119
+ const bridge = useMemo(createBridge, [store]);
120
+ const entries = useSyncExternalStore(bridge.subscribe, bridge.snapshot, bridge.snapshot);
121
+ const host = useRef(null);
122
+ const mounted = useRef(null);
123
+ const latest = useRef(props);
124
+ latest.current = props;
125
+ const adapt = () => {
126
+ const current = latest.current;
127
+ const { store: _store, components: map, registry: source, className: _className, style: _style, ...options } = current;
128
+ const complete = Object.fromEntries([
129
+ 'tabs',
130
+ 'theme',
131
+ 'tabBar',
132
+ 'getPaneState',
133
+ 'onError',
134
+ 'prepareWindow',
135
+ 'openWindow',
136
+ 'renderIcon',
137
+ 'shortcuts',
138
+ 'messages',
139
+ 'popouts',
140
+ 'confirmClose',
141
+ 'registry',
142
+ 'renderers',
143
+ ].map((key) => [key, undefined]));
144
+ if (source) {
145
+ const adapted = new PaneRegistry(source.list().map((entry) => ({ ...entry, view: bridge.renderer(entry.view) })));
146
+ return { ...complete, ...options, registry: adapted };
147
+ }
148
+ return {
149
+ ...complete,
150
+ ...options,
151
+ renderers: Object.fromEntries(Object.entries(map ?? {}).map(([key, component]) => [key, bridge.renderer(component)])),
152
+ };
153
+ };
154
+ useImperativeHandle(ref, () => {
155
+ const instance = () => {
156
+ if (!mounted.current)
157
+ throw new Error('Layout is not mounted');
158
+ return mounted.current;
159
+ };
160
+ return {
161
+ setTheme: (theme) => instance().setTheme(theme),
162
+ setTabBar: (options) => instance().setTabBar(options),
163
+ updateOptions: (options) => instance().updateOptions(options),
164
+ refreshTheme: () => instance().refreshTheme(),
165
+ retryPane: (id) => instance().retryPane(id),
166
+ exportWorkspace: () => instance().exportWorkspace(),
167
+ loadWorkspace: (input) => instance().loadWorkspace(input),
168
+ requestClose: (id, kind) => mounted.current?.requestClose(id, kind) ?? Promise.resolve(false),
169
+ popout: (id, placement) => mounted.current?.popout(id, placement) ?? Promise.resolve(false),
170
+ returnPane: (id) => mounted.current?.returnPane(id),
171
+ dispose: () => mounted.current?.dispose(),
172
+ };
173
+ }, []);
75
174
  useLayoutEffect(() => {
76
175
  let cancelled = false;
77
176
  let instance;
78
- // Pane roots must mount outside the parent React commit (flushSync is forbidden there).
79
177
  queueMicrotask(() => {
80
178
  if (cancelled || !host.current)
81
179
  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
- });
180
+ instance = mountLayout(host.current, { store, ...adapt() });
95
181
  mounted.current = instance;
96
182
  });
97
183
  return () => {
98
184
  cancelled = true;
99
- const previous = instance;
100
- queueMicrotask(() => previous?.dispose());
101
- if (mounted.current === previous)
185
+ if (mounted.current === instance)
102
186
  mounted.current = null;
187
+ queueMicrotask(() => instance?.dispose());
103
188
  };
189
+ }, [store, bridge]);
190
+ // Only changes to public options update the DOM adapter; bridge renders don't rebuild chrome.
191
+ useLayoutEffect(() => {
192
+ mounted.current?.updateOptions(adapt());
104
193
  }, [
105
- store,
106
- renderers,
107
- tabs,
108
- getPaneState,
109
- onError,
110
- prepareWindow,
111
- openWindow,
112
- renderIcon,
113
- shortcuts,
194
+ components,
195
+ registry,
196
+ rest.tabs,
197
+ rest.theme,
198
+ rest.tabBar,
199
+ rest.getPaneState,
200
+ rest.onError,
201
+ rest.prepareWindow,
202
+ rest.openWindow,
203
+ rest.renderIcon,
204
+ rest.shortcuts,
205
+ rest.messages,
206
+ rest.popouts,
207
+ rest.confirmClose,
114
208
  ]);
115
- return (_jsx("div", { ref: host, className: className, style: { width: '100%', height: '100%', ...style } }));
209
+ useLayoutEffect(() => registry?.subscribe(() => mounted.current?.updateOptions(adapt())), [registry, bridge]);
210
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: host, className: className, style: { width: '100%', height: '100%', ...style } }), entries.map((entry) => {
211
+ const { element, ...context } = entry.context;
212
+ return createPortal(_jsx(Boundary, { fallback: props.messages?.['Pane could not be rendered.'] ?? 'Pane could not be rendered.', onError: (error) => {
213
+ entry.fail(error);
214
+ entry.context.reportError(error);
215
+ }, children: _jsx(Ready, { onReady: entry.ready, children: createElement(entry.component, context) }) }), element, String(entry.id));
216
+ })] }));
116
217
  });
218
+ /** State is application-owned and shared by every registered pane in this layout. */
219
+ export const Layout = LayoutImpl;
117
220
  export function useLayoutSnapshot(store) {
118
221
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
119
222
  }
120
223
  export { LayoutStore, LayoutError, createLayout, parseLayout, validate } from 'quilt-core';
121
224
  export { TabRegistry, themes, themeFamilies } from 'quilt-vanilla';
225
+ export { PaneRegistry, parseWorkspace, dockLayout, themeProperties, defaultMessages, } from 'quilt-vanilla';
122
226
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { ComponentType, CSSProperties, ReactNode, RefAttributes, ReactElement } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { createPortal, flushSync } from 'react-dom';\nimport { mountLayout, PaneRegistry } from 'quilt-vanilla';\nimport type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';\nimport type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';\nexport type PaneProps<State = unknown> = Omit<PaneContext<State>, 'element'>;\nclass Boundary extends Component<\n { children: ReactNode; onError: (error: unknown) => void; fallback?: string },\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\n ? createElement(\n 'div',\n { role: 'alert' },\n this.props.fallback ?? 'Pane could not be rendered.',\n )\n : this.props.children;\n }\n}\n/** A separate root is created for each pane view, including companion documents. */\nexport function reactRenderer<State = unknown>(\n component: ComponentType<PaneProps<State>>,\n): PaneRenderer<State> {\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.reportError(error);\n },\n children: createElement(component, { ...props, pane }),\n }),\n );\n },\n };\n };\n}\n\ninterface PortalEntry {\n id: number;\n context: PaneContext;\n component: ComponentType<PaneProps>;\n ready: () => void;\n fail: (error: unknown) => void;\n}\nfunction Ready({ children, onReady }: { children: ReactNode; onReady: () => void }) {\n useLayoutEffect(onReady, [onReady]);\n return children;\n}\nfunction createBridge() {\n let sequence = 0;\n let entries: PortalEntry[] = [];\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const listener of listeners) listener();\n };\n const cache = new WeakMap<ComponentType<PaneProps>, PaneRenderer>();\n return {\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n snapshot: () => entries,\n renderer(component: ComponentType<PaneProps>): PaneRenderer {\n let renderer = cache.get(component);\n if (renderer) return renderer;\n renderer = (context) => {\n let resolve!: () => void, reject!: (error: unknown) => void;\n const ready = new Promise<void>((yes, no) => {\n resolve = yes;\n reject = no;\n });\n void ready.catch(() => {});\n const entry: PortalEntry = {\n id: ++sequence,\n context,\n component,\n ready: resolve,\n fail: reject,\n };\n entries = [...entries, entry];\n emit();\n return {\n ready,\n update(pane) {\n entry.context = { ...entry.context, pane };\n entries = [...entries];\n emit();\n },\n dispose() {\n entries = entries.filter((value) => value !== entry);\n emit();\n reject(new Error('Pane disposed before readiness'));\n },\n };\n };\n cache.set(component, renderer);\n return renderer;\n },\n };\n}\nexport type LayoutProps<State = unknown> = Omit<\n LayoutOptions<State>,\n 'renderers' | 'registry' | 'tabs'\n> & {\n className?: string;\n style?: CSSProperties;\n} & (\n | { registry: PaneRegistry<ComponentType<PaneProps<State>>>; components?: never; tabs?: never }\n | {\n registry?: undefined;\n components?: Record<string, ComponentType<PaneProps<State>>>;\n tabs?: import('quilt-vanilla').TabRegistry;\n }\n );\nconst LayoutImpl = forwardRef<MountedLayout, LayoutProps>(function Layout(props, ref) {\n const { store, components, registry, className, style, ...rest } = props;\n if (registry && (components || props.tabs))\n throw new Error('registry cannot be combined with components or tabs');\n const bridge = useMemo(createBridge, [store]);\n const entries = useSyncExternalStore(bridge.subscribe, bridge.snapshot, bridge.snapshot);\n const host = useRef<HTMLDivElement>(null);\n const mounted = useRef<MountedLayout | null>(null);\n const latest = useRef(props);\n latest.current = props;\n const adapt = () => {\n const current = latest.current;\n const {\n store: _store,\n components: map,\n registry: source,\n className: _className,\n style: _style,\n ...options\n } = current;\n const complete = Object.fromEntries(\n [\n 'tabs',\n 'theme',\n 'tabBar',\n 'getPaneState',\n 'onError',\n 'prepareWindow',\n 'openWindow',\n 'renderIcon',\n 'shortcuts',\n 'messages',\n 'popouts',\n 'confirmClose',\n 'registry',\n 'renderers',\n ].map((key) => [key, undefined]),\n );\n if (source) {\n const adapted = new PaneRegistry<PaneRenderer>(\n source.list().map((entry) => ({ ...entry, view: bridge.renderer(entry.view) })),\n );\n return { ...complete, ...options, registry: adapted };\n }\n return {\n ...complete,\n ...options,\n renderers: Object.fromEntries(\n Object.entries(map ?? {}).map(([key, component]) => [key, bridge.renderer(component)]),\n ),\n };\n };\n useImperativeHandle(ref, () => {\n const instance = () => {\n if (!mounted.current) throw new Error('Layout is not mounted');\n return mounted.current;\n };\n return {\n setTheme: (theme) => instance().setTheme(theme),\n setTabBar: (options) => instance().setTabBar(options),\n updateOptions: (options) => instance().updateOptions(options),\n refreshTheme: () => instance().refreshTheme(),\n retryPane: (id) => instance().retryPane(id),\n exportWorkspace: () => instance().exportWorkspace(),\n loadWorkspace: (input) => instance().loadWorkspace(input),\n requestClose: (id, kind) => mounted.current?.requestClose(id, kind) ?? Promise.resolve(false),\n popout: (id, placement) => mounted.current?.popout(id, placement) ?? Promise.resolve(false),\n returnPane: (id) => mounted.current?.returnPane(id),\n dispose: () => mounted.current?.dispose(),\n };\n }, []);\n useLayoutEffect(() => {\n let cancelled = false;\n let instance: MountedLayout | undefined;\n queueMicrotask(() => {\n if (cancelled || !host.current) return;\n instance = mountLayout(host.current, { store, ...adapt() } as LayoutOptions);\n mounted.current = instance;\n });\n return () => {\n cancelled = true;\n if (mounted.current === instance) mounted.current = null;\n queueMicrotask(() => instance?.dispose());\n };\n }, [store, bridge]);\n // Only changes to public options update the DOM adapter; bridge renders don't rebuild chrome.\n useLayoutEffect(() => {\n mounted.current?.updateOptions(adapt());\n }, [\n components,\n registry,\n rest.tabs,\n rest.theme,\n rest.tabBar,\n rest.getPaneState,\n rest.onError,\n rest.prepareWindow,\n rest.openWindow,\n rest.renderIcon,\n rest.shortcuts,\n rest.messages,\n rest.popouts,\n rest.confirmClose,\n ]);\n useLayoutEffect(\n () => registry?.subscribe(() => mounted.current?.updateOptions(adapt())),\n [registry, bridge],\n );\n return (\n <>\n <div ref={host} className={className} style={{ width: '100%', height: '100%', ...style }} />\n {entries.map((entry) => {\n const { element, ...context } = entry.context;\n return createPortal(\n <Boundary\n fallback={\n props.messages?.['Pane could not be rendered.'] ?? 'Pane could not be rendered.'\n }\n onError={(error) => {\n entry.fail(error);\n entry.context.reportError(error);\n }}\n >\n <Ready onReady={entry.ready}>{createElement(entry.component, context)}</Ready>\n </Boundary>,\n element,\n String(entry.id),\n );\n })}\n </>\n );\n});\n/** State is application-owned and shared by every registered pane in this layout. */\nexport const Layout = LayoutImpl as <State = unknown>(\n props: LayoutProps<State> & RefAttributes<MountedLayout<State>>,\n) => ReactElement | null;\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\nexport {\n PaneRegistry,\n parseWorkspace,\n dockLayout,\n themeProperties,\n defaultMessages,\n} from 'quilt-vanilla';\nexport type {\n PaneRegistration,\n WorkspacePreset,\n Messages,\n CloseRequest,\n KeyBinding,\n TabBarOptions,\n TabBarStyle,\n} from 'quilt-vanilla';\n"]}
package/dist/styles.css CHANGED
@@ -10,12 +10,12 @@
10
10
  line-height: 1.45;
11
11
  isolation: isolate;
12
12
  }
13
- .layouts *,
14
- .layouts *::before,
15
- .layouts *::after {
13
+ .layouts [data-layouts-chrome],
14
+ .layouts [data-layouts-chrome]::before,
15
+ .layouts [data-layouts-chrome]::after {
16
16
  box-sizing: border-box;
17
17
  }
18
- .layouts [hidden] {
18
+ .layouts [data-layouts-chrome][hidden] {
19
19
  display: none !important;
20
20
  }
21
21
  .layouts-stage {
@@ -96,7 +96,7 @@
96
96
  opacity: 0.4;
97
97
  cursor: default;
98
98
  }
99
- .layouts :focus-visible {
99
+ .layouts :where([data-layouts-chrome]):focus-visible {
100
100
  outline: 2px solid var(--layouts-focus, #b3decf);
101
101
  outline-offset: -2px;
102
102
  }
@@ -348,20 +348,20 @@
348
348
 
349
349
  /* Consumer-owned scrollbar roles also apply to nested application content. */
350
350
  .layouts,
351
- .layouts * {
351
+ .layouts [data-layouts-chrome] {
352
352
  scrollbar-width: thin;
353
353
  scrollbar-color: var(--layouts-scrollbar-thumb, var(--layouts-line, #39414e))
354
354
  var(--layouts-scrollbar-track, transparent);
355
355
  }
356
- .layouts ::-webkit-scrollbar {
356
+ .layouts [data-layouts-chrome]::-webkit-scrollbar {
357
357
  width: var(--layouts-scrollbar-size, 6px);
358
358
  height: var(--layouts-scrollbar-size, 6px);
359
359
  }
360
- .layouts ::-webkit-scrollbar-thumb {
360
+ .layouts [data-layouts-chrome]::-webkit-scrollbar-thumb {
361
361
  background: var(--layouts-scrollbar-thumb, var(--layouts-line, #39414e));
362
362
  border-radius: var(--layouts-radius, 5px);
363
363
  }
364
- .layouts ::-webkit-scrollbar-track {
364
+ .layouts [data-layouts-chrome]::-webkit-scrollbar-track {
365
365
  background: var(--layouts-scrollbar-track, transparent);
366
366
  }
367
367
  .layouts-submenu {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilt-react",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "React split panes and tabs with shared DOM chrome and browser window popouts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,8 +30,8 @@
30
30
  "prepack": "node ../../scripts/build-package.mjs react --dependencies"
31
31
  },
32
32
  "dependencies": {
33
- "quilt-core": "0.1.1",
34
- "quilt-vanilla": "0.1.1"
33
+ "quilt-core": "0.2.0",
34
+ "quilt-vanilla": "0.2.0"
35
35
  },
36
36
  "sideEffects": [
37
37
  "**/*.css"
package/src/index.tsx CHANGED
@@ -1,4 +1,3 @@
1
- export type { TabBarOptions, TabBarStyle } from 'quilt-vanilla';
2
1
  import {
3
2
  Component,
4
3
  createElement,
@@ -9,15 +8,15 @@ import {
9
8
  useRef,
10
9
  useSyncExternalStore,
11
10
  } from 'react';
12
- import type { ComponentType, CSSProperties, ReactNode } from 'react';
11
+ import type { ComponentType, CSSProperties, ReactNode, RefAttributes, ReactElement } from 'react';
13
12
  import { createRoot } from 'react-dom/client';
14
- import { flushSync } from 'react-dom';
15
- import { mountLayout } from 'quilt-vanilla';
13
+ import { createPortal, flushSync } from 'react-dom';
14
+ import { mountLayout, PaneRegistry } from 'quilt-vanilla';
16
15
  import type { LayoutOptions, MountedLayout, PaneContext, PaneRenderer } from 'quilt-vanilla';
17
16
  import type { Layout as LayoutSnapshot, LayoutStore } from 'quilt-core';
18
- export type PaneProps = Omit<PaneContext, 'element'>;
17
+ export type PaneProps<State = unknown> = Omit<PaneContext<State>, 'element'>;
19
18
  class Boundary extends Component<
20
- { children: ReactNode; onError: (error: unknown) => void },
19
+ { children: ReactNode; onError: (error: unknown) => void; fallback?: string },
21
20
  { failed: boolean }
22
21
  > {
23
22
  override state = { failed: false };
@@ -28,11 +27,19 @@ class Boundary extends Component<
28
27
  this.props.onError(error);
29
28
  }
30
29
  override render() {
31
- return this.state.failed ? null : this.props.children;
30
+ return this.state.failed
31
+ ? createElement(
32
+ 'div',
33
+ { role: 'alert' },
34
+ this.props.fallback ?? 'Pane could not be rendered.',
35
+ )
36
+ : this.props.children;
32
37
  }
33
38
  }
34
39
  /** A separate root is created for each pane view, including companion documents. */
35
- export function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer {
40
+ export function reactRenderer<State = unknown>(
41
+ component: ComponentType<PaneProps<State>>,
42
+ ): PaneRenderer<State> {
36
43
  return (context) => {
37
44
  const root = createRoot(context.element);
38
45
  let failure: unknown;
@@ -61,7 +68,7 @@ export function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer
61
68
  root.render(
62
69
  createElement(Boundary, {
63
70
  onError: (error) => {
64
- context.element.textContent = error instanceof Error ? error.message : String(error);
71
+ context.reportError(error);
65
72
  },
66
73
  children: createElement(component, { ...props, pane }),
67
74
  }),
@@ -70,97 +77,222 @@ export function reactRenderer(component: ComponentType<PaneProps>): PaneRenderer
70
77
  };
71
78
  };
72
79
  }
73
- export interface LayoutProps extends Omit<LayoutOptions, 'renderers'> {
74
- components: Record<string, ComponentType<PaneProps>>;
80
+
81
+ interface PortalEntry {
82
+ id: number;
83
+ context: PaneContext;
84
+ component: ComponentType<PaneProps>;
85
+ ready: () => void;
86
+ fail: (error: unknown) => void;
87
+ }
88
+ function Ready({ children, onReady }: { children: ReactNode; onReady: () => void }) {
89
+ useLayoutEffect(onReady, [onReady]);
90
+ return children;
91
+ }
92
+ function createBridge() {
93
+ let sequence = 0;
94
+ let entries: PortalEntry[] = [];
95
+ const listeners = new Set<() => void>();
96
+ const emit = () => {
97
+ for (const listener of listeners) listener();
98
+ };
99
+ const cache = new WeakMap<ComponentType<PaneProps>, PaneRenderer>();
100
+ return {
101
+ subscribe: (listener: () => void) => {
102
+ listeners.add(listener);
103
+ return () => {
104
+ listeners.delete(listener);
105
+ };
106
+ },
107
+ snapshot: () => entries,
108
+ renderer(component: ComponentType<PaneProps>): PaneRenderer {
109
+ let renderer = cache.get(component);
110
+ if (renderer) return renderer;
111
+ renderer = (context) => {
112
+ let resolve!: () => void, reject!: (error: unknown) => void;
113
+ const ready = new Promise<void>((yes, no) => {
114
+ resolve = yes;
115
+ reject = no;
116
+ });
117
+ void ready.catch(() => {});
118
+ const entry: PortalEntry = {
119
+ id: ++sequence,
120
+ context,
121
+ component,
122
+ ready: resolve,
123
+ fail: reject,
124
+ };
125
+ entries = [...entries, entry];
126
+ emit();
127
+ return {
128
+ ready,
129
+ update(pane) {
130
+ entry.context = { ...entry.context, pane };
131
+ entries = [...entries];
132
+ emit();
133
+ },
134
+ dispose() {
135
+ entries = entries.filter((value) => value !== entry);
136
+ emit();
137
+ reject(new Error('Pane disposed before readiness'));
138
+ },
139
+ };
140
+ };
141
+ cache.set(component, renderer);
142
+ return renderer;
143
+ },
144
+ };
145
+ }
146
+ export type LayoutProps<State = unknown> = Omit<
147
+ LayoutOptions<State>,
148
+ 'renderers' | 'registry' | 'tabs'
149
+ > & {
75
150
  className?: string;
76
151
  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],
152
+ } & (
153
+ | { registry: PaneRegistry<ComponentType<PaneProps<State>>>; components?: never; tabs?: never }
154
+ | {
155
+ registry?: undefined;
156
+ components?: Record<string, ComponentType<PaneProps<State>>>;
157
+ tabs?: import('quilt-vanilla').TabRegistry;
158
+ }
110
159
  );
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,
160
+ const LayoutImpl = forwardRef<MountedLayout, LayoutProps>(function Layout(props, ref) {
161
+ const { store, components, registry, className, style, ...rest } = props;
162
+ if (registry && (components || props.tabs))
163
+ throw new Error('registry cannot be combined with components or tabs');
164
+ const bridge = useMemo(createBridge, [store]);
165
+ const entries = useSyncExternalStore(bridge.subscribe, bridge.snapshot, bridge.snapshot);
166
+ const host = useRef<HTMLDivElement>(null);
167
+ const mounted = useRef<MountedLayout | null>(null);
168
+ const latest = useRef(props);
169
+ latest.current = props;
170
+ const adapt = () => {
171
+ const current = latest.current;
172
+ const {
173
+ store: _store,
174
+ components: map,
175
+ registry: source,
176
+ className: _className,
177
+ style: _style,
178
+ ...options
179
+ } = current;
180
+ const complete = Object.fromEntries(
181
+ [
182
+ 'tabs',
183
+ 'theme',
184
+ 'tabBar',
185
+ 'getPaneState',
186
+ 'onError',
187
+ 'prepareWindow',
188
+ 'openWindow',
189
+ 'renderIcon',
190
+ 'shortcuts',
191
+ 'messages',
192
+ 'popouts',
193
+ 'confirmClose',
194
+ 'registry',
195
+ 'renderers',
196
+ ].map((key) => [key, undefined]),
197
+ );
198
+ if (source) {
199
+ const adapted = new PaneRegistry<PaneRenderer>(
200
+ source.list().map((entry) => ({ ...entry, view: bridge.renderer(entry.view) })),
201
+ );
202
+ return { ...complete, ...options, registry: adapted };
203
+ }
204
+ return {
205
+ ...complete,
206
+ ...options,
207
+ renderers: Object.fromEntries(
208
+ Object.entries(map ?? {}).map(([key, component]) => [key, bridge.renderer(component)]),
209
+ ),
210
+ };
211
+ };
212
+ useImperativeHandle(ref, () => {
213
+ const instance = () => {
214
+ if (!mounted.current) throw new Error('Layout is not mounted');
215
+ return mounted.current;
216
+ };
217
+ return {
218
+ setTheme: (theme) => instance().setTheme(theme),
219
+ setTabBar: (options) => instance().setTabBar(options),
220
+ updateOptions: (options) => instance().updateOptions(options),
221
+ refreshTheme: () => instance().refreshTheme(),
222
+ retryPane: (id) => instance().retryPane(id),
223
+ exportWorkspace: () => instance().exportWorkspace(),
224
+ loadWorkspace: (input) => instance().loadWorkspace(input),
225
+ requestClose: (id, kind) => mounted.current?.requestClose(id, kind) ?? Promise.resolve(false),
226
+ popout: (id, placement) => mounted.current?.popout(id, placement) ?? Promise.resolve(false),
117
227
  returnPane: (id) => mounted.current?.returnPane(id),
118
228
  dispose: () => mounted.current?.dispose(),
119
- }),
120
- [],
121
- );
229
+ };
230
+ }, []);
122
231
  useLayoutEffect(() => {
123
232
  let cancelled = false;
124
233
  let instance: MountedLayout | undefined;
125
- // Pane roots must mount outside the parent React commit (flushSync is forbidden there).
126
234
  queueMicrotask(() => {
127
235
  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
- });
236
+ instance = mountLayout(host.current, { store, ...adapt() } as LayoutOptions);
141
237
  mounted.current = instance;
142
238
  });
143
239
  return () => {
144
240
  cancelled = true;
145
- const previous = instance;
146
- queueMicrotask(() => previous?.dispose());
147
- if (mounted.current === previous) mounted.current = null;
241
+ if (mounted.current === instance) mounted.current = null;
242
+ queueMicrotask(() => instance?.dispose());
148
243
  };
244
+ }, [store, bridge]);
245
+ // Only changes to public options update the DOM adapter; bridge renders don't rebuild chrome.
246
+ useLayoutEffect(() => {
247
+ mounted.current?.updateOptions(adapt());
149
248
  }, [
150
- store,
151
- renderers,
152
- tabs,
153
- getPaneState,
154
- onError,
155
- prepareWindow,
156
- openWindow,
157
- renderIcon,
158
- shortcuts,
249
+ components,
250
+ registry,
251
+ rest.tabs,
252
+ rest.theme,
253
+ rest.tabBar,
254
+ rest.getPaneState,
255
+ rest.onError,
256
+ rest.prepareWindow,
257
+ rest.openWindow,
258
+ rest.renderIcon,
259
+ rest.shortcuts,
260
+ rest.messages,
261
+ rest.popouts,
262
+ rest.confirmClose,
159
263
  ]);
264
+ useLayoutEffect(
265
+ () => registry?.subscribe(() => mounted.current?.updateOptions(adapt())),
266
+ [registry, bridge],
267
+ );
160
268
  return (
161
- <div ref={host} className={className} style={{ width: '100%', height: '100%', ...style }} />
269
+ <>
270
+ <div ref={host} className={className} style={{ width: '100%', height: '100%', ...style }} />
271
+ {entries.map((entry) => {
272
+ const { element, ...context } = entry.context;
273
+ return createPortal(
274
+ <Boundary
275
+ fallback={
276
+ props.messages?.['Pane could not be rendered.'] ?? 'Pane could not be rendered.'
277
+ }
278
+ onError={(error) => {
279
+ entry.fail(error);
280
+ entry.context.reportError(error);
281
+ }}
282
+ >
283
+ <Ready onReady={entry.ready}>{createElement(entry.component, context)}</Ready>
284
+ </Boundary>,
285
+ element,
286
+ String(entry.id),
287
+ );
288
+ })}
289
+ </>
162
290
  );
163
291
  });
292
+ /** State is application-owned and shared by every registered pane in this layout. */
293
+ export const Layout = LayoutImpl as <State = unknown>(
294
+ props: LayoutProps<State> & RefAttributes<MountedLayout<State>>,
295
+ ) => ReactElement | null;
164
296
  export function useLayoutSnapshot(store: LayoutStore): LayoutSnapshot {
165
297
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
166
298
  }
@@ -194,3 +326,20 @@ export type {
194
326
  PaneView,
195
327
  PaneRenderer,
196
328
  } from 'quilt-vanilla';
329
+
330
+ export {
331
+ PaneRegistry,
332
+ parseWorkspace,
333
+ dockLayout,
334
+ themeProperties,
335
+ defaultMessages,
336
+ } from 'quilt-vanilla';
337
+ export type {
338
+ PaneRegistration,
339
+ WorkspacePreset,
340
+ Messages,
341
+ CloseRequest,
342
+ KeyBinding,
343
+ TabBarOptions,
344
+ TabBarStyle,
345
+ } from 'quilt-vanilla';