pi-weave 0.3.0 → 0.3.1
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 +27 -16
- package/package.json +1 -2
- package/src/core/frontmatter.ts +3 -3
- package/src/core/graph/model.ts +2 -2
- package/src/core/graph/wikilinks.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/pi/viewer/tui/surface/detail.ts +1 -1
- package/src/web/client/dist/app.js +210 -178
- package/src/web/client/graph/Graph.tsx +18 -0
- package/src/web/client/graph/column.model.ts +19 -2
- package/src/web/client/graph/graph.model.ts +1 -1
- package/src/web/client/graph/positions.ts +1 -1
- package/src/web/client/graph/renderer.ts +9 -1
- package/src/web/client/note/Note.tsx +66 -91
- package/src/web/client/note/drafts.ts +172 -0
- package/src/web/client/note/note.model.ts +2 -34
- package/src/web/client/search/SearchPalette.tsx +9 -8
- package/src/web/client/shell/ContextRail.tsx +8 -9
- package/src/web/client/shell/Shell.tsx +371 -254
- package/src/web/client/shell/StatusBar.tsx +1 -1
- package/src/web/client/shell/focus.model.ts +2 -2
- package/src/web/client/shell/keys.model.ts +26 -31
- package/src/web/client/shell/keys.ts +1 -0
- package/src/web/client/shell/shell.model.ts +31 -107
- package/src/web/client/shell/theme.model.ts +4 -4
- package/src/web/client/shell/theme.ts +12 -93
- package/src/web/client/shell/workspace.css.ts +117 -0
- package/src/web/client/tree/Tree.tsx +43 -14
- package/src/web/client/tree/tree.model.ts +19 -0
- package/src/web/client/workspace.ts +19 -1
- package/src/web/server/routes.ts +34 -3
- package/src/web/server/server.ts +5 -0
- package/src/web/server/workspace-state.ts +121 -0
- package/src/web/shared/workspace.ts +270 -0
- package/src/web/client/selection.storage.ts +0 -69
- package/src/web/client/shell/Columns.tsx +0 -139
- package/src/web/client/shell/Divider.tsx +0 -15
- package/src/web/client/shell/Header.tsx +0 -119
- package/src/web/client/shell/drag.model.ts +0 -35
- package/src/web/client/shell/layout.model.ts +0 -132
|
@@ -14,7 +14,7 @@ export function StatusBar({ model }: { model: StatusBarModel }) {
|
|
|
14
14
|
<span class="weave-status-cwd" title={model.cwd}>
|
|
15
15
|
{model.cwd}
|
|
16
16
|
</span>
|
|
17
|
-
<span class="weave-status-sel" title="
|
|
17
|
+
<span class="weave-status-sel" title="Current item">
|
|
18
18
|
{model.selection}
|
|
19
19
|
</span>
|
|
20
20
|
<span class="weave-status-stamp" title="data as of">
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* P4's exit criterion is "the whole workspace is drivable without a mouse",
|
|
5
5
|
* and a dialog that does not trap focus fails it in the most literal way
|
|
6
|
-
* available: Tab from the last control lands on the
|
|
7
|
-
* overlay, the user is now typing into a
|
|
6
|
+
* available: Tab from the last control lands on the workspace behind the
|
|
7
|
+
* overlay, the user is now typing into a field they cannot see, and
|
|
8
8
|
* `Esc` goes to whatever they landed on. So the palette and the help overlay
|
|
9
9
|
* both trap, and both restore.
|
|
10
10
|
*
|
|
@@ -90,6 +90,7 @@ export type ShellAction =
|
|
|
90
90
|
| { readonly type: "filterTree" }
|
|
91
91
|
| { readonly type: "fitGraph" }
|
|
92
92
|
| { readonly type: "clearSelection" }
|
|
93
|
+
| { readonly type: "closeTab" }
|
|
93
94
|
/** `t` — cycle the colour theme: system → light → dark → system. */
|
|
94
95
|
| { readonly type: "cycleTheme" };
|
|
95
96
|
|
|
@@ -112,16 +113,6 @@ export const COMMAND_KEYS: Readonly<Record<string, ShellAction>> = {
|
|
|
112
113
|
*/
|
|
113
114
|
export const COLUMN_DIGITS: Readonly<Record<string, ColumnId>> = { "1": "tree", "2": "note", "3": "graph" };
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
-
* The columns a breakpoint can put off screen — the graph at `"medium"`, the
|
|
117
|
-
* tree too at `"narrow"`, never the note. The help sheet's derived rows use it
|
|
118
|
-
* to stay honest about `⌘1`/`⌘3`: at 900 px the `⌘3` command is a documented
|
|
119
|
-
* no-op, and a help line that hides that is the same lie the `disabled`
|
|
120
|
-
* search button once was. Derived from `columnsAt`, not listed, so the caveat
|
|
121
|
-
* cannot drift from the breakpoints the sheet is attached to.
|
|
122
|
-
*/
|
|
123
|
-
const COLLAPSIBLE_COLUMNS: ReadonlySet<ColumnId> = new Set(["tree", "graph"]);
|
|
124
|
-
|
|
125
116
|
/**
|
|
126
117
|
* Whether a modifier combination counts as "the platform's command key".
|
|
127
118
|
*
|
|
@@ -168,6 +159,8 @@ export const BARE_KEYS: Readonly<Record<string, ShellAction>> = {
|
|
|
168
159
|
export function shellKey(event: KeyDescriptor, ctx: KeyContext): ShellAction | null {
|
|
169
160
|
if (ctx.overlay !== null) return event.key === "Escape" ? { type: "closeOverlay" } : null;
|
|
170
161
|
|
|
162
|
+
if (event.alt && !event.meta && !event.ctrl && !event.shift && event.key.toLowerCase() === "w") return { type: "closeTab" };
|
|
163
|
+
|
|
171
164
|
if (isCommand(event)) {
|
|
172
165
|
// Lower-cased because ⌘K with caps lock on reports `"K"`, and a shortcut
|
|
173
166
|
// that stops working under caps lock is a bug report nobody can reproduce.
|
|
@@ -231,6 +224,8 @@ export interface KeyTarget {
|
|
|
231
224
|
* use, for the same reason: there is no DOM test environment (§10).
|
|
232
225
|
*/
|
|
233
226
|
export interface KeyboardEventLike {
|
|
227
|
+
readonly code?: string;
|
|
228
|
+
readonly defaultPrevented: boolean;
|
|
234
229
|
readonly key: string;
|
|
235
230
|
readonly ctrlKey: boolean;
|
|
236
231
|
readonly metaKey: boolean;
|
|
@@ -243,7 +238,7 @@ export interface KeyboardEventLike {
|
|
|
243
238
|
/** Reduce a platform event to a {@link KeyDescriptor}. */
|
|
244
239
|
export function describeKey(event: KeyboardEventLike): KeyDescriptor {
|
|
245
240
|
return {
|
|
246
|
-
key: event.key,
|
|
241
|
+
key: event.altKey && event.code === "KeyW" ? "w" : event.key,
|
|
247
242
|
ctrl: event.ctrlKey,
|
|
248
243
|
meta: event.metaKey,
|
|
249
244
|
shift: event.shiftKey,
|
|
@@ -254,27 +249,15 @@ export function describeKey(event: KeyboardEventLike): KeyDescriptor {
|
|
|
254
249
|
|
|
255
250
|
// --- where an action points ---------------------------------------------------------
|
|
256
251
|
|
|
257
|
-
/**
|
|
258
|
-
* `⌘1/2/3` → the element to focus in that column.
|
|
259
|
-
*
|
|
260
|
-
* Selectors rather than refs, and the reason is the breakpoint: below 1100 px
|
|
261
|
-
* the graph column does not exist and below 800 px the tree does not either,
|
|
262
|
-
* so a ref would be `null` for a column that is legitimately absent and the
|
|
263
|
-
* shell would need a branch per column to cope. A `querySelector` that finds
|
|
264
|
-
* nothing is already the same answer, expressed once.
|
|
265
|
-
*
|
|
266
|
-
* Each target is the column's *content*, not its `<section>`: focusing the
|
|
267
|
-
* section would put the ring around the whole pane and leave the arrow keys
|
|
268
|
-
* pointing at nothing.
|
|
269
|
-
*/
|
|
252
|
+
/** Content focus targets; the shell opens hidden sidebars/tabs before focusing. */
|
|
270
253
|
export const COLUMN_FOCUS_SELECTORS: Readonly<Record<ColumnId, string>> = {
|
|
271
|
-
tree: ".weave-
|
|
272
|
-
note: ".weave-
|
|
273
|
-
graph: ".weave-
|
|
254
|
+
tree: ".weave-sidebar-notes .weave-rows",
|
|
255
|
+
note: ".weave-pane-active .weave-note-body",
|
|
256
|
+
graph: ".weave-graph-host .weave-graph-canvas",
|
|
274
257
|
};
|
|
275
258
|
|
|
276
259
|
/** `/` → the tree's filter box. */
|
|
277
|
-
export const TREE_FILTER_SELECTOR = ".weave-
|
|
260
|
+
export const TREE_FILTER_SELECTOR = ".weave-sidebar-notes .weave-filter";
|
|
278
261
|
|
|
279
262
|
/** The slice of an element {@link focusable} produces. */
|
|
280
263
|
export interface Focusable {
|
|
@@ -336,6 +319,7 @@ export interface ShellEffects {
|
|
|
336
319
|
fitGraph(): void;
|
|
337
320
|
/** Write `null` to §1.3's `selectedId`. */
|
|
338
321
|
clearSelection(): void;
|
|
322
|
+
closeTab(): void;
|
|
339
323
|
/** `t` — advance the user's theme choice by one step in its cycle. */
|
|
340
324
|
cycleTheme(): void;
|
|
341
325
|
}
|
|
@@ -350,8 +334,7 @@ export function runShellAction(action: ShellAction, fx: ShellEffects): void {
|
|
|
350
334
|
case "closeOverlay":
|
|
351
335
|
return fx.setOverlay(null);
|
|
352
336
|
case "focusColumn":
|
|
353
|
-
// The
|
|
354
|
-
// at this breakpoint is a miss with nothing to report to.
|
|
337
|
+
// The shell reveals the target before focusing it.
|
|
355
338
|
return void fx.focusSelector(COLUMN_FOCUS_SELECTORS[action.column]);
|
|
356
339
|
case "filterTree":
|
|
357
340
|
return void fx.focusSelector(TREE_FILTER_SELECTOR);
|
|
@@ -359,6 +342,8 @@ export function runShellAction(action: ShellAction, fx: ShellEffects): void {
|
|
|
359
342
|
return fx.fitGraph();
|
|
360
343
|
case "clearSelection":
|
|
361
344
|
return fx.clearSelection();
|
|
345
|
+
case "closeTab":
|
|
346
|
+
return fx.closeTab();
|
|
362
347
|
case "cycleTheme":
|
|
363
348
|
return fx.cycleTheme();
|
|
364
349
|
}
|
|
@@ -397,10 +382,11 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
397
382
|
{
|
|
398
383
|
title: "Global",
|
|
399
384
|
entries: [
|
|
385
|
+
{ combo: "⌥W / Alt W", what: "Close the active tab" },
|
|
400
386
|
{ combo: `${cmd}K`, what: "Search notes and the repository" },
|
|
401
387
|
...Object.entries(COLUMN_DIGITS).map(([digit, column]) => ({
|
|
402
388
|
combo: `${cmd}${digit}`,
|
|
403
|
-
what:
|
|
389
|
+
what: column === "tree" ? "Open and focus the notes sidebar" : column === "graph" ? "Open and focus Graph view" : "Focus the note",
|
|
404
390
|
})),
|
|
405
391
|
{ combo: "?", what: "This help" },
|
|
406
392
|
{ combo: "t", what: "Cycle the colour theme (system / light / dark)" },
|
|
@@ -418,6 +404,14 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
418
404
|
{ combo: "Home / End", what: "First / last row" },
|
|
419
405
|
],
|
|
420
406
|
},
|
|
407
|
+
{
|
|
408
|
+
title: "Tabs and panes",
|
|
409
|
+
entries: [
|
|
410
|
+
{ combo: "← / → / Home / End", what: "Select a tab when the tab strip is focused" },
|
|
411
|
+
{ combo: `${cmd}click`, what: "Open a note or search result in a new tab" },
|
|
412
|
+
{ combo: "Pane options (···)", what: "Split right or down, move a tab, or close a pane" },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
421
415
|
{
|
|
422
416
|
title: "Graph",
|
|
423
417
|
entries: [{ combo: "g", what: "Fit the whole graph" }],
|
|
@@ -427,6 +421,7 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
427
421
|
entries: [
|
|
428
422
|
{ combo: "↑ / ↓", what: "Move through results" },
|
|
429
423
|
{ combo: "↵", what: "Open the highlighted result" },
|
|
424
|
+
{ combo: `${cmd}↵`, what: "Open the highlighted result in a new tab" },
|
|
430
425
|
{ combo: "Esc", what: "Close the palette" },
|
|
431
426
|
],
|
|
432
427
|
},
|
|
@@ -49,6 +49,7 @@ export interface KeyOptions {
|
|
|
49
49
|
*/
|
|
50
50
|
export function watchKeys(host: KeyHost, opts: KeyOptions): () => void {
|
|
51
51
|
const listener = (event: KeyboardEventLike): void => {
|
|
52
|
+
if (event.defaultPrevented) return;
|
|
52
53
|
const action = shellKey(describeKey(event), opts.context());
|
|
53
54
|
if (action === null) return;
|
|
54
55
|
event.preventDefault();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Everything the shell *decides*, as pure functions (weave-workspace §1.2).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The Recent list, per-column empty-state copy and footer status text live
|
|
5
|
+
* here rather than inside the components that render them.
|
|
6
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
|
|
@@ -14,67 +14,43 @@
|
|
|
14
14
|
* types, no `node:*`, no `src/core`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { GraphPayload, WireNodeKind
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
export
|
|
17
|
+
import type { GraphPayload, WireNodeKind } from "../../shared/wire";
|
|
18
|
+
import { kindIcon } from "../tree/tree.model";
|
|
19
|
+
import type { IconName } from "./icons.model";
|
|
20
|
+
export const NOTE_DRAG_TYPE = "application/x-weave-note";
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* The `vault:34 · repo:fresh · 127 nodes` readout from the §1.2 sketch.
|
|
26
|
-
*
|
|
27
|
-
* Kept as three fields rather than one pre-joined string so the component can
|
|
28
|
-
* put a separator between them without this module owning a `·`, and so a
|
|
29
|
-
* test asserts the numbers rather than a formatting decision.
|
|
30
|
-
*/
|
|
31
|
-
export interface HeaderSummary {
|
|
32
|
-
/** Note nodes in the vault. */
|
|
33
|
-
readonly notes: number;
|
|
34
|
-
/** Repository index freshness, or `null` when the repo is unindexed. */
|
|
35
|
-
readonly repo: WireStalenessState | null;
|
|
36
|
-
/** Every node, of every kind. */
|
|
37
|
-
readonly nodes: number;
|
|
22
|
+
/** Pane drops open only note IDs that still exist in this workspace. */
|
|
23
|
+
export function noteDropId(graph: GraphPayload | null, id: string): string | null {
|
|
24
|
+
return id.startsWith("note:") && graph?.model.nodes.some((node) => node.id === id && node.kind === "note") ? id : null;
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
export const
|
|
27
|
+
export type ColumnId = "tree" | "note" | "graph";
|
|
28
|
+
export const COLUMNS: readonly ColumnId[] = ["tree", "note", "graph"];
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* would make an empty vault read `vault:1` — a number that is technically
|
|
48
|
-
* defensible and would still be read as "there is one note in there".
|
|
49
|
-
*/
|
|
50
|
-
const VAULT_KINDS: readonly WireNodeKind[] = ["note"];
|
|
30
|
+
/** Most recent selections in this window, independent of whether their tabs stay open. */
|
|
31
|
+
export function recordVisit(previous: readonly string[], id: string | null): readonly string[] {
|
|
32
|
+
return id === null ? previous : [id, ...previous.filter((item) => item !== id)].slice(0, 100);
|
|
33
|
+
}
|
|
51
34
|
|
|
52
|
-
/**
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
const nodes = payload.model.nodes;
|
|
56
|
-
return {
|
|
57
|
-
notes: nodes.filter((node) => VAULT_KINDS.includes(node.kind)).length,
|
|
58
|
-
repo: payload.model.staleness?.state ?? null,
|
|
59
|
-
nodes: nodes.length,
|
|
60
|
-
};
|
|
35
|
+
/** A selected graph node opens only after its preview has already been shown. */
|
|
36
|
+
export function graphClickOpensTab(previewId: string | null, id: string | null): id is string {
|
|
37
|
+
return id !== null && id === previewId;
|
|
61
38
|
}
|
|
62
39
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* scanned is the ordinary first-run state, not a fault. The word the user
|
|
69
|
-
* needs is the one that implies an action they can take.
|
|
70
|
-
*/
|
|
71
|
-
export function repoLabel(repo: WireStalenessState | null): string {
|
|
72
|
-
return repo === null || repo === "missing" ? "unindexed" : repo;
|
|
40
|
+
export interface RecentEntry {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly label: string;
|
|
43
|
+
readonly icon: IconName;
|
|
44
|
+
readonly selected: boolean;
|
|
73
45
|
}
|
|
74
46
|
|
|
75
|
-
/**
|
|
76
|
-
export function
|
|
77
|
-
return
|
|
47
|
+
/** Present a frozen visit order without changing it when selection moves. */
|
|
48
|
+
export function recentEntries(ids: readonly string[], graph: GraphPayload | null, selectedId: string | null): RecentEntry[] {
|
|
49
|
+
return ids.map((id) => {
|
|
50
|
+
const node = graph?.model.nodes.find((entry) => entry.id === id);
|
|
51
|
+
const kind: WireNodeKind = node?.kind ?? (id.startsWith("vfolder:") ? "module" : id.startsWith("artifact:") ? "file" : "note");
|
|
52
|
+
return { id, label: node?.label ?? id.replace(/^[^:]+:/, ""), icon: kindIcon(kind), selected: id === selectedId };
|
|
53
|
+
});
|
|
78
54
|
}
|
|
79
55
|
|
|
80
56
|
// --- empty states ------------------------------------------------------------------
|
|
@@ -84,23 +60,6 @@ export interface EmptyStateCopy {
|
|
|
84
60
|
readonly title: string;
|
|
85
61
|
}
|
|
86
62
|
|
|
87
|
-
const EMPTY_STATES: Readonly<Record<ColumnId, EmptyStateCopy>> = {
|
|
88
|
-
tree: {
|
|
89
|
-
title: "Tree",
|
|
90
|
-
},
|
|
91
|
-
note: {
|
|
92
|
-
title: "Note",
|
|
93
|
-
},
|
|
94
|
-
graph: {
|
|
95
|
-
title: "Graph",
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/** The empty-state copy for a column. */
|
|
100
|
-
export function emptyStateFor(column: ColumnId): EmptyStateCopy {
|
|
101
|
-
return EMPTY_STATES[column];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
63
|
/** The context rail's heading and `aria-label`. */
|
|
105
64
|
export const CONTEXT_EMPTY: EmptyStateCopy = {
|
|
106
65
|
title: "Context",
|
|
@@ -156,24 +115,6 @@ export function shortStamp(stamp: string | null): string {
|
|
|
156
115
|
return match?.[1] ?? stamp;
|
|
157
116
|
}
|
|
158
117
|
|
|
159
|
-
// --- the refresh button ------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* The refresh control's glyph, as SVG path data.
|
|
163
|
-
*
|
|
164
|
-
* A drawn icon replaces the `⟳` text character for the same reason §P6.4
|
|
165
|
-
* retires the tree's glyph soup: a text arrow is whatever the platform's
|
|
166
|
-
* fallback font draws it as, while these two strokes are the brand's own
|
|
167
|
-
* weight everywhere. Stored as pure data here — §10 — so `Header.tsx` stays
|
|
168
|
-
* props-in/JSX-out and the shape is testable without a DOM. 24×24 viewBox,
|
|
169
|
-
* stroked, `currentColor`: the header recolours it on hover like any other
|
|
170
|
-
* glyph.
|
|
171
|
-
*/
|
|
172
|
-
export const REFRESH_ICON_PATHS: readonly string[] = [
|
|
173
|
-
"M21 12a9 9 0 1 1-2.64-6.36",
|
|
174
|
-
"M21 3v6h-6",
|
|
175
|
-
];
|
|
176
|
-
|
|
177
118
|
/**
|
|
178
119
|
* How often the shell re-renders on its own, in ms.
|
|
179
120
|
*
|
|
@@ -189,29 +130,12 @@ export const TICK_MS = 60_000;
|
|
|
189
130
|
|
|
190
131
|
// --- the search affordance ---------------------------------------------------------
|
|
191
132
|
|
|
192
|
-
/**
|
|
193
|
-
* The `⌘K` control.
|
|
194
|
-
*
|
|
195
|
-
* Rendered **disabled** through P1–P3, with a title saying search arrived in
|
|
196
|
-
* P4 — a deliberate choice over omitting it: the keyboard hint taught the
|
|
197
|
-
* shortcut that would exist, and a `disabled` control explaining itself is
|
|
198
|
-
* honest in a way that a working-looking box that does nothing is not.
|
|
199
|
-
*
|
|
200
|
-
* P4 made it live, and it is now a *button* rather than an `<input>`. The
|
|
201
|
-
* palette owns the only text field, so a second one in the header would be
|
|
202
|
-
* two places to type a query into and one of them would be a lie — clicking
|
|
203
|
-
* it opens the overlay and whatever was typed into the header box would be
|
|
204
|
-
* discarded. A button that looks like a search field and opens the real one
|
|
205
|
-
* is the affordance the §1.2 sketch actually describes.
|
|
206
|
-
*/
|
|
207
|
-
export const SEARCH_PLACEHOLDER = "Search…";
|
|
208
|
-
|
|
209
133
|
/** The shortcut hint. `⌘K` on Apple platforms, `Ctrl K` elsewhere. */
|
|
210
134
|
export function searchShortcut(isApple: boolean): string {
|
|
211
135
|
return isApple ? "⌘K" : "Ctrl K";
|
|
212
136
|
}
|
|
213
137
|
|
|
214
|
-
/** The
|
|
138
|
+
/** The search action's tooltip, including the key that also opens it. */
|
|
215
139
|
export function searchHint(shortcut: string): string {
|
|
216
140
|
return `Search notes and repository (${shortcut})`;
|
|
217
141
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* "Dark" and "light" alone are not enough: stripping a user of "follow my OS"
|
|
14
14
|
* is a regression the moment anyone prefers the *default* behaviour rather
|
|
15
15
|
* than either specific scheme. The cycle is `system → light → dark → system`
|
|
16
|
-
* so the control is one button, never a menu — a popover in
|
|
16
|
+
* so the control is one button, never a menu — a popover in the ribbon is
|
|
17
17
|
* a second focusable thing to reach, and the setting is not worth one.
|
|
18
18
|
*
|
|
19
19
|
* ## The attribute, not a class, not inline styles
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* ## Tier rules (§2)
|
|
33
33
|
*
|
|
34
34
|
* `src/web/client/**`. Storage arrives as the two-method port
|
|
35
|
-
*
|
|
35
|
+
* the graph position cache uses, so a fake is an object literal and quota
|
|
36
36
|
* failures stay the caller's problem — a theme the user could not persist is
|
|
37
37
|
* the same cosmetic cost as a selection.
|
|
38
38
|
*/
|
|
@@ -76,7 +76,7 @@ export function loadTheme(storage: ThemeStorage): ThemeChoice | null {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Persist the choice. Reports failure as `false
|
|
79
|
+
/** Persist the choice. Reports failure as `false`. */
|
|
80
80
|
export function saveTheme(storage: ThemeStorage, choice: ThemeChoice): boolean {
|
|
81
81
|
try {
|
|
82
82
|
storage.setItem(THEME_STORAGE_KEY, choice);
|
|
@@ -120,7 +120,7 @@ export function themeAttr(choice: ThemeChoice): "light" | "dark" | null {
|
|
|
120
120
|
// --- the control's view ------------------------------------------------------------
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* The
|
|
123
|
+
* The ribbon button's face.
|
|
124
124
|
*
|
|
125
125
|
* The glyphs are the provenance language the tree already teaches
|
|
126
126
|
* (`tree.model.ts`): filled, half and hollow of one shape. Filled `●` is
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { WORKSPACE_CSS } from "./workspace.css";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The workspace stylesheet, and how it gets past the CSP
|
|
3
5
|
* (weave-workspace §1.2, §5.2).
|
|
@@ -162,7 +164,7 @@ export const THEME_CSS = `
|
|
|
162
164
|
row 12 list rows, text inputs, inline code
|
|
163
165
|
base 13 body copy, h3-h6, preview-card text
|
|
164
166
|
body 13.5 the note column's prose
|
|
165
|
-
subhead 14 palette input, note-body h2,
|
|
167
|
+
subhead 14 palette input, note-body h2, ribbon icon buttons
|
|
166
168
|
title 15 note-body h1
|
|
167
169
|
display 20 the note's page title
|
|
168
170
|
Two earlier sizes were merged into neighbours: 9px glyphs to prov, and
|
|
@@ -196,86 +198,7 @@ export const THEME_CSS = `
|
|
|
196
198
|
${LIGHT_TOKENS}
|
|
197
199
|
}
|
|
198
200
|
body{font-size:var(--weave-px-base)}
|
|
199
|
-
#app{height:100%;display:grid;grid-template-rows:
|
|
200
|
-
|
|
201
|
-
/* header --------------------------------------------------------------- */
|
|
202
|
-
.weave-header{
|
|
203
|
-
display:flex;align-items:center;gap:14px;padding:0 var(--weave-gutter);
|
|
204
|
-
height:34px;border-bottom:1px solid var(--weave-line);background:var(--weave-panel);
|
|
205
|
-
}
|
|
206
|
-
.weave-brand{display:inline-flex;align-items:center;gap:6px;font-size:var(--weave-px-subhead);font-weight:600;letter-spacing:.01em;white-space:nowrap}
|
|
207
|
-
.weave-brand-mark{height:20px;width:20px;border-radius:var(--weave-radius)}
|
|
208
|
-
/* A button styled as a search field: the palette owns the only text input,
|
|
209
|
-
so this opens it rather than pretending to accept a query. */
|
|
210
|
-
.weave-search{
|
|
211
|
-
display:flex;align-items:center;gap:6px;flex:0 1 260px;min-width:0;
|
|
212
|
-
height:22px;padding:0 7px;font:inherit;font-size:var(--weave-px-row);text-align:left;cursor:pointer;
|
|
213
|
-
color:var(--weave-dim);background:var(--weave-bg);
|
|
214
|
-
border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
|
|
215
|
-
}
|
|
216
|
-
.weave-search:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
|
|
217
|
-
.weave-search-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
218
|
-
.weave-search kbd{
|
|
219
|
-
font-family:var(--weave-mono);font-size:var(--weave-px-caption);color:var(--weave-faint);
|
|
220
|
-
padding:1px 5px;border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);white-space:nowrap;
|
|
221
|
-
}
|
|
222
|
-
.weave-summary{
|
|
223
|
-
margin-left:auto;display:flex;gap:9px;font-family:var(--weave-mono);
|
|
224
|
-
font-size:var(--weave-px-ui);color:var(--weave-dim);white-space:nowrap;
|
|
225
|
-
}
|
|
226
|
-
.weave-summary-part+.weave-summary-part::before{content:"·";margin-right:9px;color:var(--weave-faint)}
|
|
227
|
-
.weave-refresh{
|
|
228
|
-
font:inherit;font-size:var(--weave-px-subhead);line-height:1;color:var(--weave-dim);background:none;
|
|
229
|
-
border:0;padding:3px 5px;border-radius:var(--weave-radius);cursor:pointer;
|
|
230
|
-
display:inline-flex;align-items:center;
|
|
231
|
-
}
|
|
232
|
-
.weave-refresh:hover{color:var(--weave-fg);background:var(--weave-line)}
|
|
233
|
-
/* One full 600 ms turn per click ("the request left"), not a loop: the
|
|
234
|
-
refetch is fire-and-forget and a 304 may never fire a completion signal
|
|
235
|
-
for a spinner to wait on. Reduced-motion restores the static stroke. */
|
|
236
|
-
.weave-refresh-spinning{animation:weave-refresh-turn 600ms ease-out}
|
|
237
|
-
@keyframes weave-refresh-turn{from{transform:rotate(0)}to{transform:rotate(360deg)}}
|
|
238
|
-
/* The theme cycle (shell/theme.model.ts). Same shape as the refresh button:
|
|
239
|
-
an icon-size control in a 34 px bar, glyph-only because the filled/half/
|
|
240
|
-
hollow family already means something in this workspace. */
|
|
241
|
-
.weave-theme{
|
|
242
|
-
font:inherit;font-size:var(--weave-px-base);line-height:1;color:var(--weave-dim);background:none;
|
|
243
|
-
border:0;padding:3px 6px;border-radius:var(--weave-radius);cursor:pointer;
|
|
244
|
-
}
|
|
245
|
-
.weave-theme:hover{color:var(--weave-fg);background:var(--weave-line)}
|
|
246
|
-
|
|
247
|
-
/* the grid -------------------------------------------------------------- */
|
|
248
|
-
/* Widths arrive as custom properties from layout.model.ts. Shell/layout.model
|
|
249
|
-
selects the breakpoint; CSS consumes the resulting data-columns shape. */
|
|
250
|
-
.weave-grid{display:grid;min-height:0;overflow:hidden;background:var(--weave-line)}
|
|
251
|
-
.weave-grid[data-columns="3"]{grid-template-columns:var(--weave-col-tree,22%) 1px var(--weave-col-note,46%) 1px var(--weave-col-graph,32%)}
|
|
252
|
-
.weave-grid[data-columns="2"]{grid-template-columns:var(--weave-col-tree,32%) 1px var(--weave-col-note,68%)}
|
|
253
|
-
.weave-grid[data-columns="1"]{grid-template-columns:1fr}
|
|
254
|
-
|
|
255
|
-
/* dividers -------------------------------------------------------------- */
|
|
256
|
-
.weave-divider{
|
|
257
|
-
width:1px;cursor:col-resize;touch-action:none;background:var(--weave-line);position:relative;z-index:2;
|
|
258
|
-
}
|
|
259
|
-
.weave-divider::after{content:"";position:absolute;inset:0 -4px}
|
|
260
|
-
.weave-divider:hover{background:var(--weave-faint)}
|
|
261
|
-
.weave-divider:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-1px}
|
|
262
|
-
|
|
263
|
-
.weave-col{
|
|
264
|
-
display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;
|
|
265
|
-
background:var(--weave-bg);
|
|
266
|
-
}
|
|
267
|
-
.weave-col-title{
|
|
268
|
-
margin:0;padding:5px var(--weave-gutter) 4px;font-size:var(--weave-px-caption);font-weight:600;
|
|
269
|
-
letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint);
|
|
270
|
-
border-bottom:1px solid var(--weave-line);
|
|
271
|
-
}
|
|
272
|
-
/* Title, then the graph (which takes the slack), then the context rail. The
|
|
273
|
-
1fr is what gives .weave-graph-canvas a definite height to measure, and the
|
|
274
|
-
rail's row is a fixed fraction of the column — not content-sized — so
|
|
275
|
-
moving the selection between a sparsely-connected note and a richly
|
|
276
|
-
connected one never repartitions the right-hand column: the rail scrolls
|
|
277
|
-
inside the same region instead of squeezing the graph. */
|
|
278
|
-
.weave-col-graph{display:grid;grid-template-rows:auto minmax(0,1fr) minmax(96px,40%);background:var(--weave-bg)}
|
|
201
|
+
#app{height:100%;display:grid;grid-template-rows:minmax(0,1fr) auto;background:var(--weave-bg)}
|
|
279
202
|
|
|
280
203
|
/* tree column ----------------------------------------------------------- */
|
|
281
204
|
/* --weave-depth is written per row by \`depthVar\`, through CSSOM
|
|
@@ -487,8 +410,8 @@ body{font-size:var(--weave-px-base)}
|
|
|
487
410
|
text-decoration-thickness:1px;text-underline-offset:2px;
|
|
488
411
|
}
|
|
489
412
|
.weave-note-body a:hover{text-decoration-color:var(--weave-accent)}
|
|
490
|
-
/* A ghost is a name with no note behind it:
|
|
491
|
-
|
|
413
|
+
/* A ghost is a name with no note behind it: a dashed, dimmed affordance to
|
|
414
|
+
create one. */
|
|
492
415
|
.weave-wiki{cursor:pointer;color:var(--weave-accent);text-decoration:none}
|
|
493
416
|
.weave-wiki:hover{text-decoration:underline}
|
|
494
417
|
.weave-wiki-ghost{
|
|
@@ -633,9 +556,8 @@ body{font-size:var(--weave-px-base)}
|
|
|
633
556
|
text-align:left;background:none;border:0;cursor:pointer;
|
|
634
557
|
}
|
|
635
558
|
.weave-ctx-heading:hover{color:var(--weave-fg)}
|
|
636
|
-
/* The count is a mono voice
|
|
637
|
-
|
|
638
|
-
"BACKLINKS 4" in the text face would be the one thing off-key. */
|
|
559
|
+
/* The count is a mono voice like the other status numbers, so a rail that
|
|
560
|
+
says "BACKLINKS 4" in the text face would be the one thing off-key. */
|
|
639
561
|
.weave-ctx-count{
|
|
640
562
|
margin-left:auto;font-family:var(--weave-mono);font-size:var(--weave-px-caption);
|
|
641
563
|
letter-spacing:0;color:var(--weave-faint);font-weight:400;
|
|
@@ -662,11 +584,8 @@ body{font-size:var(--weave-px-base)}
|
|
|
662
584
|
.weave-ctx-tag{margin:0 0 3px}
|
|
663
585
|
.weave-ctx-tag .weave-ctx-rows{padding-left:12px}
|
|
664
586
|
|
|
665
|
-
/*
|
|
666
|
-
/*
|
|
667
|
-
status items after the first opens with the same mid-dot, in the same
|
|
668
|
-
--weave-faint. cwd · selection · stamp reads as one grammar,
|
|
669
|
-
the same way the header's three-part summary does. */
|
|
587
|
+
/* workspace footer ------------------------------------------------------ */
|
|
588
|
+
/* The status items share one mid-dot separator in --weave-faint. */
|
|
670
589
|
.weave-status{
|
|
671
590
|
display:flex;align-items:center;gap:8px;height:22px;padding:0 var(--weave-gutter);
|
|
672
591
|
font-family:var(--weave-mono);font-size:var(--weave-px-ui);color:var(--weave-dim);
|
|
@@ -760,7 +679,7 @@ body{font-size:var(--weave-px-base)}
|
|
|
760
679
|
would need delay-unmount plumbing far past polish. Compositor-safe
|
|
761
680
|
properties only — colour, border, opacity, transform; height and padding
|
|
762
681
|
snap. */
|
|
763
|
-
.weave-row,.weave-ctx-link,.weave-chip,.weave-
|
|
682
|
+
.weave-row,.weave-ctx-link,.weave-chip,.weave-ribbon button,
|
|
764
683
|
.weave-hit,.weave-note-open{
|
|
765
684
|
transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;
|
|
766
685
|
}
|
|
@@ -826,7 +745,7 @@ export function findNonce(host: ThemeHost): string | null {
|
|
|
826
745
|
* report in the console is a far better diagnostic than a stylesheet that was
|
|
827
746
|
* never created.
|
|
828
747
|
*/
|
|
829
|
-
export function installTheme(host: ThemeHost, css: string = THEME_CSS): boolean {
|
|
748
|
+
export function installTheme(host: ThemeHost, css: string = THEME_CSS + WORKSPACE_CSS): boolean {
|
|
830
749
|
const element = host.createElement("style");
|
|
831
750
|
const nonce = findNonce(host);
|
|
832
751
|
if (nonce !== null) element.nonce = nonce;
|