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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `keys.model.ts` decides what a keystroke means; this attaches one listener
|
|
5
5
|
* and performs the answer. Four lines of it are the listener and the rest is
|
|
6
|
-
* dispatch, which is deliberate —
|
|
7
|
-
*
|
|
6
|
+
* dispatch, which is deliberate — a subscription written inline in a
|
|
7
|
+
* `useEffect` is one no
|
|
8
8
|
* test can reach.
|
|
9
9
|
*
|
|
10
10
|
* ## One listener, on the document, at the bubble phase
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Everything the shell *decides*, as pure functions (weave-workspace §1.2).
|
|
3
3
|
*
|
|
4
|
-
* The header's status summary, the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The header's status summary, the per-column empty-state copy and the status
|
|
5
|
+
* bar's text all live here rather than inside the components that render them.
|
|
6
|
+
* That is not a stylistic
|
|
7
7
|
* preference: §10 forbids adding a DOM test environment, so a conditional
|
|
8
8
|
* inside a `.tsx` is a conditional that can never be covered, and §14 lists
|
|
9
9
|
* "coverage gate blocks the UI work" as a live risk whose stated mitigation
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { GraphPayload, WireNodeKind, WireStalenessState } from "../../shared/wire";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/** The three fixed workspace surfaces. */
|
|
19
|
+
export type ColumnId = "tree" | "note" | "graph";
|
|
20
|
+
|
|
21
|
+
/** Columns in keyboard and visual order. */
|
|
22
|
+
export const COLUMNS: readonly ColumnId[] = ["tree", "note", "graph"];
|
|
21
23
|
|
|
22
24
|
// --- the header summary --------------------------------------------------------
|
|
23
25
|
|
|
@@ -77,93 +79,22 @@ export function summaryParts(summary: HeaderSummary): readonly string[] {
|
|
|
77
79
|
return [`vault:${summary.notes}`, `repo:${repoLabel(summary.repo)}`, `${summary.nodes} nodes`];
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
// --- the connection indicator ----------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* How a connection state is presented.
|
|
84
|
-
*
|
|
85
|
-
* `tone` is a class suffix rather than a colour, so the palette stays in the
|
|
86
|
-
* stylesheet where §5.2's nonce'd CSS can own it and this module stays free
|
|
87
|
-
* of presentation constants it cannot test the appearance of.
|
|
88
|
-
*/
|
|
89
|
-
export interface ConnectionView {
|
|
90
|
-
readonly label: string;
|
|
91
|
-
readonly tone: "ok" | "warn" | "bad";
|
|
92
|
-
/** The `title=` tooltip. Says what the user should expect to happen next. */
|
|
93
|
-
readonly hint: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const CONNECTION_VIEWS: Readonly<Record<ConnectionState, ConnectionView>> = {
|
|
97
|
-
live: { label: "live", tone: "ok", hint: "watching the vault and the repository for changes" },
|
|
98
|
-
reconnecting: {
|
|
99
|
-
label: "reconnecting",
|
|
100
|
-
tone: "warn",
|
|
101
|
-
// Naming the recovery matters: §6 has the client refetch everything on
|
|
102
|
-
// reopen, so the user's screen will catch up on its own and they should
|
|
103
|
-
// not go looking for a reload button.
|
|
104
|
-
hint: "the event stream dropped — retrying, and everything refetches when it returns",
|
|
105
|
-
},
|
|
106
|
-
offline: {
|
|
107
|
-
label: "offline",
|
|
108
|
-
tone: "bad",
|
|
109
|
-
hint: "the workspace server is gone — the header's refresh control retries",
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/** Present a connection state. Total over the three states of §1.3. */
|
|
114
|
-
export function connectionView(state: ConnectionState): ConnectionView {
|
|
115
|
-
return CONNECTION_VIEWS[state];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
82
|
// --- empty states ------------------------------------------------------------------
|
|
119
83
|
|
|
120
|
-
/**
|
|
121
|
-
* A column's title, and the placeholder shown while its content is someone
|
|
122
|
-
* else's phase.
|
|
123
|
-
*
|
|
124
|
-
* P1's deliverable for the three columns was an *honest* empty state naming
|
|
125
|
-
* the phase that would fill it — more useful than a spinner implying a load,
|
|
126
|
-
* and far more useful than a blank pane that reads as a bug.
|
|
127
|
-
*
|
|
128
|
-
* **As of P3 no column uses one.** All three are built, so `Columns.tsx`
|
|
129
|
-
* renders their real surfaces and each has its *own* empty states —
|
|
130
|
-
* `treeEmptyMessage`, `noteEmptyMessage` and `graphEmptyMessage`, which
|
|
131
|
-
* distinguish "loading" from "filtered to nothing" from "genuinely empty" in a
|
|
132
|
-
* way one static sentence cannot.
|
|
133
|
-
*
|
|
134
|
-
* The table stays because {@link EmptyStateCopy.title} is still every column's
|
|
135
|
-
* heading and its `aria-label`. {@link EmptyStateCopy.body} and `phase` are
|
|
136
|
-
* now dead for all three, and are kept accurate rather than deleted for one
|
|
137
|
-
* reason: `tests/web/client-shell.test.ts` asserts the `phase` values against
|
|
138
|
-
* §11, so a column whose phase silently disagreed with the doc would be the
|
|
139
|
-
* first sign that this table had stopped tracking reality. `EmptyState.tsx`
|
|
140
|
-
* itself now has no caller, and is deleted rather than kept warm — an unused
|
|
141
|
-
* component is a thing the next reader has to work out is unused.
|
|
142
|
-
*/
|
|
84
|
+
/** The title used by a column heading and its `aria-label`. */
|
|
143
85
|
export interface EmptyStateCopy {
|
|
144
86
|
readonly title: string;
|
|
145
|
-
readonly body: string;
|
|
146
|
-
/** The phase that fills this column in, e.g. `"P2"`. */
|
|
147
|
-
readonly phase: string;
|
|
148
87
|
}
|
|
149
88
|
|
|
150
89
|
const EMPTY_STATES: Readonly<Record<ColumnId, EmptyStateCopy>> = {
|
|
151
90
|
tree: {
|
|
152
91
|
title: "Tree",
|
|
153
|
-
// Built in P2; body and phase are no longer rendered. See the header.
|
|
154
|
-
body: "The vault and repository outline, expandable, filterable, with provenance markers.",
|
|
155
|
-
phase: "P2",
|
|
156
92
|
},
|
|
157
93
|
note: {
|
|
158
94
|
title: "Note",
|
|
159
|
-
body: "Select anything to read it here — rendered Markdown, front matter, tags and wikilinks.",
|
|
160
|
-
phase: "P2",
|
|
161
95
|
},
|
|
162
96
|
graph: {
|
|
163
97
|
title: "Graph",
|
|
164
|
-
// Built in P3; body and phase are no longer rendered. See the header.
|
|
165
|
-
body: "The knowledge graph, laid out and navigable, with the selection's neighbourhood highlighted.",
|
|
166
|
-
phase: "P3",
|
|
167
98
|
},
|
|
168
99
|
};
|
|
169
100
|
|
|
@@ -172,18 +103,9 @@ export function emptyStateFor(column: ColumnId): EmptyStateCopy {
|
|
|
172
103
|
return EMPTY_STATES[column];
|
|
173
104
|
}
|
|
174
105
|
|
|
175
|
-
/**
|
|
176
|
-
* The context rail's title.
|
|
177
|
-
*
|
|
178
|
-
* Built in P2.5, so `body`/`phase` are no longer rendered either — the rail's
|
|
179
|
-
* real empty states are `RAIL_EMPTY` in `context/context.model.ts`, which
|
|
180
|
-
* separates "loading" from "nothing selected" from "this node is isolated".
|
|
181
|
-
* `title` is still the rail's heading and `aria-label`.
|
|
182
|
-
*/
|
|
106
|
+
/** The context rail's heading and `aria-label`. */
|
|
183
107
|
export const CONTEXT_EMPTY: EmptyStateCopy = {
|
|
184
108
|
title: "Context",
|
|
185
|
-
body: "Links, backlinks and mentions for whatever is selected.",
|
|
186
|
-
phase: "P2",
|
|
187
109
|
};
|
|
188
110
|
|
|
189
111
|
// --- the status bar -------------------------------------------------------------------
|
|
@@ -200,7 +122,6 @@ export const CONTEXT_EMPTY: EmptyStateCopy = {
|
|
|
200
122
|
export interface StatusBarModel {
|
|
201
123
|
readonly cwd: string;
|
|
202
124
|
readonly selection: string;
|
|
203
|
-
readonly connection: ConnectionView;
|
|
204
125
|
/** `null` before the first successful graph fetch. */
|
|
205
126
|
readonly stamp: string | null;
|
|
206
127
|
}
|
|
@@ -212,13 +133,11 @@ export const NO_VALUE = "—";
|
|
|
212
133
|
export function statusBarModel(
|
|
213
134
|
cwd: string,
|
|
214
135
|
selectedId: string | null,
|
|
215
|
-
connection: ConnectionState,
|
|
216
136
|
stamp: string | null,
|
|
217
137
|
): StatusBarModel {
|
|
218
138
|
return {
|
|
219
139
|
cwd: cwd === "" ? NO_VALUE : cwd,
|
|
220
140
|
selection: selectedId ?? "nothing selected",
|
|
221
|
-
connection: connectionView(connection),
|
|
222
141
|
stamp,
|
|
223
142
|
};
|
|
224
143
|
}
|
|
@@ -239,48 +158,6 @@ export function shortStamp(stamp: string | null): string {
|
|
|
239
158
|
return match?.[1] ?? stamp;
|
|
240
159
|
}
|
|
241
160
|
|
|
242
|
-
// --- pairing columns with dividers ---------------------------------------------------
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* One rendered column, plus the divider that follows it (if any).
|
|
246
|
-
*
|
|
247
|
-
* `divider` is `null` for the last column: there are three columns and two
|
|
248
|
-
* dividers, and the trailing edge of the grid is the window, not a handle.
|
|
249
|
-
*/
|
|
250
|
-
export interface ColumnSlot {
|
|
251
|
-
readonly column: ResolvedColumn;
|
|
252
|
-
readonly divider: DividerId | null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Interleave resolved columns with the dividers between them.
|
|
257
|
-
*
|
|
258
|
-
* This exists as a model function rather than an `index + 1 < length` check
|
|
259
|
-
* inside the JSX for two reasons. The first is coverage: that check is a
|
|
260
|
-
* branch, and a branch in a `.tsx` is a branch no test can reach (§10). The
|
|
261
|
-
* second is that the naive version is *wrong at a breakpoint* — at `"medium"`
|
|
262
|
-
* only tree and note render, so the divider after `note` must not appear even
|
|
263
|
-
* though `DIVIDERS` contains one. Deriving the pairing from the columns
|
|
264
|
-
* actually being rendered, rather than from the static divider list, makes
|
|
265
|
-
* that impossible to get wrong.
|
|
266
|
-
*/
|
|
267
|
-
export function columnSlots(resolved: readonly ResolvedColumn[]): readonly ColumnSlot[] {
|
|
268
|
-
return resolved.map((column, index) => ({
|
|
269
|
-
column,
|
|
270
|
-
// A divider is *named by the column to its left* (`layout.model.ts`'s
|
|
271
|
-
// `dividerPair`), so the id is the column's own — no index arithmetic into
|
|
272
|
-
// `DIVIDERS`, which would need an unreachable `?? null` to satisfy
|
|
273
|
-
// `noUncheckedIndexedAccess` and would leave a branch no test can cover.
|
|
274
|
-
// It exists only where a next column follows to resize against.
|
|
275
|
-
divider: index < resolved.length - 1 && isDivider(column.id) ? column.id : null,
|
|
276
|
-
}));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Whether a column has a divider named after it — i.e. is not the last one. */
|
|
280
|
-
function isDivider(column: ColumnId): column is DividerId {
|
|
281
|
-
return (DIVIDERS as readonly ColumnId[]).includes(column);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
161
|
// --- the refresh button ------------------------------------------------------------
|
|
285
162
|
|
|
286
163
|
/**
|
|
@@ -45,7 +45,7 @@ export type ThemeChoice = "system" | "light" | "dark";
|
|
|
45
45
|
/** The choices in cycle order. */
|
|
46
46
|
export const THEME_CHOICES: readonly ThemeChoice[] = ["system", "light", "dark"];
|
|
47
47
|
|
|
48
|
-
/** The `localStorage` key. Namespaced and versioned
|
|
48
|
+
/** The `localStorage` key. Namespaced and versioned. */
|
|
49
49
|
export const THEME_STORAGE_KEY = "pi-weave.theme.v1";
|
|
50
50
|
|
|
51
51
|
/** The slice of `Storage` this module needs. */
|
|
@@ -144,4 +144,4 @@ export function themeButton(choice: ThemeChoice): ThemeButtonView {
|
|
|
144
144
|
glyph: THEME_GLYPHS[choice],
|
|
145
145
|
hint: `Colour theme: ${choice} — click for ${cycleTheme(choice)}`,
|
|
146
146
|
};
|
|
147
|
-
}
|
|
147
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* `page.ts` emits a nonce'd `<style>` block, but it is deliberately tiny —
|
|
8
8
|
* the custom-property palette and enough body rules that the first paint has
|
|
9
|
-
* a background colour before `app.js` parses. It knows nothing about
|
|
10
|
-
*
|
|
9
|
+
* a background colour before `app.js` parses. It knows nothing about the
|
|
10
|
+
* status bar, and it is owned by the server tier, which this
|
|
11
11
|
* work may not edit. There is also no `/app.css` route. So the shell's own
|
|
12
12
|
* rules have to travel in the bundle and be installed at runtime.
|
|
13
13
|
*
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* 1. **A literal `style="…"` attribute in markup is blocked.** That is what
|
|
20
20
|
* `'unsafe-inline'` governs for styles, and it is absent.
|
|
21
21
|
* 2. **CSSOM writes are not.** `el.style.setProperty(…)` has no CSP hook at
|
|
22
|
-
* all, which is why
|
|
23
|
-
*
|
|
22
|
+
* all, which is why dynamic note preview positions use CSSOM. Confirmed
|
|
23
|
+
* against Preact's own behaviour:
|
|
24
24
|
* `preact/src/diff/props.js` handles a `style` prop via
|
|
25
25
|
* `dom.style.cssText` or `style.setProperty`, never `setAttribute`.
|
|
26
26
|
* 3. **A script-created `<style>` element needs the nonce.** Inserting a
|
|
@@ -161,7 +161,7 @@ export const THEME_CSS = `
|
|
|
161
161
|
ui 11.5 status bar, the note's meta row, small controls
|
|
162
162
|
row 12 list rows, text inputs, inline code
|
|
163
163
|
base 13 body copy, h3-h6, preview-card text
|
|
164
|
-
body 13.5 the note column's prose
|
|
164
|
+
body 13.5 the note column's prose
|
|
165
165
|
subhead 14 palette input, note-body h2, header icon buttons
|
|
166
166
|
title 15 note-body h1
|
|
167
167
|
display 20 the note's page title
|
|
@@ -244,30 +244,21 @@ body{font-size:var(--weave-px-base)}
|
|
|
244
244
|
}
|
|
245
245
|
.weave-theme:hover{color:var(--weave-fg);background:var(--weave-line)}
|
|
246
246
|
|
|
247
|
-
/* connection indicator -------------------------------------------------- */
|
|
248
|
-
.weave-conn{display:inline-flex;align-items:center;gap:5px;font-size:var(--weave-px-ui);white-space:nowrap}
|
|
249
|
-
.weave-conn-dot{font-size:var(--weave-px-prov);line-height:1}
|
|
250
|
-
/* Reconnecting breathes instead of sitting there: the one state whose whole
|
|
251
|
-
message is "hold on, I am still working" is the one that should keep
|
|
252
|
-
saying it. The offline dot stays still — its message needs a decision,
|
|
253
|
-
not patience. The global reduced-motion kill switch restores the static
|
|
254
|
-
dot, the same way it does every animation in this sheet. */
|
|
255
|
-
.weave-conn-ok .weave-conn-dot,.weave-conn-ok{color:var(--weave-ok)}
|
|
256
|
-
.weave-conn-warn .weave-conn-dot,.weave-conn-warn{color:var(--weave-warn)}
|
|
257
|
-
.weave-conn-warn .weave-conn-dot{animation:weave-conn-pulse 1.6s ease-in-out infinite}
|
|
258
|
-
.weave-conn-bad .weave-conn-dot,.weave-conn-bad{color:var(--weave-bad)}
|
|
259
|
-
@keyframes weave-conn-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
260
|
-
|
|
261
247
|
/* the grid -------------------------------------------------------------- */
|
|
262
|
-
/*
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.weave-grid
|
|
270
|
-
.weave-
|
|
248
|
+
/* One boring grid. CSS owns the breakpoints; no saved widths, divider state
|
|
249
|
+
or viewport listener can make the shell disagree with the browser. */
|
|
250
|
+
.weave-grid{
|
|
251
|
+
display:grid;min-height:0;overflow:hidden;background:var(--weave-line);
|
|
252
|
+
grid-template-columns:minmax(180px,22fr) minmax(320px,46fr) minmax(260px,32fr);
|
|
253
|
+
}
|
|
254
|
+
@media (max-width:1099px){
|
|
255
|
+
.weave-grid{grid-template-columns:minmax(180px,32fr) minmax(320px,68fr)}
|
|
256
|
+
.weave-col-graph{display:none}
|
|
257
|
+
}
|
|
258
|
+
@media (max-width:799px){
|
|
259
|
+
.weave-grid{grid-template-columns:1fr}
|
|
260
|
+
.weave-col-tree{display:none}
|
|
261
|
+
}
|
|
271
262
|
|
|
272
263
|
.weave-col{
|
|
273
264
|
display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;
|
|
@@ -286,18 +277,8 @@ body{font-size:var(--weave-px-base)}
|
|
|
286
277
|
inside the same region instead of squeezing the graph. */
|
|
287
278
|
.weave-col-graph{display:grid;grid-template-rows:auto minmax(0,1fr) minmax(96px,40%);background:var(--weave-bg)}
|
|
288
279
|
|
|
289
|
-
/* dividers -------------------------------------------------------------- */
|
|
290
|
-
.weave-divider{
|
|
291
|
-
background:var(--weave-line);cursor:col-resize;position:relative;
|
|
292
|
-
touch-action:none;
|
|
293
|
-
}
|
|
294
|
-
/* A 1 px target is unhittable, so the hit area is widened with a pseudo
|
|
295
|
-
element rather than by making the visible rule thicker. */
|
|
296
|
-
.weave-divider::after{content:"";position:absolute;inset:0 -3px;z-index:1}
|
|
297
|
-
.weave-divider:hover,.weave-divider:focus-visible{background:var(--weave-accent);outline:none}
|
|
298
|
-
|
|
299
280
|
/* tree column ----------------------------------------------------------- */
|
|
300
|
-
/* --weave-depth is written per row by \`depthVar\`, through
|
|
281
|
+
/* --weave-depth is written per row by \`depthVar\`, through CSSOM
|
|
301
282
|
path as the column widths, and multiplied by a step this sheet owns — so
|
|
302
283
|
the tree's density stays a CSS decision. */
|
|
303
284
|
.weave-tree{display:flex;flex-direction:column;min-height:0;flex:1}
|
|
@@ -332,26 +313,17 @@ body{font-size:var(--weave-px-base)}
|
|
|
332
313
|
hover from reading as a stray grey rectangle — the row answers the pointer
|
|
333
314
|
in both channels at once. */
|
|
334
315
|
.weave-row:hover .weave-label{color:var(--weave-fg)}
|
|
335
|
-
/* The session fold (tree.model.ts's isMuted): machine-written memory that
|
|
336
|
-
accrues by the dozens with near-duplicate titles, dropped one notch so the
|
|
337
|
-
rows a human wrote keep the foreground. --weave-dim holds ≥ 4.5 on both
|
|
338
|
-
grounds, so this is quiet, not illegible — and the two states below always
|
|
339
|
-
restore full weight, which is why the model never emits both classes on
|
|
340
|
-
one row. */
|
|
341
|
-
.weave-row-muted .weave-label{color:var(--weave-dim)}
|
|
342
|
-
.weave-row-muted:hover .weave-label{color:var(--weave-fg)}
|
|
343
316
|
/* The selection has one voice. On the tinted ground every quieter token still
|
|
344
317
|
fails contrast — even --weave-dim lands under 4.5 — so a selected row's
|
|
345
318
|
kind, provenance and meta children join its label in --weave-fg. The
|
|
346
319
|
provenance glyph shape carries the distinction the colour swap drops;
|
|
347
320
|
elsewhere the hues are unaffected. The palette's hit rows need the same
|
|
348
321
|
remap: same selected ground, same failure. The label is restated last
|
|
349
|
-
|
|
350
|
-
on. */
|
|
322
|
+
so the selected row is always readable. */
|
|
351
323
|
.weave-row-on .weave-twisty,.weave-row-on .weave-kind,.weave-row-on .weave-prov,
|
|
352
324
|
.weave-row-on .weave-meta,.weave-row-on .weave-label{color:var(--weave-fg)}
|
|
353
325
|
.weave-hit-on .weave-hit-badge,.weave-hit-on .weave-hit-detail{color:var(--weave-fg)}
|
|
354
|
-
/* A newly-arrived node (a file or note added since the last update,
|
|
326
|
+
/* A newly-arrived node (a file or note added since the last update, ):
|
|
355
327
|
one short highlight that fades while the label settles from bold back to
|
|
356
328
|
normal. The class is computed from the frame diff in workspace.ts and
|
|
357
329
|
expires with it, so collapsing and re-expanding later does not replay the
|
|
@@ -378,7 +350,7 @@ body{font-size:var(--weave-px-base)}
|
|
|
378
350
|
|
|
379
351
|
/* icons ------------------------------------------------------------------
|
|
380
352
|
The sprite's only CSS: one colour (the glyph inherits the row's, so a
|
|
381
|
-
|
|
353
|
+
quiet row's icon recedes with it and a selected row's brightens with it)
|
|
382
354
|
and the twisty's rotation, which is the *same* 16px chevron the rail's
|
|
383
355
|
group headings use — right-pointing closed, rotated 90° open. The
|
|
384
356
|
rotation is not in the motion block's transition list, on purpose: that
|
|
@@ -394,8 +366,8 @@ body{font-size:var(--weave-px-base)}
|
|
|
394
366
|
so it lies on --weave-page — one step off the desk and the canvas grounds.
|
|
395
367
|
The 2px rule flush to its left edge is the spine: it takes the note's
|
|
396
368
|
provenance colour (see the --weave-spine map), so the document's origin is
|
|
397
|
-
readable peripherally, before any glyph is. Head
|
|
398
|
-
--weave-note-gutter so
|
|
369
|
+
readable peripherally, before any glyph is. Head and body share the
|
|
370
|
+
--weave-note-gutter so the page has one consistent measure. */
|
|
399
371
|
.weave-note{display:flex;flex-direction:column;min-height:0;flex:1;overflow:auto;background:var(--weave-page)}
|
|
400
372
|
.weave-note-human{--weave-spine:var(--weave-ok)}
|
|
401
373
|
.weave-note-agent{--weave-spine:var(--weave-accent)}
|
|
@@ -407,9 +379,7 @@ body{font-size:var(--weave-px-base)}
|
|
|
407
379
|
alternative, a title that scrolls away, is how a reader ends up annotating
|
|
408
380
|
the wrong file. It carries the page ground rather than a fill or a shadow,
|
|
409
381
|
so the reveal reads as the page continuing under it and not as a bar
|
|
410
|
-
arriving; the hairline is the only seam. The
|
|
411
|
-
sibling in the column flow, so it can never slide underneath — no stacking
|
|
412
|
-
contest exists between them. The head's z-index is only what keeps it above
|
|
382
|
+
arriving; the hairline is the only seam. The head's z-index is only what keeps it above
|
|
413
383
|
the prose (and the wikilinks inside it) as that prose scrolls beneath. */
|
|
414
384
|
.weave-note-head{position:sticky;top:0;z-index:2;padding:14px var(--weave-note-gutter) 9px;background:var(--weave-page);border-bottom:1px solid var(--weave-line)}
|
|
415
385
|
/* The page's largest voice, and the point of the review's headline finding:
|
|
@@ -423,13 +393,9 @@ body{font-size:var(--weave-px-base)}
|
|
|
423
393
|
so the eye can read the whole of it without ever reading it. */
|
|
424
394
|
.weave-note-meta{margin:0;display:flex;align-items:center;gap:8px;font-size:var(--weave-px-ui);color:var(--weave-dim);flex-wrap:wrap}
|
|
425
395
|
.weave-note-time{color:var(--weave-faint)}
|
|
426
|
-
/* \`Open in $EDITOR
|
|
427
|
-
bordered button — the note's loudest control, sitting between its title and
|
|
428
|
-
its prose. Now it is an icon at the end of the meta line: the hint rides
|
|
429
|
-
the \`title\`/\`aria-label\` (\`editor.model.ts\`'s OPEN_HINT/OPEN_LABEL), and
|
|
430
|
-
the control is quiet until the pointer asks for it. */
|
|
396
|
+
/* \`Open in $EDITOR\` is a quiet icon at the end of the meta line. */
|
|
431
397
|
.weave-note-open{
|
|
432
|
-
|
|
398
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
433
399
|
width:22px;height:22px;padding:0;font:inherit;color:var(--weave-faint);
|
|
434
400
|
background:none;border:0;border-radius:var(--weave-radius);cursor:pointer;
|
|
435
401
|
}
|
|
@@ -444,9 +410,7 @@ body{font-size:var(--weave-px-base)}
|
|
|
444
410
|
withdrawn at the user's call: this is a workspace, and the note shares the
|
|
445
411
|
width the desk gives it. It reads at --weave-px-body on 1.7 rather than the
|
|
446
412
|
chrome's 1.6: the extra leading is what a page of running text needs that a
|
|
447
|
-
status bar does not. Code
|
|
448
|
-
reading prose and reading raw are different postures, and the face switch
|
|
449
|
-
is the toggle. */
|
|
413
|
+
status bar does not. Code stays mono deliberately. */
|
|
450
414
|
.weave-note-body{padding:12px var(--weave-note-gutter) 28px;font-size:var(--weave-px-body);line-height:1.7;color:var(--weave-fg)}
|
|
451
415
|
.weave-note-body>*:first-child{margin-top:0}
|
|
452
416
|
.weave-note-body h1,.weave-note-body h2,.weave-note-body h3,
|
|
@@ -534,53 +498,6 @@ body{font-size:var(--weave-px-base)}
|
|
|
534
498
|
underlines a hover should not land with a thud. */
|
|
535
499
|
@keyframes weave-preview-in{from{opacity:0;transform:translateY(2px)}}
|
|
536
500
|
|
|
537
|
-
/* note editor (§11 P5) ---------------------------------------------------- */
|
|
538
|
-
/* A \`<textarea>\`, not CodeMirror: §0 V10 measured CM6 at 118 KB gzip, more
|
|
539
|
-
than the entire rest of the client. It inherits the body's type so toggling
|
|
540
|
-
\`⌘E\` does not reflow the column into a different shape. */
|
|
541
|
-
.weave-note-bar{
|
|
542
|
-
display:flex;align-items:center;gap:6px;flex-wrap:wrap;
|
|
543
|
-
padding:5px var(--weave-gutter);border-bottom:1px solid var(--weave-line);
|
|
544
|
-
}
|
|
545
|
-
/* \`weave-note-open\` is styled in the note column above (P6.3 moved it into
|
|
546
|
-
the head's meta row as an icon), and no longer shares the bar's bordered
|
|
547
|
-
button voice. */
|
|
548
|
-
.weave-note-toggle,.weave-note-save,.weave-note-action{
|
|
549
|
-
font:inherit;font-size:var(--weave-px-ui);padding:2px 8px;cursor:pointer;
|
|
550
|
-
color:var(--weave-fg);background:var(--weave-raise);
|
|
551
|
-
border:1px solid var(--weave-line);border-radius:var(--weave-radius);
|
|
552
|
-
}
|
|
553
|
-
.weave-note-toggle:hover,.weave-note-save:hover,.weave-note-action:hover{
|
|
554
|
-
border-color:var(--weave-accent);
|
|
555
|
-
}
|
|
556
|
-
.weave-note-toggle[aria-pressed="true"]{border-color:var(--weave-accent);color:var(--weave-accent)}
|
|
557
|
-
.weave-note-save:disabled{opacity:.45;cursor:default;border-color:var(--weave-line)}
|
|
558
|
-
.weave-note-dirty{color:var(--weave-accent);font-size:var(--weave-px-subhead);line-height:1}
|
|
559
|
-
.weave-note-status{font-size:var(--weave-px-ui);color:var(--weave-dim)}
|
|
560
|
-
.weave-note-status-ok{color:var(--weave-ok)}
|
|
561
|
-
.weave-note-status-warn{color:var(--weave-warn)}
|
|
562
|
-
/* The prompt takes the full row rather than sitting inline: reload-or-overwrite
|
|
563
|
-
is a decision about the user's unsaved text, and a decision squeezed between
|
|
564
|
-
two buttons reads as a hint. */
|
|
565
|
-
.weave-note-prompt{
|
|
566
|
-
flex-basis:100%;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
|
|
567
|
-
margin-top:4px;padding:5px 8px;border-radius:var(--weave-radius);
|
|
568
|
-
background:var(--weave-raise);border:1px solid var(--weave-warn);
|
|
569
|
-
}
|
|
570
|
-
.weave-note-prompt-conflict,.weave-note-prompt-collision{border-color:var(--weave-warn)}
|
|
571
|
-
.weave-note-prompt-external,.weave-note-prompt-discard{border-color:var(--weave-line)}
|
|
572
|
-
.weave-note-prompt-text{margin:0;font-size:var(--weave-px-ui);color:var(--weave-fg)}
|
|
573
|
-
.weave-note-prompt-actions{margin:0;display:flex;gap:6px;flex-wrap:wrap}
|
|
574
|
-
.weave-note-editor{
|
|
575
|
-
flex:1;min-height:240px;resize:none;
|
|
576
|
-
padding:10px var(--weave-note-gutter) 24px;
|
|
577
|
-
/* Exactly the prose body's size and leading. The invariant is documented
|
|
578
|
-
and deliberate: ⌘E must not reflow, and reading raw at the same measure
|
|
579
|
-
the prose uses is what makes the toggle a lens rather than a jump. */
|
|
580
|
-
font:inherit;font-family:var(--weave-mono);font-size:var(--weave-px-body);line-height:1.7;
|
|
581
|
-
color:var(--weave-fg);background:var(--weave-page);border:0;outline-offset:-2px;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
501
|
/* graph column ---------------------------------------------------------- */
|
|
585
502
|
/* The canvas is a plain block sigma appends its own <canvas> layers into. It
|
|
586
503
|
must have a definite size before sigma measures it, hence \`min-height\` and
|
|
@@ -691,14 +608,14 @@ body{font-size:var(--weave-px-base)}
|
|
|
691
608
|
/* status bar ------------------------------------------------------------ */
|
|
692
609
|
/* One separator vocabulary, shared with the header summary: every run of
|
|
693
610
|
status items after the first opens with the same mid-dot, in the same
|
|
694
|
-
--weave-faint. cwd · selection
|
|
611
|
+
--weave-faint. cwd · selection · stamp reads as one grammar,
|
|
695
612
|
the same way the header's three-part summary does. */
|
|
696
613
|
.weave-status{
|
|
697
614
|
display:flex;align-items:center;gap:8px;height:22px;padding:0 var(--weave-gutter);
|
|
698
615
|
font-family:var(--weave-mono);font-size:var(--weave-px-ui);color:var(--weave-dim);
|
|
699
616
|
border-top:1px solid var(--weave-line);background:var(--weave-panel);
|
|
700
617
|
}
|
|
701
|
-
.weave-status-sel::before
|
|
618
|
+
.weave-status-sel::before{content:"·";margin-right:8px;color:var(--weave-faint)}
|
|
702
619
|
.weave-status-cwd,.weave-status-sel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
703
620
|
.weave-status-cwd{max-width:38%;color:var(--weave-faint)}
|
|
704
621
|
.weave-status-sel{color:var(--weave-fg)}
|
|
@@ -767,7 +684,7 @@ body{font-size:var(--weave-px-base)}
|
|
|
767
684
|
--weave-new tint is exactly an accent at a strength text stays legible in. */
|
|
768
685
|
::selection{background:var(--weave-new)}
|
|
769
686
|
/* Every scroller in the workspace (rows, note body, rail, palette results,
|
|
770
|
-
help sheet
|
|
687
|
+
help sheet) gets the same thin chrome: the default scrollbar is a
|
|
771
688
|
15 px system object the hairline aesthetic cannot afford, and Firefox and
|
|
772
689
|
the Blink/WebKit pair cover the whole surface with these five rules. */
|
|
773
690
|
*{scrollbar-width:thin;scrollbar-color:var(--weave-line-strong) transparent}
|
|
@@ -787,18 +704,13 @@ body{font-size:var(--weave-px-base)}
|
|
|
787
704
|
properties only — colour, border, opacity, transform; height and padding
|
|
788
705
|
snap. */
|
|
789
706
|
.weave-row,.weave-ctx-link,.weave-chip,.weave-search,.weave-refresh,.weave-theme,
|
|
790
|
-
.weave-
|
|
791
|
-
.weave-note-action{
|
|
707
|
+
.weave-hit,.weave-note-open{
|
|
792
708
|
transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;
|
|
793
709
|
}
|
|
794
710
|
.weave-scrim{animation:weave-fade-in 140ms ease-out both}
|
|
795
711
|
.weave-palette,.weave-help{animation:weave-overlay-in 160ms ease-out both}
|
|
796
|
-
/* The conflict prompt mounts once per conflict, so its entrance cannot strobe
|
|
797
|
-
the way a context-rail flash would — that rail deliberately stays still. */
|
|
798
|
-
.weave-note-prompt{animation:weave-prompt-in 180ms ease-out both}
|
|
799
712
|
@keyframes weave-fade-in{from{opacity:0}}
|
|
800
713
|
@keyframes weave-overlay-in{from{opacity:0;transform:translateY(4px)}}
|
|
801
|
-
@keyframes weave-prompt-in{from{opacity:0;transform:translateY(-2px)}}
|
|
802
714
|
@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}
|
|
803
715
|
`;
|
|
804
716
|
|
package/src/web/client/state.ts
CHANGED
|
@@ -1,100 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The context bus (weave-workspace §1.3).
|
|
3
|
-
*
|
|
4
|
-
* No event bus, no pub/sub — five Preact signals. Selecting in the tree,
|
|
5
|
-
* clicking a graph node, following a wikilink and hitting a search result all
|
|
6
|
-
* write {@link selectedId}; the note column, the graph highlight and the
|
|
7
|
-
* context rail are computed from it.
|
|
8
|
-
*
|
|
9
|
-
* ## Tier rules (§2)
|
|
10
|
-
*
|
|
11
|
-
* This is `src/web/client/**`: it may import `src/web/shared` and browser
|
|
12
|
-
* deps, and must **never** import `src/core` or `node:*`. Core is
|
|
13
|
-
* Node-flavoured TypeScript that would drag `node:fs` into the bundle.
|
|
14
|
-
*
|
|
15
|
-
* ## What the P0 placeholders became
|
|
16
|
-
*
|
|
17
|
-
* This file used to declare three local interfaces behind `TODO(P1)` markers,
|
|
18
|
-
* standing in for contracts that had not been written yet. Two of them are
|
|
19
|
-
* now real and imported from `src/web/shared/wire.ts` — {@link graph} carries
|
|
20
|
-
* a `GraphPayload` and {@link noteBody} a `ViewNote`, the exact values
|
|
21
|
-
* `src/web/client/api.ts` returns. That removes a translation step that
|
|
22
|
-
* existed only to satisfy a stale type: the fetchers already produce these
|
|
23
|
-
* shapes, so anything narrower here was a downcast waiting to lose a field.
|
|
24
|
-
*
|
|
25
|
-
* The third resolved the other way. The TODO said to take `TreeState` from
|
|
26
|
-
* the wire too, and that was simply wrong: which rows a user has expanded is
|
|
27
|
-
* **not** a wire contract. It never crosses the network, the server has no
|
|
28
|
-
* opinion about it, and putting it in `wire.ts` would have made the
|
|
29
|
-
* client/server contract carry a purely local UI preference. So
|
|
30
|
-
* {@link TreeState} stays here, declared where it is owned, with the marker
|
|
31
|
-
* removed rather than carried forward.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { signal } from "@preact/signals";
|
|
35
1
|
import type { GraphPayload, NotePayload } from "../shared/wire";
|
|
36
2
|
|
|
37
|
-
/** Connection state shown in the status bar. */
|
|
38
|
-
export type ConnectionState = "live" | "reconnecting" | "offline";
|
|
39
|
-
|
|
40
3
|
/**
|
|
41
4
|
* Which tree rows are open.
|
|
42
5
|
*
|
|
43
6
|
* Client-owned, deliberately: see the module header. An interface rather than
|
|
44
7
|
* a bare array so that P2's filter text and provenance-cycling state have an
|
|
45
|
-
* obvious home that does not change this
|
|
8
|
+
* obvious home that does not change this state shape.
|
|
46
9
|
*/
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
|
|
10
|
+
export interface WorkspaceState {
|
|
11
|
+
graph: GraphPayload | null;
|
|
12
|
+
graphFailed: boolean;
|
|
13
|
+
note: NotePayload | null;
|
|
14
|
+
recentIds: ReadonlySet<string>;
|
|
15
|
+
selectedId: string | null;
|
|
50
16
|
}
|
|
51
17
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return { expanded: [] };
|
|
18
|
+
export function initialWorkspaceState(): WorkspaceState {
|
|
19
|
+
return { graph: null, graphFailed: false, note: null, recentIds: new Set(), selectedId: null };
|
|
55
20
|
}
|
|
56
|
-
|
|
57
|
-
/** The graph node id currently in context — everywhere. */
|
|
58
|
-
export const selectedId = signal<string | null>(null);
|
|
59
|
-
|
|
60
|
-
/** The whole graph, as delivered by the server. `null` until first load. */
|
|
61
|
-
export const graph = signal<GraphPayload | null>(null);
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Whether the *first* graph fetch failed.
|
|
65
|
-
*
|
|
66
|
-
* Distinct from {@link connection} on purpose: the socket can be live while a
|
|
67
|
-
* fetch 500s, and "offline" is then the wrong sentence. With a graph already
|
|
68
|
-
* on screen a failed refetch leaves the stale value standing (see
|
|
69
|
-
* `workspace.ts`'s header — a stale graph beats a blank one), so this only
|
|
70
|
-
* ever describes the boot, which is the one moment there is nothing to show.
|
|
71
|
-
*/
|
|
72
|
-
export const graphFailed = signal(false);
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Derived: fetched when {@link selectedId} names a note.
|
|
76
|
-
*
|
|
77
|
-
* A {@link NotePayload} rather than a bare `ViewNote` as of P5. The revision
|
|
78
|
-
* travels **with** the body because the editor saves against it, and a
|
|
79
|
-
* revision fetched separately would describe a state the draft was not typed
|
|
80
|
-
* against — the exact window a conflict check exists to close. The note
|
|
81
|
-
* column reads `.note` and is otherwise unchanged.
|
|
82
|
-
*/
|
|
83
|
-
export const noteBody = signal<NotePayload | null>(null);
|
|
84
|
-
|
|
85
|
-
/** Which tree rows are open. */
|
|
86
|
-
export const treeState = signal<TreeState>(initialTreeState());
|
|
87
|
-
|
|
88
|
-
/** SSE liveness (§6). */
|
|
89
|
-
export const connection = signal<ConnectionState>("live");
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Node ids that appeared in the most recent graph update (weave-workspace
|
|
93
|
-
* §6 frame → tree feedback).
|
|
94
|
-
*
|
|
95
|
-
* Empty on the mount fetch — a first load must not animate the whole tree.
|
|
96
|
-
* The controller replaces the set on every published payload and clears it
|
|
97
|
-
* after {@link ../workspace!RECENT_TTL_MS}, so a row collapsed and re-expanded
|
|
98
|
-
* minutes later does not replay its arrival animation.
|
|
99
|
-
*/
|
|
100
|
-
export const recentIds = signal<ReadonlySet<string>>(new Set());
|