@andrewcyuan/focusgrid 0.1.0 → 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.
@@ -1,6 +1,8 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
2
+ import { isEditableTarget, mountFocusGrid } from "@andrewcyuan/focusgrid/dom";
2
3
  import { ShortcutScope, useShortcuts } from "@andrewcyuan/shortcut-engine/react";
3
4
  import "@andrewcyuan/shortcut-engine";
5
+ import { FocusGridController } from "@andrewcyuan/focusgrid/core";
4
6
  import { jsx } from "react/jsx-runtime";
5
7
  //#region react/src/FocusgridController.ts
6
8
  const FocusgridController = createContext(null);
@@ -10,1101 +12,6 @@ function useGridControllers() {
10
12
  return controllers;
11
13
  }
12
14
  //#endregion
13
- //#region core/src/utils/ids.ts
14
- let fallbackId = 0;
15
- function createId(prefix = "node") {
16
- const cryptoLike = globalThis.crypto;
17
- if (cryptoLike && "randomUUID" in cryptoLike) return `${prefix}-${cryptoLike.randomUUID()}`;
18
- fallbackId += 1;
19
- return `${prefix}-${fallbackId}`;
20
- }
21
- //#endregion
22
- //#region core/src/layout/geometry.ts
23
- function computeLayout(state) {
24
- const geometry = {
25
- panes: [],
26
- handles: [],
27
- rectByNodeId: /* @__PURE__ */ new Map()
28
- };
29
- const root = computeNode(state.root, {
30
- x: 0,
31
- y: 0,
32
- width: Math.max(0, state.container.width),
33
- height: Math.max(0, state.container.height)
34
- }, geometry, state.activePaneId);
35
- return {
36
- ...geometry,
37
- root
38
- };
39
- }
40
- function normalizeSplitSizes(sizes, expectedLength) {
41
- const fallback = () => Array.from({ length: expectedLength }, () => 1 / expectedLength);
42
- if (sizes.length !== expectedLength) return fallback();
43
- const total = sizes.reduce((sum, size) => sum + Math.max(0, size), 0);
44
- if (total <= 0) return fallback();
45
- return sizes.map((size) => Math.max(0, size) / total);
46
- }
47
- function getMinimumSize(node, direction, minWidth = 0, minHeight = 0) {
48
- if (node.kind === "pane") return direction === "horizontal" ? minWidth : minHeight;
49
- if (node.orientation === direction) return node.children.reduce((sum, child) => sum + getMinimumSize(child, direction, minWidth, minHeight), 0) + Math.max(0, node.children.length - 1) * 3;
50
- return Math.max(0, ...node.children.map((child) => getMinimumSize(child, direction, minWidth, minHeight)));
51
- }
52
- function computeNode(node, rect, geometry, activePaneId) {
53
- geometry.rectByNodeId.set(node.id, rect);
54
- if (node.kind === "pane") {
55
- const pane = {
56
- paneId: node.paneId,
57
- nodeId: node.id,
58
- rect,
59
- active: node.paneId === activePaneId
60
- };
61
- geometry.panes.push(pane);
62
- return {
63
- kind: "pane",
64
- pane
65
- };
66
- }
67
- const sizes = normalizeSplitSizes(node.sizes, node.children.length);
68
- const horizontal = node.orientation === "horizontal";
69
- const axisSize = horizontal ? rect.width : rect.height;
70
- const axisStart = horizontal ? rect.x : rect.y;
71
- const handleTotal = Math.max(0, node.children.length - 1) * 3;
72
- const contentSize = Math.max(0, axisSize - handleTotal);
73
- let cursor = axisStart;
74
- const handles = [];
75
- const children = node.children.map((child, index) => {
76
- const isLast = index === node.children.length - 1;
77
- const childSize = isLast ? axisStart + axisSize - cursor : Math.floor(contentSize * (sizes[index] ?? 0));
78
- const computedChild = computeNode(child, createChildRect(node.orientation, rect, cursor, childSize), geometry, activePaneId);
79
- cursor += childSize;
80
- if (!isLast) {
81
- const handle = {
82
- id: `${node.id}:${index}`,
83
- splitId: node.id,
84
- index,
85
- direction: node.orientation,
86
- rect: createChildRect(node.orientation, rect, cursor, 3)
87
- };
88
- handles.push(handle);
89
- geometry.handles.push(handle);
90
- cursor += 3;
91
- }
92
- return computedChild;
93
- });
94
- return {
95
- kind: "split",
96
- orientation: node.orientation,
97
- children,
98
- handles
99
- };
100
- }
101
- function createChildRect(direction, rect, cursor, size) {
102
- return direction === "horizontal" ? {
103
- x: cursor,
104
- y: rect.y,
105
- width: Math.max(0, size),
106
- height: rect.height
107
- } : {
108
- x: rect.x,
109
- y: cursor,
110
- width: rect.width,
111
- height: Math.max(0, size)
112
- };
113
- }
114
- //#endregion
115
- //#region core/src/layout/resize.ts
116
- function resizeSplit(split, totalPx, index, deltaPx, snapshotSizes, minWidth = 0, minHeight = 0) {
117
- if (totalPx <= 0 || index < 0 || index >= split.children.length - 1) return split;
118
- const baseSizes = normalizeSplitSizes(snapshotSizes ?? split.sizes, split.children.length);
119
- const deltaRatio = deltaPx / totalPx;
120
- const nextSizes = [...baseSizes];
121
- nextSizes[index] += deltaRatio;
122
- nextSizes[index + 1] -= deltaRatio;
123
- const minSizes = split.children.map((child) => getMinimumSize(child, split.orientation, minWidth, minHeight) / totalPx);
124
- const currentSizes = normalizeSplitSizes(split.sizes, split.children.length);
125
- const clamped = clampAdjacentPair(nextSizes, minSizes, index);
126
- const fitted = fitNodeToAxisSize({
127
- ...split,
128
- sizes: clamped
129
- }, split.orientation, totalPx, minWidth, minHeight);
130
- if (fitted.kind !== "split" || sizesEqual(fitted.sizes, currentSizes)) return split;
131
- return fitted;
132
- }
133
- function sizesEqual(a, b) {
134
- if (a.length !== b.length) return false;
135
- return a.every((value, index) => value === b[index]);
136
- }
137
- function fitNodeToAxisSize(node, direction, axisSize, minWidth, minHeight) {
138
- if (node.kind === "pane") return node;
139
- if (node.orientation !== direction) {
140
- let changed = false;
141
- const children = node.children.map((child) => {
142
- const nextChild = fitNodeToAxisSize(child, direction, axisSize, minWidth, minHeight);
143
- changed ||= nextChild !== child;
144
- return nextChild;
145
- });
146
- return changed ? {
147
- ...node,
148
- children
149
- } : node;
150
- }
151
- const sizes = normalizeSplitSizes(node.sizes, node.children.length);
152
- const handleTotal = Math.max(0, node.children.length - 1) * 3;
153
- const contentSize = Math.max(0, axisSize - handleTotal);
154
- const fittedSizes = fitSizesToMinimums(sizes, node.children.map((child) => getMinimumSize(child, direction, minWidth, minHeight)), contentSize);
155
- let changed = !sizesEqual(fittedSizes, sizes);
156
- const children = node.children.map((child, index) => {
157
- const nextChild = fitNodeToAxisSize(child, direction, contentSize * (fittedSizes[index] ?? 0), minWidth, minHeight);
158
- changed ||= nextChild !== child;
159
- return nextChild;
160
- });
161
- return changed ? {
162
- ...node,
163
- children,
164
- sizes: fittedSizes
165
- } : node;
166
- }
167
- function fitSizesToMinimums(sizes, minSizes, contentSize) {
168
- const normalized = normalizeSplitSizes(sizes, sizes.length);
169
- if (contentSize <= 0) return normalized;
170
- if (normalized.map((size) => size * contentSize).every((size, index) => size >= (minSizes[index] ?? 0))) return normalized;
171
- const out = Array.from({ length: normalized.length }, () => 0);
172
- const fixed = Array.from({ length: normalized.length }, () => false);
173
- let remainingSize = contentSize;
174
- let remainingWeight = normalized.reduce((sum, size) => sum + size, 0);
175
- let changed = true;
176
- while (changed) {
177
- changed = false;
178
- for (let index = 0; index < normalized.length; index += 1) {
179
- if (fixed[index]) continue;
180
- const weight = remainingWeight > 0 ? normalized[index] / remainingWeight : 0;
181
- const allocated = remainingSize * weight;
182
- const minSize = minSizes[index] ?? 0;
183
- if (allocated < minSize) {
184
- out[index] = minSize;
185
- fixed[index] = true;
186
- remainingSize -= minSize;
187
- remainingWeight -= normalized[index];
188
- changed = true;
189
- }
190
- }
191
- }
192
- for (let index = 0; index < normalized.length; index += 1) {
193
- if (fixed[index]) continue;
194
- const weight = remainingWeight > 0 ? normalized[index] / remainingWeight : 0;
195
- out[index] = Math.max(0, remainingSize * weight);
196
- }
197
- const total = out.reduce((sum, size) => sum + size, 0);
198
- if (total <= 0) return normalized;
199
- return out.map((size) => size / total);
200
- }
201
- function clampAdjacentPair(sizes, minSizes, index) {
202
- const out = [...sizes];
203
- const pairTotal = out[index] + out[index + 1];
204
- const leftMin = minSizes[index] ?? 0;
205
- const rightMin = minSizes[index + 1] ?? 0;
206
- const maxLeft = Math.max(leftMin, pairTotal - rightMin);
207
- out[index] = Math.min(Math.max(out[index], leftMin), maxLeft);
208
- out[index + 1] = pairTotal - out[index];
209
- return normalizeSplitSizes(out, out.length);
210
- }
211
- //#endregion
212
- //#region core/src/layout/types.ts
213
- let PaneCommandCapability = /* @__PURE__ */ function(PaneCommandCapability) {
214
- PaneCommandCapability["ResizeX"] = "canResizeX";
215
- PaneCommandCapability["ResizeY"] = "canResizeY";
216
- PaneCommandCapability["Remove"] = "canRemove";
217
- PaneCommandCapability["SplitHorizontal"] = "canSplitHorizontal";
218
- PaneCommandCapability["SplitVertical"] = "canSplitVertical";
219
- PaneCommandCapability["SwapX"] = "canSwapX";
220
- PaneCommandCapability["SwapY"] = "canSwapY";
221
- PaneCommandCapability["Focus"] = "canFocus";
222
- return PaneCommandCapability;
223
- }({});
224
- //#endregion
225
- //#region core/src/layout/tree.ts
226
- function buildLayoutIndex(root) {
227
- const index = {
228
- nodeById: /* @__PURE__ */ new Map(),
229
- paneNodeByPaneId: /* @__PURE__ */ new Map(),
230
- parentByNodeId: /* @__PURE__ */ new Map()
231
- };
232
- visit(root, null);
233
- return index;
234
- function visit(node, parent) {
235
- index.nodeById.set(node.id, node);
236
- index.parentByNodeId.set(node.id, parent);
237
- if (node.kind === "pane") {
238
- index.paneNodeByPaneId.set(node.paneId, node);
239
- return;
240
- }
241
- for (const child of node.children) visit(child, node);
242
- }
243
- }
244
- function findPaneNode(root, paneId) {
245
- if (!paneId) return null;
246
- if (root.kind === "pane") return root.paneId === paneId ? root : null;
247
- for (const child of root.children) {
248
- const pane = findPaneNode(child, paneId);
249
- if (pane) return pane;
250
- }
251
- return null;
252
- }
253
- function findSplitNode(root, splitId) {
254
- if (root.kind === "pane") return null;
255
- if (root.id === splitId) return root;
256
- for (const child of root.children) {
257
- const split = findSplitNode(child, splitId);
258
- if (split) return split;
259
- }
260
- return null;
261
- }
262
- function collectPaneIds(root) {
263
- if (root.kind === "pane") return [root.paneId];
264
- return root.children.flatMap(collectPaneIds);
265
- }
266
- function transformLayout(node, transform) {
267
- if (node.kind === "pane") return transform(node);
268
- let childrenChanged = false;
269
- const children = node.children.map((child) => {
270
- const nextChild = transformLayout(child, transform);
271
- childrenChanged ||= nextChild !== child;
272
- return nextChild;
273
- });
274
- return transform(childrenChanged ? {
275
- ...node,
276
- children
277
- } : node);
278
- }
279
- function updatePane(root, paneId, update) {
280
- return transformLayout(root, (node) => node.kind === "pane" && node.paneId === paneId ? update(node) : node);
281
- }
282
- function markFocusedPanePath(root, paneId) {
283
- const index = buildLayoutIndex(root);
284
- const pane = index.paneNodeByPaneId.get(paneId);
285
- if (!pane) return root;
286
- const path = /* @__PURE__ */ new Map();
287
- let childId = pane.id;
288
- let parent = index.parentByNodeId.get(childId);
289
- while (parent) {
290
- path.set(parent.id, childId);
291
- childId = parent.id;
292
- parent = index.parentByNodeId.get(childId);
293
- }
294
- return transformLayout(root, (node) => {
295
- const focusedChildId = path.get(node.id);
296
- return node.kind === "split" && focusedChildId !== void 0 && node.lastFocusedChildId !== focusedChildId ? {
297
- ...node,
298
- lastFocusedChildId: focusedChildId
299
- } : node;
300
- });
301
- }
302
- function removePaneNode(node, paneId) {
303
- if (node.kind === "pane") return node.paneId === paneId ? null : node;
304
- const children = [];
305
- const sizes = [];
306
- for (let i = 0; i < node.children.length; i += 1) {
307
- const child = removePaneNode(node.children[i], paneId);
308
- if (child) {
309
- children.push(child);
310
- sizes.push(node.sizes[i] ?? 1);
311
- }
312
- }
313
- if (children.length === 0) return null;
314
- if (children.length === 1) return children[0];
315
- return {
316
- ...node,
317
- children,
318
- sizes: normalizeSplitSizes(sizes, children.length)
319
- };
320
- }
321
- //#endregion
322
- //#region core/src/layout/spatial.ts
323
- function isHorizontalDirection(direction) {
324
- return direction === "left" || direction === "right";
325
- }
326
- function isRectInDirection(rect, activeRect, direction) {
327
- const epsilon = .001;
328
- if (direction === "right") return rect.x >= activeRect.x + activeRect.width - epsilon;
329
- if (direction === "left") return rect.x + rect.width <= activeRect.x + epsilon;
330
- if (direction === "down") return rect.y >= activeRect.y + activeRect.height - epsilon;
331
- return rect.y + rect.height <= activeRect.y + epsilon;
332
- }
333
- function getEnteringEdge(rect, direction) {
334
- if (direction === "right") return rect.x;
335
- if (direction === "left") return rect.x + rect.width;
336
- if (direction === "down") return rect.y;
337
- return rect.y + rect.height;
338
- }
339
- function compareEnteringEdge(first, second, direction) {
340
- return direction === "left" || direction === "up" ? second - first : first - second;
341
- }
342
- function getPerpendicularCenter(rect, direction) {
343
- const center = getRectCenter(rect);
344
- return isHorizontalDirection(direction) ? center.y : center.x;
345
- }
346
- function getRectCenter(rect) {
347
- return {
348
- x: rect.x + rect.width / 2,
349
- y: rect.y + rect.height / 2
350
- };
351
- }
352
- function getCenterDistance(rect, center) {
353
- const rectCenter = getRectCenter(rect);
354
- return Math.abs(rectCenter.x - center.x) + Math.abs(rectCenter.y - center.y);
355
- }
356
- //#endregion
357
- //#region core/src/layout/navigation.ts
358
- function findPaneInDirection(root, layout, paneId, direction) {
359
- const index = buildLayoutIndex(root);
360
- const paneNode = index.paneNodeByPaneId.get(paneId);
361
- if (!paneNode) return null;
362
- const activePane = layout.panes.find((pane) => pane.paneId === paneId);
363
- if (!activePane) return null;
364
- let currentId = paneNode.id;
365
- let parent = index.parentByNodeId.get(currentId) ?? null;
366
- while (parent) {
367
- const sibling = findDirectionalSibling(parent, currentId, direction);
368
- if (sibling) return findTargetPaneInSubtree(sibling, layout.panes, activePane, direction);
369
- currentId = parent.id;
370
- parent = index.parentByNodeId.get(currentId) ?? null;
371
- }
372
- return null;
373
- }
374
- function findPaneForFocusCommand(root, layout, paneId, direction, overflow) {
375
- const activePane = layout.panes.find((pane) => pane.paneId === paneId);
376
- if (!activePane) return null;
377
- const index = buildLayoutIndex(root);
378
- const candidates = layout.panes.filter((pane) => pane.paneId !== paneId && index.paneNodeByPaneId.get(pane.paneId)?.["canFocus"] !== false);
379
- const target = sortFocusCandidates(candidates.filter((pane) => isRectInDirection(pane.rect, activePane.rect, direction)), activePane, direction)[0]?.paneId ?? null;
380
- if (target || !overflow) return target;
381
- return sortOverflowCandidates(candidates, direction)[0]?.paneId ?? null;
382
- }
383
- function sortFocusCandidates(candidates, activePane, direction) {
384
- const activeCenter = getRectCenter(activePane.rect);
385
- return [...candidates].sort((first, second) => {
386
- const edgeDelta = compareEnteringEdge(getEnteringEdge(first.rect, direction), getEnteringEdge(second.rect, direction), direction);
387
- if (edgeDelta !== 0) return edgeDelta;
388
- const firstPerpendicular = Math.abs(getPerpendicularCenter(first.rect, direction) - getPerpendicularCenter(activePane.rect, direction));
389
- const secondPerpendicular = Math.abs(getPerpendicularCenter(second.rect, direction) - getPerpendicularCenter(activePane.rect, direction));
390
- if (firstPerpendicular !== secondPerpendicular) return firstPerpendicular - secondPerpendicular;
391
- return getCenterDistance(first.rect, activeCenter) - getCenterDistance(second.rect, activeCenter);
392
- });
393
- }
394
- function sortOverflowCandidates(candidates, direction) {
395
- return [...candidates].sort((first, second) => {
396
- if (direction === "right") return first.rect.x - second.rect.x;
397
- if (direction === "left") return second.rect.x + second.rect.width - (first.rect.x + first.rect.width);
398
- if (direction === "down") return first.rect.y - second.rect.y;
399
- return second.rect.y + second.rect.height - (first.rect.y + first.rect.height);
400
- });
401
- }
402
- function findDirectionalSibling(parent, childId, direction) {
403
- const childIndex = parent.children.findIndex((child) => child.id === childId);
404
- if (childIndex === -1) return null;
405
- const horizontal = direction === "left" || direction === "right";
406
- if (horizontal && parent.orientation === "horizontal") {
407
- if (direction === "right") return parent.children[childIndex + 1] ?? null;
408
- return parent.children[childIndex - 1] ?? null;
409
- }
410
- if (!horizontal && parent.orientation === "vertical") {
411
- if (direction === "down") return parent.children[childIndex + 1] ?? null;
412
- return parent.children[childIndex - 1] ?? null;
413
- }
414
- return null;
415
- }
416
- function findTargetPaneInSubtree(subtree, panes, activePane, direction) {
417
- const subtreePaneIds = new Set(collectPaneIds(subtree));
418
- const candidates = panes.filter((pane) => subtreePaneIds.has(pane.paneId));
419
- if (candidates.length === 0) return null;
420
- const activeCenter = getRectCenter(activePane.rect);
421
- return candidates.map((pane) => ({
422
- pane,
423
- edge: getEnteringEdge(pane.rect, direction),
424
- perpendicularDistance: Math.abs(getPerpendicularCenter(pane.rect, direction) - getPerpendicularCenter(activePane.rect, direction)),
425
- focusMemoryRank: getFocusMemoryRank(subtree, pane.paneId),
426
- centerDistance: getCenterDistance(pane.rect, activeCenter)
427
- })).sort((first, second) => compareEnteringEdge(first.edge, second.edge, direction) || first.focusMemoryRank - second.focusMemoryRank || first.perpendicularDistance - second.perpendicularDistance || first.centerDistance - second.centerDistance)[0].pane.paneId;
428
- }
429
- function getFocusMemoryRank(subtree, paneId) {
430
- if (subtree.kind === "pane") return Number.POSITIVE_INFINITY;
431
- const rememberedChild = subtree.children.find((child) => child.id === subtree.lastFocusedChildId);
432
- if (!rememberedChild || !collectPaneIds(rememberedChild).includes(paneId)) return Number.POSITIVE_INFINITY;
433
- if (rememberedChild.kind === "pane") return 0;
434
- const nestedRank = getFocusMemoryRank(rememberedChild, paneId);
435
- return Number.isFinite(nestedRank) ? nestedRank : 1;
436
- }
437
- function resolvePaneResizeBoundary(index, nodeId, direction) {
438
- let currentId = nodeId;
439
- let parent = index.parentByNodeId.get(currentId) ?? null;
440
- while (parent) {
441
- const childIndex = parent.children.findIndex((child) => child.id === currentId);
442
- if (childIndex === -1) return null;
443
- if (isHorizontalDirection(direction) === (parent.orientation === "horizontal")) {
444
- const boundaryIndex = childIndex > 0 ? childIndex - 1 : childIndex < parent.children.length - 1 ? childIndex : null;
445
- if (boundaryIndex !== null) return {
446
- splitId: parent.id,
447
- index: boundaryIndex,
448
- deltaPxSign: direction === "right" || direction === "down" ? 1 : -1
449
- };
450
- }
451
- currentId = parent.id;
452
- parent = index.parentByNodeId.get(currentId) ?? null;
453
- }
454
- return null;
455
- }
456
- //#endregion
457
- //#region core/src/validation.ts
458
- var FocusGridStateValidationException = class extends Error {
459
- errors;
460
- constructor(errors) {
461
- const details = errors.slice(0, 3).map((error) => `${error.path}: ${error.message}`).join("; ");
462
- const suffix = errors.length > 3 ? `; ${errors.length - 3} more` : "";
463
- super(details ? `Invalid FocusGrid controller state: ${details}${suffix}` : "Invalid FocusGrid controller state.");
464
- this.errors = errors;
465
- this.name = "FocusGridStateValidationException";
466
- }
467
- };
468
- function assertValidFocusGridControllerState(input) {
469
- const result = validateFocusGridControllerState(input);
470
- if (!result.ok) throw new FocusGridStateValidationException(result.errors);
471
- }
472
- function validateFocusGridControllerState(input) {
473
- const errors = [];
474
- const nodeIds = /* @__PURE__ */ new Set();
475
- const paneIds = /* @__PURE__ */ new Set();
476
- function add(code, path, message, meta = {}) {
477
- errors.push({
478
- code,
479
- path,
480
- message,
481
- ...meta
482
- });
483
- }
484
- function fields(value, path, allowed, meta = {}) {
485
- for (const field of Object.keys(value)) if (!allowed.has(field)) add("unknown-field", `${path}.${field}`, `Unknown field "${field}".`, meta);
486
- }
487
- function size(value, path, label, meta = {}) {
488
- if (!isSize(value)) add("invalid-number", path, `${label} must be a finite, non-negative number.`, meta);
489
- }
490
- function id(value, seen, kind, path, meta) {
491
- const label = kind === "pane" ? "Pane" : "Node";
492
- if (typeof value !== "string" || value.length === 0) add(`invalid-${kind}-id`, path, `${label} id must be a non-empty string.`, meta);
493
- else if (seen.has(value)) add(`duplicate-${kind}-id`, path, `Duplicate ${kind} id "${value}".`, {
494
- ...meta,
495
- [kind === "pane" ? "paneId" : "nodeId"]: value
496
- });
497
- else seen.add(value);
498
- }
499
- function node(value, path) {
500
- if (!isRecord(value)) {
501
- add("invalid-node", path, "Layout node must be an object.");
502
- return;
503
- }
504
- const meta = {
505
- nodeId: typeof value.id === "string" ? value.id : void 0,
506
- paneId: typeof value.paneId === "string" ? value.paneId : void 0
507
- };
508
- if (value.kind !== "pane" && value.kind !== "split") {
509
- add("invalid-node-kind", `${path}.kind`, "Layout node kind must be \"pane\" or \"split\".", meta);
510
- return;
511
- }
512
- fields(value, path, value.kind === "pane" ? PANE_FIELDS : SPLIT_FIELDS, meta);
513
- id(value.id, nodeIds, "node", `${path}.id`, meta);
514
- if (value.kind === "pane") {
515
- id(value.paneId, paneIds, "pane", `${path}.paneId`, meta);
516
- for (const key of Object.values(PaneCommandCapability)) if (key in value && typeof value[key] !== "boolean") add("invalid-capability", `${path}.${key}`, `${key} must be a boolean when provided.`, meta);
517
- return;
518
- }
519
- if (value.orientation !== "horizontal" && value.orientation !== "vertical") add("invalid-direction", `${path}.orientation`, "Split orientation must be \"horizontal\" or \"vertical\".", meta);
520
- if (!Array.isArray(value.children)) add("invalid-children", `${path}.children`, "Split children must be an array.", meta);
521
- else {
522
- if (value.children.length !== 2) add("non-binary-split", `${path}.children`, "Split nodes must have exactly two children.", meta);
523
- value.children.forEach((child, index) => node(child, `${path}.children[${index}]`));
524
- }
525
- if (!Array.isArray(value.sizes)) add("invalid-sizes", `${path}.sizes`, "Split sizes must be an array.", meta);
526
- else {
527
- if (value.sizes.length !== 2) add("invalid-sizes", `${path}.sizes`, "Split sizes must contain exactly two values.", meta);
528
- value.sizes.forEach((entry, index) => {
529
- if (!isSize(entry)) add("invalid-size", `${path}.sizes[${index}]`, "Split sizes must be finite, non-negative numbers.", meta);
530
- });
531
- }
532
- if (value.lastFocusedChildId !== void 0 && typeof value.lastFocusedChildId !== "string") add("invalid-last-focused-child", `${path}.lastFocusedChildId`, "lastFocusedChildId must be a node id string when provided.", meta);
533
- if (typeof value.lastFocusedChildId === "string" && Array.isArray(value.children) && !value.children.some((child) => isRecord(child) && child.id === value.lastFocusedChildId)) add("invalid-last-focused-child", `${path}.lastFocusedChildId`, "lastFocusedChildId must reference one of the split's direct children.", {
534
- ...meta,
535
- nodeId: value.lastFocusedChildId
536
- });
537
- }
538
- if (!isRecord(input)) add("invalid-state", "$", "State must be an object.");
539
- else {
540
- fields(input, "$", STATE_FIELDS);
541
- for (const key of STATE_FIELDS) if (!(key in input)) add("missing-field", `$.${key}`, `${key} is required.`);
542
- if ("root" in input) node(input.root, "$.root");
543
- if ("activePaneId" in input) {
544
- if (input.activePaneId !== null && typeof input.activePaneId !== "string") add("invalid-active-pane", "$.activePaneId", "State must use a pane id string or null for activePaneId.");
545
- else if (typeof input.activePaneId === "string" && !paneIds.has(input.activePaneId)) add("unknown-active-pane", "$.activePaneId", `activePaneId "${input.activePaneId}" does not reference a pane in root.`, { paneId: input.activePaneId });
546
- }
547
- if ("container" in input) if (!isRecord(input.container)) add("invalid-container", "$.container", "Container must be an object.");
548
- else {
549
- fields(input.container, "$.container", CONTAINER_FIELDS);
550
- for (const key of CONTAINER_FIELDS) if (!(key in input.container)) add("missing-field", `$.container.${key}`, `${key} is required.`);
551
- else size(input.container[key], `$.container.${key}`, `container ${key}`);
552
- }
553
- }
554
- return errors.length ? {
555
- ok: false,
556
- errors
557
- } : {
558
- ok: true,
559
- state: input,
560
- errors: []
561
- };
562
- }
563
- function isRecord(input) {
564
- return typeof input === "object" && input !== null && !Array.isArray(input);
565
- }
566
- function isSize(input) {
567
- return typeof input === "number" && Number.isFinite(input) && input >= 0;
568
- }
569
- const STATE_FIELDS = /* @__PURE__ */ new Set([
570
- "root",
571
- "activePaneId",
572
- "container"
573
- ]);
574
- const CONTAINER_FIELDS = /* @__PURE__ */ new Set(["width", "height"]);
575
- const PANE_FIELDS = /* @__PURE__ */ new Set([
576
- "kind",
577
- "id",
578
- "paneId",
579
- ...Object.values(PaneCommandCapability)
580
- ]);
581
- const SPLIT_FIELDS = /* @__PURE__ */ new Set([
582
- "kind",
583
- "id",
584
- "orientation",
585
- "children",
586
- "sizes",
587
- "lastFocusedChildId"
588
- ]);
589
- //#endregion
590
- //#region core/src/controller.ts
591
- var FocusGridController = class {
592
- domController;
593
- minWidth;
594
- minHeight;
595
- directionalFocusOverflow;
596
- currentState;
597
- cachedLayout = null;
598
- paneDefaults;
599
- listeners = /* @__PURE__ */ new Set();
600
- constructor({ paneDefaults = {}, minWidth = 0, minHeight = 0, directionalFocusOverflow = false, ...state }, domController) {
601
- this.domController = domController;
602
- assertValidFocusGridControllerState(state);
603
- if (![minWidth, minHeight].every((value) => Number.isFinite(value) && value >= 0)) throw new RangeError("Minimum pane dimensions must be finite, non-negative numbers.");
604
- this.minWidth = minWidth;
605
- this.minHeight = minHeight;
606
- this.paneDefaults = paneDefaults;
607
- this.directionalFocusOverflow = directionalFocusOverflow;
608
- const root = transformLayout(state.root, (node) => node.kind === "pane" ? withPaneDefaults(node, paneDefaults) : node);
609
- this.currentState = root === state.root ? state : {
610
- ...state,
611
- root
612
- };
613
- }
614
- /** Computed render data; stable until the next committed change. */
615
- getLayout() {
616
- return this.cachedLayout ??= computeLayout(this.currentState);
617
- }
618
- getPane(paneId) {
619
- const pane = findPaneNode(this.currentState.root, paneId);
620
- return pane ? { ...pane } : null;
621
- }
622
- getContainerSize() {
623
- return { ...this.currentState.container };
624
- }
625
- getSplitSizes(splitId) {
626
- const split = findSplitNode(this.currentState.root, splitId);
627
- return split ? [...split.sizes] : null;
628
- }
629
- splitActive(side) {
630
- const pane = findPaneNode(this.currentState.root, this.currentState.activePaneId);
631
- const capability = isHorizontalDirection(side) ? "canSplitHorizontal" : "canSplitVertical";
632
- return pane && pane[capability] !== false ? this.split(pane.id, { side }) : null;
633
- }
634
- removeActive() {
635
- const pane = findPaneNode(this.currentState.root, this.currentState.activePaneId);
636
- return pane && pane["canRemove"] !== false ? this.remove(pane.paneId) : false;
637
- }
638
- focusAdjacent(direction) {
639
- const state = this.currentState;
640
- if (!state.activePaneId) return false;
641
- const target = findPaneForFocusCommand(state.root, this.getLayout(), state.activePaneId, direction, this.directionalFocusOverflow);
642
- if (!target || !this.domController.focus(target)) return false;
643
- this.focus(target);
644
- return true;
645
- }
646
- swapAdjacent(direction) {
647
- const state = this.currentState;
648
- const active = findPaneNode(state.root, state.activePaneId);
649
- if (!active) return false;
650
- const targetId = findPaneInDirection(state.root, this.getLayout(), active.paneId, direction);
651
- const target = findPaneNode(state.root, targetId);
652
- const capability = isHorizontalDirection(direction) ? "canSwapX" : "canSwapY";
653
- return target && active[capability] !== false && target[capability] !== false ? this.swap(active.paneId, target.paneId) : false;
654
- }
655
- resizeActive(direction, deltaPx) {
656
- const pane = findPaneNode(this.currentState.root, this.currentState.activePaneId);
657
- const capability = isHorizontalDirection(direction) ? "canResizeX" : "canResizeY";
658
- return pane && pane[capability] !== false ? this.resize(pane.paneId, {
659
- direction,
660
- deltaPx
661
- }) : false;
662
- }
663
- split(paneNodeId, props) {
664
- const state = this.currentState;
665
- const index = buildLayoutIndex(state.root);
666
- const target = index.nodeById.get(paneNodeId);
667
- if (target?.kind !== "pane") return null;
668
- const paneId = props.newPaneId ?? createId("pane");
669
- const id = props.newPaneNodeId ?? createId("node");
670
- const splitId = props.splitId ?? createId("split");
671
- if (index.paneNodeByPaneId.has(paneId) || index.nodeById.has(id) || index.nodeById.has(splitId) || id === splitId) return null;
672
- const { side, newPaneId, newPaneNodeId, splitId: suppliedSplitId, preserveActivePane, ...paneProps } = props;
673
- const pane = withPaneDefaults({
674
- kind: "pane",
675
- id,
676
- paneId,
677
- ...paneProps
678
- }, this.paneDefaults);
679
- const root = updatePane(state.root, target.paneId, () => ({
680
- kind: "split",
681
- id: splitId,
682
- orientation: isHorizontalDirection(side) ? "horizontal" : "vertical",
683
- children: side === "left" || side === "up" ? [pane, target] : [target, pane],
684
- sizes: [.5, .5]
685
- }));
686
- const activePaneId = preserveActivePane ? state.activePaneId : paneId;
687
- this.commit({
688
- root: activePaneId ? markFocusedPanePath(root, activePaneId) : root,
689
- activePaneId
690
- });
691
- return paneId;
692
- }
693
- remove(paneId) {
694
- const state = this.currentState;
695
- const panes = collectPaneIds(state.root);
696
- if (!panes.includes(paneId) || panes.length <= 1) return false;
697
- const root = removePaneNode(state.root, paneId);
698
- const activePaneId = state.activePaneId === paneId ? collectPaneIds(root)[0] : state.activePaneId;
699
- return this.commit({
700
- root: activePaneId ? markFocusedPanePath(root, activePaneId) : root,
701
- activePaneId
702
- });
703
- }
704
- swap(firstPaneId, secondPaneId) {
705
- if (firstPaneId === secondPaneId) return false;
706
- const state = this.currentState;
707
- const index = buildLayoutIndex(state.root);
708
- const first = index.paneNodeByPaneId.get(firstPaneId);
709
- const second = index.paneNodeByPaneId.get(secondPaneId);
710
- if (!first || !second) return false;
711
- const root = transformLayout(state.root, (node) => {
712
- if (node.id === first.id) return {
713
- ...second,
714
- id: node.id
715
- };
716
- if (node.id === second.id) return {
717
- ...first,
718
- id: node.id
719
- };
720
- return node;
721
- });
722
- return this.commit({ root: state.activePaneId ? markFocusedPanePath(root, state.activePaneId) : root });
723
- }
724
- resize(paneId, props) {
725
- const index = buildLayoutIndex(this.currentState.root);
726
- const pane = index.paneNodeByPaneId.get(paneId);
727
- if (!pane) return false;
728
- const boundary = resolvePaneResizeBoundary(index, pane.id, props.direction);
729
- return boundary ? this.resizeHandle(boundary.splitId, {
730
- index: boundary.index,
731
- deltaPx: boundary.deltaPxSign * props.deltaPx
732
- }) : false;
733
- }
734
- resizeHandle(splitId, props) {
735
- const state = this.currentState;
736
- const rect = this.getLayout().rectByNodeId.get(splitId);
737
- if (!rect) return false;
738
- const root = transformLayout(state.root, (node) => node.kind === "split" && node.id === splitId ? resizeSplit(node, node.orientation === "horizontal" ? rect.width : rect.height, props.index, props.deltaPx, props.snapshotSizes, this.minWidth, this.minHeight) : node);
739
- return this.commit({ root });
740
- }
741
- focus(paneId) {
742
- const state = this.currentState;
743
- if (!findPaneNode(state.root, paneId)) return false;
744
- const root = markFocusedPanePath(state.root, paneId);
745
- return this.commit({
746
- root,
747
- activePaneId: paneId
748
- });
749
- }
750
- updatePane(paneId, patch) {
751
- const root = updatePane(this.currentState.root, paneId, (pane) => Object.entries(patch).every(([key, value]) => Object.is(pane[key], value)) ? pane : Object.fromEntries(Object.entries({
752
- ...pane,
753
- ...patch
754
- }).filter(([, value]) => value !== void 0)));
755
- return this.commit({ root });
756
- }
757
- setContainerSize(width, height) {
758
- if (this.currentState.container.width === width && this.currentState.container.height === height) return false;
759
- return this.commit({ container: {
760
- width,
761
- height
762
- } });
763
- }
764
- commit(patch) {
765
- const previous = this.currentState;
766
- if (Object.entries(patch).every(([key, value]) => Object.is(previous[key], value))) return false;
767
- const next = {
768
- ...previous,
769
- ...patch
770
- };
771
- this.currentState = next;
772
- this.cachedLayout = null;
773
- for (const listener of this.listeners) listener(next, previous);
774
- return true;
775
- }
776
- subscribePaneEvents({ onPaneLayoutChange, onPaneClose }) {
777
- if (!onPaneLayoutChange && !onPaneClose) return () => {};
778
- return this.subscribe((next, previous) => {
779
- const previousPanes = new Map(computeLayout(previous).panes.map((pane) => [pane.paneId, pane]));
780
- const nextPanes = (next === this.currentState ? this.getLayout() : computeLayout(next)).panes;
781
- for (const pane of nextPanes) {
782
- const previousPane = previousPanes.get(pane.paneId);
783
- previousPanes.delete(pane.paneId);
784
- if (previousPane && (pane.rect.x !== previousPane.rect.x || pane.rect.y !== previousPane.rect.y || pane.rect.width !== previousPane.rect.width || pane.rect.height !== previousPane.rect.height)) onPaneLayoutChange?.({
785
- pane,
786
- previousPane,
787
- controller: this
788
- });
789
- }
790
- for (const previousPane of previousPanes.values()) onPaneClose?.({
791
- paneId: previousPane.paneId,
792
- previousPane,
793
- controller: this
794
- });
795
- });
796
- }
797
- subscribe(listener) {
798
- this.listeners.add(listener);
799
- return () => {
800
- this.listeners.delete(listener);
801
- };
802
- }
803
- };
804
- function withPaneDefaults(pane, defaults) {
805
- const definedPane = Object.fromEntries(Object.entries(pane).filter(([, value]) => value !== void 0));
806
- const next = Object.fromEntries(Object.entries({
807
- ...defaults,
808
- ...definedPane
809
- }).filter(([, value]) => value !== void 0));
810
- return Object.keys(next).length === Object.keys(pane).length && Object.entries(next).every(([key, value]) => Object.is(pane[key], value)) ? pane : next;
811
- }
812
- //#endregion
813
- //#region dom/src/frame.ts
814
- function requestFrame(callback) {
815
- if (typeof globalThis.requestAnimationFrame === "function") return {
816
- kind: "animation",
817
- id: globalThis.requestAnimationFrame(() => callback())
818
- };
819
- return {
820
- kind: "timeout",
821
- id: globalThis.setTimeout(callback, 0)
822
- };
823
- }
824
- function cancelFrame(frame) {
825
- if (frame.kind === "animation") {
826
- globalThis.cancelAnimationFrame(frame.id);
827
- return;
828
- }
829
- globalThis.clearTimeout(frame.id);
830
- }
831
- //#endregion
832
- //#region dom/src/pointer-resize.ts
833
- var PointerResizeController = class {
834
- controller;
835
- state = { phase: "idle" };
836
- onDocumentPointerMove = (event) => {
837
- this.updateResize(event);
838
- };
839
- onDocumentPointerUp = (event) => {
840
- this.endResize(event);
841
- };
842
- onDocumentPointerCancel = (event) => {
843
- this.endResize(event);
844
- };
845
- constructor(controller) {
846
- this.controller = controller;
847
- }
848
- startResize(event, handle, captureTarget) {
849
- event.preventDefault();
850
- this.destroy();
851
- const ownerDocument = this.resolveOwnerDocument(event, captureTarget);
852
- const pointerCaptureTarget = this.resolveCaptureTarget(captureTarget);
853
- const startSizes = this.controller.getSplitSizes(handle.splitId);
854
- if (!startSizes) return;
855
- this.state = {
856
- phase: "dragging",
857
- pointerId: event.pointerId,
858
- splitId: handle.splitId,
859
- index: handle.index,
860
- startX: event.clientX,
861
- startY: event.clientY,
862
- direction: handle.direction,
863
- startSizes,
864
- ownerDocument,
865
- captureTarget: pointerCaptureTarget,
866
- pendingDeltaPx: 0,
867
- pendingFrame: null
868
- };
869
- this.setPointerCapture(pointerCaptureTarget, event.pointerId);
870
- this.addDocumentListeners(ownerDocument);
871
- }
872
- updateResize(event) {
873
- if (this.state.phase !== "dragging" || this.state.pointerId !== event.pointerId) return;
874
- const deltaPx = this.state.direction === "horizontal" ? event.clientX - this.state.startX : event.clientY - this.state.startY;
875
- if (this.state.pendingFrame) {
876
- this.state = {
877
- ...this.state,
878
- pendingDeltaPx: deltaPx
879
- };
880
- return;
881
- }
882
- const pendingFrame = requestFrame(() => {
883
- if (this.state.phase !== "dragging") return;
884
- this.state = {
885
- ...this.state,
886
- pendingFrame: null
887
- };
888
- this.dispatchPendingResize();
889
- });
890
- this.state = {
891
- ...this.state,
892
- pendingDeltaPx: deltaPx,
893
- pendingFrame
894
- };
895
- }
896
- endResize(event) {
897
- if (this.state.phase !== "dragging" || this.state.pointerId !== event.pointerId) return;
898
- this.flushPendingFrame();
899
- this.finishDrag();
900
- }
901
- destroy() {
902
- this.cancelPendingFrame();
903
- this.finishDrag();
904
- }
905
- finishDrag() {
906
- if (this.state.phase !== "dragging") return;
907
- const { captureTarget, ownerDocument, pointerId } = this.state;
908
- this.removeDocumentListeners(ownerDocument);
909
- this.releasePointerCapture(captureTarget, pointerId);
910
- this.state = { phase: "idle" };
911
- }
912
- flushPendingFrame() {
913
- if (this.state.phase !== "dragging" || !this.state.pendingFrame) return;
914
- cancelFrame(this.state.pendingFrame);
915
- this.state = {
916
- ...this.state,
917
- pendingFrame: null
918
- };
919
- this.dispatchPendingResize();
920
- }
921
- dispatchPendingResize() {
922
- if (this.state.phase !== "dragging") return;
923
- this.controller.resizeHandle(this.state.splitId, {
924
- index: this.state.index,
925
- deltaPx: this.state.pendingDeltaPx,
926
- snapshotSizes: this.state.startSizes
927
- });
928
- }
929
- cancelPendingFrame() {
930
- if (this.state.phase !== "dragging" || !this.state.pendingFrame) return;
931
- cancelFrame(this.state.pendingFrame);
932
- this.state = {
933
- ...this.state,
934
- pendingFrame: null
935
- };
936
- }
937
- addDocumentListeners(ownerDocument) {
938
- ownerDocument?.addEventListener("pointermove", this.onDocumentPointerMove);
939
- ownerDocument?.addEventListener("pointerup", this.onDocumentPointerUp);
940
- ownerDocument?.addEventListener("pointercancel", this.onDocumentPointerCancel);
941
- }
942
- removeDocumentListeners(ownerDocument) {
943
- ownerDocument?.removeEventListener("pointermove", this.onDocumentPointerMove);
944
- ownerDocument?.removeEventListener("pointerup", this.onDocumentPointerUp);
945
- ownerDocument?.removeEventListener("pointercancel", this.onDocumentPointerCancel);
946
- }
947
- resolveOwnerDocument(event, captureTarget) {
948
- if (captureTarget?.ownerDocument) return captureTarget.ownerDocument;
949
- if (typeof Element !== "undefined" && event.target instanceof Element && event.target.ownerDocument) return event.target.ownerDocument;
950
- return typeof document === "undefined" ? null : document;
951
- }
952
- resolveCaptureTarget(captureTarget) {
953
- if (!captureTarget) return null;
954
- if ("setPointerCapture" in captureTarget || "releasePointerCapture" in captureTarget) return captureTarget;
955
- return null;
956
- }
957
- setPointerCapture(target, pointerId) {
958
- try {
959
- target?.setPointerCapture?.(pointerId);
960
- } catch {}
961
- }
962
- releasePointerCapture(target, pointerId) {
963
- if (!target?.releasePointerCapture) return;
964
- try {
965
- if (!target.hasPointerCapture || target.hasPointerCapture(pointerId)) target.releasePointerCapture(pointerId);
966
- } catch {}
967
- }
968
- };
969
- //#endregion
970
- //#region dom/src/resize-observer.ts
971
- function observeRootSize(root, controller) {
972
- const updateSize = ({ width, height }) => {
973
- controller.setContainerSize(Math.floor(width), Math.floor(height));
974
- };
975
- if (typeof ResizeObserver === "undefined") {
976
- updateSize(root.getBoundingClientRect());
977
- return () => {};
978
- }
979
- const observer = new ResizeObserver(([entry]) => {
980
- if (entry) updateSize(entry.contentRect);
981
- });
982
- observer.observe(root);
983
- return () => observer.disconnect();
984
- }
985
- //#endregion
986
- //#region dom/src/interactivity.ts
987
- const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
988
- "alertdialog",
989
- "button",
990
- "checkbox",
991
- "combobox",
992
- "dialog",
993
- "grid",
994
- "gridcell",
995
- "link",
996
- "listbox",
997
- "menu",
998
- "menubar",
999
- "menuitem",
1000
- "menuitemcheckbox",
1001
- "menuitemradio",
1002
- "option",
1003
- "radio",
1004
- "radiogroup",
1005
- "scrollbar",
1006
- "searchbox",
1007
- "slider",
1008
- "spinbutton",
1009
- "switch",
1010
- "tab",
1011
- "tablist",
1012
- "textbox",
1013
- "toolbar",
1014
- "tree",
1015
- "treegrid",
1016
- "treeitem"
1017
- ]);
1018
- function hasInteractiveOwner(target, boundary) {
1019
- let element = target;
1020
- while (element && element !== boundary) {
1021
- if (element instanceof HTMLElement && isInteractiveElement(element)) return true;
1022
- element = element.parentElement;
1023
- }
1024
- return boundary instanceof HTMLElement && isInteractiveElement(boundary);
1025
- }
1026
- function shouldFocusPaneShellForPointer(target, paneShell) {
1027
- return target === paneShell || !hasInteractiveOwner(target, paneShell);
1028
- }
1029
- function isInteractiveElement(element) {
1030
- if (isUnavailableElement(element)) return false;
1031
- const tagName = element.tagName.toLowerCase();
1032
- const role = element.getAttribute("role")?.toLowerCase();
1033
- 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);
1034
- }
1035
- function isUnavailableElement(element) {
1036
- let current = element;
1037
- while (current) {
1038
- 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;
1039
- const view = current.ownerDocument.defaultView;
1040
- if (view) {
1041
- const style = view.getComputedStyle(current);
1042
- if (style.display === "none" || style.visibility === "hidden") return true;
1043
- }
1044
- current = current.parentElement;
1045
- }
1046
- return false;
1047
- }
1048
- function isEditableTarget(target) {
1049
- if (!(target instanceof HTMLElement)) return false;
1050
- if (target.isContentEditable || target.getAttribute("role")?.toLowerCase() === "textbox") return true;
1051
- if (target.matches("textarea, select")) return !target.matches(":disabled");
1052
- return target instanceof HTMLInputElement && !target.disabled && !target.readOnly && [
1053
- "date",
1054
- "datetime-local",
1055
- "email",
1056
- "file",
1057
- "month",
1058
- "number",
1059
- "password",
1060
- "search",
1061
- "tel",
1062
- "text",
1063
- "time",
1064
- "url",
1065
- "week"
1066
- ].includes(target.type);
1067
- }
1068
- //#endregion
1069
- //#region dom/src/mount.ts
1070
- /** Own native grid interactions; React only attaches and detaches the root. */
1071
- function mountFocusGrid(root, controller, dom) {
1072
- dom.setRoot(root);
1073
- const resize = new PointerResizeController(controller);
1074
- const ownedTarget = (event) => {
1075
- const target = event.target;
1076
- return target instanceof Element && target.closest(".FocusgridFocusGrid") === root ? target : null;
1077
- };
1078
- const onFocus = (event) => {
1079
- const paneId = ownedTarget(event)?.closest("[data-pane-id]")?.dataset.paneId;
1080
- if (paneId) controller.focus(paneId);
1081
- };
1082
- const onPointerDown = (event) => {
1083
- const target = ownedTarget(event);
1084
- if (!target) return;
1085
- const separator = target.closest("[data-resize-handle]");
1086
- if (separator) {
1087
- const handle = controller.getLayout().handles.find((handle) => handle.id === separator.dataset.resizeHandle);
1088
- if (handle) resize.startResize(event, handle, separator);
1089
- return;
1090
- }
1091
- const pane = target.closest("[data-pane-id]");
1092
- if (!pane?.dataset.paneId) return;
1093
- controller.focus(pane.dataset.paneId);
1094
- if (shouldFocusPaneShellForPointer(target, pane)) pane.focus({ preventScroll: true });
1095
- };
1096
- root.addEventListener("focusin", onFocus, true);
1097
- root.addEventListener("pointerdown", onPointerDown);
1098
- const stopObserving = observeRootSize(root, controller);
1099
- return () => {
1100
- root.removeEventListener("focusin", onFocus, true);
1101
- root.removeEventListener("pointerdown", onPointerDown);
1102
- resize.destroy();
1103
- stopObserving();
1104
- dom.setRoot(null);
1105
- };
1106
- }
1107
- //#endregion
1108
15
  //#region react/src/default-pane-keymap.ts
1109
16
  const defaultPaneShortcutActions = [
1110
17
  {