pi-weave 0.1.7 → 0.1.9
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 +123 -37
- package/package.json +17 -5
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/cache/workspace.ts +466 -0
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +270 -23
- package/src/core/git.ts +19 -0
- package/src/core/graph/build.ts +97 -5
- package/src/core/graph/current.ts +41 -28
- package/src/core/graph/mentions.ts +170 -0
- package/src/core/graph/model.ts +39 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +14 -0
- package/src/core/openInEditor.ts +69 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/types.ts +40 -0
- package/src/core/vault.ts +739 -57
- package/src/core/view/cluster.ts +262 -0
- package/src/core/view/detail.ts +118 -0
- package/src/core/view/focus.ts +109 -0
- package/src/core/view/health.ts +156 -0
- package/src/core/view/index.ts +15 -0
- package/src/core/view/links.ts +105 -0
- package/src/core/view/time.ts +47 -0
- package/src/core/view/tree.ts +269 -0
- package/src/core/view/types.ts +39 -0
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +248 -48
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +5 -3
- package/src/pi/viewer/tui/model.ts +46 -667
- package/src/pi/viewer/tui/openNote.ts +7 -56
- package/src/pi/viewer/tui/surface/explore.ts +4 -2
- package/src/pi/viewer/web/run.ts +331 -0
- package/src/web/client/api.dom.ts +40 -0
- package/src/web/client/api.ts +472 -0
- package/src/web/client/bootstrap.ts +58 -0
- package/src/web/client/context/context.model.ts +313 -0
- package/src/web/client/dist/app.js +764 -0
- package/src/web/client/graph/Graph.tsx +209 -0
- package/src/web/client/graph/column.model.ts +372 -0
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +546 -0
- package/src/web/client/graph/positions.ts +380 -0
- package/src/web/client/graph/project.ts +153 -0
- package/src/web/client/graph/renderer.dom.ts +52 -0
- package/src/web/client/graph/renderer.ts +339 -0
- package/src/web/client/graph/scheme.ts +44 -0
- package/src/web/client/live.model.ts +275 -0
- package/src/web/client/live.ts +151 -0
- package/src/web/client/main.tsx +27 -0
- package/src/web/client/note/Editor.tsx +102 -0
- package/src/web/client/note/Note.tsx +113 -0
- package/src/web/client/note/editor.controller.ts +151 -0
- package/src/web/client/note/editor.model.ts +636 -0
- package/src/web/client/note/note.model.ts +738 -0
- package/src/web/client/search/SearchPalette.tsx +105 -0
- package/src/web/client/search/search.model.ts +588 -0
- package/src/web/client/search/search.ts +107 -0
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Columns.tsx +161 -0
- package/src/web/client/shell/ContextRail.tsx +87 -0
- package/src/web/client/shell/Divider.tsx +44 -0
- package/src/web/client/shell/FocusTrap.tsx +56 -0
- package/src/web/client/shell/Header.tsx +64 -0
- package/src/web/client/shell/HelpOverlay.tsx +70 -0
- package/src/web/client/shell/Shell.tsx +210 -0
- package/src/web/client/shell/StatusBar.tsx +28 -0
- package/src/web/client/shell/cssvars.ts +70 -0
- package/src/web/client/shell/drag.model.ts +170 -0
- package/src/web/client/shell/focus.model.ts +100 -0
- package/src/web/client/shell/keys.model.ts +453 -0
- package/src/web/client/shell/keys.ts +59 -0
- package/src/web/client/shell/layout.model.ts +526 -0
- package/src/web/client/shell/shell.model.ts +333 -0
- package/src/web/client/shell/theme.ts +490 -0
- package/src/web/client/shell/viewport.ts +29 -0
- package/src/web/client/state.ts +89 -0
- package/src/web/client/tree/Tree.tsx +141 -0
- package/src/web/client/tree/tree.model.ts +674 -0
- package/src/web/client/workspace.ts +278 -0
- package/src/web/server/page.ts +258 -0
- package/src/web/server/routes.ts +987 -0
- package/src/web/server/security.ts +361 -0
- package/src/web/server/server.ts +275 -0
- package/src/web/server/sse.ts +321 -0
- package/src/web/server/watcher.ts +507 -0
- package/src/web/shared/graph.ts +213 -0
- package/src/web/shared/layout.ts +314 -0
- package/src/web/shared/logo.ts +11 -0
- package/src/web/shared/metrics.ts +136 -0
- package/src/web/shared/view.ts +200 -0
- package/src/web/shared/wire.ts +358 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace stylesheet, and how it gets past the CSP
|
|
3
|
+
* (weave-workspace §1.2, §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the client ships CSS at all
|
|
6
|
+
*
|
|
7
|
+
* `page.ts` emits a nonce'd `<style>` block, but it is deliberately tiny —
|
|
8
|
+
* the custom-property palette and enough body rules that the first paint has
|
|
9
|
+
* a background colour before `app.js` parses. It knows nothing about a grid,
|
|
10
|
+
* a divider or a status bar, and it is owned by the server tier, which this
|
|
11
|
+
* work may not edit. There is also no `/app.css` route. So the shell's own
|
|
12
|
+
* rules have to travel in the bundle and be installed at runtime.
|
|
13
|
+
*
|
|
14
|
+
* ## The CSP path, verified rather than assumed
|
|
15
|
+
*
|
|
16
|
+
* The policy is `style-src 'nonce-{N}'` with **no `'unsafe-inline'`**. Three
|
|
17
|
+
* facts decide the implementation, and each was checked rather than recalled:
|
|
18
|
+
*
|
|
19
|
+
* 1. **A literal `style="…"` attribute in markup is blocked.** That is what
|
|
20
|
+
* `'unsafe-inline'` governs for styles, and it is absent.
|
|
21
|
+
* 2. **CSSOM writes are not.** `el.style.setProperty(…)` has no CSP hook at
|
|
22
|
+
* all, which is why `cssvars.ts` applies column widths that way and why
|
|
23
|
+
* dynamic layout works here. Confirmed against Preact's own behaviour:
|
|
24
|
+
* `preact/src/diff/props.js` handles a `style` prop via
|
|
25
|
+
* `dom.style.cssText` or `style.setProperty`, never `setAttribute`.
|
|
26
|
+
* 3. **A script-created `<style>` element needs the nonce.** Inserting a
|
|
27
|
+
* stylesheet *is* subject to `style-src`, so the element must carry a
|
|
28
|
+
* matching `nonce` or the browser drops it.
|
|
29
|
+
*
|
|
30
|
+
* (3) is the one that shapes this module. The nonce is not a constant the
|
|
31
|
+
* bundle can hold — it is fresh per response (`page.ts`) — so it has to be
|
|
32
|
+
* read from the document at runtime. The `nonce` **content attribute** is
|
|
33
|
+
* hidden by browsers (`getAttribute("nonce")` returns `""`) specifically to
|
|
34
|
+
* stop an injection from exfiltrating it; the **IDL property** `el.nonce`
|
|
35
|
+
* remains readable by same-origin script, which is exactly this case. So
|
|
36
|
+
* {@link installTheme} copies `nonce` from an element the server already
|
|
37
|
+
* nonce'd onto the one it creates.
|
|
38
|
+
*
|
|
39
|
+
* ## Testability
|
|
40
|
+
*
|
|
41
|
+
* The CSS is a pure constant and the installer takes a four-method port, so
|
|
42
|
+
* both are covered without a DOM (§10). The port is narrow enough that the
|
|
43
|
+
* real `document` satisfies it structurally.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The theme.
|
|
48
|
+
*
|
|
49
|
+
* Dark-first, per the task's brief and against `page.ts`'s light-first
|
|
50
|
+
* fallback. That inversion is intentional and safe: this sheet is installed
|
|
51
|
+
* *after* the server's, so its `:root` wins on equal specificity, and the
|
|
52
|
+
* light branch is re-stated under `prefers-color-scheme: light`. The server
|
|
53
|
+
* block keeps doing its real job — painting a correct background before this
|
|
54
|
+
* bundle has parsed — while the palette a user actually looks at is decided
|
|
55
|
+
* here.
|
|
56
|
+
*
|
|
57
|
+
* ## "Dense but calm" (§1.2)
|
|
58
|
+
*
|
|
59
|
+
* The sketch is an information-dense IDE surface, not a marketing page, so
|
|
60
|
+
* the rules below deliberately avoid the defaults that produce the opposite:
|
|
61
|
+
* no card shadows, no 24 px gutters, no rounded panels floating on a
|
|
62
|
+
* contrasting background. Density comes from a 13 px base, 4–10 px padding
|
|
63
|
+
* and 1 px hairline rules; calm comes from a **single** accent colour used
|
|
64
|
+
* only for focus and selection, three greys doing all the structural work,
|
|
65
|
+
* and no borders where a background change already separates two regions.
|
|
66
|
+
*/
|
|
67
|
+
export const THEME_CSS = `
|
|
68
|
+
:root{
|
|
69
|
+
--weave-bg:#141317;--weave-panel:#191820;--weave-fg:#e8e6ee;--weave-dim:#8f8a9c;
|
|
70
|
+
--weave-faint:#5d5869;--weave-line:#26242e;--weave-line-strong:#343141;
|
|
71
|
+
--weave-accent:#a48cff;--weave-ok:#4ade80;--weave-warn:#fbbf24;--weave-bad:#f87171;
|
|
72
|
+
--weave-new:rgba(164,140,255,.20);
|
|
73
|
+
--weave-row:26px;--weave-gutter:10px;
|
|
74
|
+
}
|
|
75
|
+
@media (prefers-color-scheme: light){
|
|
76
|
+
:root{
|
|
77
|
+
--weave-bg:#faf9f7;--weave-panel:#ffffff;--weave-fg:#1c1b19;--weave-dim:#6f6b66;
|
|
78
|
+
--weave-faint:#9a958e;--weave-line:#e6e3de;--weave-line-strong:#d2cec8;
|
|
79
|
+
--weave-accent:#6d4aff;--weave-ok:#15803d;--weave-warn:#b45309;--weave-bad:#b91c1c;
|
|
80
|
+
--weave-new:rgba(109,74,255,.14);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
body{font-size:13px}
|
|
84
|
+
#app{height:100%;display:grid;grid-template-rows:auto 1fr auto;background:var(--weave-bg)}
|
|
85
|
+
|
|
86
|
+
/* header --------------------------------------------------------------- */
|
|
87
|
+
.weave-header{
|
|
88
|
+
display:flex;align-items:center;gap:14px;padding:0 var(--weave-gutter);
|
|
89
|
+
height:34px;border-bottom:1px solid var(--weave-line);background:var(--weave-panel);
|
|
90
|
+
}
|
|
91
|
+
.weave-brand{display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:600;letter-spacing:.01em;white-space:nowrap}
|
|
92
|
+
.weave-brand-mark{height:20px;width:20px;border-radius:5px}
|
|
93
|
+
/* A button styled as a search field: the palette owns the only text input,
|
|
94
|
+
so this opens it rather than pretending to accept a query. */
|
|
95
|
+
.weave-search{
|
|
96
|
+
display:flex;align-items:center;gap:6px;flex:0 1 260px;min-width:0;
|
|
97
|
+
height:22px;padding:0 7px;font:inherit;font-size:12px;text-align:left;cursor:pointer;
|
|
98
|
+
color:var(--weave-dim);background:var(--weave-bg);
|
|
99
|
+
border:1px solid var(--weave-line-strong);border-radius:4px;
|
|
100
|
+
}
|
|
101
|
+
.weave-search:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
|
|
102
|
+
.weave-search-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
103
|
+
.weave-search kbd{
|
|
104
|
+
font-family:var(--weave-mono);font-size:10.5px;color:var(--weave-faint);
|
|
105
|
+
padding:1px 5px;border:1px solid var(--weave-line-strong);border-radius:3px;white-space:nowrap;
|
|
106
|
+
}
|
|
107
|
+
.weave-summary{
|
|
108
|
+
margin-left:auto;display:flex;gap:9px;font-family:var(--weave-mono);
|
|
109
|
+
font-size:11.5px;color:var(--weave-dim);white-space:nowrap;
|
|
110
|
+
}
|
|
111
|
+
.weave-summary-part+.weave-summary-part::before{content:"·";margin-right:9px;color:var(--weave-faint)}
|
|
112
|
+
.weave-refresh{
|
|
113
|
+
font:inherit;font-size:14px;line-height:1;color:var(--weave-dim);background:none;
|
|
114
|
+
border:0;padding:3px 5px;border-radius:4px;cursor:pointer;
|
|
115
|
+
}
|
|
116
|
+
.weave-refresh:hover{color:var(--weave-fg);background:var(--weave-line)}
|
|
117
|
+
|
|
118
|
+
/* connection indicator -------------------------------------------------- */
|
|
119
|
+
.weave-conn{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--weave-dim);white-space:nowrap}
|
|
120
|
+
.weave-conn-dot{font-size:9px;line-height:1}
|
|
121
|
+
.weave-conn-ok .weave-conn-dot,.weave-conn-ok{color:var(--weave-ok)}
|
|
122
|
+
.weave-conn-warn .weave-conn-dot,.weave-conn-warn{color:var(--weave-warn)}
|
|
123
|
+
.weave-conn-bad .weave-conn-dot,.weave-conn-bad{color:var(--weave-bad)}
|
|
124
|
+
|
|
125
|
+
/* the grid -------------------------------------------------------------- */
|
|
126
|
+
/* Widths arrive as custom properties from cssvars.ts — the CSSOM path. The
|
|
127
|
+
fallbacks keep the layout sane for the first frame and if a write is ever
|
|
128
|
+
missed. */
|
|
129
|
+
.weave-grid{display:grid;min-height:0;overflow:hidden;background:var(--weave-line)}
|
|
130
|
+
.weave-grid[data-columns="3"]{
|
|
131
|
+
grid-template-columns:var(--weave-col-tree,22%) 1px var(--weave-col-note,46%) 1px var(--weave-col-graph,32%);
|
|
132
|
+
}
|
|
133
|
+
.weave-grid[data-columns="2"]{grid-template-columns:var(--weave-col-tree,32%) 1px var(--weave-col-note,68%)}
|
|
134
|
+
.weave-grid[data-columns="1"]{grid-template-columns:1fr}
|
|
135
|
+
|
|
136
|
+
.weave-col{
|
|
137
|
+
display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;
|
|
138
|
+
background:var(--weave-bg);
|
|
139
|
+
}
|
|
140
|
+
.weave-col-title{
|
|
141
|
+
margin:0;padding:5px var(--weave-gutter) 4px;font-size:10px;font-weight:600;
|
|
142
|
+
letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint);
|
|
143
|
+
border-bottom:1px solid var(--weave-line);
|
|
144
|
+
}
|
|
145
|
+
/* Title, then the graph (which takes the slack), then the context rail. The
|
|
146
|
+
\`1fr\` is what gives \`.weave-graph-canvas\` a definite height to measure. */
|
|
147
|
+
.weave-col-graph{display:grid;grid-template-rows:auto 1fr auto;background:var(--weave-bg)}
|
|
148
|
+
|
|
149
|
+
/* dividers -------------------------------------------------------------- */
|
|
150
|
+
.weave-divider{
|
|
151
|
+
background:var(--weave-line);cursor:col-resize;position:relative;
|
|
152
|
+
touch-action:none;
|
|
153
|
+
}
|
|
154
|
+
/* A 1 px target is unhittable, so the hit area is widened with a pseudo
|
|
155
|
+
element rather than by making the visible rule thicker. */
|
|
156
|
+
.weave-divider::after{content:"";position:absolute;inset:0 -3px;z-index:1}
|
|
157
|
+
.weave-divider:hover,.weave-divider:focus-visible{background:var(--weave-accent);outline:none}
|
|
158
|
+
|
|
159
|
+
/* tree column ----------------------------------------------------------- */
|
|
160
|
+
/* --weave-depth is written per row by \`depthVar\`, through the same CSSOM
|
|
161
|
+
path as the column widths, and multiplied by a step this sheet owns — so
|
|
162
|
+
the tree's density stays a CSS decision. */
|
|
163
|
+
.weave-tree{display:flex;flex-direction:column;min-height:0;flex:1}
|
|
164
|
+
.weave-tree-controls{
|
|
165
|
+
display:flex;align-items:center;gap:6px;padding:5px var(--weave-gutter);
|
|
166
|
+
border-bottom:1px solid var(--weave-line);
|
|
167
|
+
}
|
|
168
|
+
.weave-filter{
|
|
169
|
+
flex:1;min-width:0;height:21px;padding:0 6px;font:inherit;font-size:11.5px;
|
|
170
|
+
color:var(--weave-fg);background:var(--weave-panel);
|
|
171
|
+
border:1px solid var(--weave-line-strong);border-radius:4px;
|
|
172
|
+
}
|
|
173
|
+
.weave-chip{
|
|
174
|
+
font:inherit;font-size:10.5px;line-height:1;white-space:nowrap;cursor:pointer;
|
|
175
|
+
color:var(--weave-dim);background:var(--weave-panel);padding:4px 6px;
|
|
176
|
+
border:1px solid var(--weave-line-strong);border-radius:4px;
|
|
177
|
+
}
|
|
178
|
+
.weave-chip:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
|
|
179
|
+
.weave-rows{
|
|
180
|
+
flex:1;min-height:0;overflow:auto;margin:0;padding:3px 0;list-style:none;
|
|
181
|
+
}
|
|
182
|
+
.weave-rows:focus-visible{outline-offset:-2px}
|
|
183
|
+
.weave-row{
|
|
184
|
+
display:flex;align-items:center;gap:5px;height:var(--weave-row);
|
|
185
|
+
padding-right:var(--weave-gutter);
|
|
186
|
+
padding-left:calc(var(--weave-gutter) + var(--weave-depth,0) * 13px);
|
|
187
|
+
cursor:default;white-space:nowrap;
|
|
188
|
+
}
|
|
189
|
+
.weave-row:hover{background:var(--weave-line)}
|
|
190
|
+
.weave-row-on{background:var(--weave-line-strong);color:var(--weave-fg)}
|
|
191
|
+
/* A newly-arrived node (a file or note added since the last update, §6):
|
|
192
|
+
one short highlight that fades while the label settles from bold back to
|
|
193
|
+
normal. The class is computed from the frame diff in workspace.ts and
|
|
194
|
+
expires with it, so collapsing and re-expanding later does not replay the
|
|
195
|
+
arrival, and the first load never flashes the whole tree. */
|
|
196
|
+
.weave-row-new{animation:weave-row-new 2.6s ease-out both}
|
|
197
|
+
@keyframes weave-row-new{
|
|
198
|
+
0%{background:var(--weave-new);font-weight:700}
|
|
199
|
+
40%{font-weight:700}
|
|
200
|
+
100%{background:transparent;font-weight:400}
|
|
201
|
+
}
|
|
202
|
+
.weave-twisty{width:10px;flex:none;color:var(--weave-faint);cursor:pointer;text-align:center}
|
|
203
|
+
.weave-kind{flex:none;color:var(--weave-faint);font-size:11px}
|
|
204
|
+
.weave-prov{flex:none;font-size:9px}
|
|
205
|
+
.weave-prov-human{color:var(--weave-ok)}
|
|
206
|
+
.weave-prov-agent{color:var(--weave-accent)}
|
|
207
|
+
.weave-prov-generated{color:var(--weave-faint)}
|
|
208
|
+
.weave-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
209
|
+
.weave-meta{flex:none;font-size:10.5px;color:var(--weave-faint)}
|
|
210
|
+
.weave-tree-empty{flex:1;margin:0;padding:14px var(--weave-gutter);color:var(--weave-dim)}
|
|
211
|
+
.weave-tree-count{
|
|
212
|
+
margin:0;padding:3px var(--weave-gutter);font-size:10.5px;color:var(--weave-faint);
|
|
213
|
+
border-top:1px solid var(--weave-line);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* note column ----------------------------------------------------------- */
|
|
217
|
+
.weave-note{display:flex;flex-direction:column;min-height:0;flex:1;overflow:auto}
|
|
218
|
+
.weave-note-empty{flex:1;margin:0;padding:14px var(--weave-gutter);color:var(--weave-dim);max-width:44ch;line-height:1.5}
|
|
219
|
+
.weave-note-head{padding:10px var(--weave-gutter) 8px;border-bottom:1px solid var(--weave-line)}
|
|
220
|
+
.weave-note-title{margin:0 0 4px;font-size:15px;font-weight:600;line-height:1.3;color:var(--weave-fg)}
|
|
221
|
+
.weave-note-meta{margin:0;display:flex;gap:10px;font-size:11px;color:var(--weave-dim);flex-wrap:wrap}
|
|
222
|
+
.weave-note-time{color:var(--weave-faint)}
|
|
223
|
+
.weave-note-tags{margin:5px 0 0;display:flex;gap:5px;flex-wrap:wrap}
|
|
224
|
+
.weave-tag{
|
|
225
|
+
font-size:10.5px;color:var(--weave-accent);background:var(--weave-panel);
|
|
226
|
+
padding:1px 6px;border:1px solid var(--weave-line-strong);border-radius:9px;
|
|
227
|
+
}
|
|
228
|
+
.weave-note-body{padding:10px var(--weave-gutter) 24px;line-height:1.6;max-width:78ch;color:var(--weave-fg)}
|
|
229
|
+
.weave-note-body>*:first-child{margin-top:0}
|
|
230
|
+
.weave-note-body h1,.weave-note-body h2,.weave-note-body h3,
|
|
231
|
+
.weave-note-body h4,.weave-note-body h5,.weave-note-body h6{
|
|
232
|
+
margin:18px 0 6px;font-weight:600;line-height:1.3;
|
|
233
|
+
}
|
|
234
|
+
.weave-note-body h1{font-size:15px}
|
|
235
|
+
.weave-note-body h2{font-size:14px}
|
|
236
|
+
.weave-note-body h3,.weave-note-body h4,.weave-note-body h5,.weave-note-body h6{font-size:13px}
|
|
237
|
+
.weave-note-body p,.weave-note-body ul,.weave-note-body ol,.weave-note-body blockquote{margin:0 0 10px}
|
|
238
|
+
.weave-note-body ul,.weave-note-body ol{padding-left:20px}
|
|
239
|
+
.weave-note-body li{margin:2px 0}
|
|
240
|
+
.weave-note-body blockquote{
|
|
241
|
+
padding-left:10px;border-left:2px solid var(--weave-line-strong);color:var(--weave-dim);
|
|
242
|
+
}
|
|
243
|
+
.weave-note-body code{
|
|
244
|
+
font-family:var(--weave-mono);font-size:11.5px;padding:1px 4px;
|
|
245
|
+
background:var(--weave-panel);border:1px solid var(--weave-line);border-radius:3px;
|
|
246
|
+
}
|
|
247
|
+
.weave-note-body pre{
|
|
248
|
+
margin:0 0 10px;padding:8px 10px;overflow:auto;
|
|
249
|
+
background:var(--weave-panel);border:1px solid var(--weave-line);border-radius:5px;
|
|
250
|
+
}
|
|
251
|
+
.weave-note-body pre code{padding:0;background:none;border:0}
|
|
252
|
+
.weave-note-body hr{margin:14px 0;border:0;border-top:1px solid var(--weave-line)}
|
|
253
|
+
.weave-note-body table{border-collapse:collapse;margin:0 0 10px;font-size:12px}
|
|
254
|
+
.weave-note-body th,.weave-note-body td{padding:3px 8px;border:1px solid var(--weave-line);text-align:left}
|
|
255
|
+
.weave-note-body th{color:var(--weave-dim);font-weight:600}
|
|
256
|
+
.weave-note-body img{max-width:100%;height:auto}
|
|
257
|
+
.weave-note-body a{color:var(--weave-accent);text-decoration:none}
|
|
258
|
+
.weave-note-body a:hover{text-decoration:underline}
|
|
259
|
+
/* Wikilinks carry no href — they drive the §1.3 bus, not the browser — so the
|
|
260
|
+
pointer has to be restored by hand. A ghost is a name with no note behind
|
|
261
|
+
it: Obsidian's dashed, dimmed affordance to create one. */
|
|
262
|
+
.weave-wiki{cursor:pointer;color:var(--weave-accent);text-decoration:none}
|
|
263
|
+
.weave-wiki:hover{text-decoration:underline}
|
|
264
|
+
.weave-wiki-ghost{
|
|
265
|
+
cursor:help;color:var(--weave-faint);border-bottom:1px dashed var(--weave-faint);text-decoration:none;
|
|
266
|
+
}
|
|
267
|
+
.weave-wiki-ghost:hover{text-decoration:none}
|
|
268
|
+
|
|
269
|
+
/* note editor (§11 P5) ---------------------------------------------------- */
|
|
270
|
+
/* A \`<textarea>\`, not CodeMirror: §0 V10 measured CM6 at 118 KB gzip, more
|
|
271
|
+
than the entire rest of the client. It inherits the body's type so toggling
|
|
272
|
+
\`⌘E\` does not reflow the column into a different shape. */
|
|
273
|
+
.weave-note-bar{
|
|
274
|
+
display:flex;align-items:center;gap:6px;flex-wrap:wrap;
|
|
275
|
+
padding:5px var(--weave-gutter);border-bottom:1px solid var(--weave-line);
|
|
276
|
+
}
|
|
277
|
+
.weave-note-toggle,.weave-note-save,.weave-note-open,.weave-note-action{
|
|
278
|
+
font:inherit;font-size:11px;padding:2px 8px;cursor:pointer;
|
|
279
|
+
color:var(--weave-fg);background:var(--weave-raise);
|
|
280
|
+
border:1px solid var(--weave-line);border-radius:4px;
|
|
281
|
+
}
|
|
282
|
+
.weave-note-toggle:hover,.weave-note-save:hover,.weave-note-open:hover,.weave-note-action:hover{
|
|
283
|
+
border-color:var(--weave-accent);
|
|
284
|
+
}
|
|
285
|
+
.weave-note-toggle[aria-pressed="true"]{border-color:var(--weave-accent);color:var(--weave-accent)}
|
|
286
|
+
.weave-note-save:disabled{opacity:.45;cursor:default;border-color:var(--weave-line)}
|
|
287
|
+
.weave-note-dirty{color:var(--weave-accent);font-size:14px;line-height:1}
|
|
288
|
+
.weave-note-status{font-size:11px;color:var(--weave-dim)}
|
|
289
|
+
.weave-note-status-ok{color:var(--weave-ok)}
|
|
290
|
+
.weave-note-status-warn{color:var(--weave-warn)}
|
|
291
|
+
/* The prompt takes the full row rather than sitting inline: reload-or-overwrite
|
|
292
|
+
is a decision about the user's unsaved text, and a decision squeezed between
|
|
293
|
+
two buttons reads as a hint. */
|
|
294
|
+
.weave-note-prompt{
|
|
295
|
+
flex-basis:100%;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
|
|
296
|
+
margin-top:4px;padding:5px 8px;border-radius:4px;
|
|
297
|
+
background:var(--weave-raise);border:1px solid var(--weave-warn);
|
|
298
|
+
}
|
|
299
|
+
.weave-note-prompt-conflict,.weave-note-prompt-collision{border-color:var(--weave-warn)}
|
|
300
|
+
.weave-note-prompt-external,.weave-note-prompt-discard{border-color:var(--weave-line)}
|
|
301
|
+
.weave-note-prompt-text{margin:0;font-size:11px;color:var(--weave-fg)}
|
|
302
|
+
.weave-note-prompt-actions{margin:0;display:flex;gap:6px;flex-wrap:wrap}
|
|
303
|
+
.weave-note-editor{
|
|
304
|
+
flex:1;min-height:240px;resize:none;
|
|
305
|
+
padding:10px var(--weave-gutter) 24px;
|
|
306
|
+
font:inherit;font-family:var(--weave-mono);font-size:12px;line-height:1.6;
|
|
307
|
+
color:var(--weave-fg);background:var(--weave-bg);border:0;outline-offset:-2px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* graph column ---------------------------------------------------------- */
|
|
311
|
+
/* The canvas is a plain block sigma appends its own <canvas> layers into. It
|
|
312
|
+
must have a definite size before sigma measures it, hence \`min-height\` and
|
|
313
|
+
the \`1fr\` row from \`.weave-col-graph\` above — a zero-height container makes
|
|
314
|
+
sigma refuse to render (we pass \`allowInvalidContainer\`, so it degrades to
|
|
315
|
+
blank rather than throwing). */
|
|
316
|
+
.weave-graph{display:grid;grid-template-rows:1fr auto auto;min-height:0;overflow:hidden}
|
|
317
|
+
.weave-graph-canvas{min-height:120px;min-width:0;position:relative;overflow:hidden}
|
|
318
|
+
.weave-graph-empty{
|
|
319
|
+
margin:0;padding:14px var(--weave-gutter);color:var(--weave-dim);
|
|
320
|
+
max-width:44ch;line-height:1.5;
|
|
321
|
+
}
|
|
322
|
+
.weave-graph-controls{
|
|
323
|
+
display:flex;align-items:center;gap:6px;flex-wrap:wrap;
|
|
324
|
+
padding:5px var(--weave-gutter);border-top:1px solid var(--weave-line);
|
|
325
|
+
}
|
|
326
|
+
.weave-graph-legend{
|
|
327
|
+
margin-left:auto;display:flex;gap:9px;font-size:10.5px;color:var(--weave-faint);
|
|
328
|
+
white-space:nowrap;
|
|
329
|
+
}
|
|
330
|
+
.weave-legend-on{color:var(--weave-accent)}
|
|
331
|
+
.weave-legend-near{color:var(--weave-fg)}
|
|
332
|
+
.weave-graph-count{
|
|
333
|
+
margin:0;padding:3px var(--weave-gutter);font-size:10.5px;color:var(--weave-faint);
|
|
334
|
+
border-top:1px solid var(--weave-line);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* context rail ---------------------------------------------------------- */
|
|
338
|
+
/* The \`.weave-empty\` block that used to sit above went with \`EmptyState.tsx\`
|
|
339
|
+
in P3: every column now has its own empty state (\`treeEmptyMessage\`,
|
|
340
|
+
\`noteEmptyMessage\`, \`graphEmptyMessage\`, \`RAIL_EMPTY\`), each rendered as a
|
|
341
|
+
plain paragraph, so the shared placeholder had no callers left. */
|
|
342
|
+
.weave-rail{
|
|
343
|
+
display:flex;flex-direction:column;min-height:96px;max-height:46%;overflow:auto;
|
|
344
|
+
border-top:1px solid var(--weave-line);background:var(--weave-panel);
|
|
345
|
+
}
|
|
346
|
+
.weave-ctx-empty{margin:0;padding:10px var(--weave-gutter);color:var(--weave-dim);line-height:1.5}
|
|
347
|
+
.weave-ctx-group{padding:5px var(--weave-gutter) 2px}
|
|
348
|
+
.weave-ctx-heading{
|
|
349
|
+
margin:0 0 2px;font-size:9.5px;font-weight:600;letter-spacing:.09em;color:var(--weave-faint);
|
|
350
|
+
}
|
|
351
|
+
.weave-ctx-rows,.weave-ctx-tags{margin:0;padding:0;list-style:none}
|
|
352
|
+
.weave-ctx-row{display:flex}
|
|
353
|
+
.weave-ctx-link{
|
|
354
|
+
display:flex;align-items:center;gap:5px;width:100%;min-width:0;
|
|
355
|
+
font:inherit;font-size:12px;text-align:left;color:var(--weave-fg);
|
|
356
|
+
background:none;border:0;padding:2px var(--weave-gutter) 2px 0;cursor:pointer;
|
|
357
|
+
border-radius:3px;
|
|
358
|
+
}
|
|
359
|
+
.weave-ctx-link:hover{background:var(--weave-line)}
|
|
360
|
+
.weave-ctx-row.weave-row-on .weave-ctx-link{background:var(--weave-line-strong)}
|
|
361
|
+
.weave-ctx-tag{margin:0 0 3px}
|
|
362
|
+
.weave-ctx-tag .weave-ctx-rows{padding-left:12px}
|
|
363
|
+
|
|
364
|
+
/* status bar ------------------------------------------------------------ */
|
|
365
|
+
.weave-status{
|
|
366
|
+
display:flex;align-items:center;gap:12px;height:22px;padding:0 var(--weave-gutter);
|
|
367
|
+
font-family:var(--weave-mono);font-size:11px;color:var(--weave-dim);
|
|
368
|
+
border-top:1px solid var(--weave-line);background:var(--weave-panel);
|
|
369
|
+
}
|
|
370
|
+
.weave-status-cwd,.weave-status-sel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
371
|
+
.weave-status-cwd{max-width:38%;color:var(--weave-faint)}
|
|
372
|
+
.weave-status-sel{color:var(--weave-fg)}
|
|
373
|
+
.weave-status-stamp{margin-left:auto;color:var(--weave-faint)}
|
|
374
|
+
|
|
375
|
+
/* overlays: the ⌘K palette and the ? help sheet (P4) --------------------- */
|
|
376
|
+
/* The scrim is a click target that closes, and the reason both overlays sit
|
|
377
|
+
at the top of the stacking context rather than inside a column: a dialog
|
|
378
|
+
that a column's \`overflow:hidden\` can clip is a dialog that disappears at
|
|
379
|
+
the wrong breakpoint. */
|
|
380
|
+
.weave-scrim{
|
|
381
|
+
position:fixed;inset:0;z-index:10;display:flex;justify-content:center;
|
|
382
|
+
align-items:flex-start;padding:9vh 16px 16px;background:rgba(0,0,0,.45);
|
|
383
|
+
}
|
|
384
|
+
.weave-palette,.weave-help{
|
|
385
|
+
display:flex;flex-direction:column;width:100%;max-width:560px;max-height:72vh;
|
|
386
|
+
overflow:hidden;background:var(--weave-panel);color:var(--weave-fg);
|
|
387
|
+
border:1px solid var(--weave-line-strong);border-radius:7px;
|
|
388
|
+
}
|
|
389
|
+
.weave-palette-input{
|
|
390
|
+
flex:none;height:34px;padding:0 11px;font:inherit;font-size:14px;
|
|
391
|
+
color:var(--weave-fg);background:transparent;border:0;
|
|
392
|
+
border-bottom:1px solid var(--weave-line);border-radius:0;
|
|
393
|
+
}
|
|
394
|
+
.weave-palette-input:focus-visible{outline-offset:-2px}
|
|
395
|
+
.weave-hits{flex:1;min-height:0;overflow:auto;margin:0;padding:3px 0;list-style:none}
|
|
396
|
+
.weave-hit{
|
|
397
|
+
display:flex;align-items:baseline;gap:7px;padding:3px 11px;cursor:pointer;
|
|
398
|
+
min-width:0;white-space:nowrap;
|
|
399
|
+
}
|
|
400
|
+
.weave-hit-on{background:var(--weave-line-strong)}
|
|
401
|
+
.weave-hit-badge{
|
|
402
|
+
flex:none;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--weave-faint);
|
|
403
|
+
min-width:56px;
|
|
404
|
+
}
|
|
405
|
+
.weave-hit-note .weave-hit-badge{color:var(--weave-accent)}
|
|
406
|
+
.weave-hit-label{flex:none;max-width:46%;overflow:hidden;text-overflow:ellipsis}
|
|
407
|
+
.weave-hit-detail{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:11px;color:var(--weave-dim)}
|
|
408
|
+
.weave-palette-status{margin:0;padding:14px 11px;color:var(--weave-dim);line-height:1.5}
|
|
409
|
+
.weave-palette-foot,.weave-help-foot{
|
|
410
|
+
display:flex;gap:12px;margin:0;padding:4px 11px;font-size:10.5px;color:var(--weave-faint);
|
|
411
|
+
border-top:1px solid var(--weave-line);
|
|
412
|
+
}
|
|
413
|
+
.weave-palette-hint,.weave-help-hint{margin-left:auto}
|
|
414
|
+
.weave-help-title{margin:0;padding:9px 11px;font-size:13px;font-weight:600;border-bottom:1px solid var(--weave-line)}
|
|
415
|
+
.weave-keys{flex:1;min-height:0;overflow:auto;margin:0;padding:5px 11px}
|
|
416
|
+
.weave-key-group{margin:0 0 3px;font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint)}
|
|
417
|
+
.weave-key-row{display:flex;align-items:baseline;gap:9px;padding:1px 0}
|
|
418
|
+
.weave-key-combo{
|
|
419
|
+
flex:none;min-width:74px;font-family:var(--weave-mono);font-size:11px;color:var(--weave-accent);
|
|
420
|
+
}
|
|
421
|
+
.weave-key-what{flex:1;min-width:0;font-size:12px}
|
|
422
|
+
|
|
423
|
+
/* focus ----------------------------------------------------------------- */
|
|
424
|
+
:focus-visible{outline:2px solid var(--weave-accent);outline-offset:1px}
|
|
425
|
+
@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}
|
|
426
|
+
`;
|
|
427
|
+
|
|
428
|
+
// --- installation --------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
/** A `<style>` element, as far as this module is concerned. */
|
|
431
|
+
export interface StyleElement {
|
|
432
|
+
/** The IDL property, not the content attribute. See the module header. */
|
|
433
|
+
nonce?: string | undefined;
|
|
434
|
+
textContent: string | null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The slice of `document` the installer needs.
|
|
439
|
+
*
|
|
440
|
+
* Four members, so a fake is a short object literal and the real `document`
|
|
441
|
+
* satisfies it structurally.
|
|
442
|
+
*/
|
|
443
|
+
export interface ThemeHost {
|
|
444
|
+
createElement(tag: "style"): StyleElement;
|
|
445
|
+
/** Used to find an element the server already nonce'd. */
|
|
446
|
+
querySelector(selector: string): StyleElement | null;
|
|
447
|
+
head: { appendChild(node: StyleElement): unknown };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Where to look for a nonce, in order.
|
|
452
|
+
*
|
|
453
|
+
* The server's `<style>` block first because it is guaranteed present and is
|
|
454
|
+
* the same kind of element we are about to create. The bundle's own `<script>`
|
|
455
|
+
* is the fallback: it carries the same per-response nonce, and it exists by
|
|
456
|
+
* definition, because it is the thing currently running.
|
|
457
|
+
*/
|
|
458
|
+
export const NONCE_SOURCES = ["style[nonce]", "style", "script[nonce]", "script[src]"];
|
|
459
|
+
|
|
460
|
+
/** Read the per-response nonce from the document, or `null`. */
|
|
461
|
+
export function findNonce(host: ThemeHost): string | null {
|
|
462
|
+
for (const selector of NONCE_SOURCES) {
|
|
463
|
+
const nonce = host.querySelector(selector)?.nonce;
|
|
464
|
+
if (typeof nonce === "string" && nonce !== "") return nonce;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Install {@link THEME_CSS} into the document.
|
|
471
|
+
*
|
|
472
|
+
* Returns whether a nonce was found and applied. A `false` return means the
|
|
473
|
+
* browser will refuse the sheet — the workspace still renders, using only
|
|
474
|
+
* `page.ts`'s palette and the browser's defaults, which is ugly but legible.
|
|
475
|
+
* That is the right failure: silently weakening the CSP to guarantee styling
|
|
476
|
+
* would trade a cosmetic problem for a security one.
|
|
477
|
+
*
|
|
478
|
+
* The element is appended even without a nonce, deliberately. It costs
|
|
479
|
+
* nothing, it keeps the two paths identical, and the resulting CSP violation
|
|
480
|
+
* report in the console is a far better diagnostic than a stylesheet that was
|
|
481
|
+
* never created.
|
|
482
|
+
*/
|
|
483
|
+
export function installTheme(host: ThemeHost, css: string = THEME_CSS): boolean {
|
|
484
|
+
const element = host.createElement("style");
|
|
485
|
+
const nonce = findNonce(host);
|
|
486
|
+
if (nonce !== null) element.nonce = nonce;
|
|
487
|
+
element.textContent = css;
|
|
488
|
+
host.head.appendChild(element);
|
|
489
|
+
return nonce !== null;
|
|
490
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching the viewport width (weave-workspace §1.2).
|
|
3
|
+
*
|
|
4
|
+
* The width is not a cosmetic detail here: it picks the breakpoint, which
|
|
5
|
+
* decides how many columns render, which decides how many dividers exist. So
|
|
6
|
+
* the subscription is worth a test, and a subscription written inline in a
|
|
7
|
+
* `useEffect` is one no test can reach (§10). Four lines, an injected port,
|
|
8
|
+
* and the shell keeps a one-line effect.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The slice of `window` this module reads. `window` satisfies it. */
|
|
12
|
+
export interface ViewportHost {
|
|
13
|
+
readonly innerWidth: number;
|
|
14
|
+
addEventListener(type: "resize", listener: () => void): void;
|
|
15
|
+
removeEventListener(type: "resize", listener: () => void): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Call `onChange` with the width whenever the window resizes.
|
|
20
|
+
*
|
|
21
|
+
* Returns an unsubscribe. The listener is *not* invoked eagerly: the shell
|
|
22
|
+
* already seeds its state from `initialWidth` at mount, and firing here would
|
|
23
|
+
* make the first render set state during an effect for no change in value.
|
|
24
|
+
*/
|
|
25
|
+
export function watchViewport(host: ViewportHost, onChange: (width: number) => void): () => void {
|
|
26
|
+
const listener = (): void => onChange(host.innerWidth);
|
|
27
|
+
host.addEventListener("resize", listener);
|
|
28
|
+
return () => host.removeEventListener("resize", listener);
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context bus (weave-workspace §1.3).
|
|
3
|
+
*
|
|
4
|
+
* No event bus, no pub/sub — five Preact signals. Selecting in the tree,
|
|
5
|
+
* clicking a graph node, following a wikilink and hitting a search result all
|
|
6
|
+
* write {@link selectedId}; the note column, the graph highlight and the
|
|
7
|
+
* context rail are computed from it.
|
|
8
|
+
*
|
|
9
|
+
* ## Tier rules (§2)
|
|
10
|
+
*
|
|
11
|
+
* This is `src/web/client/**`: it may import `src/web/shared` and browser
|
|
12
|
+
* deps, and must **never** import `src/core` or `node:*`. Core is
|
|
13
|
+
* Node-flavoured TypeScript that would drag `node:fs` into the bundle.
|
|
14
|
+
*
|
|
15
|
+
* ## What the P0 placeholders became
|
|
16
|
+
*
|
|
17
|
+
* This file used to declare three local interfaces behind `TODO(P1)` markers,
|
|
18
|
+
* standing in for contracts that had not been written yet. Two of them are
|
|
19
|
+
* now real and imported from `src/web/shared/wire.ts` — {@link graph} carries
|
|
20
|
+
* a `GraphPayload` and {@link noteBody} a `ViewNote`, the exact values
|
|
21
|
+
* `src/web/client/api.ts` returns. That removes a translation step that
|
|
22
|
+
* existed only to satisfy a stale type: the fetchers already produce these
|
|
23
|
+
* shapes, so anything narrower here was a downcast waiting to lose a field.
|
|
24
|
+
*
|
|
25
|
+
* The third resolved the other way. The TODO said to take `TreeState` from
|
|
26
|
+
* the wire too, and that was simply wrong: which rows a user has expanded is
|
|
27
|
+
* **not** a wire contract. It never crosses the network, the server has no
|
|
28
|
+
* opinion about it, and putting it in `wire.ts` would have made the
|
|
29
|
+
* client/server contract carry a purely local UI preference. So
|
|
30
|
+
* {@link TreeState} stays here, declared where it is owned, with the marker
|
|
31
|
+
* removed rather than carried forward.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { signal } from "@preact/signals";
|
|
35
|
+
import type { GraphPayload, NotePayload } from "../shared/wire";
|
|
36
|
+
|
|
37
|
+
/** Connection state shown in the status bar. */
|
|
38
|
+
export type ConnectionState = "live" | "reconnecting" | "offline";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Which tree rows are open.
|
|
42
|
+
*
|
|
43
|
+
* Client-owned, deliberately: see the module header. An interface rather than
|
|
44
|
+
* a bare array so that P2's filter text and provenance-cycling state have an
|
|
45
|
+
* obvious home that does not change this signal's type.
|
|
46
|
+
*/
|
|
47
|
+
export interface TreeState {
|
|
48
|
+
/** Ids of expanded tree rows. */
|
|
49
|
+
readonly expanded: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The empty tree: nothing expanded. */
|
|
53
|
+
export function initialTreeState(): TreeState {
|
|
54
|
+
return { expanded: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The graph node id currently in context — everywhere. */
|
|
58
|
+
export const selectedId = signal<string | null>(null);
|
|
59
|
+
|
|
60
|
+
/** The whole graph, as delivered by the server. `null` until first load. */
|
|
61
|
+
export const graph = signal<GraphPayload | null>(null);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derived: fetched when {@link selectedId} names a note.
|
|
65
|
+
*
|
|
66
|
+
* A {@link NotePayload} rather than a bare `ViewNote` as of P5. The revision
|
|
67
|
+
* travels **with** the body because the editor saves against it, and a
|
|
68
|
+
* revision fetched separately would describe a state the draft was not typed
|
|
69
|
+
* against — the exact window a conflict check exists to close. The note
|
|
70
|
+
* column reads `.note` and is otherwise unchanged.
|
|
71
|
+
*/
|
|
72
|
+
export const noteBody = signal<NotePayload | null>(null);
|
|
73
|
+
|
|
74
|
+
/** Which tree rows are open. */
|
|
75
|
+
export const treeState = signal<TreeState>(initialTreeState());
|
|
76
|
+
|
|
77
|
+
/** SSE liveness (§6). */
|
|
78
|
+
export const connection = signal<ConnectionState>("live");
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Node ids that appeared in the most recent graph update (weave-workspace
|
|
82
|
+
* §6 frame → tree feedback).
|
|
83
|
+
*
|
|
84
|
+
* Empty on the mount fetch — a first load must not animate the whole tree.
|
|
85
|
+
* The controller replaces the set on every published payload and clears it
|
|
86
|
+
* after {@link ../workspace!RECENT_TTL_MS}, so a row collapsed and re-expanded
|
|
87
|
+
* minutes later does not replay its arrival animation.
|
|
88
|
+
*/
|
|
89
|
+
export const recentIds = signal<ReadonlySet<string>>(new Set());
|