pi-weave 0.3.6 → 0.3.8
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/package.json +1 -1
- package/skills/weave-notepad/SKILL.md +8 -2
- package/src/web/client/dist/app.js +53 -43
- package/src/web/client/graph/column.model.ts +2 -1
- package/src/web/client/shell/ContextRail.tsx +7 -2
- package/src/web/client/shell/ResizeHandle.tsx +19 -0
- package/src/web/client/shell/Shell.tsx +40 -28
- package/src/web/client/shell/workspace.css.ts +11 -1
- package/src/web/shared/workspace.ts +12 -8
|
@@ -344,8 +344,9 @@ export function graphColumnModel(
|
|
|
344
344
|
// by a branch that is not on screen.
|
|
345
345
|
const fills = groupColors ? groupNodeColors(reduced.nodes, edges, scheme) : undefined;
|
|
346
346
|
|
|
347
|
+
const graph = renderGraph(reduced.nodes, edges, layout.positions, scheme, fills);
|
|
347
348
|
return {
|
|
348
|
-
graph
|
|
349
|
+
graph,
|
|
349
350
|
highlight: highlightFor(edges, selectedId),
|
|
350
351
|
key: layout.key,
|
|
351
352
|
cached: layout.cached,
|
|
@@ -12,17 +12,20 @@
|
|
|
12
12
|
* resolve it.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { useState } from "preact/hooks";
|
|
15
|
+
import { useRef, useState } from "preact/hooks";
|
|
16
16
|
import type { ContextModel, ContextRow, RailSectionView, RailToggles, TagGroupRow } from "../context/context.model";
|
|
17
17
|
import { contextModel, emptyRailToggles, railPanelId, railSectionView, railTagsView, railToggled } from "../context/context.model";
|
|
18
18
|
import type { GraphPayload } from "../../shared/wire";
|
|
19
19
|
import { CONTEXT_EMPTY } from "./shell.model";
|
|
20
20
|
import { Icon } from "../tree/Tree";
|
|
21
|
+
import { ResizeHandle } from "./ResizeHandle";
|
|
21
22
|
|
|
22
23
|
export interface ContextRailProps {
|
|
23
24
|
graph: GraphPayload | null;
|
|
24
25
|
selectedId: string | null;
|
|
25
26
|
onSelect: (id: string, newTab?: boolean) => void;
|
|
27
|
+
tagHeight: number;
|
|
28
|
+
onTagResize: (height: number) => void;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function Row({ row, onSelect }: { row: ContextRow; onSelect: (newTab: boolean) => void }) {
|
|
@@ -91,6 +94,7 @@ function Tag({ tag, onSelect }: { tag: TagGroupRow; onSelect: (id: string, newTa
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
export function ContextRail(props: ContextRailProps) {
|
|
97
|
+
const tagRows = useRef<HTMLUListElement | null>(null);
|
|
94
98
|
const model: ContextModel = contextModel(props.graph, props.selectedId);
|
|
95
99
|
// The user's open/closed word lives in this component — it is view state,
|
|
96
100
|
// not a workspace fact, so it does not cross the §1.3 bus and does not
|
|
@@ -110,11 +114,12 @@ export function ContextRail(props: ContextRailProps) {
|
|
|
110
114
|
{model.tags.length === 0 ? null : (
|
|
111
115
|
<section class="weave-ctx-group">
|
|
112
116
|
<Heading view={tags} onToggle={toggleFor(tags)} />
|
|
113
|
-
<ul class="weave-ctx-tags" id={railPanelId(tags.heading)} hidden={tags.collapsed}>
|
|
117
|
+
<ul class="weave-ctx-tags weave-context-tags" ref={tagRows} style={{ height: props.tagHeight }} id={railPanelId(tags.heading)} hidden={tags.collapsed}>
|
|
114
118
|
{model.tags.map((tag) => (
|
|
115
119
|
<Tag key={tag.tag} tag={tag} onSelect={props.onSelect} />
|
|
116
120
|
))}
|
|
117
121
|
</ul>
|
|
122
|
+
{tags.collapsed ? null : <ResizeHandle label="Resize tags section" horizontal min={60} max={600} value={props.tagHeight} onChange={(delta) => props.onTagResize(Math.max(60, Math.min(600, (tagRows.current?.clientHeight ?? props.tagHeight) + delta)))} />}
|
|
118
123
|
</section>
|
|
119
124
|
)}
|
|
120
125
|
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useRef } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
export function ResizeHandle(props: { label: string; horizontal?: boolean; value: number; min: number; max: number; onChange: (delta: number) => void }) {
|
|
4
|
+
const last = useRef<number | null>(null);
|
|
5
|
+
return <div class="weave-workspace-divider" role="separator" tabIndex={0} aria-label={props.label}
|
|
6
|
+
aria-orientation={props.horizontal ? "horizontal" : "vertical"} aria-valuenow={Math.round(props.value)} aria-valuemin={props.min} aria-valuemax={props.max}
|
|
7
|
+
onPointerDown={(event) => { last.current = props.horizontal ? event.clientY : event.clientX; event.currentTarget.setPointerCapture(event.pointerId); }}
|
|
8
|
+
onPointerMove={(event) => {
|
|
9
|
+
if (last.current === null) return;
|
|
10
|
+
const at = props.horizontal ? event.clientY : event.clientX;
|
|
11
|
+
props.onChange(at - last.current); last.current = at;
|
|
12
|
+
}}
|
|
13
|
+
onPointerUp={(event) => { last.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }}
|
|
14
|
+
onPointerCancel={() => { last.current = null; }}
|
|
15
|
+
onKeyDown={(event) => {
|
|
16
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
17
|
+
event.preventDefault(); props.onChange(event.key === "ArrowLeft" || event.key === "ArrowUp" ? -20 : 20);
|
|
18
|
+
}} />;
|
|
19
|
+
}
|
|
@@ -18,6 +18,7 @@ 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 { ResizeHandle } from "./ResizeHandle";
|
|
21
22
|
import { deeplinkSelection, formatHash } from "./deeplink.model";
|
|
22
23
|
import { Settings } from "./Settings";
|
|
23
24
|
import type { Preferences } from "../../shared/preferences";
|
|
@@ -112,28 +113,11 @@ function DocumentView(props: {
|
|
|
112
113
|
})} onSave={props.onSave} drafts={props.drafts} idPrefix={props.tab.id} now={props.now} /></div>;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function ResizeHandle(props: { label: string; horizontal?: boolean; value: number; min: number; max: number; onChange: (delta: number) => void }) {
|
|
116
|
-
const last = useRef<number | null>(null);
|
|
117
|
-
return <div class="weave-workspace-divider" role="separator" tabIndex={0} aria-label={props.label}
|
|
118
|
-
aria-orientation={props.horizontal ? "horizontal" : "vertical"} aria-valuenow={Math.round(props.value)} aria-valuemin={props.min} aria-valuemax={props.max}
|
|
119
|
-
onPointerDown={(event) => { last.current = props.horizontal ? event.clientY : event.clientX; event.currentTarget.setPointerCapture(event.pointerId); }}
|
|
120
|
-
onPointerMove={(event) => {
|
|
121
|
-
if (last.current === null) return;
|
|
122
|
-
const at = props.horizontal ? event.clientY : event.clientX;
|
|
123
|
-
props.onChange(at - last.current); last.current = at;
|
|
124
|
-
}}
|
|
125
|
-
onPointerUp={(event) => { last.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }}
|
|
126
|
-
onPointerCancel={() => { last.current = null; }}
|
|
127
|
-
onKeyDown={(event) => {
|
|
128
|
-
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
129
|
-
event.preventDefault(); props.onChange(event.key === "ArrowLeft" || event.key === "ArrowUp" ? -20 : 20);
|
|
130
|
-
}} />;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
116
|
export function Shell(props: ShellProps) {
|
|
134
117
|
const [data, setData] = useState(initialWorkspaceState);
|
|
135
118
|
const [layout, setLayout] = useState(() => ({ ...initialLayout(), theme: loadTheme(localStorage) ?? "system" }));
|
|
136
119
|
const [overlay, setOverlay] = useState<OverlayId>(null);
|
|
120
|
+
const [contextGraph, setContextGraph] = useState(false);
|
|
137
121
|
const [now, setNow] = useState(Date.now);
|
|
138
122
|
const [width, setWidth] = useState(props.initialWidth);
|
|
139
123
|
const [scheme, setScheme] = useState(() => schemeOf(window));
|
|
@@ -143,6 +127,7 @@ export function Shell(props: ShellProps) {
|
|
|
143
127
|
const [revision, setRevision] = useState(0);
|
|
144
128
|
const [, setDraftRevision] = useState(0);
|
|
145
129
|
const [graphSelection, setGraphSelection] = useState<string | null>(null);
|
|
130
|
+
const lastViewedDocument = useRef<string | null>(null);
|
|
146
131
|
const [graphPreviewId, setGraphPreviewId] = useState<string | null>(null);
|
|
147
132
|
const draggedTab = useRef<string | null>(null);
|
|
148
133
|
const [dropTab, setDropTab] = useState<{ paneId: string; before: string | null } | null>(null);
|
|
@@ -157,6 +142,7 @@ export function Shell(props: ShellProps) {
|
|
|
157
142
|
const linked = useRef(false);
|
|
158
143
|
const root = useRef<HTMLDivElement | null>(null);
|
|
159
144
|
const graphSlot = useRef<HTMLDivElement | null>(null);
|
|
145
|
+
const contextGraphSlot = useRef<HTMLDivElement | null>(null);
|
|
160
146
|
const fit = useRef<(() => void) | null>(null);
|
|
161
147
|
const [graphBox, setGraphBox] = useState({ left: 0, top: 0, width: 600, height: 500 });
|
|
162
148
|
const pane = activePane(layout);
|
|
@@ -164,17 +150,24 @@ export function Shell(props: ShellProps) {
|
|
|
164
150
|
const theme = themeButton(layout.theme, scheme, layout.preferences);
|
|
165
151
|
const palette = effectiveScheme(layout.theme, scheme, layout.preferences);
|
|
166
152
|
const graphTheme = useMemo(() => ({ theme: palette, accent: layout.preferences.accent }), [palette, layout.preferences.accent]);
|
|
167
|
-
const
|
|
153
|
+
const documentId = tabSelection(tab);
|
|
154
|
+
const selectedId = tab.kind === "graph" ? graphSelection : documentId;
|
|
168
155
|
const graphPane = layout.panes.find((group) => group.tabs.some((entry) => entry.id === group.activeTab && entry.kind === "graph"));
|
|
169
|
-
const
|
|
156
|
+
const fullGraphVisible = graphPane !== undefined && (width >= 850 || graphPane.id === layout.activePane);
|
|
157
|
+
const contextGraphActive = contextGraph && tab.kind === "document" && !fullGraphVisible;
|
|
170
158
|
const contextVisible = width < 1050 ? compactContext : layout.contextVisible;
|
|
159
|
+
const graphVisible = fullGraphVisible || contextGraphActive && contextVisible && documentId !== null;
|
|
171
160
|
const hasGraph = layout.panes.some((group) => group.tabs.some((entry) => entry.kind === "graph"));
|
|
172
161
|
const live = useRef({ layout, overlay, selectedId, graphPreviewId, select: (_id: string | null) => {} });
|
|
173
162
|
const previewGraphNode = (id: string | null): void => {
|
|
174
163
|
live.current.graphPreviewId = id;
|
|
175
164
|
setGraphPreviewId(id);
|
|
176
165
|
};
|
|
177
|
-
useEffect(() => {
|
|
166
|
+
useEffect(() => { previewGraphNode(null); }, [graphVisible, contextGraphActive, documentId]);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (documentId !== null) { lastViewedDocument.current = documentId; setGraphSelection(documentId); }
|
|
169
|
+
else if (tab.kind === "graph") setGraphSelection(lastViewedDocument.current);
|
|
170
|
+
}, [tab.id, tab.kind, documentId]);
|
|
178
171
|
|
|
179
172
|
const change = (next: WorkspaceLayout): void => {
|
|
180
173
|
if (mayRemoveDrafts(drafts, live.current.layout, next, () => window.confirm(DISCARD_PROMPT))) setLayout(next);
|
|
@@ -182,16 +175,25 @@ export function Shell(props: ShellProps) {
|
|
|
182
175
|
const select = (id: string | null, newTab = false, paneId = layout.activePane, keepSidebar = false): void => {
|
|
183
176
|
const next = (id === null ? openDocument : openWithPreferences)(layout, id, { newTab, paneId });
|
|
184
177
|
change(width < 850 && !keepSidebar ? { ...next, treeVisible: false } : next);
|
|
185
|
-
setCompactContext(false);
|
|
178
|
+
if (!contextGraph) setCompactContext(false);
|
|
179
|
+
};
|
|
180
|
+
const toggleContextGraph = (): void => {
|
|
181
|
+
const showing = !(contextGraphActive && contextVisible);
|
|
182
|
+
setContextGraph(showing);
|
|
183
|
+
if (showing) {
|
|
184
|
+
setLayout((current) => ({ ...focusDocument(current), contextVisible: true }));
|
|
185
|
+
setCompactContext(true);
|
|
186
|
+
}
|
|
186
187
|
};
|
|
187
188
|
const showGraph = (): void => setLayout((current) => ({ ...openGraph(current), ...(width < 850 ? { treeVisible: false } : {}) }));
|
|
188
189
|
const openGraphNode = (id: string): void => {
|
|
189
190
|
previewGraphNode(null);
|
|
191
|
+
if (contextGraphActive) { select(id, true); return; }
|
|
190
192
|
const other = layout.panes.find((group) => group.id !== graphPane?.id);
|
|
191
193
|
select(id, true, other?.id ?? graphPane?.id ?? layout.activePane);
|
|
192
194
|
};
|
|
193
195
|
const selectGraph = (id: string | null): void => {
|
|
194
|
-
setGraphSelection(id);
|
|
196
|
+
if (!contextGraphActive) setGraphSelection(id);
|
|
195
197
|
if (graphClickOpensTab(live.current.graphPreviewId, id)) openGraphNode(id);
|
|
196
198
|
else previewGraphNode(id);
|
|
197
199
|
};
|
|
@@ -229,7 +231,10 @@ export function Shell(props: ShellProps) {
|
|
|
229
231
|
if (value.info) setInfo(value.info);
|
|
230
232
|
const restored = parseWorkspaceLayout(value.layout);
|
|
231
233
|
if (restored) {
|
|
232
|
-
|
|
234
|
+
const restoredLayout = restoreWorkspace(restored);
|
|
235
|
+
setLayout(restoredLayout);
|
|
236
|
+
lastViewedDocument.current = tabSelection(activeTab(focusDocument(restoredLayout)));
|
|
237
|
+
setGraphSelection(lastViewedDocument.current);
|
|
233
238
|
setVisits(restored.panes.flatMap((pane) => pane.tabs.flatMap((tab) => tab.history)).reduce<readonly string[]>(recordVisit, []));
|
|
234
239
|
}
|
|
235
240
|
} else setPersistError(true);
|
|
@@ -319,7 +324,7 @@ export function Shell(props: ShellProps) {
|
|
|
319
324
|
}, [layout.panes.map((group) => group.activeTab).join(","), layout.activePane, width, ready, data.graph?.stamp,
|
|
320
325
|
layout.treeVisible, layout.treeWidth, layout.contextVisible, layout.contextWidth, layout.ratio, layout.split]);
|
|
321
326
|
useLayoutEffect(() => {
|
|
322
|
-
const slot = graphSlot.current;
|
|
327
|
+
const slot = contextGraphActive ? contextGraphSlot.current : graphSlot.current;
|
|
323
328
|
const parent = root.current;
|
|
324
329
|
if (!graphVisible || !slot || !parent) return;
|
|
325
330
|
const measure = (): void => {
|
|
@@ -329,7 +334,7 @@ export function Shell(props: ShellProps) {
|
|
|
329
334
|
measure();
|
|
330
335
|
const observer = new ResizeObserver(measure); observer.observe(slot); observer.observe(parent);
|
|
331
336
|
return () => observer.disconnect();
|
|
332
|
-
}, [graphVisible, layout, width]);
|
|
337
|
+
}, [contextGraphActive, graphVisible, layout, width]);
|
|
333
338
|
|
|
334
339
|
const rememberScroll = (tabId: string, scroll: number): void => setLayout((current) => ({ ...current,
|
|
335
340
|
panes: current.panes.map((group) => ({ ...group, tabs: group.tabs.map((entry) => entry.id === tabId ? { ...entry, scroll } : entry) })),
|
|
@@ -435,6 +440,7 @@ export function Shell(props: ShellProps) {
|
|
|
435
440
|
<nav class="weave-ribbon" aria-label="Workspace tools">
|
|
436
441
|
<button type="button" aria-label="Toggle notes sidebar" aria-pressed={layout.treeVisible} title="Notes" onClick={() => setLayout({ ...layout, treeVisible: !layout.treeVisible })}>▤</button>
|
|
437
442
|
<button type="button" aria-label="Open graph view" title="Graph view" onClick={showGraph}>◌</button>
|
|
443
|
+
<button type="button" aria-label="Toggle note graph" title="Note graph" aria-pressed={contextGraphActive && contextVisible} onClick={toggleContextGraph}><svg viewBox="0 0 20 20" width={18} height={18} fill="none" stroke="currentColor" stroke-width={1.5} aria-hidden="true"><rect x="2" y="2" width="16" height="16" rx="3" /><circle cx="10" cy="10" r="4" /></svg></button>
|
|
438
444
|
<button type="button" aria-label="Search notes and repository" title={searchHint(searchShortcut(looksApple(props.platform)))} onClick={() => setOverlay("search")}>
|
|
439
445
|
<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>
|
|
440
446
|
</button>
|
|
@@ -455,8 +461,14 @@ export function Shell(props: ShellProps) {
|
|
|
455
461
|
setLayout((value) => ({ ...value, ratio: Math.max(.2, Math.min(.8, value.ratio + delta / (extent ?? 800))) }));
|
|
456
462
|
}} /> : null}{renderPane(group)}</>)}
|
|
457
463
|
</main>
|
|
458
|
-
<div class="weave-sidebar-dock weave-context-dock" data-open={contextVisible} inert={!contextVisible} aria-hidden={!contextVisible} 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"><strong>Context</strong><button type="button" aria-label="Hide context sidebar" onClick={() => { root.current?.querySelector<HTMLButtonElement>('[aria-label="Toggle context sidebar"]')?.focus(); setCompactContext(false); setLayout({ ...layout, contextVisible: false }); }}>»</button></div><ContextRail graph={data.graph} selectedId={selectedId} onSelect={select} /></aside></div>
|
|
459
|
-
{hasGraph ? <div class=
|
|
464
|
+
<div class="weave-sidebar-dock weave-context-dock" data-open={contextVisible} inert={!contextVisible} aria-hidden={!contextVisible} 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"><strong>Context</strong><button type="button" aria-label="Hide context sidebar" onClick={() => { root.current?.querySelector<HTMLButtonElement>('[aria-label="Toggle context sidebar"]')?.focus(); setCompactContext(false); setLayout({ ...layout, contextVisible: false }); }}>»</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} tagHeight={layout.contextTagsHeight} onTagResize={(contextTagsHeight) => setLayout((value) => ({ ...value, contextTagsHeight }))} /></aside></div>
|
|
465
|
+
{hasGraph || contextGraph ? <div class={`weave-graph-host${contextGraphActive ? " weave-context-graph-host" : ""}`}
|
|
466
|
+
onDragOver={(event) => { if (!contextGraphActive && graphPane) tabDragOver(graphPane.id, event); }} onDrop={(event) => { if (!contextGraphActive && graphPane) tabDrop(graphPane.id, event); }}
|
|
467
|
+
onDragLeave={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDropPane(null); }}
|
|
468
|
+
onPointerDown={() => { if (!contextGraphActive && graphPane) setLayout((current) => ({ ...current, activePane: graphPane.id })); }}
|
|
469
|
+
onFocusCapture={() => { if (!contextGraphActive && graphPane) setLayout((current) => ({ ...current, activePane: graphPane.id })); }}
|
|
470
|
+
aria-hidden={!graphVisible} inert={!graphVisible} role={contextGraphActive ? "region" : undefined} aria-label={contextGraphActive ? "Note graph" : undefined}
|
|
471
|
+
style={{ ...graphBox, visibility: graphVisible ? "visible" : "hidden", pointerEvents: graphVisible ? "auto" : "none" }}>
|
|
460
472
|
<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} />
|
|
461
473
|
</div> : null}
|
|
462
474
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Tabbed workspace furniture; the graph keeps its existing palette and renderer. */
|
|
2
2
|
export const WORKSPACE_CSS = `
|
|
3
|
-
.weave-workbench{position:relative;display:flex;min-height:0;overflow:hidden;background:var(--weave-page)}
|
|
3
|
+
.weave-workbench{position:relative;isolation:isolate;display:flex;min-height:0;overflow:hidden;background:var(--weave-page)}
|
|
4
4
|
.weave-workbench button{font:inherit;color:inherit;cursor:pointer}
|
|
5
5
|
.weave-workbench button:focus-visible,.weave-workbench summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-2px}
|
|
6
6
|
.weave-workbench button:disabled{opacity:.35;cursor:default}
|
|
@@ -93,6 +93,16 @@ export const WORKSPACE_CSS = `
|
|
|
93
93
|
.weave-sidebar-context .weave-ctx-link{padding-top:7px;padding-bottom:7px}
|
|
94
94
|
.weave-graph-host{position:absolute;z-index:2;display:flex;background:var(--weave-bg)}
|
|
95
95
|
.weave-graph-host>.weave-graph{width:100%;height:100%;flex:1}
|
|
96
|
+
.weave-context-graph-section{flex:none;border-bottom:1px solid var(--weave-line)}
|
|
97
|
+
.weave-context-graph-heading{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;font-size:var(--weave-px-caption);color:var(--weave-dim)}
|
|
98
|
+
.weave-context-graph-heading button{border:0;background:none;padding:2px 6px;font-size:18px}
|
|
99
|
+
.weave-context-graph-slot{max-height:calc(100dvh - 180px);min-height:120px}
|
|
100
|
+
.weave-context-tags{min-height:60px;max-height:60vh;overflow:auto}
|
|
101
|
+
.weave-context-graph-slot>p{padding:16px;margin:0;color:var(--weave-dim);font-size:var(--weave-px-row)}
|
|
102
|
+
.weave-context-graph-host{z-index:11}
|
|
103
|
+
.weave-context-graph-host .weave-graph-legend{display:none}
|
|
104
|
+
.weave-context-graph-host .weave-graph-preview{bottom:56px;padding:8px 28px 8px 8px}
|
|
105
|
+
.weave-context-graph-host .weave-graph-preview-title{font-size:var(--weave-px-caption)}
|
|
96
106
|
.weave-graph-preview{pointer-events:none;position:absolute;z-index:2;right:8px;bottom:64px;width:min(280px,calc(100% - 16px));box-sizing:border-box;display:flex;flex-direction:column;gap:8px;padding:10px 34px 10px 10px;background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);box-shadow:0 6px 22px #0003}
|
|
97
107
|
.weave-graph-preview-info{display:flex;align-items:center;gap:8px;min-width:0}
|
|
98
108
|
.weave-graph-preview-copy{display:flex;flex-direction:column;min-width:0;gap:2px}
|
|
@@ -29,6 +29,8 @@ export interface WorkspaceLayout {
|
|
|
29
29
|
readonly contextVisible: boolean;
|
|
30
30
|
readonly treeWidth: number;
|
|
31
31
|
readonly contextWidth: number;
|
|
32
|
+
readonly contextGraphHeight: number;
|
|
33
|
+
readonly contextTagsHeight: number;
|
|
32
34
|
readonly theme: ThemeChoice;
|
|
33
35
|
readonly preferences: Preferences;
|
|
34
36
|
}
|
|
@@ -50,6 +52,8 @@ export function initialLayout(): WorkspaceLayout {
|
|
|
50
52
|
contextVisible: true,
|
|
51
53
|
treeWidth: 240,
|
|
52
54
|
contextWidth: 240,
|
|
55
|
+
contextGraphHeight: 260,
|
|
56
|
+
contextTagsHeight: 180,
|
|
53
57
|
theme: "system",
|
|
54
58
|
preferences: DEFAULT_PREFERENCES,
|
|
55
59
|
};
|
|
@@ -184,12 +188,8 @@ export function navigateTab(layout: WorkspaceLayout, paneId: string, delta: -1 |
|
|
|
184
188
|
|
|
185
189
|
export function splitPane(layout: WorkspaceLayout, direction: "right" | "down"): WorkspaceLayout {
|
|
186
190
|
if (layout.panes.length === 2) return layout.split === direction ? layout : { ...layout, split: direction };
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const id = nextId(layout, "pane");
|
|
190
|
-
let tab = emptyDocument(nextId(layout, "tab"));
|
|
191
|
-
if (current.kind === "document") tab = { ...current, id: tab.id, history: [...current.history], scroll: current.scroll };
|
|
192
|
-
return { ...layout, panes: [...layout.panes, { id, tabs: [tab], activeTab: tab.id }], activePane: id, split: direction };
|
|
191
|
+
const moved = moveTab(layout, layout.activePane, activeTab(layout).id);
|
|
192
|
+
return moved === layout ? layout : { ...moved, split: direction };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function closePane(layout: WorkspaceLayout, paneId: string): WorkspaceLayout {
|
|
@@ -246,13 +246,17 @@ function boundedId(value: unknown, prefix?: "pane" | "tab"): value is string {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
export function parseWorkspaceLayout(value: unknown): WorkspaceLayout | null {
|
|
249
|
-
if (!record(value) || !exactKeys(value, ["version", "panes", "activePane", "split", "ratio", "treeVisible", "contextVisible", "treeWidth", "contextWidth", "theme", ...
|
|
249
|
+
if (!record(value) || !exactKeys(value, ["version", "panes", "activePane", "split", "ratio", "treeVisible", "contextVisible", "treeWidth", "contextWidth", "theme", ...["preferences", "contextGraphHeight", "contextTagsHeight"].filter(key => Object.hasOwn(value, key))])) return null;
|
|
250
250
|
if (value["version"] !== 1 || !Array.isArray(value["panes"]) || value["panes"].length < 1 || value["panes"].length > 2 || !boundedId(value["activePane"], "pane")) return null;
|
|
251
251
|
if (value["split"] !== "right" && value["split"] !== "down") return null;
|
|
252
252
|
if (typeof value["ratio"] !== "number" || !Number.isFinite(value["ratio"]) || value["ratio"] < 0.1 || value["ratio"] > 0.9) return null;
|
|
253
253
|
if (typeof value["treeVisible"] !== "boolean" || typeof value["contextVisible"] !== "boolean") return null;
|
|
254
254
|
if (typeof value["treeWidth"] !== "number" || !Number.isFinite(value["treeWidth"]) || value["treeWidth"] < 120 || value["treeWidth"] > 800) return null;
|
|
255
255
|
if (typeof value["contextWidth"] !== "number" || !Number.isFinite(value["contextWidth"]) || value["contextWidth"] < 120 || value["contextWidth"] > 800) return null;
|
|
256
|
+
const contextGraphHeight = Object.hasOwn(value, "contextGraphHeight") ? value["contextGraphHeight"] : 260;
|
|
257
|
+
const contextTagsHeight = Object.hasOwn(value, "contextTagsHeight") ? value["contextTagsHeight"] : 180;
|
|
258
|
+
if (typeof contextGraphHeight !== "number" || !Number.isFinite(contextGraphHeight) || contextGraphHeight < 120 || contextGraphHeight > 800) return null;
|
|
259
|
+
if (typeof contextTagsHeight !== "number" || !Number.isFinite(contextTagsHeight) || contextTagsHeight < 60 || contextTagsHeight > 600) return null;
|
|
256
260
|
if (!isThemeChoice(value["theme"])) return null;
|
|
257
261
|
let preferences = Object.hasOwn(value, "preferences") ? parsePreferences(value["preferences"]) : DEFAULT_PREFERENCES;
|
|
258
262
|
if (preferences === null) return null;
|
|
@@ -288,7 +292,7 @@ export function parseWorkspaceLayout(value: unknown): WorkspaceLayout | null {
|
|
|
288
292
|
panes.push({ id: rawPane["id"], tabs, activeTab: rawPane["activeTab"], ...(typeof lastDocumentTab === "string" ? { lastDocumentTab } : {}) });
|
|
289
293
|
}
|
|
290
294
|
if (count > MAX_TABS || graphs > 1 || !panes.some((pane) => pane.id === value["activePane"])) return null;
|
|
291
|
-
return { version: 1, panes, activePane: value["activePane"], split: value["split"], ratio: value["ratio"], treeVisible: value["treeVisible"], contextVisible: value["contextVisible"], treeWidth: value["treeWidth"], contextWidth: value["contextWidth"], theme: value["theme"], preferences };
|
|
295
|
+
return { version: 1, panes, activePane: value["activePane"], split: value["split"], ratio: value["ratio"], treeVisible: value["treeVisible"], contextVisible: value["contextVisible"], treeWidth: value["treeWidth"], contextWidth: value["contextWidth"], contextGraphHeight, contextTagsHeight, theme: value["theme"], preferences };
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
/** Startup choice resets navigation only; appearance and sidebar preferences survive. */
|