@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.
@@ -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.mjs';
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 };