@andrewcyuan/focusgrid 0.1.0 → 0.2.1
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 +38 -6
- package/dist/dom/index.cjs +1 -0
- package/dist/dom/index.d.cts +2 -161
- package/dist/dom/index.d.ts +2 -161
- package/dist/dom/index.js +1 -0
- package/dist/react/index.cjs +5 -1098
- package/dist/react/index.d.cts +2 -169
- package/dist/react/index.d.ts +2 -169
- package/dist/react/index.js +2 -1095
- package/dist/react/styles.css +9 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# Focusgrid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create tmux-like, resizable-pane layouts. Use the included React bindings (reocmmended) or write your own bindings to the `focusgrid/core` and `focusgrid/dom` packages!
|
|
4
4
|
|
|
5
|
+
You can put any react component in the panes using the `renderPane` prop and support all basic layout actions: resize, split, delete, swap, move focus.
|
|
6
|
+
|
|
7
|
+
The react bindings are also dependent on [shortcut-engine](https://www.npmjs.com/package/@andrewcyuan/shortcut-engine) for out-of-the-box keyboard support.
|
|
8
|
+
|
|
9
|
+
**Example usage**
|
|
5
10
|
```tsx
|
|
6
11
|
import { useState } from "react";
|
|
7
12
|
import { FocusGridDomController } from "@andrewcyuan/focusgrid/dom";
|
|
@@ -28,12 +33,39 @@ export function App() {
|
|
|
28
33
|
}
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
**Customizing Focusgrid**
|
|
37
|
+
Focusgrid can be easily customized. It can be the surface in which you create your entire webapp. Here are some ways you can customize it.
|
|
38
|
+
|
|
39
|
+
- Define the grid's width and height. When these change, panes are scaled proportionally (not the left-shrinks-first strategy that `tmux` uses)
|
|
40
|
+
- Replace the keybinds by passing `keymap`
|
|
41
|
+
- Directly hit the API by using controller commands: `splitActive`, `removeActive`, `focusAdjacent`, `swapAdjacent`, and `resizeActive`. `focus(paneId)` only syncs active-pane state; `focusAdjacent(direction)` also requests DOM focus.
|
|
42
|
+
- Read layout with `getLayout()`/`useControllerLayout(controller)` (React version). You can also directly mutate the layout, although using the API is probably easier and safer.
|
|
43
|
+
- To write your own frontend framework binding, use `getPane`, `getContainerSize`, and `getSplitSizes` for specific queries, and `subscribe` for changes.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
**Styling**
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
The optional `@andrewcyuan/focusgrid/react/styles.css` stylesheet uses zero-specificity `:where(...)` selectors. Ordinary class selectors override its defaults regardless of stylesheet order, including active, hover, and focus-visible states; no extra parent selector or `!important` is needed.
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
Set these inherited variables on your grid or an ancestor to customize the default colors:
|
|
51
|
+
|
|
52
|
+
| Variable | Default | Applies to |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `--focusgrid-focus-color` | `#4d8dff` | Active pane outline |
|
|
55
|
+
| `--focusgrid-handle-idle-color` | `transparent` | Idle resize handle |
|
|
56
|
+
| `--focusgrid-handle-color` | `rgb(77 141 255 / 35%)` | Hovered or focus-visible resize handle |
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.myGrid {
|
|
60
|
+
--focusgrid-handle-idle-color: #555;
|
|
61
|
+
--focusgrid-handle-color: #aaa;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.FocusgridPaneView { outline: none; }
|
|
65
|
+
.FocusgridResizeHandle { background: #555; }
|
|
66
|
+
.FocusgridResizeHandle:hover { background: #aaa; }
|
|
67
|
+
```
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
Pane and handle positions and dimensions still come from inline styles computed by the layout controller; the zero-specificity defaults do not change that geometry.
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
Made by [Andrew Yuan](https://andrewcyuan.com)
|
package/dist/dom/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("@andrewcyuan/focusgrid/core");
|
|
2
3
|
//#region dom/src/controller.ts
|
|
3
4
|
/** Attach the renderer root; pane elements are resolved only when focus is requested. */
|
|
4
5
|
var FocusGridDomController = class {
|
package/dist/dom/index.d.cts
CHANGED
|
@@ -1,164 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
1
|
+
import { DomController, FocusGridController, PaneId } from "@andrewcyuan/focusgrid/core";
|
|
2
|
+
|
|
162
3
|
//#region dom/src/controller.d.ts
|
|
163
4
|
/** Attach the renderer root; pane elements are resolved only when focus is requested. */
|
|
164
5
|
declare class FocusGridDomController implements DomController {
|
package/dist/dom/index.d.ts
CHANGED
|
@@ -1,164 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
1
|
+
import { DomController, FocusGridController, PaneId } from "@andrewcyuan/focusgrid/core";
|
|
2
|
+
|
|
162
3
|
//#region dom/src/controller.d.ts
|
|
163
4
|
/** Attach the renderer root; pane elements are resolved only when focus is requested. */
|
|
164
5
|
declare class FocusGridDomController implements DomController {
|
package/dist/dom/index.js
CHANGED