pi-weave 0.3.0 → 0.3.2

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 (50) hide show
  1. package/README.md +40 -16
  2. package/package.json +2 -2
  3. package/src/core/frontmatter.ts +3 -3
  4. package/src/core/graph/model.ts +2 -2
  5. package/src/core/graph/wikilinks.ts +1 -1
  6. package/src/core/types.ts +1 -1
  7. package/src/pi/viewer/tui/surface/detail.ts +1 -1
  8. package/src/web/client/dist/app.js +239 -243
  9. package/src/web/client/graph/ForceTuner.tsx +14 -98
  10. package/src/web/client/graph/Graph.tsx +29 -74
  11. package/src/web/client/graph/column.model.ts +22 -15
  12. package/src/web/client/graph/graph.model.ts +31 -63
  13. package/src/web/client/graph/groups.ts +9 -7
  14. package/src/web/client/graph/positions.ts +3 -3
  15. package/src/web/client/graph/renderer.dom.ts +2 -2
  16. package/src/web/client/graph/renderer.ts +14 -5
  17. package/src/web/client/graph/tuner.model.ts +6 -180
  18. package/src/web/client/main.tsx +1 -4
  19. package/src/web/client/note/Note.tsx +77 -92
  20. package/src/web/client/note/drafts.ts +172 -0
  21. package/src/web/client/note/note.model.ts +2 -34
  22. package/src/web/client/search/SearchPalette.tsx +9 -8
  23. package/src/web/client/shell/ContextRail.tsx +8 -9
  24. package/src/web/client/shell/Settings.tsx +71 -0
  25. package/src/web/client/shell/Shell.tsx +409 -255
  26. package/src/web/client/shell/StatusBar.tsx +2 -6
  27. package/src/web/client/shell/focus.model.ts +2 -2
  28. package/src/web/client/shell/keys.model.ts +40 -39
  29. package/src/web/client/shell/keys.ts +1 -0
  30. package/src/web/client/shell/shell.model.ts +32 -128
  31. package/src/web/client/shell/theme.model.ts +27 -97
  32. package/src/web/client/shell/theme.ts +32 -238
  33. package/src/web/client/shell/workspace.css.ts +140 -0
  34. package/src/web/client/tree/Tree.tsx +43 -14
  35. package/src/web/client/tree/tree.model.ts +19 -0
  36. package/src/web/client/workspace.ts +19 -1
  37. package/src/web/server/backup.ts +59 -0
  38. package/src/web/server/routes.ts +45 -3
  39. package/src/web/server/server.ts +5 -0
  40. package/src/web/server/workspace-state.ts +121 -0
  41. package/src/web/shared/layout.ts +5 -6
  42. package/src/web/shared/preferences.ts +76 -0
  43. package/src/web/shared/themes.ts +92 -0
  44. package/src/web/shared/workspace.ts +310 -0
  45. package/src/web/client/selection.storage.ts +0 -69
  46. package/src/web/client/shell/Columns.tsx +0 -139
  47. package/src/web/client/shell/Divider.tsx +0 -15
  48. package/src/web/client/shell/Header.tsx +0 -119
  49. package/src/web/client/shell/drag.model.ts +0 -35
  50. package/src/web/client/shell/layout.model.ts +0 -132
@@ -1,3 +1,7 @@
1
+ import { THEMES, ACCENTS } from "../../shared/themes";
2
+ import type { PaletteChoice } from "../../shared/themes";
3
+ import { WORKSPACE_CSS } from "./workspace.css";
4
+
1
5
  /**
2
6
  * The workspace stylesheet, and how it gets past the CSP
3
7
  * (weave-workspace §1.2, §5.2).
@@ -43,96 +47,19 @@
43
47
  * real `document` satisfies it structurally.
44
48
  */
45
49
 
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
- * And since `shell/theme.model.ts`, the palette a user looks at is a
58
- * *choice*: `:root` carries dark, the light tokens sit in
59
- * {@link LIGHT_TOKENS} and are applied by a `data-weave-theme` attribute
60
- * (a manual selection) and by the media query narrowed with
61
- * `:root:not([data-weave-theme="dark"])` (the system default). Clearing the
62
- * attribute returns the workspace to following the OS with no client-side
63
- * listener involved. The graph follows the same resolution through
64
- * `theme.model.ts`'s `effectiveScheme`, because its WebGL palette cannot
65
- * read any of this.
66
- *
67
- * ## "Dense but calm" (§1.2)
68
- *
69
- * The sketch is an information-dense IDE surface, not a marketing page, so
70
- * the rules below deliberately avoid the defaults that produce the opposite:
71
- * no card shadows, no 24 px gutters, no rounded panels floating on a
72
- * contrasting background. Density comes from a 13 px base, 4–10 px padding
73
- * and 1 px hairline rules; calm comes from a **single** accent colour used
74
- * only for focus and selection, three greys doing all the structural work,
75
- * and no borders where a background change already separates two regions.
76
- *
77
- * ## Where the hexes come from
78
- *
79
- * Both schemes are Catppuccin (the user's call): dark is **Macchiato**, light
80
- * is **Latte**, mapped onto the desk-and-page split with one structural rule
81
- * shared by both — the note page is the `crust` ground, rails and bars are
82
- * `mantle`, the desk and graph canvas are `base`, raised controls and
83
- * hairlines are `surface0` with `surface1` for the strong hairline. Light's
84
- * topology is therefore an exact mirror of dark's (a darker sheet on a lighter
85
- * desk), so a token means the same thing in either scheme:
86
- *
87
- * - **Dark, Macchiato** — every value a stock Catppuccin label: `base #24273A`
88
- * desk, `mantle #1E2030` panel, `crust #181926` page, `surface0 #363A4F`
89
- * raise/hairline, `surface1 #494D64` strong hairline; text `#CAD3F5`,
90
- * dims `#A5ADCB`/`#939AB7`, and **Mauve `#C6A0F6` as the single accent
91
- * voice** (7.5:1 on panel) with Green/Yellow/Red as status.
92
- *
93
- * - **Light, Latte** — grounds are stock labels again (`base #EFF1F5` desk,
94
- * `mantle #E6E9EF` panel, `crust #DCE0E8` page, `surface0 #CCD0DA`,
95
- * `surface1 #BCC0CC`), and text/status hue-preservingly *deepened* to hold
96
- * 4.5:1 on the darkest grounds Latte owns: `dim #56586A` (from Subtext0,
97
- * same hue 233°), `faint #606274` (Overlay2), `accent #7113EC` (Mauve),
98
- * `ok #28641B`, `warn #7C4F10`, `bad #B20D30`. Text `#4C4F69` is stock
99
- * Text. The hue-preserving deepening keeps the palette recognisably
100
- * Catppuccin where blending toward neutral would grey it away.
101
- *
102
- * Measured ratios (WCAG, on the darkest grounds — `raise`/`surface0` for
103
- * light text tokens, `panel`/`mantle` for dark): light fg 4.5, dim 4.5,
104
- * accent 4.5, ok 4.6, warn 4.6, bad 4.5, each also ≥ 4.5 on the brighter
105
- * grounds; dark faint 4.6 on `surface0` and 5.8 on panel, dim 5.0 on
106
- * `surface0`, fg 7.6. The one below-4.5 site is `faint` *on* `raise`
107
- * (light 3.9) — every use is non-text there (scrollbar thumb, ghost
108
- * borders, icon glyphs), where WCAG asks 3:1.
109
- *
110
- * Status colours keep each scheme's temperature at ≥ 4.5:1 text contrast —
111
- * measured, not eyeballed (dark `#a6da95`/`#1e2030` 10.0, `#eed49f` 11.2,
112
- * `#ed8796` 6.5; light values are the deepened derivatives above).
113
- *
114
- * The graph cannot read these variables (WebGL — `graph.model.ts`), so
115
- * `GRAPH_PALETTE` mirrors the eight palette slots as literals; the test that makes
116
- * that copy safe asserts every mirrored hex appears in this string.
117
- */
118
- /**
119
- * The light-block tokens, interpolated into both selectors that need them —
120
- * see the {@link THEME_CSS} branch rules.
121
- */
122
- const LIGHT_TOKENS = `
123
- --weave-bg:#eff1f5;--weave-panel:#e6e9ef;--weave-raise:#ccd0da;--weave-fg:#4c4f69;--weave-dim:#56586a;
124
- --weave-faint:#606274;--weave-line:#ccd0da;--weave-line-strong:#bcc0cc;
125
- --weave-accent:#7113ec;--weave-ok:#28641b;--weave-warn:#7c4f10;--weave-bad:#b20d30;
126
- --weave-new:rgba(113,19,236,.10);
127
- --weave-page:#dce0e8;
128
- `;
50
+ /** Palette declarations are shared with the WebGL graph. */
51
+ function paletteTokens(id: PaletteChoice): string {
52
+ const { scheme, colors } = THEMES[id];
53
+ return Object.entries(colors).map(([key, value]) => `--weave-${key}:${value};`).join("")
54
+ + `--weave-new:color-mix(in srgb,var(--weave-accent) ${scheme === "light" ? 10 : 16}%,transparent);`
55
+ + `color-scheme:${scheme};`
56
+ + `--weave-vignette:radial-gradient(ellipse at center,transparent 55%,${scheme === "light" ? "rgba(0,0,0,.10)" : "rgba(0,0,0,.22)"} 100%);`
57
+ + `--weave-scrim:rgba(0,0,0,${scheme === "light" ? ".30" : ".45"});`;
58
+ }
129
59
 
130
60
  export const THEME_CSS = `
131
61
  :root{
132
- --weave-bg:#24273a;--weave-panel:#1e2030;--weave-raise:#363a4f;--weave-fg:#cad3f5;--weave-dim:#a5adcb;
133
- --weave-faint:#939ab7;--weave-line:#363a4f;--weave-line-strong:#494d64;
134
- --weave-accent:#c6a0f6;--weave-ok:#a6da95;--weave-warn:#eed49f;--weave-bad:#ed8796;
135
- --weave-new:rgba(198,160,246,.16);
62
+ ${paletteTokens("dark")}
136
63
  --weave-row:26px;--weave-gutter:10px;
137
64
  /* The reading gutter is note-only: prose wants a wider margin than chrome.
138
65
  Rails, rows and bars keep --weave-gutter, so density is a property of the
@@ -146,7 +73,7 @@ export const THEME_CSS = `
146
73
  third face was tried (serif prose) and withdrawn at the user's call; two
147
74
  voices read calmer than three. The CSP allows nothing fetched, and both
148
75
  are system stacks. */
149
- --weave-page:#181926;
76
+
150
77
  /* Two radii, per the plan: hairline-sharp for controls (a 4 px corner is
151
78
  the difference between a control and a card), one softer corner for the
152
79
  two overlays that float above the grid. Tag pills are 999px — a capsule
@@ -162,7 +89,7 @@ export const THEME_CSS = `
162
89
  row 12 list rows, text inputs, inline code
163
90
  base 13 body copy, h3-h6, preview-card text
164
91
  body 13.5 the note column's prose
165
- subhead 14 palette input, note-body h2, header icon buttons
92
+ subhead 14 palette input, note-body h2, ribbon icon buttons
166
93
  title 15 note-body h1
167
94
  display 20 the note's page title
168
95
  Two earlier sizes were merged into neighbours: 9px glyphs to prov, and
@@ -181,101 +108,15 @@ export const THEME_CSS = `
181
108
  --weave-px-row:12px;--weave-px-base:13px;--weave-px-body:13.5px;
182
109
  --weave-px-subhead:14px;--weave-px-title:15px;--weave-px-display:20px;
183
110
  }
184
- /* The light tokens, shared verbatim by the attribute branch and the media
185
- query below — one const interpolated twice is the only way CSS gets
186
- "these are the same colours" without a preprocessor. Which branch wins is
187
- theme.model.ts's business: the attribute carries a *manual* choice, the
188
- media query the system default, and the :not() is what lets the two coexist
189
- without a manual "dark" being dragged back into light by the OS. */
190
111
  @media (prefers-color-scheme: light){
191
- :root:not([data-weave-theme="dark"]){
192
- ${LIGHT_TOKENS}
193
- }
194
- }
195
- :root[data-weave-theme="light"]{
196
- ${LIGHT_TOKENS}
112
+ :root:not([data-weave-theme]){${paletteTokens("light")}}
197
113
  }
114
+ ${(Object.keys(THEMES) as PaletteChoice[]).map((id) => `:root[data-weave-theme="${id}"]{${paletteTokens(id)}}`).join("\n")}
115
+ ${Object.entries(ACCENTS).map(([id, colors]) => ["light", "dark"].map((scheme) =>
116
+ `:root[data-weave-scheme="${scheme}"][data-weave-accent="${id}"]{--weave-accent:${colors[scheme as "light" | "dark"]};}`
117
+ ).join("\n")).join("\n")}
198
118
  body{font-size:var(--weave-px-base)}
199
- #app{height:100%;display:grid;grid-template-rows:auto 1fr auto;background:var(--weave-bg)}
200
-
201
- /* header --------------------------------------------------------------- */
202
- .weave-header{
203
- display:flex;align-items:center;gap:14px;padding:0 var(--weave-gutter);
204
- height:34px;border-bottom:1px solid var(--weave-line);background:var(--weave-panel);
205
- }
206
- .weave-brand{display:inline-flex;align-items:center;gap:6px;font-size:var(--weave-px-subhead);font-weight:600;letter-spacing:.01em;white-space:nowrap}
207
- .weave-brand-mark{height:20px;width:20px;border-radius:var(--weave-radius)}
208
- /* A button styled as a search field: the palette owns the only text input,
209
- so this opens it rather than pretending to accept a query. */
210
- .weave-search{
211
- display:flex;align-items:center;gap:6px;flex:0 1 260px;min-width:0;
212
- height:22px;padding:0 7px;font:inherit;font-size:var(--weave-px-row);text-align:left;cursor:pointer;
213
- color:var(--weave-dim);background:var(--weave-bg);
214
- border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);
215
- }
216
- .weave-search:hover{color:var(--weave-fg);border-color:var(--weave-accent)}
217
- .weave-search-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
218
- .weave-search kbd{
219
- font-family:var(--weave-mono);font-size:var(--weave-px-caption);color:var(--weave-faint);
220
- padding:1px 5px;border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);white-space:nowrap;
221
- }
222
- .weave-summary{
223
- margin-left:auto;display:flex;gap:9px;font-family:var(--weave-mono);
224
- font-size:var(--weave-px-ui);color:var(--weave-dim);white-space:nowrap;
225
- }
226
- .weave-summary-part+.weave-summary-part::before{content:"·";margin-right:9px;color:var(--weave-faint)}
227
- .weave-refresh{
228
- font:inherit;font-size:var(--weave-px-subhead);line-height:1;color:var(--weave-dim);background:none;
229
- border:0;padding:3px 5px;border-radius:var(--weave-radius);cursor:pointer;
230
- display:inline-flex;align-items:center;
231
- }
232
- .weave-refresh:hover{color:var(--weave-fg);background:var(--weave-line)}
233
- /* One full 600 ms turn per click ("the request left"), not a loop: the
234
- refetch is fire-and-forget and a 304 may never fire a completion signal
235
- for a spinner to wait on. Reduced-motion restores the static stroke. */
236
- .weave-refresh-spinning{animation:weave-refresh-turn 600ms ease-out}
237
- @keyframes weave-refresh-turn{from{transform:rotate(0)}to{transform:rotate(360deg)}}
238
- /* The theme cycle (shell/theme.model.ts). Same shape as the refresh button:
239
- an icon-size control in a 34 px bar, glyph-only because the filled/half/
240
- hollow family already means something in this workspace. */
241
- .weave-theme{
242
- font:inherit;font-size:var(--weave-px-base);line-height:1;color:var(--weave-dim);background:none;
243
- border:0;padding:3px 6px;border-radius:var(--weave-radius);cursor:pointer;
244
- }
245
- .weave-theme:hover{color:var(--weave-fg);background:var(--weave-line)}
246
-
247
- /* the grid -------------------------------------------------------------- */
248
- /* Widths arrive as custom properties from layout.model.ts. Shell/layout.model
249
- selects the breakpoint; CSS consumes the resulting data-columns shape. */
250
- .weave-grid{display:grid;min-height:0;overflow:hidden;background:var(--weave-line)}
251
- .weave-grid[data-columns="3"]{grid-template-columns:var(--weave-col-tree,22%) 1px var(--weave-col-note,46%) 1px var(--weave-col-graph,32%)}
252
- .weave-grid[data-columns="2"]{grid-template-columns:var(--weave-col-tree,32%) 1px var(--weave-col-note,68%)}
253
- .weave-grid[data-columns="1"]{grid-template-columns:1fr}
254
-
255
- /* dividers -------------------------------------------------------------- */
256
- .weave-divider{
257
- width:1px;cursor:col-resize;touch-action:none;background:var(--weave-line);position:relative;z-index:2;
258
- }
259
- .weave-divider::after{content:"";position:absolute;inset:0 -4px}
260
- .weave-divider:hover{background:var(--weave-faint)}
261
- .weave-divider:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-1px}
262
-
263
- .weave-col{
264
- display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;
265
- background:var(--weave-bg);
266
- }
267
- .weave-col-title{
268
- margin:0;padding:5px var(--weave-gutter) 4px;font-size:var(--weave-px-caption);font-weight:600;
269
- letter-spacing:.09em;text-transform:uppercase;color:var(--weave-faint);
270
- border-bottom:1px solid var(--weave-line);
271
- }
272
- /* Title, then the graph (which takes the slack), then the context rail. The
273
- 1fr is what gives .weave-graph-canvas a definite height to measure, and the
274
- rail's row is a fixed fraction of the column — not content-sized — so
275
- moving the selection between a sparsely-connected note and a richly
276
- connected one never repartitions the right-hand column: the rail scrolls
277
- inside the same region instead of squeezing the graph. */
278
- .weave-col-graph{display:grid;grid-template-rows:auto minmax(0,1fr) minmax(96px,40%);background:var(--weave-bg)}
119
+ #app{height:100%;display:grid;grid-template-rows:minmax(0,1fr) auto;background:var(--weave-bg)}
279
120
 
280
121
  /* tree column ----------------------------------------------------------- */
281
122
  /* --weave-depth is written per row by \`depthVar\`, through CSSOM
@@ -487,8 +328,8 @@ body{font-size:var(--weave-px-base)}
487
328
  text-decoration-thickness:1px;text-underline-offset:2px;
488
329
  }
489
330
  .weave-note-body a:hover{text-decoration-color:var(--weave-accent)}
490
- /* A ghost is a name with no note behind it: Obsidian's dashed, dimmed
491
- affordance to create one. */
331
+ /* A ghost is a name with no note behind it: a dashed, dimmed affordance to
332
+ create one. */
492
333
  .weave-wiki{cursor:pointer;color:var(--weave-accent);text-decoration:none}
493
334
  .weave-wiki:hover{text-decoration:underline}
494
335
  .weave-wiki-ghost{
@@ -550,15 +391,7 @@ body{font-size:var(--weave-px-base)}
550
391
  faster than a dark one under a black veil. */
551
392
  .weave-graph-canvas::after{
552
393
  content:"";position:absolute;inset:0;pointer-events:none;
553
- background:radial-gradient(ellipse at center,transparent 55%,rgba(0,0,0,.22) 100%);
554
- }
555
- :root[data-weave-theme="light"] .weave-graph-canvas::after{
556
- background:radial-gradient(ellipse at center,transparent 55%,rgba(76,79,105,.10) 100%);
557
- }
558
- @media (prefers-color-scheme: light){
559
- :root:not([data-weave-theme="dark"]) .weave-graph-canvas::after{
560
- background:radial-gradient(ellipse at center,transparent 55%,rgba(76,79,105,.10) 100%);
561
- }
394
+ background:var(--weave-vignette);
562
395
  }
563
396
  .weave-graph-empty{
564
397
  margin:0;padding:14px var(--weave-gutter);color:var(--weave-dim);
@@ -584,32 +417,6 @@ body{font-size:var(--weave-px-base)}
584
417
  border-top:1px solid var(--weave-line);
585
418
  }
586
419
 
587
- /* the hidden force tuner (?sliders=1, docs/weave-workspace.md 15.7) --------- */
588
- /* Absolutely positioned over the canvas rather than given a grid row: the
589
- panel is a debug overlay and must not change the layout the graph is being
590
- judged in — moving the canvas while tuning it would falsify the very thing
591
- being looked at. */
592
- .weave-tuner{
593
- position:absolute;top:8px;right:8px;z-index:3;width:232px;max-height:calc(100% - 16px);
594
- overflow:auto;display:flex;flex-direction:column;gap:6px;padding:9px 10px;
595
- font-size:var(--weave-px-caption);color:var(--weave-fg);
596
- background:var(--weave-panel);border:1px solid var(--weave-line-strong);
597
- border-radius:var(--weave-radius);
598
- }
599
- .weave-tuner-title{margin:0;color:var(--weave-faint);letter-spacing:.04em;text-transform:uppercase}
600
- .weave-tuner-row{display:flex;flex-direction:column;gap:2px;cursor:pointer}
601
- .weave-tuner-label{display:flex;justify-content:space-between;gap:8px;color:var(--weave-dim)}
602
- .weave-tuner-label b{color:var(--weave-accent);font-weight:600;font-variant-numeric:tabular-nums}
603
- .weave-tuner-row input{width:100%;margin:0;accent-color:var(--weave-accent)}
604
- .weave-tuner-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--weave-dim)}
605
- .weave-tuner-toggle input{margin:0;accent-color:var(--weave-accent)}
606
- .weave-tuner-actions{display:flex;gap:6px;margin-top:2px;flex-wrap:wrap}
607
- .weave-tuner-snippet{
608
- margin:0;padding:6px;overflow:auto;white-space:pre;color:var(--weave-faint);
609
- background:var(--weave-bg);border:1px solid var(--weave-line);border-radius:var(--weave-radius);
610
- font-size:var(--weave-px-prov);line-height:1.45;
611
- }
612
-
613
420
  /* context rail ---------------------------------------------------------- */
614
421
  /* The \`.weave-empty\` block that used to sit above went with \`EmptyState.tsx\`
615
422
  in P3: every column now has its own empty state (\`treeEmptyMessage\`,
@@ -633,9 +440,8 @@ body{font-size:var(--weave-px-base)}
633
440
  text-align:left;background:none;border:0;cursor:pointer;
634
441
  }
635
442
  .weave-ctx-heading:hover{color:var(--weave-fg)}
636
- /* The count is a mono voice, like every other number in the chrome: the
637
- header summary and the status bar count in mono, so a rail that says
638
- "BACKLINKS 4" in the text face would be the one thing off-key. */
443
+ /* The count is a mono voice like the other status numbers, so a rail that
444
+ says "BACKLINKS 4" in the text face would be the one thing off-key. */
639
445
  .weave-ctx-count{
640
446
  margin-left:auto;font-family:var(--weave-mono);font-size:var(--weave-px-caption);
641
447
  letter-spacing:0;color:var(--weave-faint);font-weight:400;
@@ -662,11 +468,8 @@ body{font-size:var(--weave-px-base)}
662
468
  .weave-ctx-tag{margin:0 0 3px}
663
469
  .weave-ctx-tag .weave-ctx-rows{padding-left:12px}
664
470
 
665
- /* status bar ------------------------------------------------------------ */
666
- /* One separator vocabulary, shared with the header summary: every run of
667
- status items after the first opens with the same mid-dot, in the same
668
- --weave-faint. cwd · selection · stamp reads as one grammar,
669
- the same way the header's three-part summary does. */
471
+ /* workspace footer ------------------------------------------------------ */
472
+ /* The status items share one mid-dot separator in --weave-faint. */
670
473
  .weave-status{
671
474
  display:flex;align-items:center;gap:8px;height:22px;padding:0 var(--weave-gutter);
672
475
  font-family:var(--weave-mono);font-size:var(--weave-px-ui);color:var(--weave-dim);
@@ -676,7 +479,6 @@ body{font-size:var(--weave-px-base)}
676
479
  .weave-status-cwd,.weave-status-sel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
677
480
  .weave-status-cwd{max-width:38%;color:var(--weave-faint)}
678
481
  .weave-status-sel{color:var(--weave-fg)}
679
- .weave-status-stamp{margin-left:auto;color:var(--weave-faint)}
680
482
 
681
483
  /* overlays: the ⌘K palette and the ? help sheet (P4) --------------------- */
682
484
  /* The scrim is a click target that closes, and the reason both overlays sit
@@ -685,15 +487,7 @@ body{font-size:var(--weave-px-base)}
685
487
  the wrong breakpoint. */
686
488
  .weave-scrim{
687
489
  position:fixed;inset:0;z-index:10;display:flex;justify-content:center;
688
- align-items:flex-start;padding:9vh 16px 16px;background:rgba(0,0,0,.45);
689
- }
690
- /* The scrim above is tuned for the dark ground, where a neutral black veil
691
- reads as depth. On the linen ground the same veil reads as a power cut, so
692
- the light scheme gets its own — a plum-tinted veil at reduced strength,
693
- drawn from the foreground ramp rather than from grey. */
694
- :root[data-weave-theme="light"] .weave-scrim{background:rgba(76,79,105,.30)}
695
- @media (prefers-color-scheme: light){
696
- :root:not([data-weave-theme="dark"]) .weave-scrim{background:rgba(76,79,105,.30)}
490
+ align-items:flex-start;padding:9vh 16px 16px;background:var(--weave-scrim);
697
491
  }
698
492
  .weave-palette,.weave-help{
699
493
  display:flex;flex-direction:column;width:100%;max-width:560px;max-height:72vh;
@@ -760,7 +554,7 @@ body{font-size:var(--weave-px-base)}
760
554
  would need delay-unmount plumbing far past polish. Compositor-safe
761
555
  properties only — colour, border, opacity, transform; height and padding
762
556
  snap. */
763
- .weave-row,.weave-ctx-link,.weave-chip,.weave-search,.weave-refresh,.weave-theme,
557
+ .weave-row,.weave-ctx-link,.weave-chip,.weave-ribbon button,
764
558
  .weave-hit,.weave-note-open{
765
559
  transition:background-color 120ms ease,border-color 120ms ease,color 120ms ease;
766
560
  }
@@ -826,7 +620,7 @@ export function findNonce(host: ThemeHost): string | null {
826
620
  * report in the console is a far better diagnostic than a stylesheet that was
827
621
  * never created.
828
622
  */
829
- export function installTheme(host: ThemeHost, css: string = THEME_CSS): boolean {
623
+ export function installTheme(host: ThemeHost, css: string = THEME_CSS + WORKSPACE_CSS): boolean {
830
624
  const element = host.createElement("style");
831
625
  const nonce = findNonce(host);
832
626
  if (nonce !== null) element.nonce = nonce;
@@ -0,0 +1,140 @@
1
+ /** Tabbed workspace furniture; the graph keeps its existing palette and renderer. */
2
+ export const WORKSPACE_CSS = `
3
+ .weave-workbench{position:relative;display:flex;min-height:0;overflow:hidden;background:var(--weave-page)}
4
+ .weave-workbench button{font:inherit;color:inherit;cursor:pointer}
5
+ .weave-workbench button:focus-visible,.weave-workbench summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-2px}
6
+ .weave-workbench button:disabled{opacity:.35;cursor:default}
7
+ .weave-ribbon{width:44px;flex:none;display:flex;align-items:center;flex-direction:column;gap:8px;padding:12px 0;background:var(--weave-panel);border-right:1px solid var(--weave-line)}
8
+ .weave-ribbon button{width:32px;height:32px;border:0;border-radius:var(--weave-radius);background:transparent;font-size:var(--weave-px-display);color:var(--weave-dim)}
9
+ .weave-ribbon button:hover,.weave-ribbon button[aria-pressed=true]{color:var(--weave-accent);background:var(--weave-new)}
10
+ .weave-ribbon-space{flex:1}
11
+ .weave-sidebar{flex:none;min-width:0;display:flex;flex-direction:column;background:var(--weave-panel);overflow:hidden}
12
+ .weave-sidebar-dock{display:flex;flex:none;min-width:0;width:calc(var(--weave-sidebar-width) + 4px);overflow:hidden;opacity:1;visibility:visible;transition:width 200ms ease,opacity 160ms ease,visibility 0s}
13
+ .weave-sidebar-dock[data-open=false]{width:0;opacity:0;visibility:hidden;transition:width 200ms ease,opacity 160ms ease,visibility 0s 200ms}
14
+ .weave-sidebar-dock>.weave-sidebar{width:var(--weave-sidebar-width);transition:transform 200ms ease}
15
+ .weave-sidebar-dock[data-open=false]>.weave-sidebar{transform:translateX(-16px)}
16
+ .weave-context-dock[data-open=false]>.weave-sidebar{transform:translateX(16px)}
17
+ .weave-sidebar-heading{display:flex;align-items:center;gap:8px;height:46px;flex:none;padding:0 12px;color:var(--weave-dim);font-size:var(--weave-px-row)}
18
+ .weave-sidebar-heading button{border:0;background:none;padding:7px 6px;border-radius:var(--weave-radius);color:var(--weave-dim)}
19
+ .weave-sidebar-heading button[aria-pressed=true]{color:var(--weave-fg);background:var(--weave-raise)}
20
+ .weave-sidebar-heading button:last-child{margin-left:auto}
21
+ .weave-sidebar .weave-tree-controls{padding:4px 12px 10px;gap:5px;flex-wrap:wrap;border:0}
22
+ .weave-sidebar .weave-filter{height:29px;flex-basis:100%;background:var(--weave-bg);border-radius:var(--weave-radius)}
23
+ .weave-sidebar .weave-row{min-height:32px;border-radius:var(--weave-radius);font-size:var(--weave-px-base)}
24
+ .weave-sidebar [role=tree]{padding:0 8px}
25
+ .weave-sidebar .weave-meta{font-size:var(--weave-px-caption);opacity:.65}
26
+ .weave-sidebar .weave-tree-count{border:0;padding:8px 14px}
27
+ .weave-vault-label{margin-top:auto;padding:14px 16px;border-top:1px solid var(--weave-line);display:flex;flex-direction:column;gap:4px;overflow:hidden}
28
+ .weave-vault-label span{font-size:var(--weave-px-caption);color:var(--weave-faint);text-transform:uppercase;letter-spacing:.08em}
29
+ .weave-vault-label strong{font-size:var(--weave-px-row);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
30
+ .weave-recents{display:flex;flex-direction:column;gap:3px;padding:6px 10px;overflow:auto}
31
+ .weave-recents button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:0;padding:9px;border-radius:var(--weave-radius);overflow:hidden}
32
+ .weave-recents button:hover{background:var(--weave-new)}
33
+ .weave-recents button[aria-current=page]{background:var(--weave-raise);color:var(--weave-fg)}
34
+ .weave-recents .weave-recent-label{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
35
+ .weave-workspace-divider{flex:none;width:4px;background:var(--weave-panel);cursor:col-resize;touch-action:none;z-index:3}
36
+ .weave-workspace-divider:hover,.weave-workspace-divider:focus-visible{background:var(--weave-accent)}
37
+ .weave-workspace-divider[aria-orientation=horizontal]{width:auto;height:4px;cursor:row-resize}
38
+ .weave-panes{display:grid;flex:1;min-width:0;min-height:0;overflow:hidden}
39
+ .weave-pane{min-width:0;min-height:0;display:grid;grid-template-rows:42px 36px minmax(0,1fr);overflow:hidden;background:var(--weave-page)}
40
+ .weave-pane[hidden]{display:none}
41
+ .weave-pane-drop{position:relative}
42
+ .weave-pane-drop::after{content:attr(data-drop-label);position:absolute;inset:4px;z-index:20;display:grid;place-items:center;pointer-events:none;border:2px dashed var(--weave-accent);border-radius:var(--weave-radius);color:var(--weave-accent);background:var(--weave-new)}
43
+ .weave-tabs{display:flex;align-items:stretch;min-width:0;overflow-x:auto;background:var(--weave-panel);gap:3px;padding:5px 6px 0;border-bottom:1px solid var(--weave-line);scrollbar-width:thin}
44
+ .weave-tab{position:relative;display:flex;align-items:center;min-width:90px;max-width:210px;border-radius:var(--weave-radius) var(--weave-radius) 0 0;color:var(--weave-dim);flex-shrink:0;border-bottom:2px solid transparent}
45
+ .weave-tab-drop-before::before,.weave-tab-drop-after::after{content:"";position:absolute;top:2px;bottom:2px;width:3px;background:var(--weave-accent);pointer-events:none}
46
+ .weave-tab-drop-before::before{left:-3px}.weave-tab-drop-after::after{right:-3px}
47
+ .weave-tab-active{background:var(--weave-page);color:var(--weave-fg)}
48
+ .weave-pane-active .weave-tab-active{border-bottom-color:var(--weave-accent)}
49
+ .weave-tab button{border:0;background:none}
50
+ .weave-tab [role=tab]{display:flex;align-items:center;gap:8px;text-align:left;padding:8px 9px;min-width:0;height:100%;font-size:var(--weave-px-row)}
51
+ .weave-tab [role=tab]>span:nth-child(2){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
52
+ .weave-tab [role=tab]>span:first-child{color:var(--weave-faint)}
53
+ .weave-tab .weave-tab-close{margin-right:4px;padding:2px 5px;font-size:var(--weave-px-subhead);color:var(--weave-faint)}
54
+ .weave-tab-close:hover{color:var(--weave-fg);background:var(--weave-raise)}
55
+ .weave-dirty{font-size:var(--weave-px-prov);color:var(--weave-accent)}
56
+ .weave-new-tab{border:0;background:none;padding:4px 10px;font-size:var(--weave-px-display)!important;color:var(--weave-dim)!important}
57
+ .weave-pane-toolbar{display:flex;align-items:center;gap:5px;padding:0 10px;border-bottom:1px solid var(--weave-line);color:var(--weave-dim);font-size:var(--weave-px-row)}
58
+ .weave-pane-toolbar>button{width:26px;height:26px;border:0;background:none;border-radius:var(--weave-radius)}
59
+ .weave-pane-toolbar>button:hover{background:var(--weave-new)}
60
+ .weave-breadcrumb{flex:1;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 8px;font-size:var(--weave-px-ui)}
61
+ .weave-pane-menu{position:relative}
62
+ .weave-pane-menu>summary{list-style:none;cursor:pointer;padding:5px 10px;font-size:var(--weave-px-subhead)}
63
+ .weave-pane-menu>summary::-webkit-details-marker{display:none}
64
+ .weave-pane-menu>div{position:absolute;right:0;top:100%;z-index:12;width:185px;padding:5px;display:flex;flex-direction:column;background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius)}
65
+ .weave-pane-menu button{text-align:left;border:0;background:none;padding:9px 10px;font-size:var(--weave-px-row)}
66
+ .weave-pane-menu button:hover{background:var(--weave-new)}
67
+ .weave-pane-content{min-width:0;min-height:0;display:flex;overflow:hidden;position:relative}
68
+ .weave-document{display:flex;min-width:0;min-height:0;flex:1}
69
+ .weave-document .weave-note{width:100%;border-left:0}
70
+ .weave-document .weave-note-head{position:static;padding:28px max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 18px;border:0}
71
+ .weave-document .weave-note-title{font-size:var(--weave-px-display);letter-spacing:-.03em;margin-bottom:12px}
72
+ .weave-document .weave-note-body{padding:10px max(24px,calc((100% - var(--weave-reading-width,760px))/2)) 64px;font-size:var(--weave-note-size,var(--weave-px-subhead));line-height:1.8}
73
+ .weave-document .weave-note-body h1{font-size:var(--weave-px-display);line-height:1.4;margin-bottom:18px}
74
+ .weave-document .weave-note-body h2{font-size:var(--weave-px-title);margin-top:28px}
75
+ .weave-document .weave-note-editor{padding:14px max(24px,calc((100% - var(--weave-reading-width,760px))/2));min-height:220px;flex:1;font-size:var(--weave-note-size,var(--weave-px-subhead))}
76
+ .weave-document .weave-note-tags{margin-top:12px}
77
+ .weave-document .weave-note-edit,.weave-document .weave-note-save{padding:5px 10px;border-radius:var(--weave-radius)}
78
+ .weave-welcome{align-self:center;margin:auto;padding:35px;max-width:560px;color:var(--weave-dim);line-height:1.7}
79
+ .weave-welcome-mark{font-size:var(--weave-px-display);color:var(--weave-accent)}
80
+ .weave-welcome h1{font-size:var(--weave-px-display);line-height:1.4;letter-spacing:-.025em;color:var(--weave-fg);font-weight:550}
81
+ .weave-welcome p{font-size:var(--weave-px-base);margin:14px 0 24px}
82
+ .weave-welcome>div{display:flex;gap:12px;flex-wrap:wrap}
83
+ .weave-welcome button{border:1px solid var(--weave-line-strong);background:var(--weave-panel);padding:9px 13px;border-radius:var(--weave-radius);font-size:var(--weave-px-row)}
84
+ .weave-welcome button:hover{border-color:var(--weave-accent)}
85
+ .weave-welcome kbd{color:var(--weave-faint);padding-left:16px}
86
+ .weave-sidebar-context .weave-rail{flex:1;border:0;overflow:auto;padding:6px 12px}
87
+ .weave-sidebar-context .weave-ctx-link{padding-top:7px;padding-bottom:7px}
88
+ .weave-graph-host{position:absolute;z-index:2;display:flex;background:var(--weave-bg)}
89
+ .weave-graph-host>.weave-graph{width:100%;height:100%;flex:1}
90
+ .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}
91
+ .weave-graph-preview-info{display:flex;align-items:center;gap:8px;min-width:0}
92
+ .weave-graph-preview-copy{display:flex;flex-direction:column;min-width:0;gap:2px}
93
+ .weave-graph-preview-kind{font-size:var(--weave-px-caption);color:var(--weave-faint);text-transform:capitalize}
94
+ .weave-graph-preview-title{font-size:var(--weave-px-row);font-weight:550;line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere}
95
+ .weave-graph-preview-close{pointer-events:auto;position:absolute;right:5px;top:5px;width:24px;height:24px;border:0;border-radius:var(--weave-radius);background:transparent;font-size:18px;color:var(--weave-dim)}
96
+ .weave-graph-preview-close:hover{background:var(--weave-new);color:var(--weave-fg)}
97
+ .weave-graph-preview>.weave-chip{pointer-events:auto;align-self:flex-start;max-width:100%;white-space:normal;text-align:left}
98
+ .weave-footer{display:flex;align-items:center;min-width:0;background:var(--weave-panel);border-top:1px solid var(--weave-line);font-size:var(--weave-px-ui);color:var(--weave-faint)}
99
+ .weave-footer>.weave-status{flex:1;min-width:0;border:0}
100
+ .weave-footer>span{padding:4px 10px;white-space:nowrap}
101
+ .weave-footer>button{font:inherit;color:var(--weave-fg);background:none;border:0;padding:7px 12px;cursor:pointer}
102
+ @media(max-width:1049px){
103
+ .weave-context-dock{position:absolute;right:0;top:0;bottom:0;z-index:10;width:var(--weave-sidebar-width)}
104
+ .weave-sidebar-context{border-left:1px solid var(--weave-line-strong)}
105
+ .weave-context-dock>.weave-workspace-divider{display:none}
106
+ }
107
+ @media(max-width:849px){
108
+ .weave-notes-dock{position:absolute;left:44px;top:0;bottom:0;z-index:10;width:var(--weave-sidebar-width);box-shadow:8px 0 24px #0002}
109
+ .weave-sidebar-notes{border-right:1px solid var(--weave-line-strong)}
110
+ .weave-notes-dock>.weave-workspace-divider{display:none}
111
+ .weave-workbench>.weave-workspace-divider{display:none}
112
+ .weave-panes{grid-template-columns:minmax(0,1fr)!important;grid-template-rows:minmax(0,1fr)!important}
113
+ .weave-document .weave-note-head{padding:20px}
114
+ .weave-document .weave-note-body{padding:10px 20px 40px}
115
+ }
116
+ @media(prefers-reduced-motion:reduce){
117
+ .weave-sidebar-dock,.weave-sidebar-dock[data-open=false],.weave-sidebar-dock>.weave-sidebar{transition:none}
118
+ }
119
+
120
+ .weave-settings-scrim{align-items:center;padding:24px 16px}
121
+ .weave-settings button{font:inherit;color:inherit;cursor:pointer;border:0;background:transparent}
122
+ .weave-settings a{color:var(--weave-accent)}
123
+ .weave-settings{display:grid;grid-template-columns:180px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);width:min(900px,calc(100vw - 32px));height:min(680px,calc(100dvh - 48px));background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:12px;box-shadow:0 24px 80px var(--weave-scrim);overflow:hidden}
124
+ .weave-settings header{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center;padding:16px 24px;border-bottom:1px solid var(--weave-line)}
125
+ .weave-settings h2,.weave-settings h3{margin:0}.weave-settings h3{font-size:20px;margin-bottom:24px}
126
+ .weave-settings header button{font-size:24px}.weave-settings nav{padding:16px 12px;background:var(--weave-bg);overflow:auto;border-right:1px solid var(--weave-line)}
127
+ .weave-settings nav button{display:block;width:100%;text-align:left;padding:10px 12px;border-radius:6px;color:var(--weave-dim)}
128
+ .weave-settings nav button[aria-current]{color:var(--weave-accent);background:var(--weave-new)}
129
+ .weave-settings section{padding:28px;overflow:auto;min-width:0}.weave-settings p{line-height:1.6;color:var(--weave-dim)}
130
+ .weave-setting-row{display:flex;justify-content:space-between;align-items:center;gap:24px;padding:16px 0;border-bottom:1px solid var(--weave-line)}
131
+ .weave-setting-row>span:first-child{min-width:0;flex:1}.weave-setting-row strong{display:block;font-weight:500}.weave-setting-row small{display:block;color:var(--weave-dim);margin-top:6px;line-height:1.5}
132
+ .weave-settings select{background:var(--weave-raise);border:1px solid var(--weave-line-strong);border-radius:6px;padding:7px;color:var(--weave-fg);max-width:230px;font:inherit}
133
+ .weave-settings input{accent-color:var(--weave-accent)}.weave-settings input[type=checkbox]{width:18px;height:18px;flex-shrink:0}
134
+ .weave-setting-range{display:flex;align-items:center;gap:12px;width:190px}.weave-setting-range input{width:130px;min-width:0}.weave-setting-range output{min-width:42px;text-align:right;color:var(--weave-accent);font-variant-numeric:tabular-nums}
135
+ .weave-settings button:focus-visible,.weave-settings a:focus-visible,.weave-settings summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:2px}
136
+ .weave-settings-actions{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin:24px 0}.weave-settings-actions button,.weave-settings-download,.weave-settings-forces>button{display:inline-block;padding:9px 14px;border:1px solid var(--weave-line-strong);border-radius:6px;color:var(--weave-accent);background:var(--weave-raise);text-decoration:none}
137
+ .weave-settings-forces>button{margin-top:20px}.weave-settings-forces>button:disabled{opacity:.5}.weave-settings-locations dd{margin:6px 0 18px;overflow-wrap:anywhere;color:var(--weave-dim);font-family:var(--weave-font-mono)}
138
+ .weave-settings details summary{cursor:pointer;padding:12px 0}.weave-settings .weave-key-row{gap:16px;align-items:baseline}.weave-settings .weave-key-row kbd{min-width:150px;color:var(--weave-accent)}
139
+ @media(max-width:650px){.weave-settings{grid-template-columns:1fr;grid-template-rows:auto auto minmax(0,1fr)}.weave-settings nav{border-right:0;border-bottom:1px solid var(--weave-line);padding:8px 16px}.weave-settings nav select{width:100%;max-width:none}.weave-settings section{padding:20px}.weave-setting-row{align-items:flex-start;gap:12px}.weave-settings select{max-width:170px}.weave-setting-range{width:140px}.weave-setting-range input{width:90px}}
140
+ `;