pi-weave 0.1.12 → 0.1.13
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 +8 -37
- package/package.json +1 -2
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +6 -7
- package/src/core/graph/current.ts +2 -4
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/vault.ts +16 -681
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +1 -1
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -223
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +93 -219
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +21 -63
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -83
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -125
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -157
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +25 -575
- package/src/web/client/tree/tree.model.ts +8 -162
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -563
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -196
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- package/src/web/server/watcher.ts +0 -507
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The palette's effects: a timer, a fetch, and nothing else
|
|
3
|
-
* (weave-workspace §10, P4).
|
|
4
|
-
*
|
|
5
|
-
* `search.model.ts` decides *when* to issue a request and *whether* a
|
|
6
|
-
* response may be applied; it cannot arm a timer or call `fetch` without
|
|
7
|
-
* ceasing to be pure. This module is the other half — a controller that owns
|
|
8
|
-
* exactly two capabilities, both injected:
|
|
9
|
-
*
|
|
10
|
-
* - a **clock** (`now`) and a **scheduler** (`delay`), so the 140 ms debounce
|
|
11
|
-
* is exercised by a fake in microseconds rather than waited out;
|
|
12
|
-
* - a **`FetchLike`**, the same port `api.ts` takes, so no DOM is involved.
|
|
13
|
-
*
|
|
14
|
-
* The result is that the palette's entire asynchronous behaviour — debounce,
|
|
15
|
-
* re-arm, stale-response rejection, failure — is covered by ordinary unit
|
|
16
|
-
* tests, and `SearchPalette.tsx` is a `useState`, an effect and three
|
|
17
|
-
* handlers. That is §10's split applied to the one part of the workspace that
|
|
18
|
-
* is genuinely concurrent.
|
|
19
|
-
*
|
|
20
|
-
* ## Why the controller and not the component owns the sequence number
|
|
21
|
-
*
|
|
22
|
-
* A component could hold `seq` in a ref and compare on arrival. It would also
|
|
23
|
-
* be re-created by every hot reload, remounted by every breakpoint change,
|
|
24
|
-
* and untestable. Keeping the counter inside {@link SearchState}, where the
|
|
25
|
-
* reducer can reason about it, means "an older response must not overwrite a
|
|
26
|
-
* newer one" is a property with a test rather than a comment.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import type { FetchLike } from "../api";
|
|
30
|
-
import { fetchSearch } from "../api";
|
|
31
|
-
import type { SearchEvent, SearchState, SearchTransition } from "./search.model";
|
|
32
|
-
import { initialSearchState, reduceSearch } from "./search.model";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Arms a one-shot timer and returns nothing.
|
|
36
|
-
*
|
|
37
|
-
* Deliberately *not* returning a cancel handle: the debounce in
|
|
38
|
-
* `reduceSearch` is a re-arming chain precisely so that nothing ever needs
|
|
39
|
-
* cancelling, and a cancel handle here would invite a second, competing
|
|
40
|
-
* mechanism. `(fn, ms) => void setTimeout(fn, ms)` at the call site; a queue
|
|
41
|
-
* a test drains by hand in a test.
|
|
42
|
-
*/
|
|
43
|
-
export type Scheduler = (run: () => void, ms: number) => void;
|
|
44
|
-
|
|
45
|
-
/** What {@link createSearch} needs. Every capability injected. */
|
|
46
|
-
export interface SearchOptions {
|
|
47
|
-
fetch: FetchLike;
|
|
48
|
-
/** Epoch ms. `Date.now` in the browser. */
|
|
49
|
-
now: () => number;
|
|
50
|
-
delay: Scheduler;
|
|
51
|
-
/** Called after every state change, so the component can re-render. */
|
|
52
|
-
onChange: (state: SearchState) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** A running palette controller. */
|
|
56
|
-
export interface SearchHandle {
|
|
57
|
-
/** Current state. Read at render. */
|
|
58
|
-
state(): SearchState;
|
|
59
|
-
/** The text changed. */
|
|
60
|
-
setQuery(query: string): void;
|
|
61
|
-
/** Move the cursor — pointer hover, or a key the model resolved to an index. */
|
|
62
|
-
setCursor(cursor: number): void;
|
|
63
|
-
/** The palette closed: disarm the debounce. */
|
|
64
|
-
dismiss(): void;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build the controller.
|
|
69
|
-
*
|
|
70
|
-
* One function, one loop: every event goes through {@link dispatch}, which
|
|
71
|
-
* reduces, publishes, and then performs whichever of the two effects the
|
|
72
|
-
* transition asked for. There is no other path that mutates state, which is
|
|
73
|
-
* what makes "a stale response cannot win" enforceable — the check lives in
|
|
74
|
-
* the reducer and every response arrives through here.
|
|
75
|
-
*/
|
|
76
|
-
export function createSearch(opts: SearchOptions): SearchHandle {
|
|
77
|
-
let state = initialSearchState();
|
|
78
|
-
|
|
79
|
-
const dispatch = (event: SearchEvent): void => {
|
|
80
|
-
const next: SearchTransition = reduceSearch(state, event);
|
|
81
|
-
state = next.state;
|
|
82
|
-
opts.onChange(state);
|
|
83
|
-
|
|
84
|
-
if (next.schedule !== null) opts.delay(() => dispatch({ type: "tick", now: opts.now() }), next.schedule);
|
|
85
|
-
|
|
86
|
-
const request = next.request;
|
|
87
|
-
if (request === null) return;
|
|
88
|
-
// Fire and forget: `api.ts` returns failures as values, so there is
|
|
89
|
-
// nothing to reject and nothing to catch. `void` documents the floating
|
|
90
|
-
// promise rather than hiding it — the same shape `workspace.ts` uses.
|
|
91
|
-
void (async () => {
|
|
92
|
-
const result = await fetchSearch(opts.fetch, request.query);
|
|
93
|
-
dispatch(
|
|
94
|
-
result.ok
|
|
95
|
-
? { type: "response", seq: request.seq, query: request.query, hits: result.data.hits }
|
|
96
|
-
: { type: "failed", seq: request.seq },
|
|
97
|
-
);
|
|
98
|
-
})();
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
state: () => state,
|
|
103
|
-
setQuery: (query) => dispatch({ type: "query", query, now: opts.now() }),
|
|
104
|
-
setCursor: (cursor) => dispatch({ type: "cursor", cursor }),
|
|
105
|
-
dismiss: () => dispatch({ type: "dismiss" }),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A drag handle between two columns (weave-workspace §1.2).
|
|
3
|
-
*
|
|
4
|
-
* All four handlers forward straight to callbacks the shell supplies; the
|
|
5
|
-
* arithmetic is in `drag.model.ts` and `layout.model.ts`. The ARIA role is
|
|
6
|
-
* `separator` with `tabindex={0}` so the divider is reachable and operable
|
|
7
|
-
* from the keyboard, which §11's P4 requires of the whole workspace and which
|
|
8
|
-
* is far cheaper to do now than to retrofit.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { DividerId } from "./layout.model";
|
|
12
|
-
|
|
13
|
-
export interface DividerProps {
|
|
14
|
-
id: DividerId;
|
|
15
|
-
label: string;
|
|
16
|
-
onDown: (clientX: number, pointerId: number) => void;
|
|
17
|
-
onMove: (clientX: number) => void;
|
|
18
|
-
onUp: () => void;
|
|
19
|
-
onKey: (key: string) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Divider(props: DividerProps) {
|
|
23
|
-
return (
|
|
24
|
-
<div
|
|
25
|
-
class="weave-divider"
|
|
26
|
-
role="separator"
|
|
27
|
-
aria-orientation="vertical"
|
|
28
|
-
aria-label={props.label}
|
|
29
|
-
tabIndex={0}
|
|
30
|
-
onPointerDown={(event) => {
|
|
31
|
-
// Capture, so a fast drag that outruns the 1 px handle keeps
|
|
32
|
-
// delivering moves here instead of to whatever is under the pointer.
|
|
33
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
34
|
-
props.onDown(event.clientX, event.pointerId);
|
|
35
|
-
}}
|
|
36
|
-
onPointerMove={(event) => props.onMove(event.clientX)}
|
|
37
|
-
onPointerUp={(event) => {
|
|
38
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
39
|
-
props.onUp();
|
|
40
|
-
}}
|
|
41
|
-
onKeyDown={(event) => props.onKey(event.key)}
|
|
42
|
-
/>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Applying computed layout widths to an element, under a strict CSP
|
|
3
|
-
* (weave-workspace §5.2).
|
|
4
|
-
*
|
|
5
|
-
* ## The constraint, stated precisely
|
|
6
|
-
*
|
|
7
|
-
* `page.ts` serves `style-src 'nonce-{N}'` with **no `'unsafe-inline'`**. In
|
|
8
|
-
* CSP terms that blocks a literal `style="…"` *attribute in the markup the
|
|
9
|
-
* parser sees*. It does **not** block mutation of an element's
|
|
10
|
-
* `CSSStyleDeclaration` from script: `el.style.setProperty(...)`,
|
|
11
|
-
* `el.style.width = …` and `el.style.cssText = …` are CSSOM writes, and CSP
|
|
12
|
-
* has no hook on them. This is why a strict-CSP app can still have dynamic
|
|
13
|
-
* layout at all.
|
|
14
|
-
*
|
|
15
|
-
* Preact reaches the same place: `setProperty` in `preact/src/diff/props.js`
|
|
16
|
-
* handles a `style` prop by assigning `dom.style.cssText` for a string, or by
|
|
17
|
-
* `style.setProperty` / `style[key] = …` for an object. It never calls
|
|
18
|
-
* `setAttribute("style", …)`. So a `style={{…}}` prop in a component would in
|
|
19
|
-
* fact survive the CSP — verified in the installed source, not assumed.
|
|
20
|
-
*
|
|
21
|
-
* We still do not use one. The widths are written as **custom properties** by
|
|
22
|
-
* this module, and the nonce'd stylesheet owns the `grid-template-columns`
|
|
23
|
-
* rule that consumes them. The reason is not CSP but ownership: a `style`
|
|
24
|
-
* prop would put a second layout implementation in the bundle, competing with
|
|
25
|
-
* the one in CSS, and the browser's devtools would show a computed width with
|
|
26
|
-
* no rule to trace it back to. One number per column in, one rule in CSS —
|
|
27
|
-
* that is the whole contribution.
|
|
28
|
-
*
|
|
29
|
-
* ## Why the element is a parameter and the API is one function
|
|
30
|
-
*
|
|
31
|
-
* There is no DOM test environment (§10). Everything *decidable* about the
|
|
32
|
-
* widths already lives in `layout.model.ts` as pure functions returning
|
|
33
|
-
* `[name, value]` pairs; what remains here is a loop that hands those pairs
|
|
34
|
-
* to a `setProperty` this module does not own. The port below is that method
|
|
35
|
-
* and nothing else, so the untestable surface is one line long and a fake is
|
|
36
|
-
* an object literal.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The slice of `CSSStyleDeclaration` used to apply a layout.
|
|
41
|
-
*
|
|
42
|
-
* A real `HTMLElement.style` satisfies it structurally. Declared here rather
|
|
43
|
-
* than imported from the DOM lib because this module is compiled by the root
|
|
44
|
-
* `tsconfig.json` when a test imports it, and that project has no `DOM` lib.
|
|
45
|
-
*/
|
|
46
|
-
export interface StyleTarget {
|
|
47
|
-
setProperty(property: string, value: string): void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** An element with a `style`. `HTMLElement` satisfies it. */
|
|
51
|
-
export interface StyledElement {
|
|
52
|
-
readonly style: StyleTarget;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Write custom properties onto an element.
|
|
57
|
-
*
|
|
58
|
-
* Tolerates a `null` element so the caller — a `useLayoutEffect` holding a
|
|
59
|
-
* ref — does not need a guard of its own. A ref is `null` on the render
|
|
60
|
-
* before the element exists and on the one after it is removed, and both are
|
|
61
|
-
* ordinary rather than exceptional.
|
|
62
|
-
*
|
|
63
|
-
* Returns the number of properties written, which is what makes the function
|
|
64
|
-
* observable in a test without a DOM: `0` for a null element, `n` otherwise.
|
|
65
|
-
*/
|
|
66
|
-
export function applyVars(element: StyledElement | null, vars: readonly (readonly [string, string])[]): number {
|
|
67
|
-
if (element === null) return 0;
|
|
68
|
-
for (const [name, value] of vars) element.style.setProperty(name, value);
|
|
69
|
-
return vars.length;
|
|
70
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Divider dragging, as pure state (weave-workspace §1.2).
|
|
3
|
-
*
|
|
4
|
-
* `layout.model.ts` already owns the arithmetic — {@link resizeAt} converts a
|
|
5
|
-
* pixel delta into clamped, normalised fractions. What it does not own is the
|
|
6
|
-
* *gesture*: where the pointer went down, how far it has travelled since, and
|
|
7
|
-
* when the result should be persisted. That is this file, and it is separate
|
|
8
|
-
* for the usual reason — a gesture living inside `Divider.tsx` would be
|
|
9
|
-
* untestable without a DOM (§10), and a drag that mis-clamps is exactly the
|
|
10
|
-
* bug a unit test catches and a glance at the screen does not.
|
|
11
|
-
*
|
|
12
|
-
* ## Deltas are measured from the gesture's origin, never frame to frame
|
|
13
|
-
*
|
|
14
|
-
* A `pointermove` handler that applied `event.movementX` to the current
|
|
15
|
-
* layout would accumulate error: `resizeAt` clamps, so any movement the clamp
|
|
16
|
-
* swallowed is lost, and the divider then trails the pointer by however much
|
|
17
|
-
* was discarded. Storing the layout as it was at `pointerdown` and applying
|
|
18
|
-
* the *total* offset each time makes the divider track the pointer exactly,
|
|
19
|
-
* and makes dragging into a wall and back out again return to where it
|
|
20
|
-
* started rather than to wherever the drift left it.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { DividerId, LayoutState } from "./layout.model";
|
|
24
|
-
import { resizeAt } from "./layout.model";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* A gesture in progress.
|
|
28
|
-
*
|
|
29
|
-
* `origin` is the pointer x at `pointerdown`; `base` is the layout as it was
|
|
30
|
-
* at that instant. Both are needed to satisfy the total-offset rule above.
|
|
31
|
-
*/
|
|
32
|
-
export interface DragState {
|
|
33
|
-
readonly divider: DividerId;
|
|
34
|
-
readonly origin: number;
|
|
35
|
-
readonly base: LayoutState;
|
|
36
|
-
/** `setPointerCapture` id, so the shell can release exactly this pointer. */
|
|
37
|
-
readonly pointerId: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Begin a drag. */
|
|
41
|
-
export function beginDrag(divider: DividerId, clientX: number, layout: LayoutState, pointerId: number): DragState {
|
|
42
|
-
return { divider, origin: clientX, base: layout, pointerId };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The layout for a pointer position during a drag.
|
|
47
|
-
*
|
|
48
|
-
* Always derived from `base`, never from the previous frame — see the module
|
|
49
|
-
* header. `resizeAt` returns its input identically when nothing moved, so a
|
|
50
|
-
* pointer jittering by a subpixel at a clamped edge produces the same object
|
|
51
|
-
* and wakes no signal subscribers.
|
|
52
|
-
*/
|
|
53
|
-
export function dragTo(drag: DragState, clientX: number, available: number): LayoutState {
|
|
54
|
-
return resizeAt(drag.base, drag.divider, clientX - drag.origin, available);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Whether a completed drag actually changed anything.
|
|
59
|
-
*
|
|
60
|
-
* The persistence trigger. A click on a divider with no movement is a
|
|
61
|
-
* `pointerdown`/`pointerup` pair that should not write to `localStorage`, and
|
|
62
|
-
* a drag that was entirely absorbed by the clamp should not either. Compares
|
|
63
|
-
* the three fractions rather than object identity, because `resizeAt`
|
|
64
|
-
* normalises and can return an equal-but-distinct object.
|
|
65
|
-
*/
|
|
66
|
-
export function dragChanged(drag: DragState, final: LayoutState): boolean {
|
|
67
|
-
const a = drag.base.fractions;
|
|
68
|
-
const b = final.fractions;
|
|
69
|
-
return a.tree !== b.tree || a.note !== b.note || a.graph !== b.graph;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The keyboard nudge, in pixels.
|
|
74
|
-
*
|
|
75
|
-
* A divider is a `separator` with `tabindex`, so it must be operable from the
|
|
76
|
-
* keyboard — §11's P4 makes the whole workspace keyboard-drivable, and a
|
|
77
|
-
* control that can only be dragged is one that has to be retrofitted then. 24
|
|
78
|
-
* px is a visible step without being a jump.
|
|
79
|
-
*/
|
|
80
|
-
export const NUDGE_PX = 24;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Map an arrow key to a signed nudge, or `0` for any other key.
|
|
84
|
-
*
|
|
85
|
-
* Returning `0` rather than `null` lets the caller feed the result straight
|
|
86
|
-
* into `resizeAt`, which already treats a zero delta as "return the state
|
|
87
|
-
* unchanged" — one branch instead of two, and the one that exists is already
|
|
88
|
-
* covered.
|
|
89
|
-
*/
|
|
90
|
-
export function nudgeFor(key: string): number {
|
|
91
|
-
if (key === "ArrowLeft") return -NUDGE_PX;
|
|
92
|
-
if (key === "ArrowRight") return NUDGE_PX;
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// --- the gesture, as a unit -----------------------------------------------------
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* What {@link dividerHandlers} needs from the component around it.
|
|
100
|
-
*
|
|
101
|
-
* Accessors rather than values, because a handler installed on one render
|
|
102
|
-
* must see the layout as it is when the pointer moves, not as it was when the
|
|
103
|
-
* closure was built. A stale `layout` here is the classic React/Preact
|
|
104
|
-
* gesture bug: the drag applies to a snapshot and the divider jumps back on
|
|
105
|
-
* the next render.
|
|
106
|
-
*/
|
|
107
|
-
export interface DragHost {
|
|
108
|
-
layout(): LayoutState;
|
|
109
|
-
/** Container width in CSS pixels. */
|
|
110
|
-
width(): number;
|
|
111
|
-
/** Publish a new layout (a `setState`). */
|
|
112
|
-
setLayout(next: LayoutState): void;
|
|
113
|
-
/** Persist a layout. Called on release and on a keyboard nudge, never per frame. */
|
|
114
|
-
persist(layout: LayoutState): void;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** The four callbacks a {@link Divider} needs. */
|
|
118
|
-
export interface DividerHandlers {
|
|
119
|
-
onDown(divider: DividerId, clientX: number, pointerId: number): void;
|
|
120
|
-
onMove(clientX: number): void;
|
|
121
|
-
onUp(): void;
|
|
122
|
-
onKey(divider: DividerId, key: string): void;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Build the divider gesture handlers over a host.
|
|
127
|
-
*
|
|
128
|
-
* This lives here rather than as four `useCallback`s in `Shell.tsx` for the
|
|
129
|
-
* reason §10 gives: the ordering rules they encode are real logic — persist
|
|
130
|
-
* on release but not per frame, ignore a move with no gesture in progress,
|
|
131
|
-
* persist a keyboard nudge immediately because there is no release to wait
|
|
132
|
-
* for — and logic inside a `.tsx` is logic no test can reach. The component
|
|
133
|
-
* is left holding a ref and a `setState`.
|
|
134
|
-
*
|
|
135
|
-
* The mutable gesture is kept in the closure rather than in component state
|
|
136
|
-
* on purpose: a `pointermove` at 120 Hz writing to `useState` would rerender
|
|
137
|
-
* the whole shell on every frame to store a value nothing renders.
|
|
138
|
-
*/
|
|
139
|
-
export function dividerHandlers(host: DragHost): DividerHandlers {
|
|
140
|
-
let active: DragState | null = null;
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
onDown(divider, clientX, pointerId) {
|
|
144
|
-
active = beginDrag(divider, clientX, host.layout(), pointerId);
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
onMove(clientX) {
|
|
148
|
-
// No gesture in progress: a plain hover over the divider, which fires
|
|
149
|
-
// `pointermove` just as a drag does.
|
|
150
|
-
if (active !== null) host.setLayout(dragTo(active, clientX, host.width()));
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
onUp() {
|
|
154
|
-
const finished = active;
|
|
155
|
-
active = null;
|
|
156
|
-
// Persisted only if something actually moved — a click on a divider,
|
|
157
|
-
// and a drag entirely absorbed by the clamp, both write nothing.
|
|
158
|
-
if (finished !== null && dragChanged(finished, host.layout())) host.persist(host.layout());
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
onKey(divider, key) {
|
|
162
|
-
const next = resizeAt(host.layout(), divider, nudgeFor(key), host.width());
|
|
163
|
-
// `resizeAt` returns its input identically for an unhandled key, so a
|
|
164
|
-
// `Tab` or an `Enter` on a focused divider costs nothing.
|
|
165
|
-
if (next === host.layout()) return;
|
|
166
|
-
host.setLayout(next);
|
|
167
|
-
host.persist(next);
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|