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,57 +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 { FetchLike } from "../api";
|
|
33
|
-
import type { EditorEvent, EditorPrompt, EditorToolbar } from "../note/editor.model";
|
|
34
12
|
import { Note } from "../note/Note";
|
|
35
13
|
import { Tree } from "../tree/Tree";
|
|
36
14
|
import type { GraphPayload, NotePayload } from "../../shared/wire";
|
|
37
|
-
import {
|
|
38
|
-
import type { ColumnId, DividerId, ResolvedColumn } from "./layout.model";
|
|
39
|
-
import { columnVars } from "./layout.model";
|
|
15
|
+
import type { ColumnId } from "./shell.model";
|
|
40
16
|
import { ContextRail } from "./ContextRail";
|
|
41
|
-
import {
|
|
42
|
-
import type { ColumnSlot } from "./shell.model";
|
|
43
|
-
import { columnSlots, emptyStateFor } from "./shell.model";
|
|
17
|
+
import { emptyStateFor } from "./shell.model";
|
|
44
18
|
|
|
45
19
|
export interface ColumnsProps {
|
|
46
|
-
resolved: readonly ResolvedColumn[];
|
|
47
|
-
onDown: (divider: DividerId, clientX: number, pointerId: number) => void;
|
|
48
|
-
onMove: (clientX: number) => void;
|
|
49
|
-
onUp: () => void;
|
|
50
|
-
onKey: (divider: DividerId, key: string) => void;
|
|
51
20
|
/** The §1.3 context bus, as the columns see it. */
|
|
52
21
|
graph: GraphPayload | null;
|
|
53
22
|
note: NotePayload | null;
|
|
54
23
|
selectedId: string | null;
|
|
24
|
+
recentIds: ReadonlySet<string>;
|
|
55
25
|
/**
|
|
56
26
|
* The §1.3 context bus.
|
|
57
27
|
*
|
|
@@ -62,6 +32,7 @@ export interface ColumnsProps {
|
|
|
62
32
|
* a handler accepting the wider type takes without a cast.
|
|
63
33
|
*/
|
|
64
34
|
onSelect: (id: string | null) => void;
|
|
35
|
+
onOpen: (slug: string) => void;
|
|
65
36
|
/** Epoch ms for relative times, read once per render by the shell. */
|
|
66
37
|
now: number;
|
|
67
38
|
/**
|
|
@@ -75,7 +46,6 @@ export interface ColumnsProps {
|
|
|
75
46
|
renderer: RendererFactory;
|
|
76
47
|
storage: PositionStorage;
|
|
77
48
|
host: SchemeHost;
|
|
78
|
-
fetch?: FetchLike | undefined;
|
|
79
49
|
/**
|
|
80
50
|
* The scheme the shell resolved from the user's theme choice
|
|
81
51
|
* (`shell/theme.model.ts`'s `effectiveScheme`), or `null` to let the column
|
|
@@ -88,18 +58,6 @@ export interface ColumnsProps {
|
|
|
88
58
|
bootFailed: boolean;
|
|
89
59
|
/** Slot the graph column fills with its `fit`, for the global `g` key. */
|
|
90
60
|
fit: { current: (() => void) | null };
|
|
91
|
-
/**
|
|
92
|
-
* The note editor's view model (§11 P5).
|
|
93
|
-
*
|
|
94
|
-
* Derived by the shell from the one `EditorHandle`, because the editor's
|
|
95
|
-
* state outlives the note column's mount: below 800 px the column can be
|
|
96
|
-
* unmounted by a resize, and an editor owned by the component would lose
|
|
97
|
-
* an unsaved draft to a window drag.
|
|
98
|
-
*/
|
|
99
|
-
toolbar: EditorToolbar | null;
|
|
100
|
-
prompt: EditorPrompt | null;
|
|
101
|
-
draft: string;
|
|
102
|
-
send: (event: EditorEvent) => void;
|
|
103
61
|
}
|
|
104
62
|
|
|
105
63
|
/** One column: a titled region and whichever surface fills it. */
|
|
@@ -108,7 +66,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
108
66
|
return (
|
|
109
67
|
<section class={`weave-col weave-col-${id}`} aria-label={copy.title}>
|
|
110
68
|
<h2 class="weave-col-title">{copy.title}</h2>
|
|
111
|
-
{id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId}
|
|
69
|
+
{id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} recentIds={props.recentIds} onSelect={props.onSelect} now={props.now} /> : null}
|
|
112
70
|
{id === "note" ? (
|
|
113
71
|
<Note
|
|
114
72
|
note={props.note}
|
|
@@ -116,10 +74,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
116
74
|
selectedId={props.selectedId}
|
|
117
75
|
onSelect={props.onSelect}
|
|
118
76
|
now={props.now}
|
|
119
|
-
|
|
120
|
-
prompt={props.prompt}
|
|
121
|
-
draft={props.draft}
|
|
122
|
-
send={props.send}
|
|
77
|
+
onOpen={props.onOpen}
|
|
123
78
|
/>
|
|
124
79
|
) : null}
|
|
125
80
|
{id === "graph" ? (
|
|
@@ -140,37 +95,12 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
|
|
|
140
95
|
);
|
|
141
96
|
}
|
|
142
97
|
|
|
143
|
-
/** A column and, where one follows it, its divider. */
|
|
144
|
-
function Slot({ slot, props }: { slot: ColumnSlot; props: ColumnsProps }) {
|
|
145
|
-
const divider = slot.divider;
|
|
146
|
-
return (
|
|
147
|
-
<Fragment>
|
|
148
|
-
<Column id={slot.column.id} props={props} />
|
|
149
|
-
{divider === null ? null : (
|
|
150
|
-
<Divider
|
|
151
|
-
id={divider}
|
|
152
|
-
label={`Resize ${slot.column.id} column`}
|
|
153
|
-
onDown={(x, pointerId) => props.onDown(divider, x, pointerId)}
|
|
154
|
-
onMove={props.onMove}
|
|
155
|
-
onUp={props.onUp}
|
|
156
|
-
onKey={(key) => props.onKey(divider, key)}
|
|
157
|
-
/>
|
|
158
|
-
)}
|
|
159
|
-
</Fragment>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
98
|
export function Columns(props: ColumnsProps) {
|
|
164
|
-
const grid = useRef<HTMLDivElement | null>(null);
|
|
165
|
-
useLayoutEffect(() => {
|
|
166
|
-
applyVars(grid.current, columnVars(props.resolved));
|
|
167
|
-
}, [props.resolved]);
|
|
168
|
-
|
|
169
99
|
return (
|
|
170
|
-
<div class="weave-grid"
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
100
|
+
<div class="weave-grid">
|
|
101
|
+
<Column id="tree" props={props} />
|
|
102
|
+
<Column id="note" props={props} />
|
|
103
|
+
<Column id="graph" props={props} />
|
|
174
104
|
</div>
|
|
175
105
|
);
|
|
176
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,63 +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";
|
|
28
|
-
import {
|
|
22
|
+
import { openNote } from "../api";
|
|
29
23
|
import type { ColorScheme } from "../graph/graph.model";
|
|
30
24
|
import { schemeOf, watchScheme } from "../graph/scheme";
|
|
31
25
|
import { createSigmaRenderer } from "../graph/renderer.dom";
|
|
32
|
-
import { domEventSource } from "../live";
|
|
33
|
-
import { createEditor, watchUnload, type EditorHandle } from "../note/editor.controller";
|
|
34
|
-
import { editorPrompt, editorToolbar, initialEditorState, shouldBlockUnload } from "../note/editor.model";
|
|
35
26
|
import { SearchPalette } from "../search/SearchPalette";
|
|
36
27
|
import { restoreSelection, saveSelection } from "../selection.storage";
|
|
37
|
-
import {
|
|
28
|
+
import { initialWorkspaceState } from "../state";
|
|
29
|
+
import type { WorkspaceState } from "../state";
|
|
38
30
|
import type { WorkspaceHandle } from "../workspace";
|
|
39
|
-
import {
|
|
31
|
+
import { startWorkspace } from "../workspace";
|
|
40
32
|
import { Columns } from "./Columns";
|
|
41
33
|
import { deeplinkSelection, formatHash } from "./deeplink.model";
|
|
42
|
-
import { dividerHandlers } from "./drag.model";
|
|
43
34
|
import { Header } from "./Header";
|
|
44
35
|
import { HelpOverlay } from "./HelpOverlay";
|
|
45
36
|
import { watchKeys } from "./keys";
|
|
46
37
|
import { focusSelector, runShellAction } from "./keys.model";
|
|
47
|
-
import type { LayoutState } from "./layout.model";
|
|
48
|
-
import { breakpointFor, loadLayout, resolveColumns, saveLayout } from "./layout.model";
|
|
49
38
|
import { StatusBar } from "./StatusBar";
|
|
50
39
|
import type { OverlayId } from "./shell.model";
|
|
51
|
-
import { TICK_MS,
|
|
40
|
+
import { TICK_MS, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
|
|
52
41
|
import { cycleTheme, effectiveScheme, loadTheme, saveTheme, themeAttr, themeButton } from "./theme.model";
|
|
53
42
|
import type { ThemeChoice } from "./theme.model";
|
|
54
|
-
import { watchViewport } from "./viewport";
|
|
55
43
|
|
|
56
44
|
/**
|
|
57
45
|
* Write the choice onto `<html>`, so the sheet's attribute branch and the
|
|
58
|
-
* media query agree on who is in charge (see `theme.model.ts`'s header).
|
|
59
|
-
* one DOM write the theme needs, and it is here rather than in the model for
|
|
60
|
-
* 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).
|
|
61
47
|
*/
|
|
62
48
|
function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
|
|
63
49
|
if (choice === null) delete document.documentElement.dataset.weaveTheme;
|
|
@@ -67,17 +53,13 @@ function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
|
|
|
67
53
|
export interface ShellProps {
|
|
68
54
|
/** From the page bootstrap. Shown in the status bar. */
|
|
69
55
|
cwd: string;
|
|
70
|
-
/** `window.innerWidth` at mount. Injected so the first render is testable. */
|
|
71
|
-
initialWidth: number;
|
|
72
56
|
/** `navigator.platform`, for the `⌘K` vs `Ctrl K` hint. */
|
|
73
57
|
platform: string;
|
|
74
58
|
}
|
|
75
59
|
|
|
76
60
|
export function Shell(props: ShellProps) {
|
|
77
|
-
const [
|
|
61
|
+
const [workspaceState, setWorkspaceState] = useState<WorkspaceState>(initialWorkspaceState);
|
|
78
62
|
const [overlay, setOverlay] = useState<OverlayId>(null);
|
|
79
|
-
const [layout, setLayout] = useState<LayoutState>(() => loadLayout(localStorage, props.initialWidth));
|
|
80
|
-
const [editorState, setEditorState] = useState(initialEditorState);
|
|
81
63
|
// The theme: what the user picked, and what the OS is currently saying. Two
|
|
82
64
|
// states because they answer different questions — `theme` changes on a
|
|
83
65
|
// button press or the `t` key, `systemScheme` on an OS flip while the user
|
|
@@ -88,21 +70,13 @@ export function Shell(props: ShellProps) {
|
|
|
88
70
|
// Filled by the graph column at mount, cleared on unmount. The global `g`
|
|
89
71
|
// key's only route to the renderer — see `Graph.tsx`'s `fit` prop.
|
|
90
72
|
const fit = useRef<(() => void) | null>(null);
|
|
91
|
-
// The
|
|
92
|
-
//
|
|
93
|
-
const live = useRef({
|
|
94
|
-
live.current = {
|
|
95
|
-
|
|
96
|
-
// Built once and owned by the shell, not by the note column. The column is
|
|
97
|
-
// unmounted by a resize below 800 px, and an editor whose lifetime was the
|
|
98
|
-
// column's would lose an unsaved draft to a window drag.
|
|
99
|
-
const editor: EditorHandle = useMemo(
|
|
100
|
-
() => createEditor({ fetch: fetchJson, select: (id) => void select(fetchJson, id), onChange: setEditorState }),
|
|
101
|
-
[],
|
|
102
|
-
);
|
|
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 };
|
|
103
77
|
|
|
104
78
|
useEffect(() => {
|
|
105
|
-
const handle = startWorkspace({ fetch: fetchJson,
|
|
79
|
+
const handle = startWorkspace({ fetch: fetchJson, state: workspaceState, setState: setWorkspaceState });
|
|
106
80
|
workspace.current = handle;
|
|
107
81
|
return () => handle.stop();
|
|
108
82
|
}, []);
|
|
@@ -117,43 +91,31 @@ export function Shell(props: ShellProps) {
|
|
|
117
91
|
const bootHash = useRef(location.hash);
|
|
118
92
|
useEffect(() => {
|
|
119
93
|
if (!selectionRestored.current) return;
|
|
120
|
-
saveSelection(localStorage, selectedId
|
|
94
|
+
saveSelection(localStorage, workspaceState.selectedId);
|
|
121
95
|
// replaceState, not pushState: the address bar mirrors the note on
|
|
122
96
|
// screen, and reading three notes is not three history entries. The
|
|
123
97
|
// *string* is the model's (`formatHash`); this is the write itself.
|
|
124
|
-
history.replaceState(null, "", formatHash(selectedId
|
|
125
|
-
}, [selectedId
|
|
98
|
+
history.replaceState(null, "", formatHash(workspaceState.selectedId));
|
|
99
|
+
}, [workspaceState.selectedId]);
|
|
126
100
|
useEffect(() => {
|
|
127
|
-
if (selectionRestored.current || graph
|
|
101
|
+
if (selectionRestored.current || workspaceState.graph === null) return;
|
|
128
102
|
selectionRestored.current = true;
|
|
129
|
-
if (selectedId
|
|
103
|
+
if (workspaceState.selectedId !== null) return;
|
|
130
104
|
// A link wins over storage: the address bar is an explicit instruction,
|
|
131
105
|
// the saved note is a habit. A link to a note the graph does not hold is
|
|
132
106
|
// refused (see `deeplink.model.ts`) and continuity falls through.
|
|
133
|
-
const linked = deeplinkSelection(bootHash.current, graph
|
|
134
|
-
if (linked !== null) void select(
|
|
107
|
+
const linked = deeplinkSelection(bootHash.current, workspaceState.graph);
|
|
108
|
+
if (linked !== null) void workspace.current?.select(linked);
|
|
135
109
|
else {
|
|
136
|
-
const saved = restoreSelection(graph
|
|
137
|
-
if (saved !== null) void select(
|
|
110
|
+
const saved = restoreSelection(workspaceState.graph, localStorage);
|
|
111
|
+
if (saved !== null) void workspace.current?.select(saved);
|
|
138
112
|
}
|
|
139
|
-
}, [graph
|
|
140
|
-
|
|
141
|
-
// Every note that arrives, from any of the three directions it can arrive
|
|
142
|
-
// from (mount, selection, SSE refetch), so the editor can decide whether it
|
|
143
|
-
// is news or an interruption — see `editor.model.ts`'s header.
|
|
144
|
-
useEffect(() => observeNotes((payload) => editor.send({ type: "loaded", payload })), []);
|
|
145
|
-
|
|
146
|
-
// Read through a thunk, not captured: a listener registered at mount
|
|
147
|
-
// outlives every render, so a captured state would always look clean and
|
|
148
|
-
// the guard would be installed but inert.
|
|
149
|
-
useEffect(() => watchUnload(window, () => shouldBlockUnload(editor.state())), []);
|
|
150
|
-
|
|
151
|
-
useEffect(() => watchViewport(window, setWidth), []);
|
|
113
|
+
}, [workspaceState.graph]);
|
|
152
114
|
|
|
153
115
|
// The relative-time clock. Every "8h ago" in the tree and the note meta is
|
|
154
116
|
// computed from a `now` stamped per render, and a resting workspace never
|
|
155
117
|
// re-renders on its own — so the minutes used to go stale until the next
|
|
156
|
-
//
|
|
118
|
+
// poll. One interval at `TICK_MS` (the reasoning is in `shell.model.ts`);
|
|
157
119
|
// `setNow` with a new value re-renders, and a re-render is all the copy
|
|
158
120
|
// needs to catch up.
|
|
159
121
|
const [now, setNow] = useState(() => Date.now());
|
|
@@ -177,45 +139,23 @@ export function Shell(props: ShellProps) {
|
|
|
177
139
|
useEffect(
|
|
178
140
|
() =>
|
|
179
141
|
watchKeys(document, {
|
|
180
|
-
context: () => ({ overlay: live.current.overlay, hasSelection: selectedId
|
|
142
|
+
context: () => ({ overlay: live.current.overlay, hasSelection: live.current.selectedId !== null }),
|
|
181
143
|
run: (action) =>
|
|
182
144
|
runShellAction(action, {
|
|
183
145
|
setOverlay,
|
|
184
146
|
focusSelector: (selector) => focusSelector(document, selector),
|
|
185
147
|
fitGraph: () => fit.current?.(),
|
|
186
|
-
|
|
187
|
-
// selection while a draft is dirty must be refused like any
|
|
188
|
-
// other navigation, and `Esc` is the easiest way to do it by
|
|
189
|
-
// accident.
|
|
190
|
-
clearSelection: () => editor.send({ type: "navigate", id: null }),
|
|
191
|
-
toggleEdit: () => editor.send({ type: "toggle" }),
|
|
192
|
-
saveNote: () => editor.send({ type: "save" }),
|
|
148
|
+
clearSelection: () => { void workspace.current?.select(null); },
|
|
193
149
|
cycleTheme: () => setTheme((current) => cycleTheme(current)),
|
|
194
150
|
}),
|
|
195
151
|
}),
|
|
196
152
|
[],
|
|
197
153
|
);
|
|
198
154
|
|
|
199
|
-
const resolved = useMemo(() => resolveColumns(layout, width, breakpointFor(width)), [layout, width]);
|
|
200
|
-
|
|
201
|
-
// Built once: the handlers read state through `live`, so they never go
|
|
202
|
-
// stale and never need to be rebuilt.
|
|
203
|
-
const drag = useMemo(
|
|
204
|
-
() =>
|
|
205
|
-
dividerHandlers({
|
|
206
|
-
layout: () => live.current.layout,
|
|
207
|
-
width: () => live.current.width,
|
|
208
|
-
setLayout,
|
|
209
|
-
persist: (next) => void saveLayout(localStorage, next),
|
|
210
|
-
}),
|
|
211
|
-
[],
|
|
212
|
-
);
|
|
213
|
-
|
|
214
155
|
return (
|
|
215
156
|
<>
|
|
216
157
|
<Header
|
|
217
|
-
summary={summarize(graph
|
|
218
|
-
connection={connectionView(connection.value)}
|
|
158
|
+
summary={summarize(workspaceState.graph)}
|
|
219
159
|
shortcut={searchShortcut(looksApple(props.platform))}
|
|
220
160
|
onRefresh={() => workspace.current?.refresh()}
|
|
221
161
|
onSearch={() => setOverlay("search")}
|
|
@@ -223,54 +163,39 @@ export function Shell(props: ShellProps) {
|
|
|
223
163
|
onTheme={() => setTheme((current) => cycleTheme(current))}
|
|
224
164
|
/>
|
|
225
165
|
<Columns
|
|
226
|
-
resolved={resolved}
|
|
227
|
-
onDown={drag.onDown}
|
|
228
|
-
onMove={drag.onMove}
|
|
229
|
-
onUp={drag.onUp}
|
|
230
|
-
onKey={drag.onKey}
|
|
231
166
|
scheme={effectiveScheme(theme, systemScheme)}
|
|
232
|
-
bootFailed={graphFailed
|
|
233
|
-
graph={graph
|
|
234
|
-
note={
|
|
235
|
-
selectedId={selectedId
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// rather than three that each had to remember.
|
|
240
|
-
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)}
|
|
241
174
|
now={now}
|
|
242
|
-
toolbar={editorToolbar(editorState)}
|
|
243
|
-
prompt={editorPrompt(editorState)}
|
|
244
|
-
draft={editorState.draft}
|
|
245
|
-
send={editor.send}
|
|
246
175
|
// The graph column's three ports (§7.5, §10). Supplied here, at the
|
|
247
176
|
// one place that is already allowed to name browser globals, so
|
|
248
177
|
// `Graph.tsx` and everything under it takes its world as parameters.
|
|
249
178
|
renderer={createSigmaRenderer}
|
|
250
179
|
storage={localStorage}
|
|
251
180
|
host={window}
|
|
252
|
-
fetch={fetchJson}
|
|
253
181
|
fit={fit}
|
|
254
182
|
/>
|
|
255
183
|
{/*
|
|
256
184
|
`model.generatedAt`, not `stamp`. The two used to be the same string;
|
|
257
|
-
since §15.6 `stamp` is
|
|
258
|
-
|
|
185
|
+
since §15.6 `stamp` is the ETag's content digest and would render as
|
|
186
|
+
`a3f9c2…` under a label that
|
|
259
187
|
says "data as of". `generatedAt` kept the data-as-of job, which is
|
|
260
188
|
exactly what this bar wants.
|
|
261
189
|
*/}
|
|
262
190
|
<StatusBar
|
|
263
|
-
model={statusBarModel(props.cwd, selectedId
|
|
191
|
+
model={statusBarModel(props.cwd, workspaceState.selectedId, workspaceState.graph?.model.generatedAt ?? null)}
|
|
264
192
|
/>
|
|
265
193
|
{overlay === "search" ? (
|
|
266
194
|
<SearchPalette
|
|
267
|
-
graph={graph
|
|
268
|
-
|
|
269
|
-
// while a draft is dirty must park behind the UNSAVED prompt, not
|
|
270
|
-
// wipe it. This was the one navigation path that skipped the guard.
|
|
271
|
-
onSelect={(id) => editor.send({ type: "navigate", id })}
|
|
195
|
+
graph={workspaceState.graph}
|
|
196
|
+
onSelect={(id) => void workspace.current?.select(id)}
|
|
272
197
|
onClose={() => setOverlay(null)}
|
|
273
|
-
ports={{ fetch: fetchJson
|
|
198
|
+
ports={{ fetch: fetchJson }}
|
|
274
199
|
/>
|
|
275
200
|
) : null}
|
|
276
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
|
+
};
|