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.
Files changed (71) hide show
  1. package/README.md +8 -37
  2. package/package.json +1 -2
  3. package/src/core/concurrency.ts +3 -6
  4. package/src/core/frontmatter.ts +0 -53
  5. package/src/core/graph/build.ts +6 -7
  6. package/src/core/graph/current.ts +2 -4
  7. package/src/core/graph/model.ts +1 -1
  8. package/src/core/graph/wikilinks.ts +3 -3
  9. package/src/core/index.ts +26 -27
  10. package/src/core/paths.ts +0 -7
  11. package/src/core/vault.ts +16 -681
  12. package/src/core/view/detail.ts +1 -1
  13. package/src/core/view/health.ts +1 -1
  14. package/src/core/view/tree.ts +1 -1
  15. package/src/pi/index.ts +6 -85
  16. package/src/pi/summarize.ts +2 -2
  17. package/src/pi/viewer/tui/bodyStore.ts +4 -7
  18. package/src/pi/viewer/tui/branding.ts +7 -148
  19. package/src/pi/viewer/tui/run.ts +3 -17
  20. package/src/pi/viewer/tui/surface/base.ts +24 -3
  21. package/src/pi/viewer/tui/surface/explore.ts +41 -6
  22. package/src/pi/viewer/tui/workspace.ts +23 -351
  23. package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
  24. package/src/pi/viewer/web/run.ts +7 -117
  25. package/src/web/client/api.dom.ts +2 -2
  26. package/src/web/client/api.ts +14 -223
  27. package/src/web/client/bootstrap.ts +5 -14
  28. package/src/web/client/context/context.model.ts +9 -11
  29. package/src/web/client/dist/app.js +93 -219
  30. package/src/web/client/graph/dynamics.ts +5 -65
  31. package/src/web/client/graph/renderer.dom.ts +7 -8
  32. package/src/web/client/graph/renderer.ts +9 -35
  33. package/src/web/client/main.tsx +1 -1
  34. package/src/web/client/note/Note.tsx +21 -63
  35. package/src/web/client/search/SearchPalette.tsx +45 -36
  36. package/src/web/client/search/search.model.ts +33 -454
  37. package/src/web/client/shell/Columns.tsx +13 -83
  38. package/src/web/client/shell/Header.tsx +2 -10
  39. package/src/web/client/shell/Shell.tsx +50 -125
  40. package/src/web/client/shell/StatusBar.tsx +1 -4
  41. package/src/web/client/shell/icons.model.ts +4 -7
  42. package/src/web/client/shell/keys.model.ts +5 -42
  43. package/src/web/client/shell/keys.ts +2 -2
  44. package/src/web/client/shell/shell.model.ts +10 -133
  45. package/src/web/client/shell/theme.model.ts +2 -2
  46. package/src/web/client/shell/theme.ts +33 -157
  47. package/src/web/client/state.ts +9 -89
  48. package/src/web/client/tree/Tree.tsx +25 -575
  49. package/src/web/client/tree/tree.model.ts +8 -162
  50. package/src/web/client/workspace.ts +72 -242
  51. package/src/web/server/page.ts +8 -10
  52. package/src/web/server/routes.ts +30 -563
  53. package/src/web/server/server.ts +6 -145
  54. package/src/web/shared/layout.ts +72 -624
  55. package/src/web/shared/wire.ts +10 -196
  56. package/src/core/sessions.ts +0 -929
  57. package/src/pi/sessionScan.ts +0 -104
  58. package/src/pi/viewer/tui/explorer.ts +0 -586
  59. package/src/web/client/live.model.ts +0 -275
  60. package/src/web/client/live.ts +0 -151
  61. package/src/web/client/note/Editor.tsx +0 -109
  62. package/src/web/client/note/editor.controller.ts +0 -151
  63. package/src/web/client/note/editor.model.ts +0 -686
  64. package/src/web/client/search/search.ts +0 -107
  65. package/src/web/client/shell/Divider.tsx +0 -44
  66. package/src/web/client/shell/cssvars.ts +0 -70
  67. package/src/web/client/shell/drag.model.ts +0 -170
  68. package/src/web/client/shell/layout.model.ts +0 -500
  69. package/src/web/client/shell/viewport.ts +0 -29
  70. package/src/web/server/sse.ts +0 -321
  71. 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
- }