@n-uf/hypr-tiling 26.7.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/CHANGELOG.md +145 -0
- package/README.md +242 -0
- package/dist/chunk-ZCGZOWOY.mjs +9871 -0
- package/dist/devtools.cjs +12001 -0
- package/dist/devtools.d.mts +111 -0
- package/dist/devtools.d.ts +111 -0
- package/dist/devtools.mjs +2286 -0
- package/dist/drag-easing-5WbK3T82.d.ts +605 -0
- package/dist/drag-easing-KxPPNNZT.d.mts +605 -0
- package/dist/engine.cjs +9899 -0
- package/dist/engine.d.mts +314 -0
- package/dist/engine.d.ts +314 -0
- package/dist/engine.mjs +116 -0
- package/dist/index.cjs +9833 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.mjs +125 -0
- package/dist/tiling-renderer-kTlSm4H4.d.mts +2185 -0
- package/dist/tiling-renderer-kTlSm4H4.d.ts +2185 -0
- package/package.json +96 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { T as TilingLayoutNode, ab as TilingGroupNode, f as TilingSplitNode, g as TilingFocusDirection, ak as TilingLeafNode, ap as TilingMovePlacement, aR as TilingInsertionOptions, z as TilingPaneSizing, x as TilingCommand, D as TilingKeyboardAction, u as ResolvedTilingKeymap, W as ResolvedTilingKeyChord, X as ResolvedTilingKeyChordModifiers, w as TilingKeyboardEventLike, ah as TilingKeymap, ay as TilingResizeCapability, t as TilingSplitAxis, ad as TilingInteractionCapabilities, R as ResolvedTilingInteractionCapabilities } from './tiling-renderer-kTlSm4H4.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return a copy of the tree with split node `splitId`'s divider `ratio` set
|
|
5
|
+
* (clamped to the legal range). Unchanged when no split with that id exists.
|
|
6
|
+
*/
|
|
7
|
+
declare function updateSplitRatio(node: TilingLayoutNode, splitId: string, ratio: number): TilingLayoutNode;
|
|
8
|
+
/**
|
|
9
|
+
* Immutably set (or clear) the per-dimension `sizing` on a single leaf. Passing
|
|
10
|
+
* `undefined` (or a `sizing` with no static dimensions) leaves the leaf
|
|
11
|
+
* flexible. Used by the showcase per-pane static control. The result is passed
|
|
12
|
+
* through `normalizeStaticAxisFill` so a static switch can never store a
|
|
13
|
+
* both-static-along-axis edge (the second along-axis sibling lands as
|
|
14
|
+
* cross-axis-static + along-axis-fill instead) — closing the reachable
|
|
15
|
+
* static-switch gap trigger at the data layer.
|
|
16
|
+
*/
|
|
17
|
+
declare function setLeafSizing(node: TilingLayoutNode, leafId: string, sizing: TilingPaneSizing | undefined): TilingLayoutNode;
|
|
18
|
+
/** Find the leaf with id `leafId` anywhere in the tree (including inside a group), or `null`. */
|
|
19
|
+
declare function findLeafById(node: TilingLayoutNode, leafId: string): TilingLeafNode | null;
|
|
20
|
+
/**
|
|
21
|
+
* Return a copy of the tree with the two leaves' tiles exchanged in place.
|
|
22
|
+
* Unchanged when the ids are equal or either leaf is missing.
|
|
23
|
+
*/
|
|
24
|
+
declare function swapLeafTiles(node: TilingLayoutNode, firstLeafId: string, secondLeafId: string): TilingLayoutNode;
|
|
25
|
+
/** Every split node in the tree (depth-first, reading order). */
|
|
26
|
+
declare function collectSplitNodes(node: TilingLayoutNode): ReadonlyArray<TilingSplitNode>;
|
|
27
|
+
/**
|
|
28
|
+
* The leaf ids the OUTER layout sees: a leaf contributes its id; a group
|
|
29
|
+
* contributes ONLY its active member id (the group presents as one pane to the
|
|
30
|
+
* outer layout — pane-cycle / jump / directional focus see the active member).
|
|
31
|
+
* Use `readGroupMemberIds` / `collectGroups` to enumerate ALL members for tab UI.
|
|
32
|
+
*/
|
|
33
|
+
declare function readLeafNodeIds(node: TilingLayoutNode): ReadonlyArray<string>;
|
|
34
|
+
/** Every group in the tree (depth-first, reading order) — the tab-strip source. */
|
|
35
|
+
declare function collectGroups(node: TilingLayoutNode): ReadonlyArray<TilingGroupNode>;
|
|
36
|
+
/**
|
|
37
|
+
* Flatten a layout tree to the tile ids it renders, in reading order (group
|
|
38
|
+
* members reported in tab order). Handy for driving tile-order-dependent UI
|
|
39
|
+
* (e.g. a tab strip or an external index).
|
|
40
|
+
*/
|
|
41
|
+
declare function tileOrderByLeafId(node: TilingLayoutNode): ReadonlyArray<string>;
|
|
42
|
+
/**
|
|
43
|
+
* A read-only structural view of a {@link TilingLayoutNode} tree, aggregating
|
|
44
|
+
* the queries an app typically needs to drive layout-aware UI (shortcut chips,
|
|
45
|
+
* pane counters, directional focus) without walking the tree by hand or
|
|
46
|
+
* reaching for the low-level `./engine` walkers.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
interface TilingLayoutQuery {
|
|
51
|
+
/**
|
|
52
|
+
* The leaf ids the OUTER layout sees, in reading order — a leaf contributes
|
|
53
|
+
* its id; a group contributes ONLY its active member id (the group presents
|
|
54
|
+
* as one pane to the outer layout).
|
|
55
|
+
*/
|
|
56
|
+
readonly leafIds: ReadonlyArray<string>;
|
|
57
|
+
/**
|
|
58
|
+
* The tile ids the tree renders, in reading order (group members reported in
|
|
59
|
+
* tab order). Drives tile-order-dependent UI such as a tab strip.
|
|
60
|
+
*/
|
|
61
|
+
readonly tileOrder: ReadonlyArray<string>;
|
|
62
|
+
/** Every group in the tree (depth-first, reading order) — the tab-strip source. */
|
|
63
|
+
readonly groups: ReadonlyArray<TilingGroupNode>;
|
|
64
|
+
/** Every split node in the tree (depth-first, reading order). */
|
|
65
|
+
readonly splits: ReadonlyArray<TilingSplitNode>;
|
|
66
|
+
/** Whether any split node is in `"master"` layout mode. */
|
|
67
|
+
readonly hasMasterSplit: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The leaf id reached from `fromLeafId` in the given `direction` (spatial
|
|
70
|
+
* neighbor by pane geometry), or `null` when there is no pane that way.
|
|
71
|
+
*/
|
|
72
|
+
readonly neighborLeafId: (fromLeafId: string, direction: TilingFocusDirection) => string | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a read-only {@link TilingLayoutQuery} over `layout`. The single
|
|
76
|
+
* higher-level layout-inspection helper on the public API: it composes the
|
|
77
|
+
* low-level tree walkers (which stay on the `./engine` escape hatch) into the
|
|
78
|
+
* leaf/tile/group/split views and directional-neighbor lookup an app needs to
|
|
79
|
+
* render layout-aware controls.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* Inspect the tree you own in state — count panes, list groups, or find a
|
|
83
|
+
* directional neighbor — without walking it by hand:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { queryTilingLayout, type TilingLayoutNode } from "@n-uf/hypr-tiling";
|
|
87
|
+
*
|
|
88
|
+
* function paneCount(layout: TilingLayoutNode): number {
|
|
89
|
+
* return queryTilingLayout(layout).leafIds.length;
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* const query = queryTilingLayout(layout);
|
|
93
|
+
* const rightOfFocus = query.neighborLeafId(focusedLeafId, "right");
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
declare function queryTilingLayout(layout: TilingLayoutNode): TilingLayoutQuery;
|
|
99
|
+
/**
|
|
100
|
+
* Structural validity check for a layout tree — the commit-time backstop that
|
|
101
|
+
* guarantees a drag never persists a corrupt tree (orphaned / duplicated /
|
|
102
|
+
* empty-id leaf, missing split child, NaN ratio). Pure + recursive:
|
|
103
|
+
*
|
|
104
|
+
* - every leaf has a non-empty `id` and `tileId`;
|
|
105
|
+
* - every split has BOTH children present and individually valid, with a finite
|
|
106
|
+
* `ratio` in the open interval `(0, 1)`;
|
|
107
|
+
* - NO leaf id appears more than once across the whole tree (the single-instance
|
|
108
|
+
* invariant at the data layer — a duplicated dragged leaf is exactly the BUG-1
|
|
109
|
+
* class this rejects).
|
|
110
|
+
*
|
|
111
|
+
* The renderer calls this on the derived candidate BEFORE `onLayoutChange`; an
|
|
112
|
+
* invalid tree is refused (the drag falls back to cancel) so a broken commit can
|
|
113
|
+
* never reach consumer state.
|
|
114
|
+
*/
|
|
115
|
+
declare function isStructurallyValidLayout(node: TilingLayoutNode): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The subtree that gets promoted into a leaf's vacated cell when that leaf is
|
|
118
|
+
* extracted by `insertLeafAdjacent` / `moveLeafTo*`.
|
|
119
|
+
*
|
|
120
|
+
* When a leaf is removed from its parent split, `extractLeafNode` collapses the
|
|
121
|
+
* parent and the leaf's former sibling branch takes the parent's slot, absorbing
|
|
122
|
+
* the released space. That sibling branch is the "successor" of the extracted
|
|
123
|
+
* leaf. Returns `null` for a root leaf (no parent) or an unknown id.
|
|
124
|
+
*/
|
|
125
|
+
declare function siblingSubtreeForLeaf(node: TilingLayoutNode, leafId: string): TilingLayoutNode | null;
|
|
126
|
+
/**
|
|
127
|
+
* Move `sourceLeafId` adjacent to `targetLeafId`, wrapping them in a new split
|
|
128
|
+
* on the resolved axis with the source on the `placement` side. Unchanged when
|
|
129
|
+
* the source and target are the same or the source cannot be extracted.
|
|
130
|
+
*/
|
|
131
|
+
declare function insertLeafAdjacent(layout: TilingLayoutNode, sourceLeafId: string, targetLeafId: string, placement: TilingMovePlacement, options?: Partial<TilingInsertionOptions>): TilingLayoutNode;
|
|
132
|
+
/**
|
|
133
|
+
* Remove a leaf from the tree and collapse its now-single-child parent split so
|
|
134
|
+
* the leaf's former sibling subtree takes the parent's slot and absorbs the
|
|
135
|
+
* released space — the standard tiling gap-close. Returns a NEW tree (the input
|
|
136
|
+
* is never mutated).
|
|
137
|
+
*
|
|
138
|
+
* Removal + parent collapse reuses the same `extractLeafNode` machinery that
|
|
139
|
+
* `insertLeafAdjacent` / `moveLeafTo*` already rely on; this reducer exposes the
|
|
140
|
+
* gap-closed remainder directly (it is `insertLeafAdjacent` minus the re-insert).
|
|
141
|
+
* It is the pickup step of the Hyprland-style "live" drag: the dragged leaf is
|
|
142
|
+
* detached once on pickup and the remaining tree reflows once to close the gap.
|
|
143
|
+
*
|
|
144
|
+
* Returns the layout UNCHANGED when the leaf id is absent, or when it is the
|
|
145
|
+
* root leaf (a root with no parent cannot be removed without emptying the tree).
|
|
146
|
+
*/
|
|
147
|
+
declare function removeLeafTile(layout: TilingLayoutNode, leafId: string): TilingLayoutNode;
|
|
148
|
+
/**
|
|
149
|
+
* Move `sourceLeafId` out to a new root-level split, placing it on the
|
|
150
|
+
* `placement` side alongside the remainder of the tree. Unchanged when the
|
|
151
|
+
* source cannot be extracted.
|
|
152
|
+
*/
|
|
153
|
+
declare function moveLeafToRoot(layout: TilingLayoutNode, sourceLeafId: string, placement: "first" | "second", options?: Partial<TilingInsertionOptions>): TilingLayoutNode;
|
|
154
|
+
/**
|
|
155
|
+
* Move `sourceLeafId` into the existing split container `targetSplitId` on the
|
|
156
|
+
* `placement` side. Unchanged when the source cannot be extracted.
|
|
157
|
+
*/
|
|
158
|
+
declare function moveLeafToSplitContainer(layout: TilingLayoutNode, sourceLeafId: string, targetSplitId: string, placement: "first" | "second", options?: Partial<TilingInsertionOptions>): TilingLayoutNode;
|
|
159
|
+
/**
|
|
160
|
+
* Return a copy of the tree with split node `splitId`'s axis flipped
|
|
161
|
+
* (horizontal ↔ vertical). Unchanged when no split with that id exists.
|
|
162
|
+
*/
|
|
163
|
+
declare function toggleSplitAxis(node: TilingLayoutNode, splitId: string): TilingLayoutNode;
|
|
164
|
+
/** Options for `groupLeaves`: an explicit host/anchor + an explicit group id. */
|
|
165
|
+
interface GroupLeavesOptions {
|
|
166
|
+
/**
|
|
167
|
+
* The pane whose slot the merged group occupies and whose tile is the active
|
|
168
|
+
* tab — the clicked pane for the header Group button, or the resolved host for
|
|
169
|
+
* Alt+G. Defaults to the FIRST resolvable id in `leafIds` when omitted.
|
|
170
|
+
*/
|
|
171
|
+
hostLeafId?: string;
|
|
172
|
+
/** Explicit group id; defaults to `group-<hostLeafId>`. */
|
|
173
|
+
groupId?: string;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Fold a selection of leaves and/or group members into ONE flat tabbed group
|
|
177
|
+
* occupying the HOST pane's slot. Any existing group the selection touches is
|
|
178
|
+
* DISSOLVED and its full membership folded into the single result — there is
|
|
179
|
+
* never a nested group-within-a-group nor a partial group, and selecting any one
|
|
180
|
+
* member of a group pulls in the whole group (see `flatGroupMemberOrder`). The
|
|
181
|
+
* host pane's tile is the active tab and its slot hosts the merged group; every
|
|
182
|
+
* other involved slot closes and the tree reflows. Returns the layout UNCHANGED
|
|
183
|
+
* when fewer than two distinct resolvable leaves result (a group needs ≥2
|
|
184
|
+
* members) or the host is unresolvable.
|
|
185
|
+
*
|
|
186
|
+
* @remarks
|
|
187
|
+
* The no-op return (same reference) is load-bearing: gate a "Group" control on
|
|
188
|
+
* {@link canGroupMultiSelection}, which detects groupability by checking whether
|
|
189
|
+
* this operation would actually change the tree. Pass `options.hostLeafId` to
|
|
190
|
+
* pin which pane keeps the slot and becomes the active tab; otherwise the first
|
|
191
|
+
* resolvable id in `leafIds` is the host.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { groupLeaves, canGroupMultiSelection } from "@n-uf/hypr-tiling";
|
|
196
|
+
*
|
|
197
|
+
* // `selection` is a ReadonlySet<string> of leaf ids in insertion order.
|
|
198
|
+
* if (canGroupMultiSelection(layout, selection)) {
|
|
199
|
+
* const next = groupLeaves(layout, [...selection]);
|
|
200
|
+
* setLayout(next);
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @see {@link canGroupMultiSelection} to test groupability before calling.
|
|
205
|
+
* @see {@link GroupLeavesOptions} for the host-pinning option.
|
|
206
|
+
*/
|
|
207
|
+
declare function groupLeaves(layout: TilingLayoutNode, leafIds: ReadonlyArray<string>, options?: GroupLeavesOptions): TilingLayoutNode;
|
|
208
|
+
/**
|
|
209
|
+
* Explode a group back into a dwindle split chain of its members (the inverse of
|
|
210
|
+
* `groupLeaves`). A 1-member group collapses to the bare leaf. Returns the
|
|
211
|
+
* layout unchanged when `groupId` is absent.
|
|
212
|
+
*/
|
|
213
|
+
declare function ungroupNode(layout: TilingLayoutNode, groupId: string): TilingLayoutNode;
|
|
214
|
+
/**
|
|
215
|
+
* Id of the nearest focusable leaf from `fromLeafId` in the given spatial
|
|
216
|
+
* `direction` (geometric nearest-neighbour over the laid-out rectangles), or
|
|
217
|
+
* `null` when no leaf lies that way. Powers directional keyboard focus.
|
|
218
|
+
*/
|
|
219
|
+
declare function findLeafByDirection(layout: TilingLayoutNode, fromLeafId: string, direction: TilingFocusDirection): string | null;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Public command contract (HT-API-COMMAND-KEYBOARD-SURFACE, half A).
|
|
223
|
+
*
|
|
224
|
+
* `TilingCommand` (in `types.ts`) is the public API's typed dispatch-style command set
|
|
225
|
+
* — the Hyprland `dispatch` analog. This module holds the PURE logic around it:
|
|
226
|
+
* the command→capability gate (so a command targeting a disabled capability is a
|
|
227
|
+
* safe no-op, reproducing the old `matchKeymapAction` "leave the key alone"
|
|
228
|
+
* behavior) and the bridge from the internal fixed-keymap `TilingKeyboardAction`
|
|
229
|
+
* to a command, so the default keymap path and the public binding registry both
|
|
230
|
+
* funnel to the SAME renderer router.
|
|
231
|
+
*
|
|
232
|
+
* Cross-ref: `_agent/command-keyboard-api-design.md` §2; `keybindings.ts` (the
|
|
233
|
+
* chord→command registry); `pane-switching.ts` (`matchKeymapAction` /
|
|
234
|
+
* `TilingKeyboardAction`).
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The capability enable-flags a command may require. Superset of
|
|
239
|
+
* `TilingKeymapActionGuards` (which only covers the keyboard-reachable actions)
|
|
240
|
+
* with the sizing / acquire-space / resize gates the programmatic command set
|
|
241
|
+
* also touches. The renderer builds this from its resolved capabilities.
|
|
242
|
+
*/
|
|
243
|
+
interface TilingCommandGates {
|
|
244
|
+
/** Maximize / restore commands. */
|
|
245
|
+
maximizeEnabled: boolean;
|
|
246
|
+
/** Pane-switching (focus-cycle / focus-jump) commands. */
|
|
247
|
+
paneSwitchingEnabled: boolean;
|
|
248
|
+
/** Focus selection commands. */
|
|
249
|
+
focusEnabled: boolean;
|
|
250
|
+
/** Drag/keyboard rearrange (move / swap / insert) commands. */
|
|
251
|
+
rearrangeEnabled: boolean;
|
|
252
|
+
/** Per-pane title-bar sizing (`set-sizing`) command. */
|
|
253
|
+
sizingEnabled: boolean;
|
|
254
|
+
/** Directional acquire-space command. */
|
|
255
|
+
acquireSpaceEnabled: boolean;
|
|
256
|
+
/** Divider resize (`set-split-ratio` / `toggle-split-axis`) commands. */
|
|
257
|
+
resizeEnabled: boolean;
|
|
258
|
+
/** Master/stack layout-mode commands (HT-LAYOUT-MASTER-STACK). */
|
|
259
|
+
layoutEnabled: boolean;
|
|
260
|
+
/** Group / tabbed-stacking commands (HT-GROUP-TABBED-STACKING). */
|
|
261
|
+
groupingEnabled: boolean;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The single capability gate a command requires, or `null` when the command is
|
|
265
|
+
* unconditionally available. Used by `isCommandEnabled` to decide whether a
|
|
266
|
+
* dispatch (keyboard or imperative) should run or stay a no-op.
|
|
267
|
+
*/
|
|
268
|
+
declare function commandRequiredCapability(command: TilingCommand): keyof TilingCommandGates | null;
|
|
269
|
+
/**
|
|
270
|
+
* Whether `command` would do anything given the current capability gates: `true`
|
|
271
|
+
* when the command's required capability is enabled (or it requires none). A
|
|
272
|
+
* keyboard binding to a disabled-capability command stays browser-graceful (the
|
|
273
|
+
* caller does not `preventDefault`); an imperative `dispatch` of one is a no-op.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* Build your own command bar / keyboard shortcut that only fires (and only
|
|
277
|
+
* renders its button) when the target command is actually enabled — the gate the
|
|
278
|
+
* renderer itself uses for its shortcut chips:
|
|
279
|
+
*
|
|
280
|
+
* ```tsx
|
|
281
|
+
* import {
|
|
282
|
+
* resolveInteractionCapabilities,
|
|
283
|
+
* isCommandEnabled,
|
|
284
|
+
* type TilingCommand,
|
|
285
|
+
* type TilingCommandGates,
|
|
286
|
+
* type TilingCommandHandle,
|
|
287
|
+
* type TilingInteractionCapabilities,
|
|
288
|
+
* } from "@n-uf/hypr-tiling";
|
|
289
|
+
*
|
|
290
|
+
* function gatesFor(interaction?: TilingInteractionCapabilities): TilingCommandGates {
|
|
291
|
+
* const caps = resolveInteractionCapabilities(interaction);
|
|
292
|
+
* return {
|
|
293
|
+
* maximizeEnabled: caps.maximize.enable,
|
|
294
|
+
* paneSwitchingEnabled: caps.paneSwitching.enable,
|
|
295
|
+
* focusEnabled: caps.focus,
|
|
296
|
+
* rearrangeEnabled: caps.rearrange,
|
|
297
|
+
* sizingEnabled: caps.paneTitleBarControls.sizing,
|
|
298
|
+
* acquireSpaceEnabled: caps.paneTitleBarControls.acquireSpace,
|
|
299
|
+
* resizeEnabled: caps.resize !== "none",
|
|
300
|
+
* layoutEnabled: caps.masterLayout,
|
|
301
|
+
* groupingEnabled: caps.grouping.enable,
|
|
302
|
+
* };
|
|
303
|
+
* }
|
|
304
|
+
*
|
|
305
|
+
* function MaximizeButton(props: {
|
|
306
|
+
* handle: React.RefObject<TilingCommandHandle | null>;
|
|
307
|
+
* interaction?: TilingInteractionCapabilities;
|
|
308
|
+
* }) {
|
|
309
|
+
* const command: TilingCommand = { kind: "toggle-maximize" };
|
|
310
|
+
* if (!isCommandEnabled(command, gatesFor(props.interaction))) {
|
|
311
|
+
* return null; // maximize is disabled — don't render a dead control
|
|
312
|
+
* }
|
|
313
|
+
* return <button onClick={() => props.handle.current?.dispatch(command)}>Maximize</button>;
|
|
314
|
+
* }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
declare function isCommandEnabled(command: TilingCommand, gates: TilingCommandGates): boolean;
|
|
318
|
+
/**
|
|
319
|
+
* Bridge an internal fixed-keymap `TilingKeyboardAction` to the public command
|
|
320
|
+
* set. The default keymap path resolves an action via `matchKeymapAction`, then
|
|
321
|
+
* runs it through this bridge so it reaches the same `dispatchCommand` router the
|
|
322
|
+
* binding registry feeds — no duplicated action logic.
|
|
323
|
+
*/
|
|
324
|
+
declare function keyboardActionToCommand(action: TilingKeyboardAction): TilingCommand;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Documented keymap defaults. Matching keys off the PHYSICAL `KeyboardEvent.code`
|
|
328
|
+
* (not the produced `event.key`), so the bindings hold on macOS / Arc where the
|
|
329
|
+
* Option(Alt) modifier rewrites `event.key` into dead-key glyphs. These are
|
|
330
|
+
* deliberately browser-graceful: they never collide with `F11` (`F11`),
|
|
331
|
+
* `Ctrl/Cmd+W` (`KeyW`), `Ctrl/Cmd+T` (`KeyT`), or `Ctrl+Tab` (`Tab`). Every
|
|
332
|
+
* binding is Alt-based (or bare `Escape`), and matching requires an EXACT
|
|
333
|
+
* modifier state, so e.g. `Cmd+1` (`Digit1`+Meta, a browser tab shortcut) never
|
|
334
|
+
* matches the Alt-only jump family.
|
|
335
|
+
*/
|
|
336
|
+
declare const TILING_KEYMAP_DEFAULTS: ResolvedTilingKeymap;
|
|
337
|
+
/**
|
|
338
|
+
* Resolve a public keymap to a fully-resolved keymap. `undefined`/`null` → the
|
|
339
|
+
* documented defaults; a partial keymap merges at the action level (supplying
|
|
340
|
+
* one binding leaves the others at their defaults).
|
|
341
|
+
*/
|
|
342
|
+
declare function resolveKeymap(keymap?: TilingKeymap | null): ResolvedTilingKeymap;
|
|
343
|
+
/**
|
|
344
|
+
* Exact-match a keyboard event against a resolved chord. The key identity is
|
|
345
|
+
* compared on the PHYSICAL `event.code` (so macOS Option-glyph rewrites of
|
|
346
|
+
* `event.key` are irrelevant); all four modifiers are compared exactly.
|
|
347
|
+
*/
|
|
348
|
+
declare function matchKeyChord(event: TilingKeyboardEventLike, chord: ResolvedTilingKeyChord): boolean;
|
|
349
|
+
/** Capability enable flags consulted while matching a keyboard action. */
|
|
350
|
+
interface TilingKeymapActionGuards {
|
|
351
|
+
/** Gates the maximize/restore actions (`toggleMaximize` / `restore`). */
|
|
352
|
+
maximizeEnabled: boolean;
|
|
353
|
+
/** Gates the pane-switching actions (`previousPane` / `nextPane` / `jumpToPane`). */
|
|
354
|
+
paneSwitchingEnabled: boolean;
|
|
355
|
+
/** Gates the directional focus actions (`focusLeft/Right/Up/Down`). */
|
|
356
|
+
focusEnabled: boolean;
|
|
357
|
+
/** Gates move-mode entry (`enterMoveMode`); mirrors the drag-rearrange gate. */
|
|
358
|
+
rearrangeEnabled: boolean;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Resolve a keyboard event to a logical tiling action, honoring capability
|
|
362
|
+
* enable flags. Returns `null` when no binding matches or the owning capability
|
|
363
|
+
* is disabled — the caller then leaves the event alone (no `preventDefault`),
|
|
364
|
+
* keeping unhandled keys browser-graceful.
|
|
365
|
+
*/
|
|
366
|
+
declare function matchKeymapAction(event: TilingKeyboardEventLike, keymap: ResolvedTilingKeymap, guards: TilingKeymapActionGuards): TilingKeyboardAction | null;
|
|
367
|
+
/**
|
|
368
|
+
* Resolve the jump-to-N leaf id (1-based), or `null` when out of range (no-op).
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* Wire your own "jump to pane N" buttons against the leaf order from
|
|
372
|
+
* {@link queryTilingLayout}, dispatching the built-in `focus-jump` command:
|
|
373
|
+
*
|
|
374
|
+
* ```tsx
|
|
375
|
+
* import { queryTilingLayout, resolveJumpedPaneId } from "@n-uf/hypr-tiling";
|
|
376
|
+
*
|
|
377
|
+
* const { leafIds } = queryTilingLayout(layout);
|
|
378
|
+
* leafIds.forEach((_, i) => {
|
|
379
|
+
* const paneNumber = i + 1;
|
|
380
|
+
* const targetLeafId = resolveJumpedPaneId(leafIds, paneNumber); // null → out of range
|
|
381
|
+
* // render a button that dispatches { kind: "focus-jump", paneNumber } when targetLeafId != null
|
|
382
|
+
* });
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
declare function resolveJumpedPaneId(leafIds: ReadonlyArray<string>, paneNumber: number): string | null;
|
|
386
|
+
/**
|
|
387
|
+
* Resolve the next maximized-leaf state from a toggle on `focusedLeafId`:
|
|
388
|
+
* - no focused pane → unchanged (returns the current maximized id);
|
|
389
|
+
* - toggling the already-maximized pane → restore (`null`);
|
|
390
|
+
* - otherwise → maximize the focused pane.
|
|
391
|
+
*/
|
|
392
|
+
declare function resolveMaximizeToggle(currentMaximizedLeafId: string | null, focusedLeafId: string | null): string | null;
|
|
393
|
+
/** Whether a chord requires at least one held modifier (the switcher needs one to commit on release). */
|
|
394
|
+
declare function chordRequiresModifier(chord: ResolvedTilingKeyChord): boolean;
|
|
395
|
+
/** Whether a modifier set requires at least one held modifier. */
|
|
396
|
+
declare function hasAnyModifier(modifiers: ResolvedTilingKeyChordModifiers): boolean;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Pure multi-selection model for the Alt/Opt+click header → group flow
|
|
400
|
+
* (`paneSwitching.multiSelectGrouping`).
|
|
401
|
+
*
|
|
402
|
+
* The renderer is a `"use client"` DOM component that cannot be exercised under
|
|
403
|
+
* the node-only jest harness (see `_agent/tiling-architecture.md`, "Test
|
|
404
|
+
* coverage status"), so the interaction's decision logic lives here as pure
|
|
405
|
+
* functions the renderer merely wires:
|
|
406
|
+
*
|
|
407
|
+
* - `isMultiSelectModifierActive` — discriminate a plain header click from an
|
|
408
|
+
* Alt/Opt (multi-select) click.
|
|
409
|
+
* - `toggleLeafMultiSelection` — add/remove a leaf from the selection set
|
|
410
|
+
* (immutable; insertion order is preserved, so the first-selected leaf is the
|
|
411
|
+
* `group-leaves` anchor).
|
|
412
|
+
* - `pruneMultiSelection` — drop ids no longer present in the layout's
|
|
413
|
+
* outer-slot leaf set, so a removed pane never lingers selected.
|
|
414
|
+
* - `canGroupMultiSelection` — whether the current selection can be folded into
|
|
415
|
+
* one group RIGHT NOW under the existing `groupLeaves` constraint.
|
|
416
|
+
* - `resolveMultiSelectGroupCommand` — the `group-leaves` command for the
|
|
417
|
+
* current selection (or `null` when there is nothing groupable).
|
|
418
|
+
*
|
|
419
|
+
* Cross-ref: `state.ts` (`groupLeaves` — the SOLE grouping operation reused
|
|
420
|
+
* here, not reimplemented); `commands.ts` (`group-leaves` capability gate).
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
/** Minimum selection size that the existing `groupLeaves` op will act on. */
|
|
424
|
+
declare const MULTI_SELECT_GROUP_MIN_MEMBERS: number;
|
|
425
|
+
/**
|
|
426
|
+
* The modifier-key subset of a pointer/mouse event that discriminates a
|
|
427
|
+
* multi-select click from a plain click. The multi-select / grouping modifier is
|
|
428
|
+
* unified on Alt/Opt across every surface (header click + the Alt+G key), chosen
|
|
429
|
+
* for minimal cross-browser interference: `altKey` = Opt on macOS, Alt on
|
|
430
|
+
* Windows/Linux.
|
|
431
|
+
*/
|
|
432
|
+
interface MultiSelectModifierState {
|
|
433
|
+
/** Whether Alt/Opt was held (the multi-select discriminator). */
|
|
434
|
+
readonly altKey: boolean;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `true` when the event carries the multi-select modifier — Alt/Opt (`altKey`),
|
|
438
|
+
* the single chord both the header toggle and the Alt+G group key key off.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* In a custom `renderTile`, add the clicked pane to the multi-selection only when
|
|
442
|
+
* the platform multi-select modifier is held (a plain click falls through to
|
|
443
|
+
* focus):
|
|
444
|
+
*
|
|
445
|
+
* ```tsx
|
|
446
|
+
* import { isMultiSelectModifierActive } from "@n-uf/hypr-tiling";
|
|
447
|
+
*
|
|
448
|
+
* <header
|
|
449
|
+
* onClick={(event) => {
|
|
450
|
+
* if (args.isMultiSelectGroupingEnabled && isMultiSelectModifierActive(event)) {
|
|
451
|
+
* event.preventDefault();
|
|
452
|
+
* args.onToggleMultiSelect();
|
|
453
|
+
* }
|
|
454
|
+
* }}
|
|
455
|
+
* />
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
declare function isMultiSelectModifierActive(event: MultiSelectModifierState): boolean;
|
|
459
|
+
/**
|
|
460
|
+
* Immutably toggle `leafId` in `selection`: present → removed, absent → added.
|
|
461
|
+
* Returns a NEW `Set` (the input is never mutated). Insertion order is
|
|
462
|
+
* preserved for added ids, so the first-selected leaf remains the
|
|
463
|
+
* `group-leaves` anchor.
|
|
464
|
+
*/
|
|
465
|
+
declare function toggleLeafMultiSelection(selection: ReadonlySet<string>, leafId: string): Set<string>;
|
|
466
|
+
/**
|
|
467
|
+
* Drop any selected id not present in `presentLeafIds` (the layout's current
|
|
468
|
+
* outer-slot leaf ids). Returns the SAME reference when nothing is pruned, so
|
|
469
|
+
* the caller can skip a state update; otherwise a new `Set` preserving the
|
|
470
|
+
* surviving ids in their original order.
|
|
471
|
+
*/
|
|
472
|
+
declare function pruneMultiSelection(selection: ReadonlySet<string>, presentLeafIds: ReadonlyArray<string>): Set<string>;
|
|
473
|
+
/**
|
|
474
|
+
* Whether the current `selection` (in insertion order) can be folded into one
|
|
475
|
+
* group right now. Requires at least `MULTI_SELECT_GROUP_MIN_MEMBERS` ids AND
|
|
476
|
+
* that the existing `groupLeaves` op would actually change `layout` — which
|
|
477
|
+
* encodes the op's own constraint: the anchor (first id) must be a placeable
|
|
478
|
+
* slot (not already a group member) and at least two ids must resolve to leaves.
|
|
479
|
+
* A no-op `groupLeaves` (same reference back) means the selection is not
|
|
480
|
+
* groupable, so the Group control is suppressed rather than offered inert.
|
|
481
|
+
*/
|
|
482
|
+
declare function canGroupMultiSelection(layout: TilingLayoutNode, selection: ReadonlySet<string>): boolean;
|
|
483
|
+
/**
|
|
484
|
+
* Resolve the HOST pane (the slot the merged group occupies + its active tab):
|
|
485
|
+
*
|
|
486
|
+
* - the explicit `clickedLeafId` (the pane whose Group button was pressed) when
|
|
487
|
+
* it is part of the selection — the header-button path;
|
|
488
|
+
* - else (Alt+G, no click target) the `focusedLeafId` IF it is in the selection,
|
|
489
|
+
* else the FIRST-selected pane (insertion order).
|
|
490
|
+
*
|
|
491
|
+
* Returns `null` when the selection is empty (nothing to host).
|
|
492
|
+
*/
|
|
493
|
+
declare function resolveMultiSelectGroupHost(selection: ReadonlySet<string>, clickedLeafId: string | null, focusedLeafId: string | null): string | null;
|
|
494
|
+
/**
|
|
495
|
+
* The `group-leaves` command that folds the current `selection` (insertion
|
|
496
|
+
* order, expanded over any touched groups) into ONE flat tabbed group at the
|
|
497
|
+
* `hostLeafId` slot (host = active tab, listed first), or `null` when fewer than
|
|
498
|
+
* `MULTI_SELECT_GROUP_MIN_MEMBERS` are selected. When `hostLeafId` is omitted the
|
|
499
|
+
* op defaults the host to the first resolvable id. Dispatching the returned
|
|
500
|
+
* command is still gated by the `grouping` capability at the renderer's command
|
|
501
|
+
* router (a safe no-op when grouping is disabled).
|
|
502
|
+
*/
|
|
503
|
+
declare function resolveMultiSelectGroupCommand(selection: ReadonlySet<string>, hostLeafId?: string | null): TilingCommand | null;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* All-enabled defaults. An undefined capability config (or any undefined field)
|
|
507
|
+
* resolves to these via `resolveInteractionCapabilities`.
|
|
508
|
+
*
|
|
509
|
+
* The lone opt-IN exception is `paneSwitching.showContentToggle`: it defaults to
|
|
510
|
+
* `false`. That checkbox (a group tab strip's "show pane body" toggle) is a
|
|
511
|
+
* development / demo affordance — a consumer app renders its own pane content
|
|
512
|
+
* and never wants an end-user control that blanks it. Suppressed by default, the
|
|
513
|
+
* initial pane-content-visible flag pins ON (see `resolveInitialPaneContentVisible`),
|
|
514
|
+
* so panes paint their content at rest with no wiring. A tooling surface that
|
|
515
|
+
* genuinely wants the toggle (e.g. the interactive showcase) opts back in with
|
|
516
|
+
* `paneSwitching: { showContentToggle: true }`.
|
|
517
|
+
*/
|
|
518
|
+
declare const TILING_INTERACTION_CAPABILITY_DEFAULTS: ResolvedTilingInteractionCapabilities;
|
|
519
|
+
/**
|
|
520
|
+
* Interaction preset for static (config-driven) product dashboards: only height
|
|
521
|
+
* dividers resize (`resize: "vertical"`), and every interactive surface that
|
|
522
|
+
* assumes a rearrangeable/maximizable lab layout is off (no drag-rearrange, no
|
|
523
|
+
* pane focus selection, no maximize, no pane switching / tab strip). Dashboards
|
|
524
|
+
* pass this instead of hand-repeating the disable set.
|
|
525
|
+
*
|
|
526
|
+
* Per-pane title-bar controls (sizing + acquire-space) are OFF here too:
|
|
527
|
+
* dashboards ship a curated layout with PRE-CONFIGURED static panes and are
|
|
528
|
+
* resize-only by intent, so letting an end-user re-pin a pane's bbox or have one
|
|
529
|
+
* pane absorb its siblings' space would fight the authored composition. End-user
|
|
530
|
+
* per-pane sizing belongs to interactive workspaces (the showcase default), not
|
|
531
|
+
* config-driven dashboards.
|
|
532
|
+
*/
|
|
533
|
+
declare const TILING_DASHBOARD_PRESET: TilingInteractionCapabilities;
|
|
534
|
+
/**
|
|
535
|
+
* Single defaulting helper for `TilingInteractionCapabilities`. `undefined` /
|
|
536
|
+
* `null` → all enabled; a partial override merges field-by-field over the
|
|
537
|
+
* all-enabled defaults. Uses nullish coalescing so an explicit `false` (e.g.
|
|
538
|
+
* `rearrange: false`, `maximize.enable: false`) is preserved and never
|
|
539
|
+
* overridden by the default. Idempotent: re-resolving a resolved object yields
|
|
540
|
+
* the same result.
|
|
541
|
+
*/
|
|
542
|
+
declare function resolveInteractionCapabilities(capabilities?: TilingInteractionCapabilities | null): ResolvedTilingInteractionCapabilities;
|
|
543
|
+
/**
|
|
544
|
+
* Pure capability gate for a single split divider. Maps the resize capability
|
|
545
|
+
* onto a split node's `axis` per the documented axis convention (see
|
|
546
|
+
* `TilingResizeCapability`): a split with `axis: "horizontal"` is a width
|
|
547
|
+
* divider (side-by-side panes), a split with `axis: "vertical"` is a height
|
|
548
|
+
* divider (stacked panes).
|
|
549
|
+
*
|
|
550
|
+
* - `"none"` → no divider resizes.
|
|
551
|
+
* - `"both"` → every divider resizes.
|
|
552
|
+
* - `"horizontal"` → only width dividers (split `axis === "horizontal"`).
|
|
553
|
+
* - `"vertical"` → only height dividers (split `axis === "vertical"`).
|
|
554
|
+
*/
|
|
555
|
+
declare function isResizeAxisEnabled(capability: TilingResizeCapability, splitAxis: TilingSplitAxis): boolean;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Consumer-configurable drag easing — the pure resolution + validation half of
|
|
559
|
+
* the easing public-API surface (HT-ANIM-EASING-CONFIG).
|
|
560
|
+
*
|
|
561
|
+
* `DRAG_HOP_EASING` / `DRAG_REFLOW_EASING` used to be fixed module constants in
|
|
562
|
+
* the renderer (parity-report §5 #9 / §7: the one M10 remainder short of
|
|
563
|
+
* Hyprland's per-leaf `bezier`/`speed` config). They live here as defaults so
|
|
564
|
+
* the renderer can thread consumer-supplied CSS `<easing-function>` strings into
|
|
565
|
+
* the ghost-hop transit and the survivor FLIP reflow timing functions.
|
|
566
|
+
*
|
|
567
|
+
* This module is DOM-less and pure (the `node` jest environment runs it),
|
|
568
|
+
* mirroring how `ghost-transit.ts` / `survivor-reflow.ts` factor the FLIP
|
|
569
|
+
* GEOMETRY out of the renderer — but an easing STRING is a distinct rendering
|
|
570
|
+
* concern from FLIP geometry, so the resolver lives in its own module rather
|
|
571
|
+
* than muddying those geometry modules.
|
|
572
|
+
*
|
|
573
|
+
* Cross-ref: `_agent/command-keyboard-api-design.md` §5;
|
|
574
|
+
* `_agent/comparative-analysis/parity-report.md` §5 #9 / §7 (configurable easing
|
|
575
|
+
* remainder); `dynamic-tiling-renderer.tsx` (`DragPaneOverlay` hop + survivor
|
|
576
|
+
* FLIP effect consume the resolved strings).
|
|
577
|
+
*/
|
|
578
|
+
/** The default ghost hop / pickup / hop-out timing function (a snappy decel). */
|
|
579
|
+
declare const DEFAULT_DRAG_HOP_EASING: string;
|
|
580
|
+
/**
|
|
581
|
+
* The default survivor-reflow settle timing function. Equals the hop curve so
|
|
582
|
+
* the ghost and the survivors read as one coordinated motion (the renderer's
|
|
583
|
+
* `dragReflowEasing` prop falls back to the resolved `dragHopEasing` when
|
|
584
|
+
* undefined, so this default only applies when BOTH are unset).
|
|
585
|
+
*/
|
|
586
|
+
declare const DEFAULT_DRAG_REFLOW_EASING: string;
|
|
587
|
+
/**
|
|
588
|
+
* Whether `value` is a syntactically-plausible CSS `<easing-function>`: one of
|
|
589
|
+
* the global keywords, or a `cubic-bezier(...)` / `linear(...)` / `steps(...)`
|
|
590
|
+
* functional form. This is a SHAPE check (not a full CSS parse) — enough to keep
|
|
591
|
+
* a malformed / empty string from reaching the compositor as a broken
|
|
592
|
+
* `transition`, without re-implementing the CSS grammar. Leading / trailing
|
|
593
|
+
* whitespace is ignored; matching is case-insensitive on the keyword / function
|
|
594
|
+
* name.
|
|
595
|
+
*/
|
|
596
|
+
declare function isCssEasing(value: string): boolean;
|
|
597
|
+
/**
|
|
598
|
+
* Resolve a consumer-supplied easing to a usable CSS timing function: returns
|
|
599
|
+
* `value` when it is a plausible easing (`isCssEasing`), else `fallback`. An
|
|
600
|
+
* `undefined` / `null` / blank / malformed value collapses to the fallback, so
|
|
601
|
+
* the renderer always writes a valid `transition` timing function.
|
|
602
|
+
*/
|
|
603
|
+
declare function resolveDragEasing(value: string | undefined | null, fallback: string): string;
|
|
604
|
+
|
|
605
|
+
export { resolveMultiSelectGroupHost as A, setLeafSizing as B, siblingSubtreeForLeaf as C, swapLeafTiles as D, tileOrderByLeafId as E, toggleLeafMultiSelection as F, type GroupLeavesOptions as G, toggleSplitAxis as H, ungroupNode as I, updateSplitRatio as J, DEFAULT_DRAG_HOP_EASING as K, DEFAULT_DRAG_REFLOW_EASING as L, MULTI_SELECT_GROUP_MIN_MEMBERS as M, type MultiSelectModifierState as N, TILING_DASHBOARD_PRESET as O, TILING_INTERACTION_CAPABILITY_DEFAULTS as P, type TilingCommandGates as Q, type TilingLayoutQuery as R, isCommandEnabled as S, TILING_KEYMAP_DEFAULTS as T, isMultiSelectModifierActive as U, queryTilingLayout as V, resolveInteractionCapabilities as W, resolveJumpedPaneId as X, type TilingKeymapActionGuards as a, chordRequiresModifier as b, canGroupMultiSelection as c, collectGroups as d, collectSplitNodes as e, commandRequiredCapability as f, findLeafByDirection as g, findLeafById as h, groupLeaves as i, hasAnyModifier as j, insertLeafAdjacent as k, isCssEasing as l, isResizeAxisEnabled as m, isStructurallyValidLayout as n, keyboardActionToCommand as o, matchKeyChord as p, matchKeymapAction as q, moveLeafToRoot as r, moveLeafToSplitContainer as s, pruneMultiSelection as t, readLeafNodeIds as u, removeLeafTile as v, resolveDragEasing as w, resolveKeymap as x, resolveMaximizeToggle as y, resolveMultiSelectGroupCommand as z };
|