nebulosity 1.0.0 → 1.1.0

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 (180) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +11 -11
  3. package/.next/diagnostics/route-bundle-stats.json +10 -10
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +5 -5
  6. package/.next/server/app/_global-error/page/build-manifest.json +3 -3
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page/build-manifest.json +3 -3
  14. package/.next/server/app/_not-found/page/next-font-manifest.json +2 -3
  15. package/.next/server/app/_not-found/page.js +3 -2
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +27 -20
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +27 -20
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +34 -26
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/index.html +1 -1
  23. package/.next/server/app/index.rsc +29 -23
  24. package/.next/server/app/index.segments/__PAGE__.segment.rsc +33 -24
  25. package/.next/server/app/index.segments/_full.segment.rsc +29 -23
  26. package/.next/server/app/index.segments/_tree.segment.rsc +4 -5
  27. package/.next/server/app/page/build-manifest.json +3 -3
  28. package/.next/server/app/page/next-font-manifest.json +2 -3
  29. package/.next/server/app/page.js +4 -4
  30. package/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/.next/server/chunks/[root-of-the-server]__06blyow8irfw3._.js +3 -3
  32. package/.next/server/chunks/[root-of-the-server]__0bmqko-d8fd5k._.js +3 -3
  33. package/.next/server/chunks/[root-of-the-server]__0cmmlmvta4i0g._.js +3 -3
  34. package/.next/server/chunks/[root-of-the-server]__0jgr1dt1j95ku._.js +3 -3
  35. package/.next/server/chunks/[root-of-the-server]__0m0n163465-zb._.js +3 -3
  36. package/.next/server/chunks/[root-of-the-server]__0p7epsgp2b5m3._.js +3 -3
  37. package/.next/server/chunks/[root-of-the-server]__1i5e5sqg45ens._.js +3 -3
  38. package/.next/server/chunks/[root-of-the-server]__1vjt0neva-819._.js +3 -3
  39. package/.next/server/chunks/_0uqus0ehnijt-._.js +1 -1
  40. package/.next/server/chunks/lib_background_start_ts_11u_1uu8b9yuy._.js +1 -1
  41. package/.next/server/chunks/ssr/[root-of-the-server]__0_3l5tdzqy-td._.js +3 -0
  42. package/.next/server/chunks/ssr/[root-of-the-server]__10ockmlli-lun._.js +3 -0
  43. package/.next/server/chunks/ssr/{[root-of-the-server]__0dyligeswbb47._.js → [root-of-the-server]__16-gclvix9mjd._.js} +1 -1
  44. package/.next/server/chunks/ssr/[root-of-the-server]__1auodc3c0rppy._.js +3 -0
  45. package/.next/server/chunks/ssr/[root-of-the-server]__1cf6qt9agg_9-._.js +3 -3
  46. package/.next/server/chunks/ssr/[root-of-the-server]__1i9r59yk4i69p._.js +3 -0
  47. package/.next/server/chunks/ssr/[root-of-the-server]__1l3qp8u9yc_ig._.js +3 -0
  48. package/.next/server/chunks/ssr/_1p-0wsx9qfm68._.js +13 -0
  49. package/.next/server/chunks/ssr/app_map_18iu6j19jsqmu._.js +3 -0
  50. package/.next/server/chunks/ssr/node_modules_next_dist_1r3iqtbm45lqp._.js +3 -0
  51. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js +3 -0
  52. package/.next/server/middleware-build-manifest.js +11 -11
  53. package/.next/server/next-font-manifest.js +1 -1
  54. package/.next/server/next-font-manifest.json +4 -6
  55. package/.next/server/pages/404.html +1 -1
  56. package/.next/server/pages/500.html +1 -1
  57. package/.next/server/preview-props.json +3 -3
  58. package/.next/server/server-reference-manifest.js +1 -1
  59. package/.next/server/server-reference-manifest.json +1 -1
  60. package/.next/static/chunks/{1rljem7o1jtdo.js → 18kfexnrqek07.js} +1 -1
  61. package/.next/static/chunks/1hwbgd4nvvhg8.js +1 -0
  62. package/.next/static/chunks/25r4lkkf1shfo.css +3 -0
  63. package/.next/static/chunks/35ubvk2ivdjgs.js +1 -0
  64. package/.next/static/chunks/3ieqvmcg3hofj.js +31 -0
  65. package/.next/static/chunks/3w00e1w2125pp.js +11 -0
  66. package/.next/static/media/733da6bd8dac84c8-s.3hlisc-pwq13z.woff2 +0 -0
  67. package/.next/static/media/784318cea1dfcfa4-s.0-oo9_1x_xmvg.woff2 +0 -0
  68. package/.next/static/media/7e114457660b62ed-s.36fqqi66-3tjb.woff2 +0 -0
  69. package/.next/static/media/85e86a88f68a624a-s.357dd_6abeqge.woff2 +0 -0
  70. package/.next/static/media/9a321a2c78eff0b5-s.p.1bkzbscqrt8rl.woff2 +0 -0
  71. package/.next/static/media/9fecbd3e5ea9bccb-s.3_z45zcoc-xoz.woff2 +0 -0
  72. package/.next/static/media/c64053b510252f55-s.1kd-r_s9smv5m.woff2 +0 -0
  73. package/.next/static/media/c6a5c2fdc12da45c-s.p.17-aodiw50953.woff2 +0 -0
  74. package/.next/static/media/f263782c6a4bddee-s.1gyfqm5yhhzs7.woff2 +0 -0
  75. package/.next/trace +1 -1
  76. package/.next/trace-build +1 -1
  77. package/README.md +13 -4
  78. package/app/globals.css +91 -54
  79. package/app/layout.tsx +20 -11
  80. package/app/map/BackButton.tsx +5 -5
  81. package/app/map/BoxActions.tsx +20 -12
  82. package/app/map/Breadcrumb.tsx +132 -46
  83. package/app/map/DetailPanel.tsx +408 -297
  84. package/app/map/Explorer.tsx +268 -0
  85. package/app/map/Header.tsx +105 -0
  86. package/app/map/HelpDialog.tsx +37 -34
  87. package/app/map/Legend.tsx +19 -75
  88. package/app/map/LineCard.tsx +187 -0
  89. package/app/map/MapBox.tsx +73 -38
  90. package/app/map/MapCanvas.tsx +344 -115
  91. package/app/map/MapToolbar.tsx +377 -0
  92. package/app/map/MapView.tsx +217 -213
  93. package/app/map/RepoMenu.tsx +13 -12
  94. package/app/map/RoutedEdge.tsx +48 -16
  95. package/app/map/SearchPalette.tsx +6 -6
  96. package/app/map/SourceViewer.tsx +19 -17
  97. package/app/map/StatusBar.tsx +166 -0
  98. package/app/map/embed.ts +22 -2
  99. package/app/map/emphasis.ts +94 -8
  100. package/app/map/explorerRows.ts +85 -0
  101. package/app/map/flowNodes.ts +12 -7
  102. package/app/map/format.ts +2 -2
  103. package/app/map/history.ts +4 -3
  104. package/app/map/inspector.ts +113 -0
  105. package/app/map/labels.ts +66 -7
  106. package/app/map/narrow.ts +165 -85
  107. package/app/map/opening.ts +16 -0
  108. package/app/map/portals.ts +2 -2
  109. package/app/map/routes.ts +1060 -55
  110. package/app/map/runs.ts +108 -0
  111. package/app/map/shell.ts +91 -0
  112. package/app/map/shortcuts.ts +2 -1
  113. package/app/map/summaries.ts +41 -5
  114. package/app/map/text.ts +9 -1
  115. package/app/map/theme.ts +197 -0
  116. package/app/map/useDismiss.ts +20 -6
  117. package/app/map/useEditorBridge.ts +12 -1
  118. package/app/map/useThemeKind.ts +28 -0
  119. package/app/map/useWindowSize.ts +37 -0
  120. package/app/map/viewport.ts +134 -16
  121. package/app/map/weights.ts +80 -0
  122. package/app/page.tsx +3 -5
  123. package/lib/background/child.ts +2 -0
  124. package/lib/background/service.ts +26 -10
  125. package/lib/contracts.ts +25 -2
  126. package/lib/db/migrations/0011_plain_summaries_prompt_version.sql +1 -0
  127. package/lib/db/migrations/meta/0011_snapshot.json +1498 -0
  128. package/lib/db/migrations/meta/_journal.json +7 -0
  129. package/lib/db/schema.ts +2 -1
  130. package/lib/db/store/map.ts +6 -0
  131. package/lib/db/store/summaries.ts +14 -8
  132. package/lib/engine/describe.ts +2 -0
  133. package/lib/index/summarize/budget.ts +5 -2
  134. package/lib/index/summarize/claude.ts +19 -4
  135. package/lib/index/summarize/compare.ts +2 -2
  136. package/lib/index/summarize/plan.ts +73 -13
  137. package/lib/index/summarize/preview.ts +12 -9
  138. package/lib/index/summarize/prompts.ts +122 -25
  139. package/lib/index/summarize/run.ts +48 -18
  140. package/lib/index/summarize/snapshot.ts +7 -2
  141. package/lib/index/summarize/status.ts +3 -2
  142. package/lib/tools/text.ts +3 -2
  143. package/mcp/server.ts +1 -1
  144. package/package.json +4 -2
  145. package/scripts/commands/summarize-command.ts +27 -9
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0ekw8p2ramybv._.js +0 -3
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0fwbsqt9e2tr-._.js +0 -3
  148. package/.next/server/chunks/ssr/[root-of-the-server]__17_rqmx62cz59._.js +0 -3
  149. package/.next/server/chunks/ssr/[root-of-the-server]__1zfp6sltse4hf._.js +0 -3
  150. package/.next/server/chunks/ssr/_1d2woi6toj_0_._.js +0 -3
  151. package/.next/server/chunks/ssr/_1wfc1zz26g5ji._.js +0 -9
  152. package/.next/server/chunks/ssr/app_0gw6epmxyea3q._.js +0 -3
  153. package/.next/server/chunks/ssr/node_modules_@swc_helpers_cjs__interop_require_default_cjs_1ztp13a5szri_._.js +0 -3
  154. package/.next/server/chunks/ssr/node_modules_next_dist_1c5t_vsn6u8fr._.js +0 -3
  155. package/.next/static/chunks/02x9pxyl2d8om.css +0 -3
  156. package/.next/static/chunks/0p-0bv2dzer18.js +0 -7
  157. package/.next/static/chunks/10u7p2gj02qay.js +0 -1
  158. package/.next/static/chunks/31qp8jfkw0x2h.js +0 -31
  159. package/.next/static/media/123d516c9baeb953-s.1qc6dx89jd2qv.woff2 +0 -0
  160. package/.next/static/media/41ba59f553543ecf-s.3cb5lpr2l6xud.woff2 +0 -0
  161. package/.next/static/media/44ed10da57ebfb0e-s.2y65deuc30pmy.woff2 +0 -0
  162. package/.next/static/media/48eea41d8317494f-s.p.0wiir8udbzjvx.woff2 +0 -0
  163. package/.next/static/media/49b1d53681a91a99-s.1yd4_i3e35931.woff2 +0 -0
  164. package/.next/static/media/67bba87670b402d1-s.1u30mmyq6lnsd.woff2 +0 -0
  165. package/.next/static/media/6fca7e74603dd412-s.02z5rrbg5nt92.woff2 +0 -0
  166. package/.next/static/media/73263b2957b73100-s.2qt3tdwt-y9x_.woff2 +0 -0
  167. package/.next/static/media/7ac0b1cd3e695dd0-s.1i-sca43-qwng.woff2 +0 -0
  168. package/.next/static/media/916cd8a3bde3edf8-s.p.0oace-s_gkfks.woff2 +0 -0
  169. package/.next/static/media/a45aa1001bf9176f-s.2vemzm0itqzfy.woff2 +0 -0
  170. package/.next/static/media/a74d2542cc4c775f-s.29yke99gs9qpn.woff2 +0 -0
  171. package/.next/static/media/ab664c0e419cc841-s.3cvcemwuzoywn.woff2 +0 -0
  172. package/.next/static/media/c1b11b726b36a5e5-s.2rixgazldzomk.woff2 +0 -0
  173. package/.next/static/media/d6c1ff7e86974212-s.p.40sczeszzbjw1.woff2 +0 -0
  174. package/.next/static/media/faab88e4be272d29-s.0rgnxg2b64rzs.woff2 +0 -0
  175. package/app/map/ActivityBar.tsx +0 -81
  176. package/app/map/StatusPill.tsx +0 -93
  177. package/app/map/TopBar.tsx +0 -84
  178. /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_buildManifest.js +0 -0
  179. /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_clientMiddlewareManifest.js +0 -0
  180. /package/.next/static/{prDi039MhjlFqDOOwZ4aR → of6twfEZ3j95xtfQB_NSL}/_ssgManifest.js +0 -0
@@ -2,45 +2,47 @@
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { ReactFlowProvider } from "@xyflow/react";
5
- import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
6
5
  import type { BridgeFromMap, BridgeToMap, LevelResponse, LiveEvent } from "@/lib/contracts";
7
6
  import { api, ApiError } from "./api";
8
- import BackButton from "./BackButton";
9
7
  import Breadcrumb from "./Breadcrumb";
10
8
  import DetailPanel from "./DetailPanel";
11
9
  import HelpDialog from "./HelpDialog";
12
- import ActivityBar from "./ActivityBar";
13
10
  import { AGENT_ACTIVE_MS, agentFocus } from "./activity";
14
- import { isBusyLevel } from "./emphasis";
15
- import { formatCount } from "./format";
11
+ import { isBusyLevel, selectionLines, sidesWithoutLines, weakLinesHidden, type LineDirection } from "./emphasis";
16
12
  import MapCanvas from "./MapCanvas";
17
13
  import { followStep, historyDepth, historyStep, mapUrl, placeFromSearch, type MapPlace } from "./history";
18
- import { BANNER_TOP, bandClear, bannerForm, CARD_TOP, cardClear, PANEL_WIDTH, panelLayout, pillForm, type BannerForm, type PillForm } from "./narrow";
14
+ import { bannerForm, CARD_CLEAR, type BannerForm } from "./narrow";
19
15
  import { boxFor, parentOf } from "./paths";
20
16
  import { clearStart, markStart, measureAfterPaint } from "./perf";
21
- import { pageNote, selectionShown } from "./portals";
17
+ import { selectionShown } from "./portals";
22
18
  import { mapShortcut } from "./shortcuts";
23
19
  import { nextPulses, type Pulse } from "./pulses";
24
20
  import RepoMenu from "./RepoMenu";
25
21
  import SearchPalette from "./SearchPalette";
26
- import StatusPill from "./StatusPill";
27
- import TopBar from "./TopBar";
28
- import { EMBEDDED, useEditorBridge } from "./useEditorBridge";
22
+ import { applyTheme, themeInputOf } from "./theme";
23
+ import Explorer from "./Explorer";
24
+ import Header from "./Header";
25
+ import Legend from "./Legend";
26
+ import MapToolbar from "./MapToolbar";
27
+ import { BELOW_SHARE, EXPLORER_WIDTH, INSPECTOR_WIDTH, RAIL_WIDTH, shellLayout } from "./shell";
28
+ import StatusBar from "./StatusBar";
29
+ import { EMBEDDED, useEditorBridge, useEmbedded } from "./useEditorBridge";
29
30
  import { useIndexStatus } from "./useIndexStatus";
30
31
  import { useLive } from "./useLive";
31
32
  import { useSize } from "./useSize";
33
+ import { useWindowSize } from "./useWindowSize";
32
34
 
33
35
  /**
34
36
  * Levels fetched for one repo and index version (ADR-036), by folder and page: going back up a level is instant until
35
37
  * the index moves.
36
38
  */
37
39
  type LevelCache = { repo: string; version: number; levels: ReadonlyMap<string, LevelResponse> };
38
- /** Set once the help has opened by itself, so it does so only on a first visit. */
39
- const HELP_SEEN = "nebulosity.helpSeen";
40
40
 
41
41
  const cacheKey = (place: MapPlace): string => `${place.path}\0${place.page}`;
42
42
  /** How long a failed jump's message stays up when nothing else moves the Map. */
43
43
  const FOCUS_ERROR_MS = 6_000;
44
+ /** The inspector as a drawer over the canvas's right side: 320 px, leaving at least 56 px of canvas beside it. */
45
+ const DRAWER_WIDTH = "min(320px, calc(100% - 56px))";
44
46
 
45
47
  /**
46
48
  * A jump to a path (focus(), below), and how it was asked for. The editor's `follow` rewrites the history entry rather
@@ -96,7 +98,7 @@ function MapScreen(){
96
98
  setDetailsSelection(selected);
97
99
  setDetailsFor(null);
98
100
  }
99
- // Whether the history entry on screen has one of the Map's own steps behind it, for Back inside VS Code's panel
101
+ // Whether the history entry on screen has one of the Map's own steps behind it, for the header's Back
100
102
  // (BackButton, history.ts's historyDepth). It shows only once a level is drawn, in the browser, so reading the
101
103
  // history here can't make the server's render and the browser's differ.
102
104
  const [canGoBack, setCanGoBack] = useState(() => typeof window !== "undefined" && historyDepth(window.history.state) > 0);
@@ -118,14 +120,27 @@ function MapScreen(){
118
120
  // a busy level draws only the hovered or selected box's lines, unless the user asks for all of them; the choice
119
121
  // holds across levels until it's turned off (owner, 2026-10-12)
120
122
  const [showAllLines, setShowAllLines] = useState(false);
123
+ // Strong links only, on until it's turned off, and like Show all lines it holds across levels (ADR-070)
124
+ const [strongOnly, setStrongOnly] = useState(true);
125
+ // Which side of the selected box's lines is highlighted, the other drawn gray (emphasis.ts). It belongs to the box it
126
+ // was picked for, so it's All again whenever the selection changes: another box, none, another folder or page, or
127
+ // Back (the owner, 2026-10-29: "the default is view all"). Reset during render, as detailsFor is above.
128
+ const [direction, setDirection] = useState<LineDirection>("all");
129
+ const [directionFor, setDirectionFor] = useState(selected);
130
+ if (directionFor !== selected){
131
+ setDirectionFor(selected);
132
+ setDirection("all");
133
+ }
121
134
  const [pulses, setPulses] = useState<ReadonlyMap<string, Pulse>>(new Map());
122
135
  // the Map area's width: whether the panel covers the canvas and the page pill's wording go by it (narrow.ts); the
123
136
  // pill's width and the banner's height decide where the banner goes
124
- const [mainRef, mainSize] = useSize<HTMLElement>();
125
- const mainWidth = mainSize?.width ?? null;
126
- const [pillRef, pillSize] = useSize<HTMLDivElement>();
137
+ // the window's size, which the layout goes by (shell.ts), and the error banner's, which picks its form (narrow.ts)
138
+ const windowSize = useWindowSize();
139
+ // inside VS Code's panel, as the page draws it (hydration-safe)
140
+ const embedded = useEmbedded();
141
+ const [mainRef] = useSize<HTMLElement>();
127
142
  const [bandRef, bandSize] = useSize<HTMLDivElement>();
128
- const [cardRef, cardSize] = useSize<HTMLDivElement>();
143
+ const [cardRef] = useSize<HTMLDivElement>();
129
144
 
130
145
  // the focus effect's failure handler reads where the Map is when the answer comes back, not when it was asked, and
131
146
  // the editor's `drawn` reads it when it is posted
@@ -219,7 +234,7 @@ function MapScreen(){
219
234
  // A link that names the same place another way (?path=src%2Fvs, ?page=1) is rewritten instead, so Back needs one
220
235
  // press (history.ts). Inside VS Code's panel the address keeps `embed=vscode` (P8), and a move an editor's follow
221
236
  // made rewrites the entry instead of pushing one, so switching tabs doesn't fill Back (followStep). Each entry carries
222
- // how many of the Map's own steps lie behind it (historyDepth), for the Back button there.
237
+ // how many of the Map's own steps lie behind it (historyDepth), for the header's Back.
223
238
  useEffect(() => {
224
239
  const step = followStep(historyStep(window.location.pathname + window.location.search, place, EMBEDDED), historyMode.current);
225
240
  historyMode.current = "push";
@@ -284,13 +299,19 @@ function MapScreen(){
284
299
 
285
300
  /**
286
301
  * What the editor asks for (API_CONTRACTS §20): `follow` when its active file changed, a deliberate `show`, and the
287
- * files to light. Only the Map's own clicks open files in the editor, so nothing here echoes back (V1.8 plan P10).
302
+ * files to light, and its color theme. Only the Map's own clicks open files in the editor, so nothing here echoes back
303
+ * (V1.8 plan P10).
288
304
  */
289
305
  const onEditorMessage = useCallback((message: BridgeToMap) => {
290
306
  if (message.type === "nebulosity.follow") focus(message.path, { history: "replace", quiet: true });
291
307
  else if (message.type === "nebulosity.show") focus(message.path, { quiet: true });
292
308
  else if (message.type === "nebulosity.highlight") setEditorHighlights(message.paths);
293
309
  else if (message.type === "nebulosity.clear") setEditorHighlights([]);
310
+ else if (message.type === "nebulosity.theme"){
311
+ // the editor's own grays (ADR-070): set on the page's root, over globals.css's Dark 2026
312
+ const theme = themeInputOf(message.kind, message.colors);
313
+ if (theme !== null) applyTheme(document.documentElement, theme);
314
+ }
294
315
  }, [focus]);
295
316
 
296
317
  /**
@@ -307,19 +328,23 @@ function MapScreen(){
307
328
  // each `ready` it posts drawnNow(), for a hello that lands after the level was drawn.
308
329
  const editor = useEditorBridge(onEditorMessage, drawnNow);
309
330
 
310
- /** A box clicked on the canvas. Inside VS Code's panel a file box also opens in the editor (design §4.4). */
331
+ /**
332
+ * A box clicked on the canvas or in the explorer. It only selects, inside VS Code's panel as in a browser tab, so you
333
+ * can look around the Map while the editor keeps its file (the owner, 2026-10-29, ADR-062 amended).
334
+ */
311
335
  const select = useCallback((target: string | null) => {
312
336
  if (target !== null) markStart("map:select");
313
337
  setSelected(target);
314
- if (editor !== null && target !== null && level?.nodes.some((n) => n.path === target && n.kind === "file")){
315
- editor.post({ type: "nebulosity.open", path: target });
316
- }
317
- }, [editor, level]);
338
+ }, []);
318
339
 
319
- /** A declaration picked in the panel, inside VS Code's panel: it opens at its line in the editor (design §4.4). */
340
+ /**
341
+ * Inside VS Code's panel, the editor opens a file when it's asked to: Open in VS Code in the inspector or View source
342
+ * (at its line there), a double-click on the file's box, or the Open beside it in a narrow panel (API_CONTRACTS §20).
343
+ */
320
344
  const openInEditor = useCallback((path: string, line?: number) => {
321
345
  editor?.post({ type: "nebulosity.open", path, ...(line === undefined ? {} : { line }) });
322
346
  }, [editor]);
347
+ const onOpenFile = editor === null ? undefined : openInEditor;
323
348
 
324
349
 
325
350
  /** A tool call from Claude Code (PIVOT_SPEC.md §8): pulse the boxes it touched, and run its UI action. */
@@ -355,6 +380,14 @@ function MapScreen(){
355
380
  // tick.
356
381
  const agentKey = level === null ? "" : [...agentFocus(level, live.recent, highlights, now, clearedAt, editorHighlights)].sort().join("\0");
357
382
  const agent = useMemo(() => new Set(agentKey === "" ? [] : agentKey.split("\0")), [agentKey]);
383
+ // The sides of the selection with no line on the page can't be picked (they'd highlight nothing), and one picked
384
+ // before the index moved its lines away counts as All.
385
+ const selectedOnLevel = selectionShown(level, selected);
386
+ const emptySides = useMemo(() => (level === null ? [] : sidesWithoutLines(level.edges, selectedOnLevel)), [level, selectedOnLevel]);
387
+ const shownDirection = emptySides.includes(direction) ? "all" : direction;
388
+ // the boxes joined to the selection by a highlighted line, which the explorer lights as the canvas does
389
+ const linked = useMemo(() => new Set(level === null ? [] : selectionLines(level.edges, selectedOnLevel, shownDirection)
390
+ .flatMap((e) => [e.source, e.target])), [level, selectedOnLevel, shownDirection]);
358
391
 
359
392
  const onSwitched = useCallback(() => {
360
393
  setPlace({ path: "", page: 0 });
@@ -388,26 +421,13 @@ function MapScreen(){
388
421
  if (drawn !== null) editor.post(drawn);
389
422
  });
390
423
  }, [selected, editor, drawnNow]);
391
- /** The repo's name in the top bar goes back to the top level, like a site's name goes home (three-task test). */
424
+ /** The repo's name in the breadcrumb goes back to the top level, like a site's name goes home (three-task test). */
392
425
  const goHome = useCallback(() => open(""), [open]);
393
426
  const closeHelp = useCallback(() => setHelpOpen(false), []);
394
-
395
- // The help opens by itself the first time this browser opens the Map (owner review). A browser that keeps no
396
- // storage (a private window) just doesn't get it unasked; the ? button still works.
397
- useEffect(() => {
398
- const timer = setTimeout(() => {
399
- try{
400
- if (window.localStorage.getItem(HELP_SEEN) !== null) return;
401
- window.localStorage.setItem(HELP_SEEN, "1");
402
- setHelpOpen(true);
403
- } catch{
404
- // no storage: nothing to remember the first visit by
405
- }
406
- }, 0);
407
- return () => clearTimeout(timer);
408
- }, []);
427
+ // The help no longer opens by itself on a first visit: the legend sentence under the canvas says what the marks mean,
428
+ // and the full help stays behind ? (the Workbench redesign, ADR-070).
409
429
  const closePanel = useCallback(() => setSelected(null), []);
410
- // over the canvas, the panel's × closes the details and leaves the box selected, with its actions (BoxActions)
430
+ // over the canvas, the inspector's × closes the details and leaves the box selected, with its actions (BoxActions)
411
431
  const closeDetails = useCallback(() => setDetailsFor(null), []);
412
432
  const showDetails = useCallback((path: string) => setDetailsFor(path), []);
413
433
 
@@ -421,14 +441,15 @@ function MapScreen(){
421
441
  const hasSelection = shownSelected !== null;
422
442
  // Claude's alone: the editor's go when What depends' pick closes
423
443
  const hasHighlights = highlights.length > 0;
424
- // the panel: 400 px beside the canvas, or over all of it when too little canvas would be left (narrow.ts); over it,
425
- // only once the box's Details asks for it
426
- const panel = panelLayout(mainWidth);
427
- // Beside the canvas the panel is the selection's details, so they count as asked for: a window narrowed while they're
428
- // open keeps them, now over the canvas, rather than swapping them for the box's actions (as the reset above, during
429
- // render, and once per selection).
430
- if (!panel.covers && shownSelected !== null && detailsFor !== shownSelected) setDetailsFor(shownSelected);
431
- const detailsOverCanvas = panel.covers && shownSelected !== null && detailsFor === shownSelected;
444
+ // The layout at this window's size (shell.ts): where the explorer and the inspector go, what the bars keep, and the
445
+ // least height before the page scrolls down. Where the inspector would cover the whole canvas, a click only selects
446
+ // and the box offers Open and Details beside it (BoxActions); everywhere else the inspector shows the selection.
447
+ const shell = shellLayout({ width: windowSize.width, height: windowSize.height, embedded, selected: shownSelected !== null });
448
+ const covers = shell.inspector === "cover";
449
+ // Anywhere but over the whole canvas the inspector is the selection's details, so they count as asked for: a window
450
+ // narrowed while they're open keeps them, now over the canvas, rather than swapping them for the box's actions.
451
+ if (!covers && shownSelected !== null && detailsFor !== shownSelected) setDetailsFor(shownSelected);
452
+ const detailsOverCanvas = covers && shownSelected !== null && detailsFor === shownSelected;
432
453
  useEffect(() => {
433
454
  const onKey = (event: KeyboardEvent): void => {
434
455
  const shortcut = mapShortcut(event, {
@@ -446,7 +467,7 @@ function MapScreen(){
446
467
  event.preventDefault();
447
468
  setPaletteOpen(true);
448
469
  } else if (shortcut.type === "help") setHelpOpen(true);
449
- // over the canvas, Escape closes the details first, as the panel's × does, and the box stays selected
470
+ // over the canvas, Escape closes the details first, as the inspector's × does, and the box stays selected
450
471
  else if (shortcut.type === "deselect"){
451
472
  if (detailsOverCanvas) setDetailsFor(null);
452
473
  else setSelected(null);
@@ -458,107 +479,153 @@ function MapScreen(){
458
479
  return () => window.removeEventListener("keydown", onKey);
459
480
  }, [levelPath, open, page, pageCount, turnPage, hasSelection, hasHighlights, clearHighlights, detailsOverCanvas]);
460
481
 
482
+ // how many lines Strong links only hides right now, before a hover adds any back
483
+ const weakHidden = level !== null && strongOnly ? weakLinesHidden(level.edges, shownSelected, agent) : 0;
461
484
  const jumpError = focusError !== null && focusError.place === cacheKey(place) && focusError.selected === selected ? focusError.error : null;
462
485
  const banner = levelError ?? jumpError;
463
486
 
464
- const panelOpen = shownSelected !== null && level !== null && (!panel.covers || detailsOverCanvas);
465
- const covered = panelOpen && panel.covers;
466
- // the selected box's actions, while the panel would cover the canvas and its details aren't asked for yet
467
- const actionsFor = panel.covers && !panelOpen ? shownSelected : null;
468
- const panelWidth = panelOpen && !covered ? PANEL_WIDTH : 0;
469
- const pill = level !== null && pageCount > 1 && !covered;
470
- // the error banner over the canvas: a line in the pill's band, or a card where that can't hold it; over a covering
471
- // panel it's the panel's last row instead (narrow.ts)
472
- const pillWidth = pill ? pillSize?.width ?? null : null;
487
+ // the inspector shows the selection, or beside the canvas with nothing selected the folder on screen; over the whole
488
+ // canvas only once the box's Details asks for it
489
+ const inspectorPath = shownSelected ?? (shell.inspector === "beside" && level !== null ? level.path : null);
490
+ const inspectorOpen = level !== null && inspectorPath !== null && shell.inspector !== "none" && (!covers || detailsOverCanvas);
491
+ const covered = inspectorOpen && covers;
492
+ // the selected box's actions, while the inspector would cover the canvas and its details aren't asked for yet
493
+ const actionsFor = covers && !inspectorOpen ? shownSelected : null;
473
494
  const bannerOnCanvas = banner !== null && !covered;
495
+ const drawerOpen = inspectorOpen && shell.inspector === "drawer";
474
496
  const bannerAs = bannerForm(bandSize?.height ?? null);
475
- const bannerBottom = !bannerOnCanvas ? 0 : bannerAs === "band" ? BANNER_TOP + (bandSize?.height ?? 0) : CARD_TOP + (cardSize?.height ?? 0);
497
+ const busy = level !== null && isBusyLevel(level.edges.length);
498
+ const quietLines = !showAllLines && busy;
499
+ const items = level === null ? 0 : level.nodes.filter((n) => n.kind !== "portal").length;
476
500
 
477
501
  if (statusError?.code === "INDEX_EMPTY") return <EmptyState />;
478
502
 
503
+ const toolbar = level && (
504
+ <MapToolbar
505
+ level={level}
506
+ strongOnly={strongOnly}
507
+ onStrongOnly={setStrongOnly}
508
+ busy={busy}
509
+ showAllLines={showAllLines}
510
+ onShowAllLines={setShowAllLines}
511
+ onTurnPage={turnPage}
512
+ canvasWidth={shell.canvasWidth}
513
+ inHeader={shell.toolbarInHeader}
514
+ narrow={windowSize.width < 480}
515
+ helpInMenu={embedded || windowSize.width < 480}
516
+ onHelp={() => setHelpOpen(true)}
517
+ // the header hides its search icon under 360 px while the toolbar shares it (Header)
518
+ searchInMenu={shell.toolbarInHeader && windowSize.width < 360}
519
+ onSearch={() => setPaletteOpen(true)}
520
+ hasSelection={shownSelected !== null}
521
+ direction={shownDirection}
522
+ onDirection={setDirection}
523
+ emptySides={emptySides}
524
+ />
525
+ );
526
+ const statusBar = (dots: boolean) => (
527
+ <StatusBar
528
+ status={status}
529
+ recent={live.recent}
530
+ connected={live.connected}
531
+ highlightCount={highlights.length}
532
+ onClear={clearHighlights}
533
+ items={items}
534
+ weakHidden={weakHidden}
535
+ embedded={embedded}
536
+ dots={dots}
537
+ />
538
+ );
539
+ const inspector = inspectorOpen && inspectorPath !== null && level && shell.inspector !== "none" && (
540
+ <DetailPanel
541
+ key={inspectorPath}
542
+ path={inspectorPath}
543
+ level={level}
544
+ indexVersion={version ?? 0}
545
+ summariesOff={status?.stages.summaries.state === "off"}
546
+ onFocus={focus}
547
+ onOpen={open}
548
+ onTurnPage={turnPage}
549
+ onClose={covers ? closeDetails : closePanel}
550
+ onOpenInEditor={onOpenFile}
551
+ placement={shell.inspector}
552
+ onScreenFolder={shownSelected === null}
553
+ alert={covered ? banner?.message : undefined}
554
+ />
555
+ );
556
+
557
+ // The page fills the window; below the layout's least height it keeps that height and scrolls down, never sideways
558
+ // (shell.ts). Header, then the explorer, the canvas's column and the inspector beside it, then the status bar.
479
559
  return (
480
- <div className="absolute inset-0 flex flex-col">
481
- <TopBar
482
- repo={repo ? <RepoMenu repo={repo} onSwitched={onSwitched} onHome={goHome} /> : null}
560
+ <div data-testid="map-shell" style={{ minHeight: shell.minHeight }} className="flex h-dvh flex-col">
561
+ <Header
562
+ crumb={level && repo
563
+ ? <Breadcrumb repo={<RepoMenu repo={repo} onSwitched={onSwitched} onHome={goHome} />} repoRoot={repo} trail={level.breadcrumb} onOpen={open} compact={windowSize.width < 360} />
564
+ : <span className="min-w-0 flex-1" />}
483
565
  onSearch={() => setPaletteOpen(true)}
484
- status={<StatusPill status={status} />}
485
566
  onHelp={() => setHelpOpen(true)}
567
+ embedded={embedded}
568
+ canGoBack={canGoBack}
569
+ toolbar={shell.toolbarInHeader ? toolbar : undefined}
570
+ status={shell.status === "inHeader" ? statusBar(true) : undefined}
486
571
  />
487
- <div className="pointer-events-auto relative z-10 flex h-10 items-center justify-between gap-4 border-b border-hairline bg-canvas px-4">
488
- {level && repo ? (
489
- EMBEDDED ? (
490
- // inside VS Code's panel, Back comes first (BackButton): the webview has no browser Back
491
- <div className="flex min-w-0 flex-1 items-center gap-2">
492
- <BackButton enabled={canGoBack} />
493
- <Breadcrumb repo={repo} trail={level.breadcrumb} onOpen={open} />
494
- </div>
495
- ) : <Breadcrumb repo={repo} trail={level.breadcrumb} onOpen={open} />
496
- ) : <span />}
497
- <div className="flex shrink-0 items-center gap-4 text-[12px] text-text-2">
498
- {level && isBusyLevel(level.edges.length) && (
499
- <span data-testid="lines-note">
500
- {/* a narrow window keeps the toggle alone (its title says the rest), so the path keeps its room */}
501
- <span className="max-[768px]:hidden">
502
- {showAllLines ? `All ${formatCount(level.edges.length)} lines shown` : "Busy folder: lines show on hover"}
503
- {" · "}
504
- </span>
505
- <button
506
- type="button"
507
- data-testid="lines-toggle"
508
- onClick={() => setShowAllLines((all) => !all)}
509
- title={showAllLines ? `All ${formatCount(level.edges.length)} lines are shown` : "A busy folder: a box's lines show when you hover or select it"}
510
- className="text-control underline underline-offset-2 hover:text-text"
511
- >
512
- {showAllLines ? "Show on hover only" : "Show all lines"}
513
- </button>
514
- </span>
572
+ <div className="flex min-h-0 flex-1">
573
+ {level && shell.explorer !== "none" && (
574
+ <div style={{ width: shell.explorer === "full" ? EXPLORER_WIDTH : RAIL_WIDTH }} className="relative z-10 shrink-0">
575
+ <Explorer level={level} selected={shownSelected} linked={linked} form={shell.explorer} onSelect={select} onOpen={open} />
576
+ </div>
577
+ )}
578
+ <div className="flex min-w-0 flex-1 flex-col">
579
+ {!shell.toolbarInHeader && toolbar}
580
+ <main ref={mainRef} className="relative min-h-0 flex-1 overflow-hidden">
581
+ {level && (
582
+ <MapCanvas
583
+ level={level}
584
+ selected={shownSelected}
585
+ agent={agent}
586
+ pulses={pulses}
587
+ onSelect={select}
588
+ onOpen={open}
589
+ onTurnPage={turnPage}
590
+ onDrawn={onDrawn}
591
+ quietLines={quietLines}
592
+ strongOnly={strongOnly}
593
+ direction={shownDirection}
594
+ covered={covered}
595
+ actionsFor={actionsFor}
596
+ onDetails={showDetails}
597
+ onOpenFile={onOpenFile}
598
+ />
599
+ )}
600
+ {!level && !levelError && <p className="absolute inset-0 grid place-items-center px-4 text-center text-[13px] text-text-2">Loading the map…</p>}
601
+ {/* left of a drawer, which would cover the line's end: centred in what's left, and measured there, so
602
+ a line that needs more than two lines there becomes the card */}
603
+ {banner && bannerOnCanvas && (
604
+ <div style={{ right: drawerOpen ? DRAWER_WIDTH : 0 }} className="pointer-events-none absolute inset-y-0 left-0">
605
+ <ErrorBanner message={banner.message} form={bannerAs} bandRef={bandRef} cardRef={cardRef} />
606
+ </div>
607
+ )}
608
+ {/* over the canvas: a drawer on its right, or the whole of it. The line's hover card keeps clear of
609
+ it, as of the banner and a box's actions (LineCard) */}
610
+ {(shell.inspector === "drawer" || shell.inspector === "cover") && inspector && (
611
+ <div data-inspector-place={shell.inspector} data-keep-clear style={{ width: shell.inspector === "drawer" ? DRAWER_WIDTH : "100%" }} className="absolute inset-y-0 right-0 z-10">
612
+ {inspector}
613
+ </div>
614
+ )}
615
+ </main>
616
+ {level && shell.legend && (
617
+ <Legend doorways={level.nodes.some((n) => n.kind === "portal")} agent={agent.size > 0} quiet={quietLines} />
618
+ )}
619
+ {shell.inspector === "below" && inspector && (
620
+ <div data-inspector-place="below" style={{ height: `${BELOW_SHARE * 100}%` }} className="min-h-0 shrink-0">{inspector}</div>
515
621
  )}
516
622
  </div>
517
- </div>
518
- <main ref={mainRef} className="relative flex-1">
519
- {level && (
520
- <MapCanvas
521
- level={level}
522
- selected={shownSelected}
523
- agent={agent}
524
- pulses={pulses}
525
- onSelect={select}
526
- onOpen={open}
527
- onTurnPage={turnPage}
528
- onDrawn={onDrawn}
529
- quietLines={!showAllLines && isBusyLevel(level.edges.length)}
530
- panelWidth={panelWidth}
531
- covered={covered}
532
- bannerBottom={bannerBottom}
533
- actionsFor={actionsFor}
534
- onDetails={showDetails}
535
- />
536
- )}
537
- {level && pill && (
538
- <PagePill sizeRef={pillRef} level={level} onTurn={turnPage} panelWidth={panelWidth} form={pillForm(mainWidth === null ? null : mainWidth - panelWidth)} />
539
- )}
540
- {!level && !levelError && <p className="absolute inset-0 grid place-items-center px-4 text-center text-[13px] text-text-2">Loading the map…</p>}
541
- {banner && bannerOnCanvas && (
542
- <ErrorBanner message={banner.message} form={bannerAs} panelWidth={panelWidth} pillWidth={pillWidth} bandRef={bandRef} cardRef={cardRef} />
623
+ {shell.inspector === "beside" && inspector && (
624
+ <div data-inspector-place="beside" style={{ width: INSPECTOR_WIDTH }} className="min-h-0 shrink-0">{inspector}</div>
543
625
  )}
544
- {panelOpen && shownSelected !== null && level && (
545
- <DetailPanel
546
- key={shownSelected}
547
- path={shownSelected}
548
- level={level}
549
- indexVersion={version ?? 0}
550
- onFocus={focus}
551
- onOpen={open}
552
- onTurnPage={turnPage}
553
- onClose={panel.covers ? closeDetails : closePanel}
554
- onOpenInEditor={editor === null ? undefined : openInEditor}
555
- covers={panel.covers}
556
- alert={covered ? banner?.message : undefined}
557
- />
558
- )}
559
- </main>
560
- <ActivityBar recent={live.recent} connected={live.connected} highlightCount={highlights.length} onClear={clearHighlights} />
561
- {helpOpen && <HelpDialog onClose={closeHelp} />}
626
+ </div>
627
+ {shell.status === "bar" && statusBar(false)}
628
+ {helpOpen && <HelpDialog onClose={closeHelp} embedded={embedded} />}
562
629
  {paletteOpen && (
563
630
  <SearchPalette
564
631
  onPick={(hit) => {
@@ -572,99 +639,36 @@ function MapScreen(){
572
639
  );
573
640
  }
574
641
 
575
- /**
576
- * Which page of a big folder is on screen, in the canvas's top-right corner with a button either side: the one place to
577
- * turn the page with the mouse (← and → turn it too). The three-task test found the top bar's note went unseen; a bar at
578
- * the bottom of the canvas then read as loud and, in the boxes' colour, blended in (owner review). Big arrows at the
579
- * canvas's sides went too, after a second critique: two ways to do one thing, and the right one sat on a box when the
580
- * panel was open. Like the other overlays it's a solid card in the logo's violet with no outline, so it reads as neither
581
- * a box nor a line, its buttons cyan; it moves left of the panel when the panel opens, by the panel's own width. On a
582
- * narrow canvas it says less (narrow.ts's pillForm): "Page 2 of 5", then "2 / 5", with the whole note as its title.
583
- */
584
- function PagePill({ sizeRef, level, onTurn, panelWidth, form }: {
585
- /** Measures the pill, which the error banner keeps clear of. */
586
- sizeRef: (element: HTMLDivElement | null) => void;
587
- level: LevelResponse;
588
- onTurn: (page: number) => void;
589
- panelWidth: number;
590
- form: PillForm;
591
- }){
592
- const step = (side: "left" | "right") => {
593
- const next = side === "left" ? level.page - 1 : level.page + 1;
594
- const Icon = side === "left" ? IconChevronLeft : IconChevronRight;
595
- const off = next < 0 || next >= level.pageCount;
596
- return (
597
- <button
598
- type="button"
599
- data-testid={`page-${side === "left" ? "previous" : "next"}`}
600
- disabled={off}
601
- onClick={() => onTurn(next)}
602
- aria-label={`${side === "left" ? "Previous" : "Next"} page`}
603
- title={`${side === "left" ? "Previous" : "Next"} page (${side === "left" ? "←" : "→"})`}
604
- className="grid h-9 w-9 place-items-center rounded-md text-control hover:bg-overlay-line disabled:opacity-35 disabled:hover:bg-transparent"
605
- >
606
- <Icon aria-hidden size={22} stroke={2} />
607
- </button>
608
- );
609
- };
610
- // "Page 2 of 5 · 44 of 182 folders and files": the page in the text color, the count quieter
611
- const note = pageNote(level);
612
- const [pagePart, ...rest] = note.split(" · ");
613
- return (
614
- <div
615
- ref={sizeRef}
616
- style={{ right: panelWidth + 16 }}
617
- className="absolute top-4 z-[7] flex items-center gap-1.5 rounded-lg bg-overlay p-1"
618
- >
619
- {step("left")}
620
- <span data-testid="page-note" title={form === "full" ? undefined : note} className="whitespace-nowrap px-1 text-[14px] text-text">
621
- {form === "numbers" ? `${level.page + 1} / ${level.pageCount}` : pagePart}
622
- {form === "full" && rest.length > 0 && <span className="text-overlay-text-2"> · {rest.join(" · ")}</span>}
623
- </span>
624
- {step("right")}
625
- </div>
626
- );
627
- }
628
-
629
642
  /**
630
643
  * The Map's error line over the canvas (a failed jump, a level that wouldn't load: VISUAL_REFRESH.md §10). It takes no
631
- * clicks, so it never stands between the pointer and the boxes or controls under it.
632
- * - **band** (its place at normal sizes): 24 px down in the page pill's band, centred over the canvas beside the panel.
633
- * It keeps the pill's width clear on its right, and as much on its left so it stays centred; with less room the left
634
- * side gives way first, so the line moves left, then wraps.
635
- * - **card**, where the band would need more than its two lines: a solid card in the canvas's top-left corner that reads
636
- * over the boxes, its right edge clear of the pill and of the zoom controls.
644
+ * clicks, so it never stands between the pointer and the boxes under it.
645
+ * - **band** (its place at normal sizes): 24 px down, centred over the canvas, wrapping as it must.
646
+ * - **card**, where the band would need more than its two lines: a solid card in the canvas's top-left corner.
637
647
  * The band is always laid out, unseen while the card shows, so its height (which picks the form) is always known.
638
648
  */
639
- function ErrorBanner({ message, form, panelWidth, pillWidth, bandRef, cardRef }: {
649
+ function ErrorBanner({ message, form, bandRef, cardRef }: {
640
650
  message: string;
641
651
  form: BannerForm;
642
- panelWidth: number;
643
- pillWidth: number | null;
644
652
  bandRef: (element: HTMLDivElement | null) => void;
645
653
  cardRef: (element: HTMLDivElement | null) => void;
646
654
  }){
647
- const clear = bandClear(pillWidth);
648
655
  return (
649
656
  <>
650
657
  <div
651
658
  ref={bandRef}
652
659
  role={form === "band" ? "alert" : undefined}
653
660
  aria-hidden={form === "band" ? undefined : true}
654
- style={{ right: panelWidth }}
655
- className={`pointer-events-none absolute left-0 top-6 flex justify-center px-4 text-[13px] text-danger ${form === "band" ? "" : "invisible"}`}
661
+ className={`pointer-events-none absolute inset-x-0 top-6 z-[7] flex justify-center px-4 text-[13px] text-danger ${form === "band" ? "" : "invisible"}`}
656
662
  >
657
- {/* the left side gives way before the line does; the right side never does */}
658
- <span aria-hidden style={{ flex: `0 1 ${clear}px` }} />
659
- <p style={{ maxWidth: `calc(100% - ${clear}px)` }} className="shrink-0 text-center [overflow-wrap:anywhere]">{message}</p>
660
- <span aria-hidden style={{ width: clear }} className="shrink-0" />
663
+ <p data-keep-clear={form === "band" ? true : undefined} className="max-w-full rounded-[5px] bg-canvas px-2 text-center [overflow-wrap:anywhere]">{message}</p>
661
664
  </div>
662
665
  {form === "card" && (
663
666
  <div
664
667
  ref={cardRef}
665
668
  role="alert"
666
- style={{ maxWidth: `calc(100% - ${CARD_TOP + panelWidth + cardClear(pillWidth)}px)` }}
667
- className="pointer-events-none absolute left-4 top-4 z-[7] rounded-lg border border-hairline bg-panel px-3 py-2 text-[13px] text-danger [overflow-wrap:anywhere]"
669
+ data-keep-clear
670
+ style={{ maxWidth: `calc(100% - ${CARD_CLEAR}px)` }}
671
+ className="pointer-events-none absolute left-4 top-4 z-[7] rounded-md border border-divider bg-panel px-3 py-2 text-[13px] text-danger [overflow-wrap:anywhere]"
668
672
  >
669
673
  {message}
670
674
  </div>
@@ -675,11 +679,11 @@ function ErrorBanner({ message, form, panelWidth, pillWidth, bandRef, cardRef }:
675
679
 
676
680
  function EmptyState(){
677
681
  return (
678
- <div className="absolute inset-0 grid place-items-center px-4">
679
- <div className="max-w-md space-y-2 text-center text-[14px] text-text-2">
682
+ <div className="grid h-dvh place-items-center px-4">
683
+ <div className="max-w-md space-y-2 text-center text-[14px] text-text-2 [overflow-wrap:anywhere]">
680
684
  <p className="text-[15px] font-medium text-text">No repository indexed yet.</p>
681
- <p>Index one from a terminal, then reload: <code className="rounded-md bg-code-bg px-1 font-mono text-[13px] text-code">nebulosity index &lt;path-to-repo&gt;</code></p>
682
- <p>In a checkout of Nebulosity: <code className="rounded-md bg-code-bg px-1 font-mono text-[13px] text-code">npm run index -- &lt;path-to-repo&gt;</code></p>
685
+ <p>Index one from a terminal with <code className="rounded-md bg-node px-1 font-mono text-[13px] text-text">nebulosity index &lt;path-to-repo&gt;</code>, then reload.</p>
686
+ <p>In a checkout of Nebulosity it&apos;s <code className="rounded-md bg-node px-1 font-mono text-[13px] text-text">npm run index -- &lt;path-to-repo&gt;</code>.</p>
683
687
  </div>
684
688
  </div>
685
689
  );