@andrewcyuan/focusgrid 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.
@@ -0,0 +1,318 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region dom/src/controller.ts
3
+ /** Attach the renderer root; pane elements are resolved only when focus is requested. */
4
+ var FocusGridDomController = class {
5
+ root = null;
6
+ setRoot(root) {
7
+ this.root = root;
8
+ }
9
+ focus(paneId) {
10
+ const root = this.root;
11
+ if (!root) return false;
12
+ const pane = [...root.querySelectorAll("[data-pane-id]")].find((element) => element.dataset.paneId === paneId && element.closest(".FocusgridFocusGrid") === root);
13
+ if (!pane) return false;
14
+ const ownsFocus = () => pane.contains(root.ownerDocument.activeElement);
15
+ if (ownsFocus()) return true;
16
+ pane.focus({ preventScroll: true });
17
+ return ownsFocus();
18
+ }
19
+ };
20
+ //#endregion
21
+ //#region dom/src/frame.ts
22
+ function requestFrame(callback) {
23
+ if (typeof globalThis.requestAnimationFrame === "function") return {
24
+ kind: "animation",
25
+ id: globalThis.requestAnimationFrame(() => callback())
26
+ };
27
+ return {
28
+ kind: "timeout",
29
+ id: globalThis.setTimeout(callback, 0)
30
+ };
31
+ }
32
+ function cancelFrame(frame) {
33
+ if (frame.kind === "animation") {
34
+ globalThis.cancelAnimationFrame(frame.id);
35
+ return;
36
+ }
37
+ globalThis.clearTimeout(frame.id);
38
+ }
39
+ //#endregion
40
+ //#region dom/src/pointer-resize.ts
41
+ var PointerResizeController = class {
42
+ controller;
43
+ state = { phase: "idle" };
44
+ onDocumentPointerMove = (event) => {
45
+ this.updateResize(event);
46
+ };
47
+ onDocumentPointerUp = (event) => {
48
+ this.endResize(event);
49
+ };
50
+ onDocumentPointerCancel = (event) => {
51
+ this.endResize(event);
52
+ };
53
+ constructor(controller) {
54
+ this.controller = controller;
55
+ }
56
+ startResize(event, handle, captureTarget) {
57
+ event.preventDefault();
58
+ this.destroy();
59
+ const ownerDocument = this.resolveOwnerDocument(event, captureTarget);
60
+ const pointerCaptureTarget = this.resolveCaptureTarget(captureTarget);
61
+ const startSizes = this.controller.getSplitSizes(handle.splitId);
62
+ if (!startSizes) return;
63
+ this.state = {
64
+ phase: "dragging",
65
+ pointerId: event.pointerId,
66
+ splitId: handle.splitId,
67
+ index: handle.index,
68
+ startX: event.clientX,
69
+ startY: event.clientY,
70
+ direction: handle.direction,
71
+ startSizes,
72
+ ownerDocument,
73
+ captureTarget: pointerCaptureTarget,
74
+ pendingDeltaPx: 0,
75
+ pendingFrame: null
76
+ };
77
+ this.setPointerCapture(pointerCaptureTarget, event.pointerId);
78
+ this.addDocumentListeners(ownerDocument);
79
+ }
80
+ updateResize(event) {
81
+ if (this.state.phase !== "dragging" || this.state.pointerId !== event.pointerId) return;
82
+ const deltaPx = this.state.direction === "horizontal" ? event.clientX - this.state.startX : event.clientY - this.state.startY;
83
+ if (this.state.pendingFrame) {
84
+ this.state = {
85
+ ...this.state,
86
+ pendingDeltaPx: deltaPx
87
+ };
88
+ return;
89
+ }
90
+ const pendingFrame = requestFrame(() => {
91
+ if (this.state.phase !== "dragging") return;
92
+ this.state = {
93
+ ...this.state,
94
+ pendingFrame: null
95
+ };
96
+ this.dispatchPendingResize();
97
+ });
98
+ this.state = {
99
+ ...this.state,
100
+ pendingDeltaPx: deltaPx,
101
+ pendingFrame
102
+ };
103
+ }
104
+ endResize(event) {
105
+ if (this.state.phase !== "dragging" || this.state.pointerId !== event.pointerId) return;
106
+ this.flushPendingFrame();
107
+ this.finishDrag();
108
+ }
109
+ destroy() {
110
+ this.cancelPendingFrame();
111
+ this.finishDrag();
112
+ }
113
+ finishDrag() {
114
+ if (this.state.phase !== "dragging") return;
115
+ const { captureTarget, ownerDocument, pointerId } = this.state;
116
+ this.removeDocumentListeners(ownerDocument);
117
+ this.releasePointerCapture(captureTarget, pointerId);
118
+ this.state = { phase: "idle" };
119
+ }
120
+ flushPendingFrame() {
121
+ if (this.state.phase !== "dragging" || !this.state.pendingFrame) return;
122
+ cancelFrame(this.state.pendingFrame);
123
+ this.state = {
124
+ ...this.state,
125
+ pendingFrame: null
126
+ };
127
+ this.dispatchPendingResize();
128
+ }
129
+ dispatchPendingResize() {
130
+ if (this.state.phase !== "dragging") return;
131
+ this.controller.resizeHandle(this.state.splitId, {
132
+ index: this.state.index,
133
+ deltaPx: this.state.pendingDeltaPx,
134
+ snapshotSizes: this.state.startSizes
135
+ });
136
+ }
137
+ cancelPendingFrame() {
138
+ if (this.state.phase !== "dragging" || !this.state.pendingFrame) return;
139
+ cancelFrame(this.state.pendingFrame);
140
+ this.state = {
141
+ ...this.state,
142
+ pendingFrame: null
143
+ };
144
+ }
145
+ addDocumentListeners(ownerDocument) {
146
+ ownerDocument?.addEventListener("pointermove", this.onDocumentPointerMove);
147
+ ownerDocument?.addEventListener("pointerup", this.onDocumentPointerUp);
148
+ ownerDocument?.addEventListener("pointercancel", this.onDocumentPointerCancel);
149
+ }
150
+ removeDocumentListeners(ownerDocument) {
151
+ ownerDocument?.removeEventListener("pointermove", this.onDocumentPointerMove);
152
+ ownerDocument?.removeEventListener("pointerup", this.onDocumentPointerUp);
153
+ ownerDocument?.removeEventListener("pointercancel", this.onDocumentPointerCancel);
154
+ }
155
+ resolveOwnerDocument(event, captureTarget) {
156
+ if (captureTarget?.ownerDocument) return captureTarget.ownerDocument;
157
+ if (typeof Element !== "undefined" && event.target instanceof Element && event.target.ownerDocument) return event.target.ownerDocument;
158
+ return typeof document === "undefined" ? null : document;
159
+ }
160
+ resolveCaptureTarget(captureTarget) {
161
+ if (!captureTarget) return null;
162
+ if ("setPointerCapture" in captureTarget || "releasePointerCapture" in captureTarget) return captureTarget;
163
+ return null;
164
+ }
165
+ setPointerCapture(target, pointerId) {
166
+ try {
167
+ target?.setPointerCapture?.(pointerId);
168
+ } catch {}
169
+ }
170
+ releasePointerCapture(target, pointerId) {
171
+ if (!target?.releasePointerCapture) return;
172
+ try {
173
+ if (!target.hasPointerCapture || target.hasPointerCapture(pointerId)) target.releasePointerCapture(pointerId);
174
+ } catch {}
175
+ }
176
+ };
177
+ //#endregion
178
+ //#region dom/src/resize-observer.ts
179
+ function observeRootSize(root, controller) {
180
+ const updateSize = ({ width, height }) => {
181
+ controller.setContainerSize(Math.floor(width), Math.floor(height));
182
+ };
183
+ if (typeof ResizeObserver === "undefined") {
184
+ updateSize(root.getBoundingClientRect());
185
+ return () => {};
186
+ }
187
+ const observer = new ResizeObserver(([entry]) => {
188
+ if (entry) updateSize(entry.contentRect);
189
+ });
190
+ observer.observe(root);
191
+ return () => observer.disconnect();
192
+ }
193
+ //#endregion
194
+ //#region dom/src/interactivity.ts
195
+ const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
196
+ "alertdialog",
197
+ "button",
198
+ "checkbox",
199
+ "combobox",
200
+ "dialog",
201
+ "grid",
202
+ "gridcell",
203
+ "link",
204
+ "listbox",
205
+ "menu",
206
+ "menubar",
207
+ "menuitem",
208
+ "menuitemcheckbox",
209
+ "menuitemradio",
210
+ "option",
211
+ "radio",
212
+ "radiogroup",
213
+ "scrollbar",
214
+ "searchbox",
215
+ "slider",
216
+ "spinbutton",
217
+ "switch",
218
+ "tab",
219
+ "tablist",
220
+ "textbox",
221
+ "toolbar",
222
+ "tree",
223
+ "treegrid",
224
+ "treeitem"
225
+ ]);
226
+ function hasInteractiveOwner(target, boundary) {
227
+ let element = target;
228
+ while (element && element !== boundary) {
229
+ if (element instanceof HTMLElement && isInteractiveElement(element)) return true;
230
+ element = element.parentElement;
231
+ }
232
+ return boundary instanceof HTMLElement && isInteractiveElement(boundary);
233
+ }
234
+ function shouldFocusPaneShellForPointer(target, paneShell) {
235
+ return target === paneShell || !hasInteractiveOwner(target, paneShell);
236
+ }
237
+ function isInteractiveElement(element) {
238
+ if (isUnavailableElement(element)) return false;
239
+ const tagName = element.tagName.toLowerCase();
240
+ const role = element.getAttribute("role")?.toLowerCase();
241
+ return element.isContentEditable || element.tabIndex >= 0 || tagName === "a" && element.hasAttribute("href") || tagName === "button" || tagName === "input" || tagName === "select" || tagName === "textarea" || tagName === "dialog" || tagName === "summary" || role !== void 0 && INTERACTIVE_ROLES.has(role);
242
+ }
243
+ function isUnavailableElement(element) {
244
+ let current = element;
245
+ while (current) {
246
+ if (current.hidden || current.hasAttribute("hidden") || current.hasAttribute("inert") || current.getAttribute("aria-hidden") === "true" || current.getAttribute("aria-disabled") === "true" || "disabled" in current && Boolean(current.disabled)) return true;
247
+ const view = current.ownerDocument.defaultView;
248
+ if (view) {
249
+ const style = view.getComputedStyle(current);
250
+ if (style.display === "none" || style.visibility === "hidden") return true;
251
+ }
252
+ current = current.parentElement;
253
+ }
254
+ return false;
255
+ }
256
+ function isEditableTarget(target) {
257
+ if (!(target instanceof HTMLElement)) return false;
258
+ if (target.isContentEditable || target.getAttribute("role")?.toLowerCase() === "textbox") return true;
259
+ if (target.matches("textarea, select")) return !target.matches(":disabled");
260
+ return target instanceof HTMLInputElement && !target.disabled && !target.readOnly && [
261
+ "date",
262
+ "datetime-local",
263
+ "email",
264
+ "file",
265
+ "month",
266
+ "number",
267
+ "password",
268
+ "search",
269
+ "tel",
270
+ "text",
271
+ "time",
272
+ "url",
273
+ "week"
274
+ ].includes(target.type);
275
+ }
276
+ //#endregion
277
+ //#region dom/src/mount.ts
278
+ /** Own native grid interactions; React only attaches and detaches the root. */
279
+ function mountFocusGrid(root, controller, dom) {
280
+ dom.setRoot(root);
281
+ const resize = new PointerResizeController(controller);
282
+ const ownedTarget = (event) => {
283
+ const target = event.target;
284
+ return target instanceof Element && target.closest(".FocusgridFocusGrid") === root ? target : null;
285
+ };
286
+ const onFocus = (event) => {
287
+ const paneId = ownedTarget(event)?.closest("[data-pane-id]")?.dataset.paneId;
288
+ if (paneId) controller.focus(paneId);
289
+ };
290
+ const onPointerDown = (event) => {
291
+ const target = ownedTarget(event);
292
+ if (!target) return;
293
+ const separator = target.closest("[data-resize-handle]");
294
+ if (separator) {
295
+ const handle = controller.getLayout().handles.find((handle) => handle.id === separator.dataset.resizeHandle);
296
+ if (handle) resize.startResize(event, handle, separator);
297
+ return;
298
+ }
299
+ const pane = target.closest("[data-pane-id]");
300
+ if (!pane?.dataset.paneId) return;
301
+ controller.focus(pane.dataset.paneId);
302
+ if (shouldFocusPaneShellForPointer(target, pane)) pane.focus({ preventScroll: true });
303
+ };
304
+ root.addEventListener("focusin", onFocus, true);
305
+ root.addEventListener("pointerdown", onPointerDown);
306
+ const stopObserving = observeRootSize(root, controller);
307
+ return () => {
308
+ root.removeEventListener("focusin", onFocus, true);
309
+ root.removeEventListener("pointerdown", onPointerDown);
310
+ resize.destroy();
311
+ stopObserving();
312
+ dom.setRoot(null);
313
+ };
314
+ }
315
+ //#endregion
316
+ exports.FocusGridDomController = FocusGridDomController;
317
+ exports.isEditableTarget = isEditableTarget;
318
+ exports.mountFocusGrid = mountFocusGrid;
@@ -0,0 +1,177 @@
1
+ //#region core/src/layout/types.d.ts
2
+ type PaneId = string;
3
+ type NodeId = string;
4
+ type Orientation = "horizontal" | "vertical";
5
+ declare const cardinalDirections: readonly ["left", "right", "up", "down"];
6
+ type CardinalDirection = (typeof cardinalDirections)[number];
7
+ declare enum PaneCommandCapability {
8
+ ResizeX = "canResizeX",
9
+ ResizeY = "canResizeY",
10
+ Remove = "canRemove",
11
+ SplitHorizontal = "canSplitHorizontal",
12
+ SplitVertical = "canSplitVertical",
13
+ SwapX = "canSwapX",
14
+ SwapY = "canSwapY",
15
+ Focus = "canFocus"
16
+ }
17
+ type PaneCommandCapabilityInput = Partial<Record<PaneCommandCapability, boolean>>;
18
+ type PaneNode = {
19
+ kind: "pane";
20
+ id: NodeId;
21
+ paneId: PaneId;
22
+ } & PaneCommandCapabilityInput;
23
+ type SplitNode = {
24
+ kind: "split";
25
+ id: NodeId;
26
+ orientation: Orientation;
27
+ children: LayoutNode[];
28
+ sizes: number[];
29
+ lastFocusedChildId?: NodeId;
30
+ };
31
+ type LayoutNode = PaneNode | SplitNode;
32
+ type Rect = {
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ };
38
+ type ComputedPane = {
39
+ paneId: PaneId;
40
+ nodeId: NodeId;
41
+ rect: Rect;
42
+ active: boolean;
43
+ };
44
+ type ComputedHandle = {
45
+ id: string;
46
+ splitId: NodeId;
47
+ index: number;
48
+ rect: Rect;
49
+ direction: Orientation;
50
+ };
51
+ type ComputedNode = {
52
+ kind: "pane";
53
+ pane: ComputedPane;
54
+ } | {
55
+ kind: "split";
56
+ orientation: Orientation;
57
+ children: ComputedNode[];
58
+ handles: ComputedHandle[];
59
+ };
60
+ type ComputedLayout = {
61
+ root: ComputedNode;
62
+ rectByNodeId: Map<NodeId, Rect>;
63
+ panes: ComputedPane[];
64
+ handles: ComputedHandle[];
65
+ };
66
+ type FocusGridControllerState = {
67
+ root: LayoutNode;
68
+ activePaneId: PaneId | null;
69
+ container: {
70
+ width: number;
71
+ height: number;
72
+ };
73
+ };
74
+ //#endregion
75
+ //#region core/src/controller.d.ts
76
+ interface DomController {
77
+ focus(paneId: PaneId): boolean;
78
+ }
79
+ type Listener = (next: FocusGridControllerState, previous: FocusGridControllerState) => void;
80
+ type PaneLayoutChangeEvent = {
81
+ pane: ComputedPane;
82
+ previousPane: ComputedPane;
83
+ controller: FocusGridController;
84
+ };
85
+ type PaneCloseEvent = {
86
+ paneId: PaneId;
87
+ previousPane: ComputedPane;
88
+ controller: FocusGridController;
89
+ };
90
+ type PaneEventHandlers = {
91
+ onPaneLayoutChange?: (event: PaneLayoutChangeEvent) => void;
92
+ onPaneClose?: (event: PaneCloseEvent) => void;
93
+ };
94
+ type PaneDefaults = PaneCommandCapabilityInput;
95
+ type FocusGridControllerProps = FocusGridControllerState & {
96
+ paneDefaults?: PaneDefaults;
97
+ minWidth?: number;
98
+ minHeight?: number;
99
+ directionalFocusOverflow?: boolean;
100
+ };
101
+ type SplitPaneOptions = Partial<Omit<PaneNode, "kind" | "id" | "paneId">> & {
102
+ side: CardinalDirection;
103
+ newPaneId?: PaneId;
104
+ newPaneNodeId?: NodeId;
105
+ splitId?: NodeId;
106
+ preserveActivePane?: boolean;
107
+ };
108
+ type ResizePaneOptions = {
109
+ direction: CardinalDirection;
110
+ deltaPx: number;
111
+ };
112
+ type ResizeHandleOptions = {
113
+ index: number;
114
+ deltaPx: number;
115
+ snapshotSizes?: number[];
116
+ };
117
+ declare class FocusGridController {
118
+ private readonly domController;
119
+ readonly minWidth: number;
120
+ readonly minHeight: number;
121
+ readonly directionalFocusOverflow: boolean;
122
+ private currentState;
123
+ private cachedLayout;
124
+ private readonly paneDefaults;
125
+ private readonly listeners;
126
+ constructor({
127
+ paneDefaults,
128
+ minWidth,
129
+ minHeight,
130
+ directionalFocusOverflow,
131
+ ...state
132
+ }: FocusGridControllerProps, domController: DomController);
133
+ /** Computed render data; stable until the next committed change. */
134
+ getLayout(): ComputedLayout;
135
+ getPane(paneId: PaneId): PaneNode | null;
136
+ getContainerSize(): {
137
+ width: number;
138
+ height: number;
139
+ };
140
+ getSplitSizes(splitId: NodeId): number[] | null;
141
+ splitActive(side: CardinalDirection): PaneId | null;
142
+ removeActive(): boolean;
143
+ focusAdjacent(direction: CardinalDirection): boolean;
144
+ swapAdjacent(direction: CardinalDirection): boolean;
145
+ resizeActive(direction: CardinalDirection, deltaPx: number): boolean;
146
+ split(paneNodeId: NodeId, props: SplitPaneOptions): PaneId | null;
147
+ remove(paneId: PaneId): boolean;
148
+ swap(firstPaneId: PaneId, secondPaneId: PaneId): boolean;
149
+ resize(paneId: PaneId, props: ResizePaneOptions): boolean;
150
+ resizeHandle(splitId: NodeId, props: ResizeHandleOptions): boolean;
151
+ focus(paneId: PaneId): boolean;
152
+ updatePane(paneId: PaneId, patch: Partial<Omit<PaneNode, "kind" | "id" | "paneId">>): boolean;
153
+ setContainerSize(width: number, height: number): boolean;
154
+ private commit;
155
+ subscribePaneEvents({
156
+ onPaneLayoutChange,
157
+ onPaneClose
158
+ }: PaneEventHandlers): () => void;
159
+ subscribe(listener: Listener): () => void;
160
+ }
161
+ //#endregion
162
+ //#region dom/src/controller.d.ts
163
+ /** Attach the renderer root; pane elements are resolved only when focus is requested. */
164
+ declare class FocusGridDomController implements DomController {
165
+ private root;
166
+ setRoot(root: HTMLElement | null): void;
167
+ focus(paneId: PaneId): boolean;
168
+ }
169
+ //#endregion
170
+ //#region dom/src/mount.d.ts
171
+ /** Own native grid interactions; React only attaches and detaches the root. */
172
+ declare function mountFocusGrid(root: HTMLElement, controller: FocusGridController, dom: FocusGridDomController): () => void;
173
+ //#endregion
174
+ //#region dom/src/interactivity.d.ts
175
+ declare function isEditableTarget(target: EventTarget | null): boolean;
176
+ //#endregion
177
+ export { FocusGridDomController, isEditableTarget, mountFocusGrid };
@@ -0,0 +1,177 @@
1
+ //#region core/src/layout/types.d.ts
2
+ type PaneId = string;
3
+ type NodeId = string;
4
+ type Orientation = "horizontal" | "vertical";
5
+ declare const cardinalDirections: readonly ["left", "right", "up", "down"];
6
+ type CardinalDirection = (typeof cardinalDirections)[number];
7
+ declare enum PaneCommandCapability {
8
+ ResizeX = "canResizeX",
9
+ ResizeY = "canResizeY",
10
+ Remove = "canRemove",
11
+ SplitHorizontal = "canSplitHorizontal",
12
+ SplitVertical = "canSplitVertical",
13
+ SwapX = "canSwapX",
14
+ SwapY = "canSwapY",
15
+ Focus = "canFocus"
16
+ }
17
+ type PaneCommandCapabilityInput = Partial<Record<PaneCommandCapability, boolean>>;
18
+ type PaneNode = {
19
+ kind: "pane";
20
+ id: NodeId;
21
+ paneId: PaneId;
22
+ } & PaneCommandCapabilityInput;
23
+ type SplitNode = {
24
+ kind: "split";
25
+ id: NodeId;
26
+ orientation: Orientation;
27
+ children: LayoutNode[];
28
+ sizes: number[];
29
+ lastFocusedChildId?: NodeId;
30
+ };
31
+ type LayoutNode = PaneNode | SplitNode;
32
+ type Rect = {
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ };
38
+ type ComputedPane = {
39
+ paneId: PaneId;
40
+ nodeId: NodeId;
41
+ rect: Rect;
42
+ active: boolean;
43
+ };
44
+ type ComputedHandle = {
45
+ id: string;
46
+ splitId: NodeId;
47
+ index: number;
48
+ rect: Rect;
49
+ direction: Orientation;
50
+ };
51
+ type ComputedNode = {
52
+ kind: "pane";
53
+ pane: ComputedPane;
54
+ } | {
55
+ kind: "split";
56
+ orientation: Orientation;
57
+ children: ComputedNode[];
58
+ handles: ComputedHandle[];
59
+ };
60
+ type ComputedLayout = {
61
+ root: ComputedNode;
62
+ rectByNodeId: Map<NodeId, Rect>;
63
+ panes: ComputedPane[];
64
+ handles: ComputedHandle[];
65
+ };
66
+ type FocusGridControllerState = {
67
+ root: LayoutNode;
68
+ activePaneId: PaneId | null;
69
+ container: {
70
+ width: number;
71
+ height: number;
72
+ };
73
+ };
74
+ //#endregion
75
+ //#region core/src/controller.d.ts
76
+ interface DomController {
77
+ focus(paneId: PaneId): boolean;
78
+ }
79
+ type Listener = (next: FocusGridControllerState, previous: FocusGridControllerState) => void;
80
+ type PaneLayoutChangeEvent = {
81
+ pane: ComputedPane;
82
+ previousPane: ComputedPane;
83
+ controller: FocusGridController;
84
+ };
85
+ type PaneCloseEvent = {
86
+ paneId: PaneId;
87
+ previousPane: ComputedPane;
88
+ controller: FocusGridController;
89
+ };
90
+ type PaneEventHandlers = {
91
+ onPaneLayoutChange?: (event: PaneLayoutChangeEvent) => void;
92
+ onPaneClose?: (event: PaneCloseEvent) => void;
93
+ };
94
+ type PaneDefaults = PaneCommandCapabilityInput;
95
+ type FocusGridControllerProps = FocusGridControllerState & {
96
+ paneDefaults?: PaneDefaults;
97
+ minWidth?: number;
98
+ minHeight?: number;
99
+ directionalFocusOverflow?: boolean;
100
+ };
101
+ type SplitPaneOptions = Partial<Omit<PaneNode, "kind" | "id" | "paneId">> & {
102
+ side: CardinalDirection;
103
+ newPaneId?: PaneId;
104
+ newPaneNodeId?: NodeId;
105
+ splitId?: NodeId;
106
+ preserveActivePane?: boolean;
107
+ };
108
+ type ResizePaneOptions = {
109
+ direction: CardinalDirection;
110
+ deltaPx: number;
111
+ };
112
+ type ResizeHandleOptions = {
113
+ index: number;
114
+ deltaPx: number;
115
+ snapshotSizes?: number[];
116
+ };
117
+ declare class FocusGridController {
118
+ private readonly domController;
119
+ readonly minWidth: number;
120
+ readonly minHeight: number;
121
+ readonly directionalFocusOverflow: boolean;
122
+ private currentState;
123
+ private cachedLayout;
124
+ private readonly paneDefaults;
125
+ private readonly listeners;
126
+ constructor({
127
+ paneDefaults,
128
+ minWidth,
129
+ minHeight,
130
+ directionalFocusOverflow,
131
+ ...state
132
+ }: FocusGridControllerProps, domController: DomController);
133
+ /** Computed render data; stable until the next committed change. */
134
+ getLayout(): ComputedLayout;
135
+ getPane(paneId: PaneId): PaneNode | null;
136
+ getContainerSize(): {
137
+ width: number;
138
+ height: number;
139
+ };
140
+ getSplitSizes(splitId: NodeId): number[] | null;
141
+ splitActive(side: CardinalDirection): PaneId | null;
142
+ removeActive(): boolean;
143
+ focusAdjacent(direction: CardinalDirection): boolean;
144
+ swapAdjacent(direction: CardinalDirection): boolean;
145
+ resizeActive(direction: CardinalDirection, deltaPx: number): boolean;
146
+ split(paneNodeId: NodeId, props: SplitPaneOptions): PaneId | null;
147
+ remove(paneId: PaneId): boolean;
148
+ swap(firstPaneId: PaneId, secondPaneId: PaneId): boolean;
149
+ resize(paneId: PaneId, props: ResizePaneOptions): boolean;
150
+ resizeHandle(splitId: NodeId, props: ResizeHandleOptions): boolean;
151
+ focus(paneId: PaneId): boolean;
152
+ updatePane(paneId: PaneId, patch: Partial<Omit<PaneNode, "kind" | "id" | "paneId">>): boolean;
153
+ setContainerSize(width: number, height: number): boolean;
154
+ private commit;
155
+ subscribePaneEvents({
156
+ onPaneLayoutChange,
157
+ onPaneClose
158
+ }: PaneEventHandlers): () => void;
159
+ subscribe(listener: Listener): () => void;
160
+ }
161
+ //#endregion
162
+ //#region dom/src/controller.d.ts
163
+ /** Attach the renderer root; pane elements are resolved only when focus is requested. */
164
+ declare class FocusGridDomController implements DomController {
165
+ private root;
166
+ setRoot(root: HTMLElement | null): void;
167
+ focus(paneId: PaneId): boolean;
168
+ }
169
+ //#endregion
170
+ //#region dom/src/mount.d.ts
171
+ /** Own native grid interactions; React only attaches and detaches the root. */
172
+ declare function mountFocusGrid(root: HTMLElement, controller: FocusGridController, dom: FocusGridDomController): () => void;
173
+ //#endregion
174
+ //#region dom/src/interactivity.d.ts
175
+ declare function isEditableTarget(target: EventTarget | null): boolean;
176
+ //#endregion
177
+ export { FocusGridDomController, isEditableTarget, mountFocusGrid };