pi-weave 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +8 -37
  2. package/package.json +1 -2
  3. package/src/core/concurrency.ts +3 -6
  4. package/src/core/frontmatter.ts +0 -53
  5. package/src/core/graph/build.ts +6 -7
  6. package/src/core/graph/current.ts +2 -4
  7. package/src/core/graph/model.ts +1 -1
  8. package/src/core/graph/wikilinks.ts +3 -3
  9. package/src/core/index.ts +26 -27
  10. package/src/core/paths.ts +0 -7
  11. package/src/core/vault.ts +16 -681
  12. package/src/core/view/detail.ts +1 -1
  13. package/src/core/view/health.ts +1 -1
  14. package/src/core/view/tree.ts +1 -1
  15. package/src/pi/index.ts +6 -85
  16. package/src/pi/summarize.ts +2 -2
  17. package/src/pi/viewer/tui/bodyStore.ts +4 -7
  18. package/src/pi/viewer/tui/branding.ts +7 -148
  19. package/src/pi/viewer/tui/run.ts +3 -17
  20. package/src/pi/viewer/tui/surface/base.ts +24 -3
  21. package/src/pi/viewer/tui/surface/explore.ts +41 -6
  22. package/src/pi/viewer/tui/workspace.ts +23 -351
  23. package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
  24. package/src/pi/viewer/web/run.ts +7 -117
  25. package/src/web/client/api.dom.ts +2 -2
  26. package/src/web/client/api.ts +14 -223
  27. package/src/web/client/bootstrap.ts +5 -14
  28. package/src/web/client/context/context.model.ts +9 -11
  29. package/src/web/client/dist/app.js +93 -219
  30. package/src/web/client/graph/dynamics.ts +5 -65
  31. package/src/web/client/graph/renderer.dom.ts +7 -8
  32. package/src/web/client/graph/renderer.ts +9 -35
  33. package/src/web/client/main.tsx +1 -1
  34. package/src/web/client/note/Note.tsx +21 -63
  35. package/src/web/client/search/SearchPalette.tsx +45 -36
  36. package/src/web/client/search/search.model.ts +33 -454
  37. package/src/web/client/shell/Columns.tsx +13 -83
  38. package/src/web/client/shell/Header.tsx +2 -10
  39. package/src/web/client/shell/Shell.tsx +50 -125
  40. package/src/web/client/shell/StatusBar.tsx +1 -4
  41. package/src/web/client/shell/icons.model.ts +4 -7
  42. package/src/web/client/shell/keys.model.ts +5 -42
  43. package/src/web/client/shell/keys.ts +2 -2
  44. package/src/web/client/shell/shell.model.ts +10 -133
  45. package/src/web/client/shell/theme.model.ts +2 -2
  46. package/src/web/client/shell/theme.ts +33 -157
  47. package/src/web/client/state.ts +9 -89
  48. package/src/web/client/tree/Tree.tsx +25 -575
  49. package/src/web/client/tree/tree.model.ts +8 -162
  50. package/src/web/client/workspace.ts +72 -242
  51. package/src/web/server/page.ts +8 -10
  52. package/src/web/server/routes.ts +30 -563
  53. package/src/web/server/server.ts +6 -145
  54. package/src/web/shared/layout.ts +72 -624
  55. package/src/web/shared/wire.ts +10 -196
  56. package/src/core/sessions.ts +0 -929
  57. package/src/pi/sessionScan.ts +0 -104
  58. package/src/pi/viewer/tui/explorer.ts +0 -586
  59. package/src/web/client/live.model.ts +0 -275
  60. package/src/web/client/live.ts +0 -151
  61. package/src/web/client/note/Editor.tsx +0 -109
  62. package/src/web/client/note/editor.controller.ts +0 -151
  63. package/src/web/client/note/editor.model.ts +0 -686
  64. package/src/web/client/search/search.ts +0 -107
  65. package/src/web/client/shell/Divider.tsx +0 -44
  66. package/src/web/client/shell/cssvars.ts +0 -70
  67. package/src/web/client/shell/drag.model.ts +0 -170
  68. package/src/web/client/shell/layout.model.ts +0 -500
  69. package/src/web/client/shell/viewport.ts +0 -29
  70. package/src/web/server/sse.ts +0 -321
  71. package/src/web/server/watcher.ts +0 -507
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * `page.ts` emits a nonce'd `<style>` block, but it is deliberately tiny —
8
8
  * the custom-property palette and enough body rules that the first paint has
9
- * a background colour before `app.js` parses. It knows nothing about a grid,
10
- * a divider or a status bar, and it is owned by the server tier, which this
9
+ * a background colour before `app.js` parses. It knows nothing about the
10
+ * status bar, and it is owned by the server tier, which this
11
11
  * work may not edit. There is also no `/app.css` route. So the shell's own
12
12
  * rules have to travel in the bundle and be installed at runtime.
13
13
  *
@@ -19,8 +19,8 @@
19
19
  * 1. **A literal `style="…"` attribute in markup is blocked.** That is what
20
20
  * `'unsafe-inline'` governs for styles, and it is absent.
21
21
  * 2. **CSSOM writes are not.** `el.style.setProperty(…)` has no CSP hook at
22
- * all, which is why `cssvars.ts` applies column widths that way and why
23
- * dynamic layout works here. Confirmed against Preact's own behaviour:
22
+ * all, which is why dynamic note preview positions use CSSOM. Confirmed
23
+ * against Preact's own behaviour:
24
24
  * `preact/src/diff/props.js` handles a `style` prop via
25
25
  * `dom.style.cssText` or `style.setProperty`, never `setAttribute`.
26
26
  * 3. **A script-created `<style>` element needs the nonce.** Inserting a
@@ -161,7 +161,7 @@ export const THEME_CSS = `
161
161
  ui 11.5 status bar, the note's meta row, small controls
162
162
  row 12 list rows, text inputs, inline code
163
163
  base 13 body copy, h3-h6, preview-card text
164
- body 13.5 the note column's prose and its mono editor
164
+ body 13.5 the note column's prose
165
165
  subhead 14 palette input, note-body h2, header icon buttons
166
166
  title 15 note-body h1
167
167
  display 20 the note's page title
@@ -244,30 +244,21 @@ body{font-size:var(--weave-px-base)}
244
244
  }
245
245
  .weave-theme:hover{color:var(--weave-fg);background:var(--weave-line)}
246
246
 
247
- /* connection indicator -------------------------------------------------- */
248
- .weave-conn{display:inline-flex;align-items:center;gap:5px;font-size:var(--weave-px-ui);white-space:nowrap}
249
- .weave-conn-dot{font-size:var(--weave-px-prov);line-height:1}
250
- /* Reconnecting breathes instead of sitting there: the one state whose whole
251
- message is "hold on, I am still working" is the one that should keep
252
- saying it. The offline dot stays still — its message needs a decision,
253
- not patience. The global reduced-motion kill switch restores the static
254
- dot, the same way it does every animation in this sheet. */
255
- .weave-conn-ok .weave-conn-dot,.weave-conn-ok{color:var(--weave-ok)}
256
- .weave-conn-warn .weave-conn-dot,.weave-conn-warn{color:var(--weave-warn)}
257
- .weave-conn-warn .weave-conn-dot{animation:weave-conn-pulse 1.6s ease-in-out infinite}
258
- .weave-conn-bad .weave-conn-dot,.weave-conn-bad{color:var(--weave-bad)}
259
- @keyframes weave-conn-pulse{0%,100%{opacity:1}50%{opacity:.35}}
260
-
261
247
  /* the grid -------------------------------------------------------------- */
262
- /* Widths arrive as custom properties from cssvars.ts — the CSSOM path. The
263
- fallbacks keep the layout sane for the first frame and if a write is ever
264
- missed. */
265
- .weave-grid{display:grid;min-height:0;overflow:hidden;background:var(--weave-line)}
266
- .weave-grid[data-columns="3"]{
267
- grid-template-columns:var(--weave-col-tree,22%) 1px var(--weave-col-note,46%) 1px var(--weave-col-graph,32%);
268
- }
269
- .weave-grid[data-columns="2"]{grid-template-columns:var(--weave-col-tree,32%) 1px var(--weave-col-note,68%)}
270
- .weave-grid[data-columns="1"]{grid-template-columns:1fr}
248
+ /* One boring grid. CSS owns the breakpoints; no saved widths, divider state
249
+ or viewport listener can make the shell disagree with the browser. */
250
+ .weave-grid{
251
+ display:grid;min-height:0;overflow:hidden;background:var(--weave-line);
252
+ grid-template-columns:minmax(180px,22fr) minmax(320px,46fr) minmax(260px,32fr);
253
+ }
254
+ @media (max-width:1099px){
255
+ .weave-grid{grid-template-columns:minmax(180px,32fr) minmax(320px,68fr)}
256
+ .weave-col-graph{display:none}
257
+ }
258
+ @media (max-width:799px){
259
+ .weave-grid{grid-template-columns:1fr}
260
+ .weave-col-tree{display:none}
261
+ }
271
262
 
272
263
  .weave-col{
273
264
  display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;
@@ -286,18 +277,8 @@ body{font-size:var(--weave-px-base)}
286
277
  inside the same region instead of squeezing the graph. */
287
278
  .weave-col-graph{display:grid;grid-template-rows:auto minmax(0,1fr) minmax(96px,40%);background:var(--weave-bg)}
288
279
 
289
- /* dividers -------------------------------------------------------------- */
290
- .weave-divider{
291
- background:var(--weave-line);cursor:col-resize;position:relative;
292
- touch-action:none;
293
- }
294
- /* A 1 px target is unhittable, so the hit area is widened with a pseudo
295
- element rather than by making the visible rule thicker. */
296
- .weave-divider::after{content:"";position:absolute;inset:0 -3px;z-index:1}
297
- .weave-divider:hover,.weave-divider:focus-visible{background:var(--weave-accent);outline:none}
298
-
299
280
  /* tree column ----------------------------------------------------------- */
300
- /* --weave-depth is written per row by \`depthVar\`, through the same CSSOM
281
+ /* --weave-depth is written per row by \`depthVar\`, through CSSOM
301
282
  path as the column widths, and multiplied by a step this sheet owns — so
302
283
  the tree's density stays a CSS decision. */
303
284
  .weave-tree{display:flex;flex-direction:column;min-height:0;flex:1}
@@ -316,8 +297,6 @@ body{font-size:var(--weave-px-base)}
316
297
  border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
317
298
  }
318
299
  .weave-chip:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
319
- .weave-chip-bad{color:var(--weave-bad);border-color:var(--weave-bad)}
320
- .weave-chip-bad:hover{color:var(--weave-fg);background:var(--weave-bad);border-color:var(--weave-bad)}
321
300
  .weave-rows{
322
301
  flex:1;min-height:0;overflow:auto;margin:0;padding:3px 0;list-style:none;
323
302
  }
@@ -329,45 +308,22 @@ body{font-size:var(--weave-px-base)}
329
308
  cursor:default;white-space:nowrap;
330
309
  }
331
310
  .weave-row:hover{background:var(--weave-line)}
332
- .weave-row.weave-row-droptarget{
333
- background:var(--weave-new);outline:1px dashed var(--weave-accent);outline-offset:-1px;
334
- }
335
- .weave-tree-new-folder{
336
- display:flex;align-items:center;gap:6px;padding:4px var(--weave-gutter);
337
- border-bottom:1px solid var(--weave-line);background:var(--weave-panel);
338
- }
339
- .weave-row-del{
340
- display:none;margin-left:auto;padding:0 4px;font:inherit;font-size:var(--weave-px-row);
341
- line-height:1;color:var(--weave-faint);background:none;border:0;cursor:pointer;
342
- border-radius:var(--weave-radius);
343
- }
344
- .weave-row:hover .weave-row-del{display:inline-flex;align-items:center}
345
- .weave-row-del:hover{color:var(--weave-bad);background:var(--weave-panel)}
346
311
  /* A hover changes two things, in this order: the ground appears under the
347
312
  row, then the label steps up to fg. The second half is what stops the
348
313
  hover from reading as a stray grey rectangle — the row answers the pointer
349
314
  in both channels at once. */
350
315
  .weave-row:hover .weave-label{color:var(--weave-fg)}
351
- /* The session fold (tree.model.ts's isMuted): machine-written memory that
352
- accrues by the dozens with near-duplicate titles, dropped one notch so the
353
- rows a human wrote keep the foreground. --weave-dim holds ≥ 4.5 on both
354
- grounds, so this is quiet, not illegible — and the two states below always
355
- restore full weight, which is why the model never emits both classes on
356
- one row. */
357
- .weave-row-muted .weave-label{color:var(--weave-dim)}
358
- .weave-row-muted:hover .weave-label{color:var(--weave-fg)}
359
316
  /* The selection has one voice. On the tinted ground every quieter token still
360
317
  fails contrast — even --weave-dim lands under 4.5 — so a selected row's
361
318
  kind, provenance and meta children join its label in --weave-fg. The
362
319
  provenance glyph shape carries the distinction the colour swap drops;
363
320
  elsewhere the hues are unaffected. The palette's hit rows need the same
364
321
  remap: same selected ground, same failure. The label is restated last
365
- because .weave-row-muted outranks the inheritance the row-on rule relies
366
- on. */
322
+ so the selected row is always readable. */
367
323
  .weave-row-on .weave-twisty,.weave-row-on .weave-kind,.weave-row-on .weave-prov,
368
324
  .weave-row-on .weave-meta,.weave-row-on .weave-label{color:var(--weave-fg)}
369
325
  .weave-hit-on .weave-hit-badge,.weave-hit-on .weave-hit-detail{color:var(--weave-fg)}
370
- /* A newly-arrived node (a file or note added since the last update, §6):
326
+ /* A newly-arrived node (a file or note added since the last update, ):
371
327
  one short highlight that fades while the label settles from bold back to
372
328
  normal. The class is computed from the frame diff in workspace.ts and
373
329
  expires with it, so collapsing and re-expanding later does not replay the
@@ -394,7 +350,7 @@ body{font-size:var(--weave-px-base)}
394
350
 
395
351
  /* icons ------------------------------------------------------------------
396
352
  The sprite's only CSS: one colour (the glyph inherits the row's, so a
397
- muted row's icon recedes with it and a selected row's brightens with it)
353
+ quiet row's icon recedes with it and a selected row's brightens with it)
398
354
  and the twisty's rotation, which is the *same* 16px chevron the rail's
399
355
  group headings use — right-pointing closed, rotated 90° open. The
400
356
  rotation is not in the motion block's transition list, on purpose: that
@@ -410,8 +366,8 @@ body{font-size:var(--weave-px-base)}
410
366
  so it lies on --weave-page — one step off the desk and the canvas grounds.
411
367
  The 2px rule flush to its left edge is the spine: it takes the note's
412
368
  provenance colour (see the --weave-spine map), so the document's origin is
413
- readable peripherally, before any glyph is. Head, body and editor share the
414
- --weave-note-gutter so toggling the editor does not reflow the measure. */
369
+ readable peripherally, before any glyph is. Head and body share the
370
+ --weave-note-gutter so the page has one consistent measure. */
415
371
  .weave-note{display:flex;flex-direction:column;min-height:0;flex:1;overflow:auto;background:var(--weave-page)}
416
372
  .weave-note-human{--weave-spine:var(--weave-ok)}
417
373
  .weave-note-agent{--weave-spine:var(--weave-accent)}
@@ -423,9 +379,7 @@ body{font-size:var(--weave-px-base)}
423
379
  alternative, a title that scrolls away, is how a reader ends up annotating
424
380
  the wrong file. It carries the page ground rather than a fill or a shadow,
425
381
  so the reveal reads as the page continuing under it and not as a bar
426
- arriving; the hairline is the only seam. The editor bar is the *next*
427
- sibling in the column flow, so it can never slide underneath — no stacking
428
- contest exists between them. The head's z-index is only what keeps it above
382
+ arriving; the hairline is the only seam. The head's z-index is only what keeps it above
429
383
  the prose (and the wikilinks inside it) as that prose scrolls beneath. */
430
384
  .weave-note-head{position:sticky;top:0;z-index:2;padding:14px var(--weave-note-gutter) 9px;background:var(--weave-page);border-bottom:1px solid var(--weave-line)}
431
385
  /* The page's largest voice, and the point of the review's headline finding:
@@ -439,11 +393,7 @@ body{font-size:var(--weave-px-base)}
439
393
  so the eye can read the whole of it without ever reading it. */
440
394
  .weave-note-meta{margin:0;display:flex;align-items:center;gap:8px;font-size:var(--weave-px-ui);color:var(--weave-dim);flex-wrap:wrap}
441
395
  .weave-note-time{color:var(--weave-faint)}
442
- /* \`Open in $EDITOR\`, demoted (P6.3). It used to be the bar's full-width
443
- bordered button — the note's loudest control, sitting between its title and
444
- its prose. Now it is an icon at the end of the meta line: the hint rides
445
- the \`title\`/\`aria-label\` (\`editor.model.ts\`'s OPEN_HINT/OPEN_LABEL), and
446
- the control is quiet until the pointer asks for it. */
396
+ /* \`Open in $EDITOR\` is a quiet icon at the end of the meta line. */
447
397
  .weave-note-open{
448
398
  display:inline-flex;align-items:center;justify-content:center;
449
399
  width:22px;height:22px;padding:0;font:inherit;color:var(--weave-faint);
@@ -460,9 +410,7 @@ body{font-size:var(--weave-px-base)}
460
410
  withdrawn at the user's call: this is a workspace, and the note shares the
461
411
  width the desk gives it. It reads at --weave-px-body on 1.7 rather than the
462
412
  chrome's 1.6: the extra leading is what a page of running text needs that a
463
- status bar does not. Code and the raw editor stay mono deliberately —
464
- reading prose and reading raw are different postures, and the face switch
465
- is the toggle. */
413
+ status bar does not. Code stays mono deliberately. */
466
414
  .weave-note-body{padding:12px var(--weave-note-gutter) 28px;font-size:var(--weave-px-body);line-height:1.7;color:var(--weave-fg)}
467
415
  .weave-note-body>*:first-child{margin-top:0}
468
416
  .weave-note-body h1,.weave-note-body h2,.weave-note-body h3,
@@ -550,53 +498,6 @@ body{font-size:var(--weave-px-base)}
550
498
  underlines a hover should not land with a thud. */
551
499
  @keyframes weave-preview-in{from{opacity:0;transform:translateY(2px)}}
552
500
 
553
- /* note editor (§11 P5) ---------------------------------------------------- */
554
- /* A \`<textarea>\`, not CodeMirror: §0 V10 measured CM6 at 118 KB gzip, more
555
- than the entire rest of the client. It inherits the body's type so toggling
556
- \`⌘E\` does not reflow the column into a different shape. */
557
- .weave-note-bar{
558
- display:flex;align-items:center;gap:6px;flex-wrap:wrap;
559
- padding:5px var(--weave-gutter);border-bottom:1px solid var(--weave-line);
560
- }
561
- /* \`weave-note-open\` is styled in the note column above (P6.3 moved it into
562
- the head's meta row as an icon), and no longer shares the bar's bordered
563
- button voice. */
564
- .weave-note-toggle,.weave-note-save,.weave-note-action{
565
- font:inherit;font-size:var(--weave-px-ui);padding:2px 8px;cursor:pointer;
566
- color:var(--weave-fg);background:var(--weave-raise);
567
- border:1px solid var(--weave-line);border-radius:var(--weave-radius);
568
- }
569
- .weave-note-toggle:hover,.weave-note-save:hover,.weave-note-action:hover{
570
- border-color:var(--weave-accent);
571
- }
572
- .weave-note-toggle[aria-pressed="true"]{border-color:var(--weave-accent);color:var(--weave-accent)}
573
- .weave-note-save:disabled{opacity:.45;cursor:default;border-color:var(--weave-line)}
574
- .weave-note-dirty{color:var(--weave-accent);font-size:var(--weave-px-subhead);line-height:1}
575
- .weave-note-status{font-size:var(--weave-px-ui);color:var(--weave-dim)}
576
- .weave-note-status-ok{color:var(--weave-ok)}
577
- .weave-note-status-warn{color:var(--weave-warn)}
578
- /* The prompt takes the full row rather than sitting inline: reload-or-overwrite
579
- is a decision about the user's unsaved text, and a decision squeezed between
580
- two buttons reads as a hint. */
581
- .weave-note-prompt{
582
- flex-basis:100%;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
583
- margin-top:4px;padding:5px 8px;border-radius:var(--weave-radius);
584
- background:var(--weave-raise);border:1px solid var(--weave-warn);
585
- }
586
- .weave-note-prompt-conflict,.weave-note-prompt-collision{border-color:var(--weave-warn)}
587
- .weave-note-prompt-external,.weave-note-prompt-discard{border-color:var(--weave-line)}
588
- .weave-note-prompt-text{margin:0;font-size:var(--weave-px-ui);color:var(--weave-fg)}
589
- .weave-note-prompt-actions{margin:0;display:flex;gap:6px;flex-wrap:wrap}
590
- .weave-note-editor{
591
- flex:1;min-height:240px;resize:none;
592
- padding:10px var(--weave-note-gutter) 24px;
593
- /* Exactly the prose body's size and leading. The invariant is documented
594
- and deliberate: ⌘E must not reflow, and reading raw at the same measure
595
- the prose uses is what makes the toggle a lens rather than a jump. */
596
- font:inherit;font-family:var(--weave-mono);font-size:var(--weave-px-body);line-height:1.7;
597
- color:var(--weave-fg);background:var(--weave-page);border:0;outline-offset:-2px;
598
- }
599
-
600
501
  /* graph column ---------------------------------------------------------- */
601
502
  /* The canvas is a plain block sigma appends its own <canvas> layers into. It
602
503
  must have a definite size before sigma measures it, hence \`min-height\` and
@@ -707,14 +608,14 @@ body{font-size:var(--weave-px-base)}
707
608
  /* status bar ------------------------------------------------------------ */
708
609
  /* One separator vocabulary, shared with the header summary: every run of
709
610
  status items after the first opens with the same mid-dot, in the same
710
- --weave-faint. cwd · selection | stamp · connection reads as one grammar,
611
+ --weave-faint. cwd · selection · stamp reads as one grammar,
711
612
  the same way the header's three-part summary does. */
712
613
  .weave-status{
713
614
  display:flex;align-items:center;gap:8px;height:22px;padding:0 var(--weave-gutter);
714
615
  font-family:var(--weave-mono);font-size:var(--weave-px-ui);color:var(--weave-dim);
715
616
  border-top:1px solid var(--weave-line);background:var(--weave-panel);
716
617
  }
717
- .weave-status-sel::before,.weave-conn::before{content:"·";margin-right:8px;color:var(--weave-faint)}
618
+ .weave-status-sel::before{content:"·";margin-right:8px;color:var(--weave-faint)}
718
619
  .weave-status-cwd,.weave-status-sel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
719
620
  .weave-status-cwd{max-width:38%;color:var(--weave-faint)}
720
621
  .weave-status-sel{color:var(--weave-fg)}
@@ -737,31 +638,11 @@ body{font-size:var(--weave-px-base)}
737
638
  @media (prefers-color-scheme: light){
738
639
  :root:not([data-weave-theme="dark"]) .weave-scrim{background:rgba(76,79,105,.30)}
739
640
  }
740
- .weave-palette,.weave-help,.weave-dialog{
641
+ .weave-palette,.weave-help{
741
642
  display:flex;flex-direction:column;width:100%;max-width:560px;max-height:72vh;
742
643
  overflow:hidden;background:var(--weave-panel);color:var(--weave-fg);
743
644
  border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);
744
645
  }
745
- .weave-dialog{max-width:380px;padding:16px;overflow:visible}
746
- .weave-dialog-title{margin:0 0 8px;font-size:var(--weave-px-subhead);font-weight:600;color:var(--weave-fg)}
747
- .weave-dialog-body{margin:0 0 16px;font-size:var(--weave-px-row);line-height:1.5;color:var(--weave-dim)}
748
- .weave-dialog-input{width:100%;margin-bottom:12px;box-sizing:border-box}
749
- .weave-dialog-actions{display:flex;justify-content:flex-end;gap:8px}
750
- .weave-menu-backdrop{position:fixed;inset:0;z-index:9}
751
- .weave-menu{
752
- position:fixed;left:var(--weave-menu-x,0);top:var(--weave-menu-y,0);z-index:10;
753
- min-width:160px;padding:4px 0;background:var(--weave-panel);
754
- border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius-pop);
755
- }
756
- .weave-menu-item{
757
- display:flex;align-items:center;gap:8px;width:100%;height:26px;padding:0 10px;
758
- font:inherit;font-size:var(--weave-px-row);text-align:left;color:var(--weave-fg);
759
- background:none;border:0;cursor:pointer;white-space:nowrap;
760
- }
761
- .weave-menu-item:hover{background:var(--weave-line)}
762
- .weave-menu-item-bad{color:var(--weave-bad)}
763
- .weave-menu-item-bad:hover{color:var(--weave-bad);background:var(--weave-line)}
764
- .weave-menu-sep{height:1px;margin:4px 0;background:var(--weave-line);border:0}
765
646
  .weave-palette-input{
766
647
  flex:none;height:34px;padding:0 11px;font:inherit;font-size:var(--weave-px-subhead);
767
648
  color:var(--weave-fg);background:transparent;border:0;
@@ -803,7 +684,7 @@ body{font-size:var(--weave-px-base)}
803
684
  --weave-new tint is exactly an accent at a strength text stays legible in. */
804
685
  ::selection{background:var(--weave-new)}
805
686
  /* Every scroller in the workspace (rows, note body, rail, palette results,
806
- help sheet, editor) gets the same thin chrome: the default scrollbar is a
687
+ help sheet) gets the same thin chrome: the default scrollbar is a
807
688
  15 px system object the hairline aesthetic cannot afford, and Firefox and
808
689
  the Blink/WebKit pair cover the whole surface with these five rules. */
809
690
  *{scrollbar-width:thin;scrollbar-color:var(--weave-line-strong) transparent}
@@ -823,18 +704,13 @@ body{font-size:var(--weave-px-base)}
823
704
  properties only — colour, border, opacity, transform; height and padding
824
705
  snap. */
825
706
  .weave-row,.weave-ctx-link,.weave-chip,.weave-search,.weave-refresh,.weave-theme,
826
- .weave-divider,.weave-hit,.weave-note-toggle,.weave-note-save,.weave-note-open,
827
- .weave-note-action{
707
+ .weave-hit,.weave-note-open{
828
708
  transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;
829
709
  }
830
710
  .weave-scrim{animation:weave-fade-in 140ms ease-out both}
831
711
  .weave-palette,.weave-help{animation:weave-overlay-in 160ms ease-out both}
832
- /* The conflict prompt mounts once per conflict, so its entrance cannot strobe
833
- the way a context-rail flash would — that rail deliberately stays still. */
834
- .weave-note-prompt{animation:weave-prompt-in 180ms ease-out both}
835
712
  @keyframes weave-fade-in{from{opacity:0}}
836
713
  @keyframes weave-overlay-in{from{opacity:0;transform:translateY(4px)}}
837
- @keyframes weave-prompt-in{from{opacity:0;transform:translateY(-2px)}}
838
714
  @media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}
839
715
  `;
840
716
 
@@ -1,100 +1,20 @@
1
- /**
2
- * The context bus (weave-workspace §1.3).
3
- *
4
- * No event bus, no pub/sub — five Preact signals. Selecting in the tree,
5
- * clicking a graph node, following a wikilink and hitting a search result all
6
- * write {@link selectedId}; the note column, the graph highlight and the
7
- * context rail are computed from it.
8
- *
9
- * ## Tier rules (§2)
10
- *
11
- * This is `src/web/client/**`: it may import `src/web/shared` and browser
12
- * deps, and must **never** import `src/core` or `node:*`. Core is
13
- * Node-flavoured TypeScript that would drag `node:fs` into the bundle.
14
- *
15
- * ## What the P0 placeholders became
16
- *
17
- * This file used to declare three local interfaces behind `TODO(P1)` markers,
18
- * standing in for contracts that had not been written yet. Two of them are
19
- * now real and imported from `src/web/shared/wire.ts` — {@link graph} carries
20
- * a `GraphPayload` and {@link noteBody} a `ViewNote`, the exact values
21
- * `src/web/client/api.ts` returns. That removes a translation step that
22
- * existed only to satisfy a stale type: the fetchers already produce these
23
- * shapes, so anything narrower here was a downcast waiting to lose a field.
24
- *
25
- * The third resolved the other way. The TODO said to take `TreeState` from
26
- * the wire too, and that was simply wrong: which rows a user has expanded is
27
- * **not** a wire contract. It never crosses the network, the server has no
28
- * opinion about it, and putting it in `wire.ts` would have made the
29
- * client/server contract carry a purely local UI preference. So
30
- * {@link TreeState} stays here, declared where it is owned, with the marker
31
- * removed rather than carried forward.
32
- */
33
-
34
- import { signal } from "@preact/signals";
35
1
  import type { GraphPayload, NotePayload } from "../shared/wire";
36
2
 
37
- /** Connection state shown in the status bar. */
38
- export type ConnectionState = "live" | "reconnecting" | "offline";
39
-
40
3
  /**
41
4
  * Which tree rows are open.
42
5
  *
43
6
  * Client-owned, deliberately: see the module header. An interface rather than
44
7
  * a bare array so that P2's filter text and provenance-cycling state have an
45
- * obvious home that does not change this signal's type.
8
+ * obvious home that does not change this state shape.
46
9
  */
47
- export interface TreeState {
48
- /** Ids of expanded tree rows. */
49
- readonly expanded: readonly string[];
10
+ export interface WorkspaceState {
11
+ graph: GraphPayload | null;
12
+ graphFailed: boolean;
13
+ note: NotePayload | null;
14
+ recentIds: ReadonlySet<string>;
15
+ selectedId: string | null;
50
16
  }
51
17
 
52
- /** The empty tree: nothing expanded. */
53
- export function initialTreeState(): TreeState {
54
- return { expanded: [] };
18
+ export function initialWorkspaceState(): WorkspaceState {
19
+ return { graph: null, graphFailed: false, note: null, recentIds: new Set(), selectedId: null };
55
20
  }
56
-
57
- /** The graph node id currently in context — everywhere. */
58
- export const selectedId = signal<string | null>(null);
59
-
60
- /** The whole graph, as delivered by the server. `null` until first load. */
61
- export const graph = signal<GraphPayload | null>(null);
62
-
63
- /**
64
- * Whether the *first* graph fetch failed.
65
- *
66
- * Distinct from {@link connection} on purpose: the socket can be live while a
67
- * fetch 500s, and "offline" is then the wrong sentence. With a graph already
68
- * on screen a failed refetch leaves the stale value standing (see
69
- * `workspace.ts`'s header — a stale graph beats a blank one), so this only
70
- * ever describes the boot, which is the one moment there is nothing to show.
71
- */
72
- export const graphFailed = signal(false);
73
-
74
- /**
75
- * Derived: fetched when {@link selectedId} names a note.
76
- *
77
- * A {@link NotePayload} rather than a bare `ViewNote` as of P5. The revision
78
- * travels **with** the body because the editor saves against it, and a
79
- * revision fetched separately would describe a state the draft was not typed
80
- * against — the exact window a conflict check exists to close. The note
81
- * column reads `.note` and is otherwise unchanged.
82
- */
83
- export const noteBody = signal<NotePayload | null>(null);
84
-
85
- /** Which tree rows are open. */
86
- export const treeState = signal<TreeState>(initialTreeState());
87
-
88
- /** SSE liveness (§6). */
89
- export const connection = signal<ConnectionState>("live");
90
-
91
- /**
92
- * Node ids that appeared in the most recent graph update (weave-workspace
93
- * §6 frame → tree feedback).
94
- *
95
- * Empty on the mount fetch — a first load must not animate the whole tree.
96
- * The controller replaces the set on every published payload and clears it
97
- * after {@link ../workspace!RECENT_TTL_MS}, so a row collapsed and re-expanded
98
- * minutes later does not replay its arrival animation.
99
- */
100
- export const recentIds = signal<ReadonlySet<string>>(new Set());