pi-weave 0.1.11 → 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 +148 -132
- package/package.json +1 -2
- package/src/core/cache/workspace.ts +26 -9
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +12 -8
- package/src/core/graph/current.ts +4 -6
- 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/slug.ts +13 -0
- package/src/core/types.ts +1 -0
- package/src/core/vault.ts +45 -467
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +15 -3
- 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 -176
- 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 +77 -167
- 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 +37 -60
- package/src/web/client/note/note.model.ts +23 -0
- 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 -81
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -123
- 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 -121
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +24 -97
- package/src/web/client/tree/tree.model.ts +8 -56
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -415
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -178
- 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,56 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The three-column grid
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The one interesting line is the `useLayoutEffect`: column widths reach the
|
|
6
|
-
* DOM as **custom properties**, written through `applyVars` →
|
|
7
|
-
* `style.setProperty`. That is the CSSOM path, which `style-src 'nonce-…'`
|
|
8
|
-
* does not govern — see `cssvars.ts` for the verified reasoning — and it
|
|
9
|
-
* leaves the actual `grid-template-columns` rule in the nonce'd stylesheet
|
|
10
|
-
* where it can be read.
|
|
11
|
-
*
|
|
12
|
-
* `useLayoutEffect` rather than `useEffect` so the widths land before paint;
|
|
13
|
-
* with `useEffect` the first frame renders at whatever the CSS fallback says
|
|
14
|
-
* and then jumps.
|
|
15
|
-
*
|
|
16
|
-
* The column/divider pairing is {@link columnSlots}, not an index check here:
|
|
17
|
-
* it is breakpoint-sensitive and therefore worth a test.
|
|
18
|
-
*
|
|
19
|
-
* All three columns are built as of P3, so {@link Column} dispatches on the
|
|
20
|
-
* column id. That ternary chain is the one piece of branching in the file and
|
|
21
|
-
* it is a *routing* decision, not a product one: each arm is a bare element,
|
|
22
|
-
* and everything those components then decide lives in their own `.model.ts`.
|
|
2
|
+
* The fixed three-column grid and context rail. CSS owns responsive hiding;
|
|
3
|
+
* keeping all three surfaces in the DOM preserves keyboard focus targets when
|
|
4
|
+
* the viewport changes without a resize listener or persisted layout state.
|
|
23
5
|
*/
|
|
24
6
|
|
|
25
|
-
import { Fragment } from "preact";
|
|
26
|
-
import { useLayoutEffect, useRef } from "preact/hooks";
|
|
27
7
|
import { Graph } from "../graph/Graph";
|
|
28
8
|
import type { ColorScheme } from "../graph/graph.model";
|
|
29
9
|
import type { PositionStorage } from "../graph/positions";
|
|
30
10
|
import type { RendererFactory } from "../graph/renderer";
|
|
31
11
|
import type { SchemeHost } from "../graph/scheme";
|
|
32
|
-
import type { EditorEvent, EditorPrompt, EditorToolbar } from "../note/editor.model";
|
|
33
12
|
import { Note } from "../note/Note";
|
|
34
13
|
import { Tree } from "../tree/Tree";
|
|
35
14
|
import type { GraphPayload, NotePayload } from "../../shared/wire";
|
|
36
|
-
import {
|
|
37
|
-
import type { ColumnId, DividerId, ResolvedColumn } from "./layout.model";
|
|
38
|
-
import { columnVars } from "./layout.model";
|
|
15
|
+
import type { ColumnId } from "./shell.model";
|
|
39
16
|
import { ContextRail } from "./ContextRail";
|
|
40
|
-
import {
|
|
41
|
-
import type { ColumnSlot } from "./shell.model";
|
|
42
|
-
import { columnSlots, emptyStateFor } from "./shell.model";
|
|
17
|
+
import { emptyStateFor } from "./shell.model";
|
|
43
18
|
|
|
44
19
|
export interface ColumnsProps {
|
|
45
|
-
resolved: readonly ResolvedColumn[];
|
|
46
|
-
onDown: (divider: DividerId, clientX: number, pointerId: number) => void;
|
|
47
|
-
onMove: (clientX: number) => void;
|
|
48
|
-
onUp: () => void;
|
|
49
|
-
onKey: (divider: DividerId, key: string) => void;
|
|
50
20
|
/** The §1.3 context bus, as the columns see it. */
|
|
51
21
|
graph: GraphPayload | null;
|
|
52
22
|
note: NotePayload | null;
|
|
53
23
|
selectedId: string | null;
|
|
24
|
+
recentIds: ReadonlySet<string>;
|
|
54
25
|
/**
|
|
55
26
|
* The §1.3 context bus.
|
|
56
27
|
*
|
|
@@ -61,6 +32,7 @@ export interface ColumnsProps {
|
|
|
61
32
|
* a handler accepting the wider type takes without a cast.
|
|
62
33
|
*/
|
|
63
34
|
onSelect: (id: string | null) => void;
|
|
35
|
+
onOpen: (slug: string) => void;
|
|
64
36
|
/** Epoch ms for relative times, read once per render by the shell. */
|
|
65
37
|
now: number;
|
|
66
38
|
/**
|
|
@@ -86,18 +58,6 @@ export interface ColumnsProps {
|
|
|
86
58
|
bootFailed: boolean;
|
|
87
59
|
/** Slot the graph column fills with its `fit`, for the global `g` key. */
|
|
88
60
|
fit: { current: (() => void) | null };
|
|
89
|
-
/**
|
|
90
|
-
* The note editor's view model (§11 P5).
|
|
91
|
-
*
|
|
92
|
-
* Derived by the shell from the one `EditorHandle`, because the editor's
|
|
93
|
-
* state outlives the note column's mount: below 800 px the column can be
|
|
94
|
-
* unmounted by a resize, and an editor owned by the component would lose
|
|
95
|
-
* an unsaved draft to a window drag.
|
|
96
|
-
*/
|
|
97
|
-
toolbar: EditorToolbar | null;
|
|
98
|
-
prompt: EditorPrompt | null;
|
|
99
|
-
draft: string;
|
|
100
|
-
send: (event: EditorEvent) => void;
|
|
101
61
|
}
|
|
102
62
|
|
|
103
63
|
/** One column: a titled region and whichever surface fills it. */
|
|
@@ -106,7 +66,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
106
66
|
return (
|
|
107
67
|
<section class={`weave-col weave-col-${id}`} aria-label={copy.title}>
|
|
108
68
|
<h2 class="weave-col-title">{copy.title}</h2>
|
|
109
|
-
{id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} onSelect={props.onSelect} now={props.now} /> : null}
|
|
69
|
+
{id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} recentIds={props.recentIds} onSelect={props.onSelect} now={props.now} /> : null}
|
|
110
70
|
{id === "note" ? (
|
|
111
71
|
<Note
|
|
112
72
|
note={props.note}
|
|
@@ -114,10 +74,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
114
74
|
selectedId={props.selectedId}
|
|
115
75
|
onSelect={props.onSelect}
|
|
116
76
|
now={props.now}
|
|
117
|
-
|
|
118
|
-
prompt={props.prompt}
|
|
119
|
-
draft={props.draft}
|
|
120
|
-
send={props.send}
|
|
77
|
+
onOpen={props.onOpen}
|
|
121
78
|
/>
|
|
122
79
|
) : null}
|
|
123
80
|
{id === "graph" ? (
|
|
@@ -138,37 +95,12 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
138
95
|
);
|
|
139
96
|
}
|
|
140
97
|
|
|
141
|
-
/** A column and, where one follows it, its divider. */
|
|
142
|
-
function Slot({ slot, props }: { slot: ColumnSlot; props: ColumnsProps }) {
|
|
143
|
-
const divider = slot.divider;
|
|
144
|
-
return (
|
|
145
|
-
<Fragment>
|
|
146
|
-
<Column id={slot.column.id} props={props} />
|
|
147
|
-
{divider === null ? null : (
|
|
148
|
-
<Divider
|
|
149
|
-
id={divider}
|
|
150
|
-
label={`Resize ${slot.column.id} column`}
|
|
151
|
-
onDown={(x, pointerId) => props.onDown(divider, x, pointerId)}
|
|
152
|
-
onMove={props.onMove}
|
|
153
|
-
onUp={props.onUp}
|
|
154
|
-
onKey={(key) => props.onKey(divider, key)}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
</Fragment>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
98
|
export function Columns(props: ColumnsProps) {
|
|
162
|
-
const grid = useRef<HTMLDivElement | null>(null);
|
|
163
|
-
useLayoutEffect(() => {
|
|
164
|
-
applyVars(grid.current, columnVars(props.resolved));
|
|
165
|
-
}, [props.resolved]);
|
|
166
|
-
|
|
167
99
|
return (
|
|
168
|
-
<div class="weave-grid"
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
100
|
+
<div class="weave-grid">
|
|
101
|
+
<Column id="tree" props={props} />
|
|
102
|
+
<Column id="note" props={props} />
|
|
103
|
+
<Column id="graph" props={props} />
|
|
172
104
|
</div>
|
|
173
105
|
);
|
|
174
106
|
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* The header bar (weave-workspace §1.2).
|
|
3
3
|
*
|
|
4
4
|
* Title, the `⌘K` search button, the `vault:N · repo:… · N nodes` summary,
|
|
5
|
-
* the refresh button
|
|
6
|
-
* comes from `shell.model.ts`.
|
|
5
|
+
* the refresh button. Every string comes from `shell.model.ts`.
|
|
7
6
|
*
|
|
8
7
|
* The search control was `disabled` through P1–P3 because search was P4 and
|
|
9
8
|
* an affordance that looks live but is not would be the dishonest option. P4
|
|
@@ -16,12 +15,11 @@
|
|
|
16
15
|
import { useState } from "preact/hooks";
|
|
17
16
|
import { LOGO_MARK_B64, LOGO_MARK_MIME } from "../../shared/logo";
|
|
18
17
|
import type { ThemeButtonView } from "./theme.model";
|
|
19
|
-
import type {
|
|
18
|
+
import type { HeaderSummary } from "./shell.model";
|
|
20
19
|
import { REFRESH_ICON_PATHS, SEARCH_PLACEHOLDER, searchHint, summaryParts } from "./shell.model";
|
|
21
20
|
|
|
22
21
|
export interface HeaderProps {
|
|
23
22
|
summary: HeaderSummary;
|
|
24
|
-
connection: ConnectionView;
|
|
25
23
|
shortcut: string;
|
|
26
24
|
onRefresh: () => void;
|
|
27
25
|
/** Opens the ⌘K palette. The same action the global key performs. */
|
|
@@ -116,12 +114,6 @@ export function Header(props: HeaderProps) {
|
|
|
116
114
|
{props.theme.glyph}
|
|
117
115
|
</button>
|
|
118
116
|
<RefreshButton onRefresh={props.onRefresh} />
|
|
119
|
-
<span class={`weave-conn weave-conn-${props.connection.tone}`} title={props.connection.hint}>
|
|
120
|
-
<span class="weave-conn-dot" aria-hidden="true">
|
|
121
|
-
●
|
|
122
|
-
</span>
|
|
123
|
-
{props.connection.label}
|
|
124
|
-
</span>
|
|
125
117
|
</header>
|
|
126
118
|
);
|
|
127
119
|
}
|
|
@@ -1,62 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The workspace shell
|
|
2
|
+
* The workspace shell.
|
|
3
3
|
*
|
|
4
|
-
* Header, three
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and the fetch/
|
|
8
|
-
*
|
|
4
|
+
* Header, three fixed columns, context rail, status bar. This component holds
|
|
5
|
+
* the wiring and nothing else — every value it renders comes from pure
|
|
6
|
+
* functions in the shell models,
|
|
7
|
+
* and the fetch/poll loop is `workspace.ts`. What is left here is hooks:
|
|
8
|
+
* state in, callbacks out.
|
|
9
9
|
*
|
|
10
|
-
* The
|
|
10
|
+
* The main effects are one-liners over injected units:
|
|
11
11
|
*
|
|
12
|
-
* 1. **mount** — `startWorkspace` fetches the graph and
|
|
13
|
-
* the returned `stop` is the cleanup, so a hot reload cannot leak a
|
|
14
|
-
*
|
|
15
|
-
* 2. **resize** — `watchViewport` keeps `width` current, because the width
|
|
16
|
-
* picks the breakpoint, which decides how many columns exist.
|
|
17
|
-
* 3. **keys** — `watchKeys` attaches the one global `keydown` listener
|
|
12
|
+
* 1. **mount** — `startWorkspace` fetches the graph and starts polling;
|
|
13
|
+
* the returned `stop` is the cleanup, so a hot reload cannot leak a timer.
|
|
14
|
+
* 2. **keys** — `watchKeys` attaches the one global `keydown` listener
|
|
18
15
|
* (§11 P4). Its context is read through `live`, because a listener
|
|
19
16
|
* registered at mount outlives every render and a captured overlay flag
|
|
20
17
|
* would let `⌘K` stack a palette on top of itself.
|
|
21
|
-
*
|
|
22
|
-
* Layout persistence is not an effect: `dividerHandlers` writes to
|
|
23
|
-
* `localStorage` on release and on a keyboard nudge, never per frame.
|
|
24
18
|
*/
|
|
25
19
|
|
|
26
|
-
import { useEffect,
|
|
20
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
27
21
|
import { fetchJson } from "../api.dom";
|
|
22
|
+
import { openNote } from "../api";
|
|
28
23
|
import type { ColorScheme } from "../graph/graph.model";
|
|
29
24
|
import { schemeOf, watchScheme } from "../graph/scheme";
|
|
30
25
|
import { createSigmaRenderer } from "../graph/renderer.dom";
|
|
31
|
-
import { domEventSource } from "../live";
|
|
32
|
-
import { createEditor, watchUnload, type EditorHandle } from "../note/editor.controller";
|
|
33
|
-
import { editorPrompt, editorToolbar, initialEditorState, shouldBlockUnload } from "../note/editor.model";
|
|
34
26
|
import { SearchPalette } from "../search/SearchPalette";
|
|
35
27
|
import { restoreSelection, saveSelection } from "../selection.storage";
|
|
36
|
-
import {
|
|
28
|
+
import { initialWorkspaceState } from "../state";
|
|
29
|
+
import type { WorkspaceState } from "../state";
|
|
37
30
|
import type { WorkspaceHandle } from "../workspace";
|
|
38
|
-
import {
|
|
31
|
+
import { startWorkspace } from "../workspace";
|
|
39
32
|
import { Columns } from "./Columns";
|
|
40
33
|
import { deeplinkSelection, formatHash } from "./deeplink.model";
|
|
41
|
-
import { dividerHandlers } from "./drag.model";
|
|
42
34
|
import { Header } from "./Header";
|
|
43
35
|
import { HelpOverlay } from "./HelpOverlay";
|
|
44
36
|
import { watchKeys } from "./keys";
|
|
45
37
|
import { focusSelector, runShellAction } from "./keys.model";
|
|
46
|
-
import type { LayoutState } from "./layout.model";
|
|
47
|
-
import { breakpointFor, loadLayout, resolveColumns, saveLayout } from "./layout.model";
|
|
48
38
|
import { StatusBar } from "./StatusBar";
|
|
49
39
|
import type { OverlayId } from "./shell.model";
|
|
50
|
-
import { TICK_MS,
|
|
40
|
+
import { TICK_MS, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
|
|
51
41
|
import { cycleTheme, effectiveScheme, loadTheme, saveTheme, themeAttr, themeButton } from "./theme.model";
|
|
52
42
|
import type { ThemeChoice } from "./theme.model";
|
|
53
|
-
import { watchViewport } from "./viewport";
|
|
54
43
|
|
|
55
44
|
/**
|
|
56
45
|
* Write the choice onto `<html>`, so the sheet's attribute branch and the
|
|
57
|
-
* media query agree on who is in charge (see `theme.model.ts`'s header).
|
|
58
|
-
* one DOM write the theme needs, and it is here rather than in the model for
|
|
59
|
-
* the same reason `applyVars` is: it is an effect, not a decision.
|
|
46
|
+
* media query agree on who is in charge (see `theme.model.ts`'s header).
|
|
60
47
|
*/
|
|
61
48
|
function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
|
|
62
49
|
if (choice === null) delete document.documentElement.dataset.weaveTheme;
|
|
@@ -66,17 +53,13 @@ function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
|
|
|
66
53
|
export interface ShellProps {
|
|
67
54
|
/** From the page bootstrap. Shown in the status bar. */
|
|
68
55
|
cwd: string;
|
|
69
|
-
/** `window.innerWidth` at mount. Injected so the first render is testable. */
|
|
70
|
-
initialWidth: number;
|
|
71
56
|
/** `navigator.platform`, for the `⌘K` vs `Ctrl K` hint. */
|
|
72
57
|
platform: string;
|
|
73
58
|
}
|
|
74
59
|
|
|
75
60
|
export function Shell(props: ShellProps) {
|
|
76
|
-
const [
|
|
61
|
+
const [workspaceState, setWorkspaceState] = useState<WorkspaceState>(initialWorkspaceState);
|
|
77
62
|
const [overlay, setOverlay] = useState<OverlayId>(null);
|
|
78
|
-
const [layout, setLayout] = useState<LayoutState>(() => loadLayout(localStorage, props.initialWidth));
|
|
79
|
-
const [editorState, setEditorState] = useState(initialEditorState);
|
|
80
63
|
// The theme: what the user picked, and what the OS is currently saying. Two
|
|
81
64
|
// states because they answer different questions — `theme` changes on a
|
|
82
65
|
// button press or the `t` key, `systemScheme` on an OS flip while the user
|
|
@@ -87,21 +70,13 @@ export function Shell(props: ShellProps) {
|
|
|
87
70
|
// Filled by the graph column at mount, cleared on unmount. The global `g`
|
|
88
71
|
// key's only route to the renderer — see `Graph.tsx`'s `fit` prop.
|
|
89
72
|
const fit = useRef<(() => void) | null>(null);
|
|
90
|
-
// The
|
|
91
|
-
//
|
|
92
|
-
const live = useRef({
|
|
93
|
-
live.current = {
|
|
94
|
-
|
|
95
|
-
// Built once and owned by the shell, not by the note column. The column is
|
|
96
|
-
// unmounted by a resize below 800 px, and an editor whose lifetime was the
|
|
97
|
-
// column's would lose an unsaved draft to a window drag.
|
|
98
|
-
const editor: EditorHandle = useMemo(
|
|
99
|
-
() => createEditor({ fetch: fetchJson, select: (id) => void select(fetchJson, id), onChange: setEditorState }),
|
|
100
|
-
[],
|
|
101
|
-
);
|
|
73
|
+
// The global key listener reads through this so a handler registered at
|
|
74
|
+
// mount still sees the current overlay.
|
|
75
|
+
const live = useRef({ overlay, selectedId: workspaceState.selectedId });
|
|
76
|
+
live.current = { overlay, selectedId: workspaceState.selectedId };
|
|
102
77
|
|
|
103
78
|
useEffect(() => {
|
|
104
|
-
const handle = startWorkspace({ fetch: fetchJson,
|
|
79
|
+
const handle = startWorkspace({ fetch: fetchJson, state: workspaceState, setState: setWorkspaceState });
|
|
105
80
|
workspace.current = handle;
|
|
106
81
|
return () => handle.stop();
|
|
107
82
|
}, []);
|
|
@@ -116,43 +91,31 @@ export function Shell(props: ShellProps) {
|
|
|
116
91
|
const bootHash = useRef(location.hash);
|
|
117
92
|
useEffect(() => {
|
|
118
93
|
if (!selectionRestored.current) return;
|
|
119
|
-
saveSelection(localStorage, selectedId
|
|
94
|
+
saveSelection(localStorage, workspaceState.selectedId);
|
|
120
95
|
// replaceState, not pushState: the address bar mirrors the note on
|
|
121
96
|
// screen, and reading three notes is not three history entries. The
|
|
122
97
|
// *string* is the model's (`formatHash`); this is the write itself.
|
|
123
|
-
history.replaceState(null, "", formatHash(selectedId
|
|
124
|
-
}, [selectedId
|
|
98
|
+
history.replaceState(null, "", formatHash(workspaceState.selectedId));
|
|
99
|
+
}, [workspaceState.selectedId]);
|
|
125
100
|
useEffect(() => {
|
|
126
|
-
if (selectionRestored.current || graph
|
|
101
|
+
if (selectionRestored.current || workspaceState.graph === null) return;
|
|
127
102
|
selectionRestored.current = true;
|
|
128
|
-
if (selectedId
|
|
103
|
+
if (workspaceState.selectedId !== null) return;
|
|
129
104
|
// A link wins over storage: the address bar is an explicit instruction,
|
|
130
105
|
// the saved note is a habit. A link to a note the graph does not hold is
|
|
131
106
|
// refused (see `deeplink.model.ts`) and continuity falls through.
|
|
132
|
-
const linked = deeplinkSelection(bootHash.current, graph
|
|
133
|
-
if (linked !== null) void select(
|
|
107
|
+
const linked = deeplinkSelection(bootHash.current, workspaceState.graph);
|
|
108
|
+
if (linked !== null) void workspace.current?.select(linked);
|
|
134
109
|
else {
|
|
135
|
-
const saved = restoreSelection(graph
|
|
136
|
-
if (saved !== null) void select(
|
|
110
|
+
const saved = restoreSelection(workspaceState.graph, localStorage);
|
|
111
|
+
if (saved !== null) void workspace.current?.select(saved);
|
|
137
112
|
}
|
|
138
|
-
}, [graph
|
|
139
|
-
|
|
140
|
-
// Every note that arrives, from any of the three directions it can arrive
|
|
141
|
-
// from (mount, selection, SSE refetch), so the editor can decide whether it
|
|
142
|
-
// is news or an interruption — see `editor.model.ts`'s header.
|
|
143
|
-
useEffect(() => observeNotes((payload) => editor.send({ type: "loaded", payload })), []);
|
|
144
|
-
|
|
145
|
-
// Read through a thunk, not captured: a listener registered at mount
|
|
146
|
-
// outlives every render, so a captured state would always look clean and
|
|
147
|
-
// the guard would be installed but inert.
|
|
148
|
-
useEffect(() => watchUnload(window, () => shouldBlockUnload(editor.state())), []);
|
|
149
|
-
|
|
150
|
-
useEffect(() => watchViewport(window, setWidth), []);
|
|
113
|
+
}, [workspaceState.graph]);
|
|
151
114
|
|
|
152
115
|
// The relative-time clock. Every "8h ago" in the tree and the note meta is
|
|
153
116
|
// computed from a `now` stamped per render, and a resting workspace never
|
|
154
117
|
// re-renders on its own — so the minutes used to go stale until the next
|
|
155
|
-
//
|
|
118
|
+
// poll. One interval at `TICK_MS` (the reasoning is in `shell.model.ts`);
|
|
156
119
|
// `setNow` with a new value re-renders, and a re-render is all the copy
|
|
157
120
|
// needs to catch up.
|
|
158
121
|
const [now, setNow] = useState(() => Date.now());
|
|
@@ -176,45 +139,23 @@ export function Shell(props: ShellProps) {
|
|
|
176
139
|
useEffect(
|
|
177
140
|
() =>
|
|
178
141
|
watchKeys(document, {
|
|
179
|
-
context: () => ({ overlay: live.current.overlay, hasSelection: selectedId
|
|
142
|
+
context: () => ({ overlay: live.current.overlay, hasSelection: live.current.selectedId !== null }),
|
|
180
143
|
run: (action) =>
|
|
181
144
|
runShellAction(action, {
|
|
182
145
|
setOverlay,
|
|
183
146
|
focusSelector: (selector) => focusSelector(document, selector),
|
|
184
147
|
fitGraph: () => fit.current?.(),
|
|
185
|
-
|
|
186
|
-
// selection while a draft is dirty must be refused like any
|
|
187
|
-
// other navigation, and `Esc` is the easiest way to do it by
|
|
188
|
-
// accident.
|
|
189
|
-
clearSelection: () => editor.send({ type: "navigate", id: null }),
|
|
190
|
-
toggleEdit: () => editor.send({ type: "toggle" }),
|
|
191
|
-
saveNote: () => editor.send({ type: "save" }),
|
|
148
|
+
clearSelection: () => { void workspace.current?.select(null); },
|
|
192
149
|
cycleTheme: () => setTheme((current) => cycleTheme(current)),
|
|
193
150
|
}),
|
|
194
151
|
}),
|
|
195
152
|
[],
|
|
196
153
|
);
|
|
197
154
|
|
|
198
|
-
const resolved = useMemo(() => resolveColumns(layout, width, breakpointFor(width)), [layout, width]);
|
|
199
|
-
|
|
200
|
-
// Built once: the handlers read state through `live`, so they never go
|
|
201
|
-
// stale and never need to be rebuilt.
|
|
202
|
-
const drag = useMemo(
|
|
203
|
-
() =>
|
|
204
|
-
dividerHandlers({
|
|
205
|
-
layout: () => live.current.layout,
|
|
206
|
-
width: () => live.current.width,
|
|
207
|
-
setLayout,
|
|
208
|
-
persist: (next) => void saveLayout(localStorage, next),
|
|
209
|
-
}),
|
|
210
|
-
[],
|
|
211
|
-
);
|
|
212
|
-
|
|
213
155
|
return (
|
|
214
156
|
<>
|
|
215
157
|
<Header
|
|
216
|
-
summary={summarize(graph
|
|
217
|
-
connection={connectionView(connection.value)}
|
|
158
|
+
summary={summarize(workspaceState.graph)}
|
|
218
159
|
shortcut={searchShortcut(looksApple(props.platform))}
|
|
219
160
|
onRefresh={() => workspace.current?.refresh()}
|
|
220
161
|
onSearch={() => setOverlay("search")}
|
|
@@ -222,26 +163,15 @@ export function Shell(props: ShellProps) {
|
|
|
222
163
|
onTheme={() => setTheme((current) => cycleTheme(current))}
|
|
223
164
|
/>
|
|
224
165
|
<Columns
|
|
225
|
-
resolved={resolved}
|
|
226
|
-
onDown={drag.onDown}
|
|
227
|
-
onMove={drag.onMove}
|
|
228
|
-
onUp={drag.onUp}
|
|
229
|
-
onKey={drag.onKey}
|
|
230
166
|
scheme={effectiveScheme(theme, systemScheme)}
|
|
231
|
-
bootFailed={graphFailed
|
|
232
|
-
graph={graph
|
|
233
|
-
note={
|
|
234
|
-
selectedId={selectedId
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// rather than three that each had to remember.
|
|
239
|
-
onSelect={(id) => editor.send({ type: "navigate", id })}
|
|
167
|
+
bootFailed={workspaceState.graphFailed}
|
|
168
|
+
graph={workspaceState.graph}
|
|
169
|
+
note={workspaceState.note}
|
|
170
|
+
selectedId={workspaceState.selectedId}
|
|
171
|
+
recentIds={workspaceState.recentIds}
|
|
172
|
+
onSelect={(id) => void workspace.current?.select(id)}
|
|
173
|
+
onOpen={(slug) => void openNote(fetchJson, slug)}
|
|
240
174
|
now={now}
|
|
241
|
-
toolbar={editorToolbar(editorState)}
|
|
242
|
-
prompt={editorPrompt(editorState)}
|
|
243
|
-
draft={editorState.draft}
|
|
244
|
-
send={editor.send}
|
|
245
175
|
// The graph column's three ports (§7.5, §10). Supplied here, at the
|
|
246
176
|
// one place that is already allowed to name browser globals, so
|
|
247
177
|
// `Graph.tsx` and everything under it takes its world as parameters.
|
|
@@ -252,23 +182,20 @@ export function Shell(props: ShellProps) {
|
|
|
252
182
|
/>
|
|
253
183
|
{/*
|
|
254
184
|
`model.generatedAt`, not `stamp`. The two used to be the same string;
|
|
255
|
-
since §15.6 `stamp` is
|
|
256
|
-
|
|
185
|
+
since §15.6 `stamp` is the ETag's content digest and would render as
|
|
186
|
+
`a3f9c2…` under a label that
|
|
257
187
|
says "data as of". `generatedAt` kept the data-as-of job, which is
|
|
258
188
|
exactly what this bar wants.
|
|
259
189
|
*/}
|
|
260
190
|
<StatusBar
|
|
261
|
-
model={statusBarModel(props.cwd, selectedId
|
|
191
|
+
model={statusBarModel(props.cwd, workspaceState.selectedId, workspaceState.graph?.model.generatedAt ?? null)}
|
|
262
192
|
/>
|
|
263
193
|
{overlay === "search" ? (
|
|
264
194
|
<SearchPalette
|
|
265
|
-
graph={graph
|
|
266
|
-
|
|
267
|
-
// while a draft is dirty must park behind the UNSAVED prompt, not
|
|
268
|
-
// wipe it. This was the one navigation path that skipped the guard.
|
|
269
|
-
onSelect={(id) => editor.send({ type: "navigate", id })}
|
|
195
|
+
graph={workspaceState.graph}
|
|
196
|
+
onSelect={(id) => void workspace.current?.select(id)}
|
|
270
197
|
onClose={() => setOverlay(null)}
|
|
271
|
-
ports={{ fetch: fetchJson
|
|
198
|
+
ports={{ fetch: fetchJson }}
|
|
272
199
|
/>
|
|
273
200
|
) : null}
|
|
274
201
|
{overlay === "help" ? (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The status bar (weave-workspace §1.2).
|
|
3
3
|
*
|
|
4
|
-
* Working directory, current selection, data-as-of stamp
|
|
4
|
+
* Working directory, current selection, and data-as-of stamp. The
|
|
5
5
|
* model is built by `statusBarModel`; this renders it.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -20,9 +20,6 @@ export function StatusBar({ model }: { model: StatusBarModel }) {
|
|
|
20
20
|
<span class="weave-status-stamp" title="data as of">
|
|
21
21
|
{shortStamp(model.stamp)}
|
|
22
22
|
</span>
|
|
23
|
-
<span class={`weave-conn weave-conn-${model.connection.tone}`} title={model.connection.hint}>
|
|
24
|
-
{model.connection.label}
|
|
25
|
-
</span>
|
|
26
23
|
</footer>
|
|
27
24
|
);
|
|
28
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The inline icon sprite (Tier 6,
|
|
2
|
+
* The inline icon sprite (Tier 6, P6.4).
|
|
3
3
|
*
|
|
4
4
|
* ## Why a sprite of path data
|
|
5
5
|
*
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* ## Why the renderer is not here
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* : this module decides (which glyphs exist, which are filled, how fat a
|
|
30
30
|
* dot is), and the `.tsx` renders (`<svg>` JSX from {@link IconDef}). That
|
|
31
31
|
* also keeps this file free of JSX and DOM types, so the root `tsconfig.json`
|
|
32
32
|
* project compiles its tests.
|
|
@@ -43,8 +43,7 @@ export type IconName =
|
|
|
43
43
|
| "entryPoint"
|
|
44
44
|
| "gitState"
|
|
45
45
|
| "external"
|
|
46
|
-
| "file"
|
|
47
|
-
| "session";
|
|
46
|
+
| "file";
|
|
48
47
|
|
|
49
48
|
/** The box every path is drawn in, and the stroke the sheet's aesthetic asks for. */
|
|
50
49
|
export const ICON_BOX = 16;
|
|
@@ -82,8 +81,6 @@ export const ICONS: Readonly<Record<IconName, IconDef>> = {
|
|
|
82
81
|
vault: { d: ["M8 2.6 13.4 8 8 13.4 2.6 8Z"], filled: true },
|
|
83
82
|
/** A note page with a folded corner — the vault's contents. */
|
|
84
83
|
note: { d: ["M4.5 2.75h4.75l2.25 2.25v8.25h-7Z", "M9.25 2.75V5h2.25"], filled: false },
|
|
85
|
-
/** A session memory page: a rounded speech bubble with a tail. */
|
|
86
|
-
session: { d: ["M3 3.75h10v6.5H6.75L3.75 13v-2.75H3Z"], filled: false },
|
|
87
84
|
/** The repository root, a framed card with a header rule — the TUI's ▣. */
|
|
88
85
|
repository: { d: ["M2.75 4.25h10.5v7.5H2.75Z", "M2.75 6.75h10.5"], filled: false },
|
|
89
86
|
/** A folder — the tree's module / synthesized vault directory. */
|
|
@@ -98,4 +95,4 @@ export const ICONS: Readonly<Record<IconName, IconDef>> = {
|
|
|
98
95
|
external: { d: ["M4.5 11.5l7-7", "M6.75 4.5h4.75v4.75"], filled: false },
|
|
99
96
|
/** A plain repo file, an unfolded page. */
|
|
100
97
|
file: { d: ["M4.5 2.75h7v10.5h-7Z"], filled: false },
|
|
101
|
-
};
|
|
98
|
+
};
|
|
@@ -25,19 +25,9 @@
|
|
|
25
25
|
* | --- | --- |
|
|
26
26
|
* | `⌘K` / `Ctrl K` | always — a modifier shortcut is unambiguous even mid-word |
|
|
27
27
|
* | `⌘1` `⌘2` `⌘3` | always |
|
|
28
|
-
* | `⌘E` `⌘S` | always — the editor's two keys (§11 P5.4) |
|
|
29
28
|
* | `Esc` | an overlay is open, or something is selected. Never otherwise |
|
|
30
29
|
* | `/` `g` `?` `t` | **only** when focus is not in a text field and no overlay is open |
|
|
31
30
|
*
|
|
32
|
-
* `⌘S` is claimed **even while typing**, which is the one place this map
|
|
33
|
-
* deliberately overrides the browser. It has to be: the browser's `⌘S` is
|
|
34
|
-
* "save this page as HTML", which in a note editor is never what the user
|
|
35
|
-
* meant, and the moment they most want to save is the moment their cursor is
|
|
36
|
-
* in the textarea. `⌘E` is claimed on the same grounds — the browser has no
|
|
37
|
-
* default for it, and toggling out of the editor is a thing to do *from*
|
|
38
|
-
* inside the editor. Neither fires when an overlay is open, because the
|
|
39
|
-
* palette owns every key but `Escape`.
|
|
40
|
-
*
|
|
41
31
|
* `j` / `k` are not here at all: they are tree-scoped, so they live in
|
|
42
32
|
* `tree.model.ts`'s `normalizeTreeKey` where the tree's own handler applies
|
|
43
33
|
* them — a global `j` would move the tree selection while the user is looking
|
|
@@ -51,9 +41,7 @@
|
|
|
51
41
|
* `⌘K`, which would otherwise re-open the palette on top of itself.
|
|
52
42
|
*/
|
|
53
43
|
|
|
54
|
-
import { COLUMNS,
|
|
55
|
-
import type { ColumnId } from "./layout.model";
|
|
56
|
-
import type { OverlayId } from "./shell.model";
|
|
44
|
+
import { COLUMNS, type ColumnId, type OverlayId } from "./shell.model";
|
|
57
45
|
|
|
58
46
|
// --- the event, without the DOM ---------------------------------------------------
|
|
59
47
|
|
|
@@ -102,25 +90,17 @@ export type ShellAction =
|
|
|
102
90
|
| { readonly type: "filterTree" }
|
|
103
91
|
| { readonly type: "fitGraph" }
|
|
104
92
|
| { readonly type: "clearSelection" }
|
|
105
|
-
/** `⌘E` — toggle the note column between read and edit (§11 P5.4). */
|
|
106
|
-
| { readonly type: "toggleEdit" }
|
|
107
|
-
/** `⌘S` — save the open draft. */
|
|
108
|
-
| { readonly type: "saveNote" }
|
|
109
93
|
/** `t` — cycle the colour theme: system → light → dark → system. */
|
|
110
94
|
| { readonly type: "cycleTheme" };
|
|
111
95
|
|
|
112
96
|
/**
|
|
113
97
|
* The command-key letters, as data.
|
|
114
98
|
*
|
|
115
|
-
* A table for the same reason {@link COLUMN_DIGITS} is one:
|
|
116
|
-
*
|
|
117
|
-
* hand-written cases, and the help sheet is generated from it so it cannot
|
|
118
|
-
* document a key the code does not implement.
|
|
99
|
+
* A table for the same reason {@link COLUMN_DIGITS} is one: the help sheet is
|
|
100
|
+
* generated from it so it cannot document a key the code does not implement.
|
|
119
101
|
*/
|
|
120
102
|
export const COMMAND_KEYS: Readonly<Record<string, ShellAction>> = {
|
|
121
103
|
k: { type: "openSearch" },
|
|
122
|
-
e: { type: "toggleEdit" },
|
|
123
|
-
s: { type: "saveNote" },
|
|
124
104
|
};
|
|
125
105
|
|
|
126
106
|
/**
|
|
@@ -140,9 +120,7 @@ export const COLUMN_DIGITS: Readonly<Record<string, ColumnId>> = { "1": "tree",
|
|
|
140
120
|
* search button once was. Derived from `columnsAt`, not listed, so the caveat
|
|
141
121
|
* cannot drift from the breakpoints the sheet is attached to.
|
|
142
122
|
*/
|
|
143
|
-
const COLLAPSIBLE_COLUMNS: ReadonlySet<ColumnId> = new Set(
|
|
144
|
-
COLUMNS.filter((c) => columnsAt("medium").includes(c) === false || columnsAt("narrow").includes(c) === false),
|
|
145
|
-
);
|
|
123
|
+
const COLLAPSIBLE_COLUMNS: ReadonlySet<ColumnId> = new Set(["tree", "graph"]);
|
|
146
124
|
|
|
147
125
|
/**
|
|
148
126
|
* Whether a modifier combination counts as "the platform's command key".
|
|
@@ -249,7 +227,7 @@ export interface KeyTarget {
|
|
|
249
227
|
* The slice of `KeyboardEvent` the subscription reads.
|
|
250
228
|
*
|
|
251
229
|
* Structural, so the platform's satisfies it without a cast and a fake is an
|
|
252
|
-
* object literal — the same port shape
|
|
230
|
+
* object literal — the same port shape as `HttpResponse`
|
|
253
231
|
* use, for the same reason: there is no DOM test environment (§10).
|
|
254
232
|
*/
|
|
255
233
|
export interface KeyboardEventLike {
|
|
@@ -358,10 +336,6 @@ export interface ShellEffects {
|
|
|
358
336
|
fitGraph(): void;
|
|
359
337
|
/** Write `null` to §1.3's `selectedId`. */
|
|
360
338
|
clearSelection(): void;
|
|
361
|
-
/** `⌘E` — dispatch `toggle` into the note editor. */
|
|
362
|
-
toggleEdit(): void;
|
|
363
|
-
/** `⌘S` — dispatch `save` into the note editor. */
|
|
364
|
-
saveNote(): void;
|
|
365
339
|
/** `t` — advance the user's theme choice by one step in its cycle. */
|
|
366
340
|
cycleTheme(): void;
|
|
367
341
|
}
|
|
@@ -385,10 +359,6 @@ export function runShellAction(action: ShellAction, fx: ShellEffects): void {
|
|
|
385
359
|
return fx.fitGraph();
|
|
386
360
|
case "clearSelection":
|
|
387
361
|
return fx.clearSelection();
|
|
388
|
-
case "toggleEdit":
|
|
389
|
-
return fx.toggleEdit();
|
|
390
|
-
case "saveNote":
|
|
391
|
-
return fx.saveNote();
|
|
392
362
|
case "cycleTheme":
|
|
393
363
|
return fx.cycleTheme();
|
|
394
364
|
}
|
|
@@ -448,13 +418,6 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
448
418
|
{ combo: "Home / End", what: "First / last row" },
|
|
449
419
|
],
|
|
450
420
|
},
|
|
451
|
-
{
|
|
452
|
-
title: "Note",
|
|
453
|
-
entries: [
|
|
454
|
-
{ combo: `${cmd}E`, what: "Toggle read / edit" },
|
|
455
|
-
{ combo: `${cmd}S`, what: "Save the open draft" },
|
|
456
|
-
],
|
|
457
|
-
},
|
|
458
421
|
{
|
|
459
422
|
title: "Graph",
|
|
460
423
|
entries: [{ combo: "g", what: "Fit the whole graph" }],
|