pi-weave 0.3.9 → 0.3.11
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 +7 -3
- package/package.json +1 -1
- package/src/web/client/context/context.model.ts +5 -0
- package/src/web/client/dist/app.js +400 -167
- package/src/web/client/graph/Graph.tsx +148 -12
- package/src/web/client/graph/column.model.ts +58 -1
- package/src/web/client/graph/graph.model.ts +114 -0
- package/src/web/client/graph/renderer.ts +21 -7
- package/src/web/client/graph/spotlight.model.ts +476 -0
- package/src/web/client/note/Note.tsx +212 -59
- package/src/web/client/note/Replay.tsx +18 -7
- package/src/web/client/note/drafts.ts +69 -6
- package/src/web/client/note/editbar.model.ts +153 -0
- package/src/web/client/note/note.model.ts +65 -1
- package/src/web/client/note/replay.model.ts +75 -0
- package/src/web/client/search/SearchPalette.tsx +103 -34
- package/src/web/client/search/search.model.ts +533 -26
- package/src/web/client/shell/Shell.tsx +79 -21
- package/src/web/client/shell/StatusBar.tsx +1 -0
- package/src/web/client/shell/icons.model.ts +68 -1
- package/src/web/client/shell/keys.model.ts +11 -1
- package/src/web/client/shell/shell.model.ts +48 -1
- package/src/web/client/shell/theme.model.ts +3 -0
- package/src/web/client/shell/theme.ts +260 -61
- package/src/web/client/shell/workspace.css.ts +38 -11
- package/src/web/client/trail/TrailStrip.tsx +0 -1
- package/src/web/client/trail/trail.model.ts +61 -6
- package/src/web/client/tree/Tree.tsx +3 -7
- package/src/web/shared/themes.ts +1 -1
- package/src/web/shared/workspace.ts +10 -2
|
@@ -18,8 +18,9 @@ import { initialWorkspaceState } from "../state";
|
|
|
18
18
|
import { startWorkspace, watchNote } from "../workspace";
|
|
19
19
|
import type { WorkspaceHandle } from "../workspace";
|
|
20
20
|
import { ContextRail } from "./ContextRail";
|
|
21
|
+
import { contextCount, contextModel } from "../context/context.model";
|
|
21
22
|
import { startTrailPoll } from "../trail/trail";
|
|
22
|
-
import { TRAIL_TICK_MS, trailLive, trailMarks, trailTurns } from "../trail/trail.model";
|
|
23
|
+
import { TRAIL_TICK_MS, agentSearch, agentSpotlight, trailLive, trailMarks, trailTurns } from "../trail/trail.model";
|
|
23
24
|
import type { TrailReplay } from "../trail/trail.model";
|
|
24
25
|
import { ResizeHandle } from "./ResizeHandle";
|
|
25
26
|
import { deeplinkSelection, formatHash } from "./deeplink.model";
|
|
@@ -31,7 +32,7 @@ import { watchKeys } from "./keys";
|
|
|
31
32
|
import { COLUMN_FOCUS_SELECTORS, TREE_FILTER_SELECTOR, focusSelector, runShellAction } from "./keys.model";
|
|
32
33
|
import { StatusBar } from "./StatusBar";
|
|
33
34
|
import type { OverlayId } from "./shell.model";
|
|
34
|
-
import { NOTE_DRAG_TYPE, noteDropId, TICK_MS, graphClickOpensTab, looksApple, recentEntries, recordVisit, searchHint, searchShortcut, statusBarModel } from "./shell.model";
|
|
35
|
+
import { NOTE_DRAG_TYPE, noteDropId, TICK_MS, graphClickOpensTab, looksApple, openInSplit, recentEntries, recordVisit, searchHint, searchShortcut, statusBarModel } from "./shell.model";
|
|
35
36
|
import { toggleTheme, effectiveScheme, loadTheme, saveTheme, themeButton } from "./theme.model";
|
|
36
37
|
|
|
37
38
|
export interface ShellProps { cwd: string; initialWidth: number; platform: string }
|
|
@@ -135,6 +136,15 @@ export function Shell(props: ShellProps) {
|
|
|
135
136
|
const [graphSelection, setGraphSelection] = useState<string | null>(null);
|
|
136
137
|
const lastViewedDocument = useRef<string | null>(null);
|
|
137
138
|
const [graphPreviewId, setGraphPreviewId] = useState<string | null>(null);
|
|
139
|
+
// The graph spotlight's query (`graph/spotlight.model.ts`). Held here, not
|
|
140
|
+
// in the column, so other UI can light the graph up — see `lightUp`.
|
|
141
|
+
const [graphQuery, setGraphQuery] = useState("");
|
|
142
|
+
// What the search palette listed when it lit the graph up, for that query.
|
|
143
|
+
// Typing a different query in the spotlight leaves these behind.
|
|
144
|
+
const [graphFound, setGraphFound] = useState<{ query: string; ids: readonly string[] } | null>(null);
|
|
145
|
+
// The agent search the spotlight is showing (its trail `seq`), or `null`
|
|
146
|
+
// when the spotlight is the person's — see `agentSpotlight`.
|
|
147
|
+
const [agentLit, setAgentLit] = useState<number | null>(null);
|
|
138
148
|
const draggedTab = useRef<string | null>(null);
|
|
139
149
|
const [dropTab, setDropTab] = useState<{ paneId: string; before: string | null } | null>(null);
|
|
140
150
|
const [dropPane, setDropPane] = useState<string | null>(null);
|
|
@@ -162,6 +172,11 @@ export function Shell(props: ShellProps) {
|
|
|
162
172
|
const fullGraphVisible = graphPane !== undefined && (width >= 850 || graphPane.id === layout.activePane);
|
|
163
173
|
const contextGraphActive = contextGraph && tab.kind === "document" && !fullGraphVisible;
|
|
164
174
|
const contextVisible = width < 1050 ? compactContext : layout.contextVisible;
|
|
175
|
+
// Context floats as a card over the page's right margin unless pinned as a column (C1).
|
|
176
|
+
const contextFloating = !layout.contextPinned;
|
|
177
|
+
const contextTotal = useMemo(() => contextCount(contextModel(data.graph, selectedId)), [data.graph, selectedId]);
|
|
178
|
+
const cardWidth = Math.max(240, Math.min(320, layout.contextWidth));
|
|
179
|
+
const showContext = (): void => { if (width < 1050) setCompactContext(true); else setLayout({ ...layout, contextVisible: true }); };
|
|
165
180
|
const graphVisible = fullGraphVisible || contextGraphActive && contextVisible && documentId !== null;
|
|
166
181
|
const hasGraph = layout.panes.some((group) => group.tabs.some((entry) => entry.kind === "graph"));
|
|
167
182
|
const live = useRef({ layout, overlay, selectedId, graphPreviewId, select: (_id: string | null) => {} });
|
|
@@ -203,11 +218,29 @@ export function Shell(props: ShellProps) {
|
|
|
203
218
|
if (graphClickOpensTab(live.current.graphPreviewId, id)) openGraphNode(id);
|
|
204
219
|
else previewGraphNode(id);
|
|
205
220
|
};
|
|
221
|
+
/** The graph sheet's "Open in split": beside the graph, which stays on screen. */
|
|
222
|
+
const openGraphNodeInSplit = (id: string): void => {
|
|
223
|
+
previewGraphNode(null);
|
|
224
|
+
change(openInSplit(layout, id, graphPane?.id));
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Open the graph view with `query` lit up in its spotlight. The entry point
|
|
228
|
+
* for other UI (the search palette's "light up in graph"); `""` clears it.
|
|
229
|
+
* `ids` are the palette's results, so a note found by its text lights too.
|
|
230
|
+
*/
|
|
231
|
+
const lightUp = (query: string, ids: readonly string[] = []): void => {
|
|
232
|
+
setGraphQuery(query);
|
|
233
|
+
setGraphFound({ query, ids });
|
|
234
|
+
setAgentLit(null);
|
|
235
|
+
showGraph();
|
|
236
|
+
};
|
|
237
|
+
const typeSpotlight = (query: string): void => { setGraphQuery(query); setAgentLit(null); };
|
|
206
238
|
live.current = { layout, overlay, selectedId, graphPreviewId, select };
|
|
207
239
|
const mayMutate = (slugs: readonly string[]): boolean => mayMutateDrafts(drafts, slugs, () => window.confirm("Discard unsaved changes to the affected notes before changing files?"));
|
|
208
240
|
const save: SaveNote = async (slug, body, options) => {
|
|
209
241
|
const result = await saveNote(fetchJson, slug, body, options?.raw === true);
|
|
210
|
-
|
|
242
|
+
// An autosave is quiet: the edit bar says "Not saved" instead of an alert per pause.
|
|
243
|
+
if (!result.ok) { if (options?.quiet !== true) window.alert(result.message); return false; }
|
|
211
244
|
setRevision((value) => value + 1); workspace.current?.refresh(); return true;
|
|
212
245
|
};
|
|
213
246
|
const create = async (title: string, paneId: string): Promise<boolean> => {
|
|
@@ -295,6 +328,23 @@ export function Shell(props: ShellProps) {
|
|
|
295
328
|
}, [trailGlowing]);
|
|
296
329
|
const glows = useMemo(() => trailMarks(trail, trailNow, replay), [trail, trailNow, replay]);
|
|
297
330
|
const turns = useMemo(() => trailTurns(trail, data.graph), [trail, data.graph]);
|
|
331
|
+
// The agent's searches light up in the graph view's spotlight, like the
|
|
332
|
+
// palette's "light up in graph", without switching to the graph.
|
|
333
|
+
const search = useMemo(() => agentSearch(trail), [trail]);
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
const composing = graphQuery.trim() !== "" && document.activeElement?.classList.contains("weave-spotlight-input") === true;
|
|
336
|
+
const next = agentSpotlight(agentLit, search, composing);
|
|
337
|
+
if (next === null) return;
|
|
338
|
+
setGraphQuery(next.query);
|
|
339
|
+
setGraphFound({ query: next.query, ids: next.ids });
|
|
340
|
+
setAgentLit(next.agent);
|
|
341
|
+
}, [search?.seq]);
|
|
342
|
+
// A floating card stays out of the way when it has nothing to say (no
|
|
343
|
+
// selection, no agent turn), and over the graph, whose own sheet is its context.
|
|
344
|
+
const coveredPane = layout.split === "down" ? layout.panes[0] : layout.panes.at(-1);
|
|
345
|
+
const graphUnderCard = coveredPane?.tabs.find((entry) => entry.id === coveredPane.activeTab)?.kind === "graph";
|
|
346
|
+
const contextIdle = contextFloating && (graphUnderCard || selectedId === null && turns.length === 0);
|
|
347
|
+
const contextOpen = contextVisible && !contextIdle;
|
|
298
348
|
const replayTurn = (turn: number): void => { const at = Date.now(); setTrailNow(at); setReplay({ turn, startedAt: at }); };
|
|
299
349
|
useEffect(() => {
|
|
300
350
|
const tick = window.setInterval(() => setNow(Date.now()), TICK_MS);
|
|
@@ -329,6 +379,10 @@ export function Shell(props: ShellProps) {
|
|
|
329
379
|
},
|
|
330
380
|
closeTab: () => { const current = live.current.layout; change(closeTab(current, current.activePane, activeTab(current).id)); },
|
|
331
381
|
toggleTheme: () => setLayout((current) => ({ ...current, theme: toggleTheme(current.theme, schemeOf(window), current.preferences) })),
|
|
382
|
+
toggleContext: () => {
|
|
383
|
+
if (window.innerWidth < 1050) setCompactContext((value) => !value);
|
|
384
|
+
else setLayout((current) => ({ ...current, contextVisible: !current.contextVisible }));
|
|
385
|
+
},
|
|
332
386
|
focusSelector: (selector) => {
|
|
333
387
|
if (selector === COLUMN_FOCUS_SELECTORS.graph) setLayout((current) => ({ ...openGraph(current), ...(window.innerWidth < 850 ? { treeVisible: false } : {}) }));
|
|
334
388
|
else if (selector === COLUMN_FOCUS_SELECTORS.tree || selector === TREE_FILTER_SELECTOR) setLayout((current) => ({ ...current, treeVisible: true }));
|
|
@@ -400,7 +454,9 @@ export function Shell(props: ShellProps) {
|
|
|
400
454
|
const renderPane = (group: Pane) => {
|
|
401
455
|
const current = group.tabs.find((entry) => entry.id === group.activeTab)!;
|
|
402
456
|
const isActive = group.id === layout.activePane;
|
|
403
|
-
|
|
457
|
+
// The floating card sits over the right-hand pane (both, stacked); its text makes room.
|
|
458
|
+
const underCard = contextFloating && contextOpen && (layout.split === "down" || group === layout.panes.at(-1));
|
|
459
|
+
return <section key={group.id} class={`weave-pane${isActive ? " weave-pane-active" : ""}${dropPane === group.id ? " weave-pane-drop" : ""}${underCard ? " weave-pane-under-card" : ""}`} data-drop-label={draggedTab.current === null ? "Open note here" : "Move tab here"} aria-label={`Workspace pane ${layout.panes.indexOf(group) + 1}`}
|
|
404
460
|
onDragOver={(event) => tabDragOver(group.id, event)} onDrop={(event) => tabDrop(group.id, event)}
|
|
405
461
|
onDragLeave={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDropPane(null); }}
|
|
406
462
|
hidden={width < 850 && !isActive} onFocusCapture={() => { if (!isActive) setLayout((value) => ({ ...value, activePane: group.id })); }}
|
|
@@ -431,7 +487,7 @@ export function Shell(props: ShellProps) {
|
|
|
431
487
|
const index = event.key === "Home" ? 0 : event.key === "End" ? group.tabs.length - 1 : (group.tabs.indexOf(entry) + offset + group.tabs.length) % group.tabs.length;
|
|
432
488
|
const next = group.tabs[index]!; setLayout(activateTab(layout, group.id, next.id));
|
|
433
489
|
requestAnimationFrame(() => document.getElementById(`tab-${next.id}`)?.focus());
|
|
434
|
-
}}><span aria-hidden="true"
|
|
490
|
+
}}><span aria-hidden="true"><Icon name={entry.kind === "graph" ? "graph" : "note"} class="weave-icon" /></span><span>{titleOf(entry, data.graph)}</span>
|
|
435
491
|
{drafts.isDirty((tabSelection(entry) ?? "").slice(5)) ? <span class="weave-dirty" aria-label="Unsaved changes">●</span> : null}</button>
|
|
436
492
|
<button type="button" class="weave-tab-close" aria-label={`Close ${titleOf(entry, data.graph)}`} onClick={() => change(closeTab(layout, group.id, entry.id))}>×</button>
|
|
437
493
|
</div>)}
|
|
@@ -460,23 +516,21 @@ export function Shell(props: ShellProps) {
|
|
|
460
516
|
};
|
|
461
517
|
|
|
462
518
|
return <>
|
|
463
|
-
<div class="weave-workbench" style={{ "--weave-note-size": `${layout.preferences.fontSize}px`, "--weave-reading-width": layout.preferences.readable ? "760px" : "100%" }} ref={root} onDragEnd={() => { draggedTab.current = null; setDropPane(null); setDropTab(null); }}>
|
|
519
|
+
<div class="weave-workbench" style={{ "--weave-note-size": `${layout.preferences.fontSize}px`, "--weave-reading-width": layout.preferences.readable ? "760px" : "100%", "--weave-card-width": `${cardWidth}px` }} ref={root} onDragEnd={() => { draggedTab.current = null; setDropPane(null); setDropTab(null); }}>
|
|
464
520
|
<nav class="weave-ribbon" aria-label="Workspace tools">
|
|
465
|
-
<button type="button" aria-label="Toggle notes sidebar" aria-pressed={layout.treeVisible} title="Notes" onClick={() => setLayout({ ...layout, treeVisible: !layout.treeVisible })}
|
|
466
|
-
<button type="button" aria-label="Open graph view" title="Graph view" onClick={showGraph}
|
|
467
|
-
<button type="button" aria-label="Toggle note graph" title="Note graph" aria-pressed={contextGraphActive && contextVisible} onClick={toggleContextGraph}><
|
|
468
|
-
<button type="button" aria-label="Search notes and repository" title={searchHint(searchShortcut(looksApple(props.platform)))} onClick={() => setOverlay("search")}>
|
|
469
|
-
<svg viewBox="0 0 20 20" width={16} height={16} fill="none" stroke="currentColor" stroke-width={1.7} aria-hidden="true"><circle cx="8.5" cy="8.5" r="5.5" /><path d="m13 13 4 4" /></svg>
|
|
470
|
-
</button>
|
|
521
|
+
<button type="button" aria-label="Toggle notes sidebar" aria-pressed={layout.treeVisible} title="Notes" onClick={() => setLayout({ ...layout, treeVisible: !layout.treeVisible })}><Icon name="sidebar" class="weave-icon" /></button>
|
|
522
|
+
<button type="button" aria-label="Open graph view" title="Graph view" onClick={showGraph}><Icon name="graph" class="weave-icon" /></button>
|
|
523
|
+
<button type="button" aria-label="Toggle note graph" title="Note graph" aria-pressed={contextGraphActive && contextVisible} onClick={toggleContextGraph}><Icon name="noteGraph" class="weave-icon" /></button>
|
|
524
|
+
<button type="button" aria-label="Search notes and repository" title={searchHint(searchShortcut(looksApple(props.platform)))} onClick={() => setOverlay("search")}><Icon name="search" class="weave-icon" /></button>
|
|
471
525
|
<span class="weave-ribbon-space" />
|
|
472
|
-
<button type="button" aria-label="Toggle context sidebar" aria-pressed={contextVisible} title="Context" onClick={() => width < 1050 ? setCompactContext(!compactContext) : setLayout({ ...layout, contextVisible: !layout.contextVisible })}
|
|
473
|
-
<button type="button" aria-label="Refresh workspace" title="Refresh workspace" onClick={() => { workspace.current?.refresh(); setRevision((value) => value + 1); }}
|
|
474
|
-
<button type="button" aria-label={theme.hint} title={theme.hint} onClick={() => setLayout({ ...layout, theme: toggleTheme(layout.theme, scheme, layout.preferences) })}
|
|
475
|
-
<button type="button" aria-label="Settings" title="Settings" onClick={() => setOverlay("settings")}
|
|
526
|
+
<button type="button" aria-label="Toggle context sidebar" aria-pressed={contextVisible} title="Context" onClick={() => width < 1050 ? setCompactContext(!compactContext) : setLayout({ ...layout, contextVisible: !layout.contextVisible })}><Icon name="context" class="weave-icon" /></button>
|
|
527
|
+
<button type="button" aria-label="Refresh workspace" title="Refresh workspace" onClick={() => { workspace.current?.refresh(); setRevision((value) => value + 1); }}><Icon name="refresh" class="weave-icon" /></button>
|
|
528
|
+
<button type="button" aria-label={theme.hint} title={theme.hint} onClick={() => setLayout({ ...layout, theme: toggleTheme(layout.theme, scheme, layout.preferences) })}><Icon name={theme.icon} class="weave-icon" /></button>
|
|
529
|
+
<button type="button" aria-label="Settings" title="Settings" onClick={() => setOverlay("settings")}><Icon name="settings" class="weave-icon" /></button>
|
|
476
530
|
</nav>
|
|
477
531
|
<div class="weave-sidebar-dock weave-notes-dock" data-open={layout.treeVisible} inert={!layout.treeVisible} aria-hidden={!layout.treeVisible} style={{ "--weave-sidebar-width": `${Math.min(layout.treeWidth, width - 90)}px` }}><aside class="weave-sidebar weave-sidebar-notes" aria-label="Notes sidebar">
|
|
478
532
|
<div class="weave-sidebar-heading"><button type="button" aria-pressed={!recentMode} onClick={() => setRecentMode(false)}>Files</button><button type="button" aria-pressed={recentMode} onClick={() => setRecentMode(true)}>Recent</button><button type="button" aria-label="Hide notes sidebar" onClick={() => { root.current?.querySelector<HTMLButtonElement>('[aria-label="Toggle notes sidebar"]')?.focus(); setLayout({ ...layout, treeVisible: false }); }}>«</button></div>
|
|
479
|
-
{recentMode ? <RecentList visits={visits} graph={data.graph} selectedId={selectedId} onSelect={select} /> : <Tree graph={data.graph} selectedId={selectedId} recentIds={data.recentIds}
|
|
533
|
+
{recentMode ? <RecentList visits={visits} graph={data.graph} selectedId={selectedId} onSelect={select} /> : <Tree graph={data.graph} selectedId={selectedId} recentIds={data.recentIds} onSelect={(id, newTab, keepSidebar) => select(id, newTab, layout.activePane, keepSidebar)} onMutate={mayMutate} onRefresh={() => workspace.current?.refresh()} now={now} />}
|
|
480
534
|
<div class="weave-vault-label"><span>Workspace</span><strong title={props.cwd}>{props.cwd.split(/[\\/]/).filter(Boolean).pop() ?? "Weave"}</strong></div>
|
|
481
535
|
</aside><ResizeHandle label="Resize notes sidebar" min={180} max={420} value={layout.treeWidth} onChange={(delta) => setLayout((value) => ({ ...value, treeWidth: Math.max(180, Math.min(420, value.treeWidth + delta)) }))} /></div>
|
|
482
536
|
<main class={`weave-panes weave-split-${layout.split}`} style={layout.panes.length === 2 && width >= 850 ? { [layout.split === "right" ? "gridTemplateColumns" : "gridTemplateRows"]: `minmax(0, ${layout.ratio}fr) 4px minmax(0, ${1 - layout.ratio}fr)` } : {}}>
|
|
@@ -485,7 +539,10 @@ export function Shell(props: ShellProps) {
|
|
|
485
539
|
setLayout((value) => ({ ...value, ratio: Math.max(.2, Math.min(.8, value.ratio + delta / (extent ?? 800))) }));
|
|
486
540
|
}} /> : null}{renderPane(group)}</>)}
|
|
487
541
|
</main>
|
|
488
|
-
<div class=
|
|
542
|
+
<div class={`weave-sidebar-dock weave-context-dock${contextFloating ? " weave-context-float" : ""}`} data-open={contextOpen} inert={!contextOpen} aria-hidden={!contextOpen} style={{ "--weave-sidebar-width": `${layout.contextWidth}px` }}><ResizeHandle label="Resize context sidebar" min={180} max={400} value={layout.contextWidth} onChange={(delta) => setLayout((value) => ({ ...value, contextWidth: Math.max(180, Math.min(400, value.contextWidth - delta)) }))} /><aside class="weave-sidebar weave-sidebar-context" aria-label="Context sidebar"><div class="weave-sidebar-heading weave-context-heading"><strong>Context</strong><span class="weave-context-count">{contextTotal}</span>
|
|
543
|
+
<button type="button" class="weave-context-pin" aria-pressed={layout.contextPinned} aria-label={layout.contextPinned ? "Float context over the page" : "Dock context as a column"} title={layout.contextPinned ? "Float over the page" : "Dock as a column"} onClick={() => setLayout({ ...layout, contextPinned: !layout.contextPinned })}><Icon name="pin" class="weave-icon" /></button>
|
|
544
|
+
<button type="button" aria-label="Hide context sidebar" title="Fold (])" onClick={() => { root.current?.querySelector<HTMLButtonElement>('[aria-label="Toggle context sidebar"]')?.focus(); setCompactContext(false); setLayout({ ...layout, contextVisible: false }); }}>{contextFloating ? <Icon name="chevron" class="weave-icon weave-icon-open" /> : "»"}</button></div>{contextGraphActive ? <section class="weave-context-graph-section" aria-label="Note connections"><div class="weave-context-graph-heading"><strong>Note graph</strong><button type="button" aria-label="Hide note graph" onClick={() => { root.current?.querySelector<HTMLButtonElement>('[aria-label="Toggle note graph"]')?.focus(); setContextGraph(false); }}>×</button></div><div class="weave-context-graph-slot" ref={contextGraphSlot} style={{ height: layout.contextGraphHeight }}>{documentId === null ? <p>Select a note to see its connections.</p> : null}</div><ResizeHandle label="Resize note graph" horizontal min={120} max={800} value={graphBox.height} onChange={(delta) => setLayout((value) => ({ ...value, contextGraphHeight: Math.max(120, Math.min(800, (contextGraphSlot.current?.clientHeight ?? value.contextGraphHeight) + delta)) }))} /></section> : null}<ContextRail graph={data.graph} selectedId={selectedId} onSelect={select} trail={turns} trailTurn={replay?.turn ?? null} onReplay={replayTurn} tagHeight={layout.contextTagsHeight} onTagResize={(contextTagsHeight) => setLayout((value) => ({ ...value, contextTagsHeight }))} /></aside></div>
|
|
545
|
+
{contextFloating && !contextVisible && !contextIdle ? <button type="button" class="weave-context-pill" aria-label={`Show context, ${contextTotal} items`} title="Show context (])" onClick={showContext}><Icon name="context" class="weave-icon" />Context<span class="weave-context-count">{contextTotal}</span></button> : null}
|
|
489
546
|
{hasGraph || contextGraph ? <div class={`weave-graph-host${contextGraphActive ? " weave-context-graph-host" : ""}`}
|
|
490
547
|
onDragOver={(event) => { if (!contextGraphActive && graphPane) tabDragOver(graphPane.id, event); }} onDrop={(event) => { if (!contextGraphActive && graphPane) tabDrop(graphPane.id, event); }}
|
|
491
548
|
onDragLeave={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDropPane(null); }}
|
|
@@ -493,15 +550,16 @@ export function Shell(props: ShellProps) {
|
|
|
493
550
|
onFocusCapture={() => { if (!contextGraphActive && graphPane) setLayout((current) => ({ ...current, activePane: graphPane.id })); }}
|
|
494
551
|
aria-hidden={!graphVisible} inert={!graphVisible} role={contextGraphActive ? "region" : undefined} aria-label={contextGraphActive ? "Note graph" : undefined}
|
|
495
552
|
style={{ ...graphBox, visibility: graphVisible ? "visible" : "hidden", pointerEvents: graphVisible ? "auto" : "none" }}>
|
|
496
|
-
<Graph graph={data.graph} selectedId={selectedId} previewId={graphPreviewId} onSelect={selectGraph} onOpen={openGraphNode} renderer={createSigmaRenderer} storage={localStorage} host={window} scheme={graphTheme} forces={layout.preferences.forces} groupColors={layout.preferences.groupColors} bootFailed={data.graphFailed} fit={fit} trail={glows}
|
|
553
|
+
<Graph graph={data.graph} selectedId={selectedId} previewId={graphPreviewId} onSelect={selectGraph} onOpen={openGraphNode} renderer={createSigmaRenderer} storage={localStorage} host={window} scheme={graphTheme} forces={layout.preferences.forces} groupColors={layout.preferences.groupColors} bootFailed={data.graphFailed} fit={fit} trail={glows}
|
|
554
|
+
{...(contextGraphActive ? {} : { query: graphQuery, ...(graphFound?.query === graphQuery ? { found: graphFound.ids } : {}), byAgent: agentLit !== null, onQuery: typeSpotlight, onOpenSplit: openGraphNodeInSplit })} now={now} />
|
|
497
555
|
</div> : null}
|
|
498
556
|
</div>
|
|
499
|
-
<div class="weave-footer"><StatusBar model={statusBarModel(props.cwd, selectedId)} />
|
|
557
|
+
<div class="weave-footer"><StatusBar model={statusBarModel(props.cwd, selectedId, searchShortcut(looksApple(props.platform)))} />
|
|
500
558
|
{persistError ? <span role="status">Workspace restoration unavailable</span> : null}
|
|
501
559
|
{drafts.dirtySlugs().length ? <span>{drafts.dirtySlugs().length} unsaved</span> : null}
|
|
502
560
|
{width < 850 && layout.panes.length > 1 ? <button type="button" onClick={() => setLayout({ ...layout, activePane: layout.panes.find((group) => group.id !== pane.id)!.id })}>Switch pane</button> : null}
|
|
503
561
|
</div>
|
|
504
|
-
{overlay === "search" ? <SearchPalette graph={data.graph} onSelect={select} onClose={() => setOverlay(null)} ports={{ fetch: fetchJson }} /> : null}
|
|
562
|
+
{overlay === "search" ? <SearchPalette graph={data.graph} onSelect={select} onLightUp={lightUp} onClose={() => setOverlay(null)} ports={{ fetch: fetchJson }} /> : null}
|
|
505
563
|
{overlay === "settings" ? <Settings compact={width <= 650} layout={layout} info={info} cwd={props.cwd} shortcut={searchShortcut(looksApple(props.platform))} onChange={(next) => { change(next); setCompactContext(next.contextVisible); }} onRefresh={() => { workspace.current?.refresh(); setRevision((value) => value + 1); }} onClose={() => setOverlay(null)} /> : null}
|
|
506
564
|
{overlay === "help" ? <HelpOverlay shortcut={searchShortcut(looksApple(props.platform))} onClose={() => setOverlay(null)} /> : null}
|
|
507
565
|
</>;
|
|
@@ -43,7 +43,30 @@ export type IconName =
|
|
|
43
43
|
| "entryPoint"
|
|
44
44
|
| "gitState"
|
|
45
45
|
| "external"
|
|
46
|
-
| "file"
|
|
46
|
+
| "file"
|
|
47
|
+
// Workspace chrome (the ribbon, tabs, the edit bar and the Context card).
|
|
48
|
+
| "sidebar"
|
|
49
|
+
| "graph"
|
|
50
|
+
| "noteGraph"
|
|
51
|
+
| "search"
|
|
52
|
+
| "context"
|
|
53
|
+
| "refresh"
|
|
54
|
+
| "settings"
|
|
55
|
+
| "sun"
|
|
56
|
+
| "moon"
|
|
57
|
+
| "system"
|
|
58
|
+
| "history"
|
|
59
|
+
| "edit"
|
|
60
|
+
| "check"
|
|
61
|
+
| "heading"
|
|
62
|
+
| "bold"
|
|
63
|
+
| "italic"
|
|
64
|
+
| "link"
|
|
65
|
+
| "wikilink"
|
|
66
|
+
| "task"
|
|
67
|
+
| "pin"
|
|
68
|
+
| "close"
|
|
69
|
+
| "folder";
|
|
47
70
|
|
|
48
71
|
/** The box every path is drawn in, and the stroke the sheet's aesthetic asks for. */
|
|
49
72
|
export const ICON_BOX = 16;
|
|
@@ -95,4 +118,48 @@ export const ICONS: Readonly<Record<IconName, IconDef>> = {
|
|
|
95
118
|
external: { d: ["M4.5 11.5l7-7", "M6.75 4.5h4.75v4.75"], filled: false },
|
|
96
119
|
/** A plain repo file, an unfolded page. */
|
|
97
120
|
file: { d: ["M4.5 2.75h7v10.5h-7Z"], filled: false },
|
|
121
|
+
/** A window with its left panel ruled off: the notes sidebar. */
|
|
122
|
+
sidebar: { d: ["M2.75 3.25h10.5v9.5H2.75Z", "M6.25 3.25v9.5"], filled: false },
|
|
123
|
+
/** Three linked nodes: the graph view. */
|
|
124
|
+
graph: { d: ["M5.5 4a1.5 1.5 0 1 1-3 0a1.5 1.5 0 1 1 3 0Z", "M13.5 5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 1 1 3 0Z", "M8.5 12a1.5 1.5 0 1 1-3 0a1.5 1.5 0 1 1 3 0Z", "M5.5 4.3 10.5 4.8M4.6 5.4l1.9 5.2M11.3 6.3 8.3 10.8"], filled: false },
|
|
125
|
+
/** A framed node: the current note's own graph. */
|
|
126
|
+
noteGraph: { d: ["M3.25 2.75h9.5v10.5h-9.5Z", "M10.5 8a2.5 2.5 0 1 1-5 0a2.5 2.5 0 1 1 5 0Z"], filled: false },
|
|
127
|
+
/** A lens. */
|
|
128
|
+
search: { d: ["M11 7a4 4 0 1 1-8 0a4 4 0 1 1 8 0Z", "m10 10 3.5 3.5"], filled: false },
|
|
129
|
+
/** A window with its right panel ruled off: Context. */
|
|
130
|
+
context: { d: ["M2.75 3.25h10.5v9.5H2.75Z", "M9.75 3.25v9.5"], filled: false },
|
|
131
|
+
/** An open loop with its arrowhead. */
|
|
132
|
+
refresh: { d: ["M13 8a5 5 0 1 1-1.5-3.55", "M13 2.75V5.5h-2.75"], filled: false },
|
|
133
|
+
/** A hub with eight spokes. */
|
|
134
|
+
settings: { d: ["M6.50 3.65L6.89 1.70L9.11 1.70L9.50 3.65A4.6 4.6 0 0 1 11.02 4.53L12.90 3.89L14.01 5.81L12.52 7.12A4.6 4.6 0 0 1 12.52 8.88L14.01 10.19L12.90 12.11L11.02 11.47A4.6 4.6 0 0 1 9.50 12.35L9.11 14.30L6.89 14.30L6.50 12.35A4.6 4.6 0 0 1 4.98 11.47L3.10 12.11L1.99 10.19L3.48 8.88A4.6 4.6 0 0 1 3.48 7.12L1.99 5.81L3.10 3.89L4.98 4.53A4.6 4.6 0 0 1 6.50 3.65Z", "M10 8a2 2 0 1 1-4 0a2 2 0 1 1 4 0Z"], filled: false },
|
|
135
|
+
/** A light theme. */
|
|
136
|
+
sun: { d: ["M10.5 8a2.5 2.5 0 1 1-5 0a2.5 2.5 0 1 1 5 0Z", "M8 1.75V3M8 13v1.25M1.75 8H3M13 8h1.25M3.6 3.6l.9.9M11.5 11.5l.9.9M3.6 12.4l.9-.9M11.5 4.5l.9-.9"], filled: false },
|
|
137
|
+
/** A dark theme. */
|
|
138
|
+
moon: { d: ["M13 9.75A5.25 5.25 0 0 1 6.25 3a5.25 5.25 0 1 0 6.75 6.75Z"], filled: false },
|
|
139
|
+
/** Follow the system: a circle split down the middle. */
|
|
140
|
+
system: { d: ["M13.25 8a5.25 5.25 0 1 1-10.5 0a5.25 5.25 0 1 1 10.5 0Z", "M8 2.75v10.5"], filled: false },
|
|
141
|
+
/** A clock face: the note's history. */
|
|
142
|
+
history: { d: ["M13.25 8a5.25 5.25 0 1 1-10.5 0a5.25 5.25 0 1 1 10.5 0Z", "M8 5v3l2 1.5"], filled: false },
|
|
143
|
+
/** A pencil. */
|
|
144
|
+
edit: { d: ["M10.75 2.75 13.25 5.25 6 12.5H3.5V10Z", "M9.25 4.25l2.5 2.5"], filled: false },
|
|
145
|
+
/** A tick. */
|
|
146
|
+
check: { d: ["m3.25 8.5 3 3 6.5-6.75"], filled: false },
|
|
147
|
+
/** A heading mark. */
|
|
148
|
+
heading: { d: ["M4 3v10M12 3v10M4 8h8"], filled: false },
|
|
149
|
+
/** Bold. */
|
|
150
|
+
bold: { d: ["M4.5 3h4a2.5 2.5 0 0 1 0 5h-4Z", "M4.5 8h4.75a2.5 2.5 0 0 1 0 5H4.5Z"], filled: false },
|
|
151
|
+
/** Italic. */
|
|
152
|
+
italic: { d: ["M7 3h5M4 13h5M10 3 6 13"], filled: false },
|
|
153
|
+
/** Two chain links. */
|
|
154
|
+
link: { d: ["M6.75 9.25l2.5-2.5", "M7.25 4.75 8.5 3.5a2.5 2.5 0 0 1 3.5 3.5l-1.25 1.25", "M8.75 11.25 7.5 12.5a2.5 2.5 0 0 1-3.5-3.5l1.25-1.25"], filled: false },
|
|
155
|
+
/** Square brackets: a [[wikilink]]. */
|
|
156
|
+
wikilink: { d: ["M5.5 3H3.5v10h2", "M10.5 3h2v10h-2"], filled: false },
|
|
157
|
+
/** A ticked box: a task line. */
|
|
158
|
+
task: { d: ["M3 3h10v10H3Z", "m5.5 8 1.75 1.75L10.75 6.25"], filled: false },
|
|
159
|
+
/** A pushpin: dock the Context card. */
|
|
160
|
+
pin: { d: ["M6 2.75h4l-.5 4 2 2v1h-7v-1l2-2Z", "M8 9.75v3.5"], filled: false },
|
|
161
|
+
/** A cross. */
|
|
162
|
+
close: { d: ["m4.25 4.25 7.5 7.5M11.75 4.25l-7.5 7.5"], filled: false },
|
|
163
|
+
/** A folder tab, for the crumb above a note's title. */
|
|
164
|
+
folder: { d: ["M2.75 4.25h3.5l1.25 1.5h5.75v6.5H2.75Z"], filled: false },
|
|
98
165
|
};
|
|
@@ -99,7 +99,9 @@ export type ShellAction =
|
|
|
99
99
|
| { readonly type: "clearSelection" }
|
|
100
100
|
| { readonly type: "closeTab" }
|
|
101
101
|
/** `t` — switch between the selected light and dark themes. */
|
|
102
|
-
| { readonly type: "toggleTheme" }
|
|
102
|
+
| { readonly type: "toggleTheme" }
|
|
103
|
+
/** `]` — fold or unfold the Context card. */
|
|
104
|
+
| { readonly type: "toggleContext" };
|
|
103
105
|
|
|
104
106
|
/**
|
|
105
107
|
* The command-key letters, as data.
|
|
@@ -155,6 +157,7 @@ export const BARE_KEYS: Readonly<Record<string, ShellAction>> = {
|
|
|
155
157
|
g: { type: "fitGraph" },
|
|
156
158
|
"?": { type: "openHelp" },
|
|
157
159
|
t: { type: "toggleTheme" },
|
|
160
|
+
"]": { type: "toggleContext" },
|
|
158
161
|
};
|
|
159
162
|
|
|
160
163
|
/**
|
|
@@ -337,6 +340,8 @@ export interface ShellEffects {
|
|
|
337
340
|
closeTab(): void;
|
|
338
341
|
/** `t` — switch between the user's selected light and dark themes. */
|
|
339
342
|
toggleTheme(): void;
|
|
343
|
+
/** `]` — fold or unfold the Context card (or column). */
|
|
344
|
+
toggleContext(): void;
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
/** Perform an action. Total over {@link ShellAction}. */
|
|
@@ -363,6 +368,8 @@ export function runShellAction(action: ShellAction, fx: ShellEffects): void {
|
|
|
363
368
|
return fx.closeTab();
|
|
364
369
|
case "toggleTheme":
|
|
365
370
|
return fx.toggleTheme();
|
|
371
|
+
case "toggleContext":
|
|
372
|
+
return fx.toggleContext();
|
|
366
373
|
}
|
|
367
374
|
}
|
|
368
375
|
|
|
@@ -408,6 +415,7 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
408
415
|
})),
|
|
409
416
|
{ combo: "?", what: "This help" },
|
|
410
417
|
{ combo: "t", what: "Switch between selected light and dark themes" },
|
|
418
|
+
{ combo: "]", what: "Fold or unfold Context" },
|
|
411
419
|
{ combo: "Esc", what: "Clear the selection from the tree or graph, or close an overlay" },
|
|
412
420
|
],
|
|
413
421
|
},
|
|
@@ -441,6 +449,8 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
|
|
|
441
449
|
{ combo: "↑ / ↓", what: "Move through results" },
|
|
442
450
|
{ combo: "↵", what: "Open the highlighted result" },
|
|
443
451
|
{ combo: `${cmd}↵`, what: "Open the highlighted result in a new tab" },
|
|
452
|
+
{ combo: "Tab / ⇧Tab", what: "Switch between All, Notes, Code and Tags" },
|
|
453
|
+
{ combo: "⇧↵", what: "Light up the matches in the graph" },
|
|
444
454
|
{ combo: "Esc", what: "Close the palette" },
|
|
445
455
|
],
|
|
446
456
|
},
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { GraphPayload, WireNodeKind } from "../../shared/wire";
|
|
18
|
+
import { activePane, moveTab, openDocument } from "../../shared/workspace";
|
|
19
|
+
import type { WorkspaceLayout } from "../../shared/workspace";
|
|
18
20
|
import { kindIcon } from "../tree/tree.model";
|
|
19
21
|
import type { IconName } from "./icons.model";
|
|
20
22
|
export const NOTE_DRAG_TYPE = "application/x-weave-note";
|
|
@@ -37,6 +39,39 @@ export function graphClickOpensTab(previewId: string | null, id: string | null):
|
|
|
37
39
|
return id !== null && id === previewId;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Open `id` beside the graph — the graph sheet's "Open in split".
|
|
44
|
+
*
|
|
45
|
+
* With two panes, the note opens as a new tab in the pane that is *not* the
|
|
46
|
+
* graph's. With one, it opens in a new second pane and the graph's pane keeps
|
|
47
|
+
* the graph on screen: the tab is opened in the graph's pane and then moved,
|
|
48
|
+
* which is the same `moveTab` the tab strip's "Split right" uses, so the new
|
|
49
|
+
* pane is an ordinary one. The split direction is left as the layout has it.
|
|
50
|
+
*
|
|
51
|
+
* The note is shown in the split either way, which is what the button asks
|
|
52
|
+
* for; only which pane is active follows the user's `focusNewTabs`
|
|
53
|
+
* preference. Unchanged when the tab budget is spent.
|
|
54
|
+
*/
|
|
55
|
+
export function openInSplit(layout: WorkspaceLayout, id: string, graphPaneId: string | undefined): WorkspaceLayout {
|
|
56
|
+
const graph = layout.panes.find((pane) => pane.id === graphPaneId) ?? activePane(layout);
|
|
57
|
+
const other = layout.panes.find((pane) => pane.id !== graph.id);
|
|
58
|
+
const opened = openDocument(layout, id, { newTab: true, paneId: other?.id ?? graph.id });
|
|
59
|
+
if (opened === layout) return layout;
|
|
60
|
+
let next = opened;
|
|
61
|
+
if (other === undefined) {
|
|
62
|
+
// One pane, so it is the graph's, and the new tab is its last.
|
|
63
|
+
const { lastDocumentTab: _opened, ...pane } = opened.panes[0]!;
|
|
64
|
+
const tab = pane.tabs.at(-1)!;
|
|
65
|
+
// Put the graph back in front of its pane before the new tab leaves it, so
|
|
66
|
+
// `moveTab` keeps the graph active there instead of the tab beside it.
|
|
67
|
+
const restored: WorkspaceLayout = { ...opened, panes: [
|
|
68
|
+
{ ...pane, activeTab: graph.activeTab, ...(graph.lastDocumentTab === undefined ? {} : { lastDocumentTab: graph.lastDocumentTab }) },
|
|
69
|
+
] };
|
|
70
|
+
next = moveTab(restored, graph.id, tab.id);
|
|
71
|
+
}
|
|
72
|
+
return layout.preferences.focusNewTabs ? next : { ...next, activePane: graph.id };
|
|
73
|
+
}
|
|
74
|
+
|
|
40
75
|
export interface RecentEntry {
|
|
41
76
|
readonly id: string;
|
|
42
77
|
readonly label: string;
|
|
@@ -78,7 +113,10 @@ export const CONTEXT_EMPTY: EmptyStateCopy = {
|
|
|
78
113
|
*/
|
|
79
114
|
export interface StatusBarModel {
|
|
80
115
|
readonly cwd: string;
|
|
116
|
+
/** The selection as a reader names it: a note's file path, not its node id. */
|
|
81
117
|
readonly selection: string;
|
|
118
|
+
/** The quiet hint at the far end, "⌘K search"; empty when none is given. */
|
|
119
|
+
readonly hint: string;
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
/** The `—` shown where a value is genuinely absent, not zero. */
|
|
@@ -88,13 +126,22 @@ export const NO_VALUE = "—";
|
|
|
88
126
|
export function statusBarModel(
|
|
89
127
|
cwd: string,
|
|
90
128
|
selectedId: string | null,
|
|
129
|
+
shortcut = "",
|
|
91
130
|
): StatusBarModel {
|
|
92
131
|
return {
|
|
93
132
|
cwd: cwd === "" ? NO_VALUE : cwd,
|
|
94
|
-
selection: selectedId
|
|
133
|
+
selection: selectionLabel(selectedId),
|
|
134
|
+
hint: shortcut === "" ? "" : `${shortcut} search`,
|
|
95
135
|
};
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
/** `note:projects/launch` reads as the file it is, `projects/launch.md`. */
|
|
139
|
+
function selectionLabel(selectedId: string | null): string {
|
|
140
|
+
if (selectedId === null) return "nothing selected";
|
|
141
|
+
if (selectedId.startsWith("note:") && selectedId.length > "note:".length) return `${selectedId.slice("note:".length)}.md`;
|
|
142
|
+
return selectedId;
|
|
143
|
+
}
|
|
144
|
+
|
|
98
145
|
/**
|
|
99
146
|
* How often the shell re-renders on its own, in ms.
|
|
100
147
|
*
|
|
@@ -60,6 +60,8 @@ export function themeAttr(choice: ThemeChoice): PaletteChoice | null {
|
|
|
60
60
|
|
|
61
61
|
export interface ThemeButtonView {
|
|
62
62
|
readonly glyph: "●" | "◐" | "○";
|
|
63
|
+
/** The ribbon's icon for the same state. */
|
|
64
|
+
readonly icon: "system" | "sun" | "moon";
|
|
63
65
|
readonly label: string;
|
|
64
66
|
readonly hint: string;
|
|
65
67
|
}
|
|
@@ -71,6 +73,7 @@ function themeName(choice: ThemeChoice): string {
|
|
|
71
73
|
export function themeButton(choice: ThemeChoice, systemScheme: ColorScheme = "dark", pair: ThemePair = DEFAULT_PREFERENCES): ThemeButtonView {
|
|
72
74
|
return {
|
|
73
75
|
glyph: choice === "system" ? "◐" : THEMES[choice].scheme === "light" ? "○" : "●",
|
|
76
|
+
icon: choice === "system" ? "system" : THEMES[choice].scheme === "light" ? "sun" : "moon",
|
|
74
77
|
label: themeName(choice),
|
|
75
78
|
hint: `Colour theme: ${themeName(choice)} — click for ${themeName(toggleTheme(choice, systemScheme, pair))}`,
|
|
76
79
|
};
|